diff options
Diffstat (limited to 'activerecord')
362 files changed, 8312 insertions, 5469 deletions
diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index d0e900c305..97af75546d 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,3 +1,438 @@ +* Raise error when has_many through is defined before through association + + Fixes #26834 + + *Chris Holmes* + +* Deprecate passing `name` to `indexes`. + + *Ryuta Kamizono* + +* Remove deprecated tasks: `db:test:clone`, `db:test:clone_schema`, `db:test:clone_structure`. + + *Rafel Mendonça França* + +* Compare deserialized values for `PostgreSQL::OID::Hstore` types when + calling `ActiveRecord::Dirty#changed_in_place?`. + + Fixes #27502. + + *Jon Moss* + +* Raise `ArgumentError` when passing an `ActiveRecord::Base` instance to `.find`, + `.exists?` and `.update`. + + *Rafael Mendonça França* + +* Respect precision option for arrays of timestamps. + + Fixes #27514. + + *Sean Griffin* + +* Optimize slow model instantiation when using STI and `store_full_sti_class = false` option. + + *Konstantin Lazarev* + +* Add `touch` option to counter cache modifying methods. + + Works when updating, resetting, incrementing and decrementing counters: + + # Touches `updated_at`/`updated_on`. + Topic.increment_counter(:messages_count, 1, touch: true) + Topic.decrement_counter(:messages_count, 1, touch: true) + + # Touches `last_discussed_at`. + Topic.reset_counters(18, :messages, touch: :last_discussed_at) + + # Touches `updated_at` and `last_discussed_at`. + Topic.update_counters(18, messages_count: 5, touch: %i( updated_at last_discussed_at )) + + Fixes #26724. + + *Jarred Trost* + +* Remove deprecated `#uniq`, `#uniq!`, and `#uniq_value`. + + *Ryuta Kamizono* + +* Remove deprecated `#insert_sql`, `#update_sql`, and `#delete_sql`. + + *Ryuta Kamizono* + +* Remove deprecated `#use_transactional_fixtures` configuration. + + *Rafael Mendonça França* + +* Remove deprecated `#raise_in_transactional_callbacks` configuration. + + *Rafael Mendonça França* + +* Remove deprecated `#load_schema_for`. + + *Rafael Mendonça França* + +* Remove deprecated conditions parameter from `#destroy_all` and `#delete_all`. + + *Rafael Mendonça França* + +* Remove deprecated support to passing arguments to `#select` when a block is provided. + + *Rafael Mendonça França* + +* Remove deprecated support to query using commas on LIMIT. + + *Rafael Mendonça França* + +* Remove deprecated support to passing a class as a value in a query. + + *Rafael Mendonça França* + +* Raise `ActiveRecord::IrreversibleOrderError` when using `last` with an irreversible + order. + + *Rafael Mendonça França* + +* Raise when a `has_many :through` association has an ambiguous reflection name. + + *Rafael Mendonça França* + +* Raise when `ActiveRecord::Migration` is inherited from directly. + + *Rafael Mendonça França* + +* Remove deprecated `original_exception` argument in `ActiveRecord::StatementInvalid#initialize` + and `ActiveRecord::StatementInvalid#original_exception`. + + *Rafael Mendonça França* + +* `#tables` and `#table_exists?` return only tables and not views. + + All the deprecations on those methods were removed. + + *Rafael Mendonça França* + +* Remove deprecated `name` argument from `#tables`. + + *Rafael Mendonça França* + +* Remove deprecated support to passing a column to `#quote`. + + *Rafael Mendonça França* + +* Set `:time` as a timezone aware type and remove deprecation when + `config.active_record.time_zone_aware_types` is not explictly set. + + *Rafael Mendonça França* + +* Remove deprecated force reload argument in singular and collection association readers. + + *Rafael Mendonça França* + +* Remove deprecated `activerecord.errors.messages.restrict_dependent_destroy.one` and + `activerecord.errors.messages.restrict_dependent_destroy.many` i18n scopes. + + *Rafael Mendonça França* + +* Allow passing extra flags to `db:structure:load` and `db:structure:dump` + + Introduces `ActiveRecord::Tasks::DatabaseTasks.structure_(load|dump)_flags` to customize the + eventual commands run against the database, e.g. mysqldump/pg_dump. + + *Kir Shatrov* + +* Notifications see frozen SQL string. + + Fixes #23774. + + *Richard Monette* + +* RuntimeErrors are no longer translated to `ActiveRecord::StatementInvalid`. + + *Richard Monette* + +* Change the schema cache format to use YAML instead of Marshal. + + *Kir Shatrov* + +* Support index length and order options using both string and symbol + column names. + + Fixes #27243. + + *Ryuta Kamizono* + +* Raise `ActiveRecord::RangeError` when values that executed are out of range. + + *Ryuta Kamizono* + +* Raise `ActiveRecord::NotNullViolation` when a record cannot be inserted + or updated because it would violate a not null constraint. + + *Ryuta Kamizono* + +* Emulate db trigger behaviour for after_commit :destroy, :update. + + Race conditions can occur when an ActiveRecord is destroyed + twice or destroyed and updated. The callbacks should only be + triggered once, similar to a SQL database trigger. + + *Stefan Budeanu* + +* Moved `DecimalWithoutScale`, `Text`, and `UnsignedInteger` from Active Model to Active Record. + + *Iain Beeston* + +* Fix `write_attribute` method to check whether an attribute is aliased or not, and + use the aliased attribute name if needed. + + *Prathamesh Sonpatki* + +* Fix `read_attribute` method to check whether an attribute is aliased or not, and + use the aliased attribute name if needed. + + Fixes #26417. + + *Prathamesh Sonpatki* + +* PostgreSQL & MySQL: Use big integer as primary key type for new tables. + + *Jon McCartie*, *Pavel Pravosud* + +* Change the type argument of `ActiveRecord::Base#attribute` to be optional. + The default is now `ActiveRecord::Type::Value.new`, which provides no type + casting behavior. + + *Sean Griffin* + +* Don't treat unsigned integers with zerofill as signed. + + Fixes #27125. + + *Ryuta Kamizono* + +* Fix the uniqueness validation scope with a polymorphic association. + + *Sergey Alekseev* + +* Raise `ActiveRecord::RecordNotFound` from collection `*_ids` setters + for unknown IDs with a better error message. + + Changes the collection `*_ids` setters to cast provided IDs the data + type of the primary key set in the association, not the model + primary key. + + *Dominic Cleal* + +* For PostgreSQL >= 9.4 use `pgcrypto`'s `gen_random_uuid()` instead of + `uuid-ossp`'s UUID generation function. + + *Yuji Yaginuma*, *Yaw Boakye* + +* Introduce `Model#reload_<association>` to bring back the behavior + of `Article.category(true)` where `category` is a singular + association. + + The force reloading of the association reader was deprecated + in #20888. Unfortunately the suggested alternative of + `article.reload.category` does not expose the same behavior. + + This patch adds a reader method with the prefix `reload_` for + singular associations. This method has the same semantics as + passing true to the association reader used to have. + + *Yves Senn* + +* Make sure eager loading `ActiveRecord::Associations` also loads + constants defined in `ActiveRecord::Associations::Preloader`. + + *Yves Senn* + +* Allow `ActionController::Parameters`-like objects to be passed as + values for Postgres HStore columns. + + Fixes #26904. + + *Jon Moss* + +* Added `stat` method to `ActiveRecord::ConnectionAdapters::ConnectionPool`. + + Example: + + ActiveRecord::Base.connection_pool.stat # => + { size: 15, connections: 1, busy: 1, dead: 0, idle: 0, waiting: 0, checkout_timeout: 5 } + + *Pavel Evstigneev* + +* Avoid `unscope(:order)` when `limit_value` is presented for `count` + and `exists?`. + + If `limit_value` is presented, records fetching order is very important + for performance. We should not unscope the order in the case. + + *Ryuta Kamizono* + +* Fix an Active Record `DateTime` field `NoMethodError` caused by incomplete + datetime. + + Fixes #24195. + + *Sen Zhang* + +* Allow `slice` to take an array of methods(without the need for splatting). + + *Cohen Carlisle* + +* Improved partial writes with HABTM and has many through associations + to fire database query only if relation has been changed. + + Fixes #19663. + + *Mehmet Emin İNAÇ* + +* Deprecate passing arguments and block at the same time to + `ActiveRecord::QueryMethods#select`. + + *Prathamesh Sonpatki* + +* Optimistic locking: Added ability to update `locking_column` value. + Ignore optimistic locking if trying to update with new `locking_column` value. + + *bogdanvlviv* + +* Fixed: Optimistic locking does not work well with `null` in the database. + + Fixes #26024. + + *bogdanvlviv* + +* Fixed support for case insensitive comparisons of `text` columns in + PostgreSQL. + + *Edho Arief* + +* Serialize JSON attribute value `nil` as SQL `NULL`, not JSON `null`. + + *Trung Duc Tran* + +* Return `true` from `update_attribute` when the value of the attribute + to be updated is unchanged. + + Fixes #26593. + + *Prathamesh Sonpatki* + +* Always store errors details information with symbols. + + When the association is autosaved we were storing the details with + string keys. This was creating inconsistency with other details that are + added using the `Errors#add` method. It was also inconsistent with the + `Errors#messages` storage. + + To fix this inconsistency we are always storing with symbols. This will + cause a small breaking change because in those cases the details could + be accessed as strings keys but now it can not. + + Fix #26499. + + *Rafael Mendonça França*, *Marcus Vieira* + +* Calling `touch` on a model using optimistic locking will now leave the model + in a non-dirty state with no attribute changes. + + Fixes #26496. + + *Jakob Skjerning* + +* Using a mysql2 connection after it fails to reconnect will now have an error message + saying the connection is closed rather than an undefined method error message. + + *Dylan Thacker-Smith* + +* PostgreSQL array columns will now respect the encoding of strings contained + in the array. + + Fixes #26326. + + *Sean Griffin* + +* Inverse association instances will now be set before `after_find` or + `after_initialize` callbacks are run. + + Fixes #26320. + + *Sean Griffin* + +* Remove unnecessarily association load when a `belongs_to` association has already been + loaded then the foreign key is changed directly and the record saved. + + *James Coleman* + +* Remove standardized column types/arguments spaces in schema dump. + + *Tim Petricola* + +* Avoid loading records from database when they are already loaded using + the `pluck` method on a collection. + + Fixes #25921. + + *Ryuta Kamizono* + +* Remove text default treated as an empty string in non-strict mode for + consistency with other types. + + Strict mode controls how MySQL handles invalid or missing values in + data-change statements such as INSERT or UPDATE. If strict mode is not + in effect, MySQL inserts adjusted values for invalid or missing values + and produces warnings. + + def test_mysql_not_null_defaults_non_strict + using_strict(false) do + with_mysql_not_null_table do |klass| + record = klass.new + assert_nil record.non_null_integer + assert_nil record.non_null_string + assert_nil record.non_null_text + assert_nil record.non_null_blob + + record.save! + record.reload + + assert_equal 0, record.non_null_integer + assert_equal "", record.non_null_string + assert_equal "", record.non_null_text + assert_equal "", record.non_null_blob + end + end + end + + https://dev.mysql.com/doc/refman/5.7/en/sql-mode.html#sql-mode-strict + + *Ryuta Kamizono* + +* Sqlite3 migrations to add a column to an existing table can now be + successfully rolled back when the column was given and invalid column + type. + + Fixes #26087. + + *Travis O'Neill* + +* Deprecate `sanitize_conditions`. Use `sanitize_sql` instead. + + *Ryuta Kamizono* + +* Doing count on relations that contain LEFT OUTER JOIN Arel node no longer + force a DISTINCT. This solves issues when using count after a left_joins. + + *Maxime Handfield Lapointe* + +* RecordNotFound raised by association.find exposes `id`, `primary_key` and + `model` methods to be consistent with RecordNotFound raised by Record.find. + + *Michel Pigassou* + * Hashes can once again be passed to setters of `composed_of`, if all of the mapping methods are methods implemented on `Hash`. @@ -10,7 +445,7 @@ *Takeshi Akima* * Virtual attributes will no longer raise when read on models loaded from the - database + database. *Sean Griffin* @@ -47,8 +482,8 @@ *Xavier Noria* * Using `group` with an attribute that has a custom type will properly cast - the hash keys after calling a calculation method like `count`. - + the hash keys after calling a calculation method like `count`. + Fixes #25595. *Sean Griffin* @@ -83,7 +518,7 @@ *Sean Griffin* * Ensure hashes can be assigned to attributes created using `composed_of`. - + Fixes #25210. *Sean Griffin* diff --git a/activerecord/MIT-LICENSE b/activerecord/MIT-LICENSE index 1f496cf280..f9e4444f07 100644 --- a/activerecord/MIT-LICENSE +++ b/activerecord/MIT-LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2004-2016 David Heinemeier Hansson +Copyright (c) 2004-2017 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/Rakefile b/activerecord/Rakefile index 543ab2eaba..7be3d851f1 100644 --- a/activerecord/Rakefile +++ b/activerecord/Rakefile @@ -50,7 +50,7 @@ end Rake::TestTask.new(adapter => "#{adapter}:env") { |t| adapter_short = adapter == "db2" ? adapter : adapter[/^[a-z0-9]+/] t.libs << "test" - t.test_files = (Dir.glob( "test/cases/**/*_test.rb" ).reject { + t.test_files = (Dir.glob("test/cases/**/*_test.rb").reject { |x| x.include?("/adapters/") } + Dir.glob("test/cases/adapters/#{adapter_short}/**/*_test.rb")) @@ -66,8 +66,8 @@ 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) - end or raise "Failures" + sh(Gem.ruby, "-w" , "-Itest", file) + end || raise("Failures") end end end @@ -111,11 +111,6 @@ namespace :db do config = ARTest.config["connections"]["postgresql"] %x( createdb -E UTF8 -T template0 #{config["arunit"]["database"]} ) %x( createdb -E UTF8 -T template0 #{config["arunit2"]["database"]} ) - - # prepare hstore - if %x( createdb --version ).strip.gsub(/(.*)(\d\.\d\.\d)$/, "\\2") < "9.1.0" - puts "Please prepare hstore data type. See http://www.postgresql.org/docs/current/static/hstore.html" - end end desc "Drop the PostgreSQL test databases" diff --git a/activerecord/activerecord.gemspec b/activerecord/activerecord.gemspec index 2cd8f179dd..0b37e9076c 100644 --- a/activerecord/activerecord.gemspec +++ b/activerecord/activerecord.gemspec @@ -24,5 +24,5 @@ Gem::Specification.new do |s| s.add_dependency "activesupport", version s.add_dependency "activemodel", version - s.add_dependency "arel", "~> 7.0" + s.add_dependency "arel", "~> 8.0" end diff --git a/activerecord/bin/test b/activerecord/bin/test index 23add35d45..3a9547e5c1 100755 --- a/activerecord/bin/test +++ b/activerecord/bin/test @@ -17,5 +17,3 @@ module Minitest end Minitest.extensions.unshift "active_record" - -exit Minitest.run(ARGV) diff --git a/activerecord/examples/performance.rb b/activerecord/examples/performance.rb index f2fe8875b9..3257dd4ad7 100644 --- a/activerecord/examples/performance.rb +++ b/activerecord/examples/performance.rb @@ -2,7 +2,7 @@ require "active_record" require "benchmark/ips" TIME = (ENV["BENCHMARK_TIME"] || 20).to_i -RECORDS = (ENV["BENCHMARK_RECORDS"] || TIME*1000).to_i +RECORDS = (ENV["BENCHMARK_RECORDS"] || TIME * 1000).to_i conn = { adapter: "sqlite3", database: ":memory:" } @@ -42,7 +42,7 @@ class Exhibit < ActiveRecord::Base def self.feel(exhibits) exhibits.each(&:feel) end end -def progress_bar(int); print "." if (int%100).zero? ; end +def progress_bar(int); print "." if (int % 100).zero? ; end puts "Generating data..." diff --git a/activerecord/lib/active_record.rb b/activerecord/lib/active_record.rb index fb7b8df01d..250f48fad9 100644 --- a/activerecord/lib/active_record.rb +++ b/activerecord/lib/active_record.rb @@ -1,5 +1,5 @@ #-- -# Copyright (c) 2004-2016 David Heinemeier Hansson +# Copyright (c) 2004-2017 David Heinemeier Hansson # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -68,7 +68,6 @@ module ActiveRecord autoload :StatementCache autoload :Store autoload :Suppressor - autoload :TableMetadata autoload :Timestamp autoload :Transactions autoload :Translation @@ -101,6 +100,7 @@ module ActiveRecord end autoload :Result + autoload :TableMetadata end module Coders diff --git a/activerecord/lib/active_record/aggregations.rb b/activerecord/lib/active_record/aggregations.rb index 55076c4314..10cbd5429c 100644 --- a/activerecord/lib/active_record/aggregations.rb +++ b/activerecord/lib/active_record/aggregations.rb @@ -15,170 +15,170 @@ module ActiveRecord private - def clear_aggregation_cache # :nodoc: + def clear_aggregation_cache @aggregation_cache.clear if persisted? end - def init_internals # :nodoc: + def init_internals @aggregation_cache = {} super end - # Active Record implements aggregation through a macro-like class method called #composed_of - # for representing attributes as value objects. It expresses relationships like "Account [is] - # composed of Money [among other things]" or "Person [is] composed of [an] address". Each call - # to the macro adds a description of how the value objects are created from the attributes of - # the entity object (when the entity is initialized either as a new object or from finding an - # existing object) and how it can be turned back into attributes (when the entity is saved to - # the database). - # - # class Customer < ActiveRecord::Base - # composed_of :balance, class_name: "Money", mapping: %w(amount currency) - # composed_of :address, mapping: [ %w(address_street street), %w(address_city city) ] - # end - # - # The customer class now has the following methods to manipulate the value objects: - # * <tt>Customer#balance, Customer#balance=(money)</tt> - # * <tt>Customer#address, Customer#address=(address)</tt> - # - # These methods will operate with value objects like the ones described below: - # - # class Money - # include Comparable - # attr_reader :amount, :currency - # EXCHANGE_RATES = { "USD_TO_DKK" => 6 } - # - # def initialize(amount, currency = "USD") - # @amount, @currency = amount, currency - # end - # - # def exchange_to(other_currency) - # exchanged_amount = (amount * EXCHANGE_RATES["#{currency}_TO_#{other_currency}"]).floor - # Money.new(exchanged_amount, other_currency) - # end - # - # def ==(other_money) - # amount == other_money.amount && currency == other_money.currency - # end - # - # def <=>(other_money) - # if currency == other_money.currency - # amount <=> other_money.amount - # else - # amount <=> other_money.exchange_to(currency).amount - # end - # end - # end - # - # class Address - # attr_reader :street, :city - # def initialize(street, city) - # @street, @city = street, city - # end - # - # def close_to?(other_address) - # city == other_address.city - # end - # - # def ==(other_address) - # city == other_address.city && street == other_address.street - # end - # end - # - # Now it's possible to access attributes from the database through the value objects instead. If - # you choose to name the composition the same as the attribute's name, it will be the only way to - # access that attribute. That's the case with our +balance+ attribute. You interact with the value - # objects just like you would with any other attribute: - # - # customer.balance = Money.new(20) # sets the Money value object and the attribute - # customer.balance # => Money value object - # customer.balance.exchange_to("DKK") # => Money.new(120, "DKK") - # customer.balance > Money.new(10) # => true - # customer.balance == Money.new(20) # => true - # customer.balance < Money.new(5) # => false - # - # Value objects can also be composed of multiple attributes, such as the case of Address. The order - # of the mappings will determine the order of the parameters. - # - # customer.address_street = "Hyancintvej" - # customer.address_city = "Copenhagen" - # customer.address # => Address.new("Hyancintvej", "Copenhagen") - # - # customer.address = Address.new("May Street", "Chicago") - # customer.address_street # => "May Street" - # customer.address_city # => "Chicago" - # - # == Writing value objects - # - # Value objects are immutable and interchangeable objects that represent a given value, such as - # a Money object representing $5. Two Money objects both representing $5 should be equal (through - # methods such as <tt>==</tt> and <tt><=></tt> from Comparable if ranking makes sense). This is - # unlike entity objects where equality is determined by identity. An entity class such as Customer can - # easily have two different objects that both have an address on Hyancintvej. Entity identity is - # determined by object or relational unique identifiers (such as primary keys). Normal - # ActiveRecord::Base classes are entity objects. - # - # It's also important to treat the value objects as immutable. Don't allow the Money object to have - # its amount changed after creation. Create a new Money object with the new value instead. The - # <tt>Money#exchange_to</tt> method is an example of this. It returns a new value object instead of changing - # its own values. Active Record won't persist value objects that have been changed through means - # other than the writer method. - # - # The immutable requirement is enforced by Active Record by freezing any object assigned as a value - # object. Attempting to change it afterwards will result in a +RuntimeError+. - # - # Read more about value objects on http://c2.com/cgi/wiki?ValueObject and on the dangers of not - # keeping value objects immutable on http://c2.com/cgi/wiki?ValueObjectsShouldBeImmutable - # - # == Custom constructors and converters - # - # By default value objects are initialized by calling the <tt>new</tt> constructor of the value - # class passing each of the mapped attributes, in the order specified by the <tt>:mapping</tt> - # option, as arguments. If the value class doesn't support this convention then #composed_of allows - # a custom constructor to be specified. - # - # When a new value is assigned to the value object, the default assumption is that the new value - # is an instance of the value class. Specifying a custom converter allows the new value to be automatically - # converted to an instance of value class if necessary. - # - # For example, the +NetworkResource+ model has +network_address+ and +cidr_range+ attributes that should be - # aggregated using the +NetAddr::CIDR+ value class (http://www.rubydoc.info/gems/netaddr/1.5.0/NetAddr/CIDR). - # The constructor for the value class is called +create+ and it expects a CIDR address string as a parameter. - # New values can be assigned to the value object using either another +NetAddr::CIDR+ object, a string - # or an array. The <tt>:constructor</tt> and <tt>:converter</tt> options can be used to meet - # these requirements: - # - # class NetworkResource < ActiveRecord::Base - # composed_of :cidr, - # class_name: 'NetAddr::CIDR', - # mapping: [ %w(network_address network), %w(cidr_range bits) ], - # allow_nil: true, - # constructor: Proc.new { |network_address, cidr_range| NetAddr::CIDR.create("#{network_address}/#{cidr_range}") }, - # converter: Proc.new { |value| NetAddr::CIDR.create(value.is_a?(Array) ? value.join('/') : value) } - # end - # - # # This calls the :constructor - # network_resource = NetworkResource.new(network_address: '192.168.0.1', cidr_range: 24) - # - # # These assignments will both use the :converter - # network_resource.cidr = [ '192.168.2.1', 8 ] - # network_resource.cidr = '192.168.0.1/24' - # - # # This assignment won't use the :converter as the value is already an instance of the value class - # network_resource.cidr = NetAddr::CIDR.create('192.168.2.1/8') - # - # # Saving and then reloading will use the :constructor on reload - # network_resource.save - # network_resource.reload - # - # == Finding records by a value object - # - # Once a #composed_of relationship is specified for a model, records can be loaded from the database - # by specifying an instance of the value object in the conditions hash. The following example - # finds all customers with +balance_amount+ equal to 20 and +balance_currency+ equal to "USD": - # - # Customer.where(balance: Money.new(20, "USD")) - # + # Active Record implements aggregation through a macro-like class method called #composed_of + # for representing attributes as value objects. It expresses relationships like "Account [is] + # composed of Money [among other things]" or "Person [is] composed of [an] address". Each call + # to the macro adds a description of how the value objects are created from the attributes of + # the entity object (when the entity is initialized either as a new object or from finding an + # existing object) and how it can be turned back into attributes (when the entity is saved to + # the database). + # + # class Customer < ActiveRecord::Base + # composed_of :balance, class_name: "Money", mapping: %w(amount currency) + # composed_of :address, mapping: [ %w(address_street street), %w(address_city city) ] + # end + # + # The customer class now has the following methods to manipulate the value objects: + # * <tt>Customer#balance, Customer#balance=(money)</tt> + # * <tt>Customer#address, Customer#address=(address)</tt> + # + # These methods will operate with value objects like the ones described below: + # + # class Money + # include Comparable + # attr_reader :amount, :currency + # EXCHANGE_RATES = { "USD_TO_DKK" => 6 } + # + # def initialize(amount, currency = "USD") + # @amount, @currency = amount, currency + # end + # + # def exchange_to(other_currency) + # exchanged_amount = (amount * EXCHANGE_RATES["#{currency}_TO_#{other_currency}"]).floor + # Money.new(exchanged_amount, other_currency) + # end + # + # def ==(other_money) + # amount == other_money.amount && currency == other_money.currency + # end + # + # def <=>(other_money) + # if currency == other_money.currency + # amount <=> other_money.amount + # else + # amount <=> other_money.exchange_to(currency).amount + # end + # end + # end + # + # class Address + # attr_reader :street, :city + # def initialize(street, city) + # @street, @city = street, city + # end + # + # def close_to?(other_address) + # city == other_address.city + # end + # + # def ==(other_address) + # city == other_address.city && street == other_address.street + # end + # end + # + # Now it's possible to access attributes from the database through the value objects instead. If + # you choose to name the composition the same as the attribute's name, it will be the only way to + # access that attribute. That's the case with our +balance+ attribute. You interact with the value + # objects just like you would with any other attribute: + # + # customer.balance = Money.new(20) # sets the Money value object and the attribute + # customer.balance # => Money value object + # customer.balance.exchange_to("DKK") # => Money.new(120, "DKK") + # customer.balance > Money.new(10) # => true + # customer.balance == Money.new(20) # => true + # customer.balance < Money.new(5) # => false + # + # Value objects can also be composed of multiple attributes, such as the case of Address. The order + # of the mappings will determine the order of the parameters. + # + # customer.address_street = "Hyancintvej" + # customer.address_city = "Copenhagen" + # customer.address # => Address.new("Hyancintvej", "Copenhagen") + # + # customer.address = Address.new("May Street", "Chicago") + # customer.address_street # => "May Street" + # customer.address_city # => "Chicago" + # + # == Writing value objects + # + # Value objects are immutable and interchangeable objects that represent a given value, such as + # a Money object representing $5. Two Money objects both representing $5 should be equal (through + # methods such as <tt>==</tt> and <tt><=></tt> from Comparable if ranking makes sense). This is + # unlike entity objects where equality is determined by identity. An entity class such as Customer can + # easily have two different objects that both have an address on Hyancintvej. Entity identity is + # determined by object or relational unique identifiers (such as primary keys). Normal + # ActiveRecord::Base classes are entity objects. + # + # It's also important to treat the value objects as immutable. Don't allow the Money object to have + # its amount changed after creation. Create a new Money object with the new value instead. The + # <tt>Money#exchange_to</tt> method is an example of this. It returns a new value object instead of changing + # its own values. Active Record won't persist value objects that have been changed through means + # other than the writer method. + # + # The immutable requirement is enforced by Active Record by freezing any object assigned as a value + # object. Attempting to change it afterwards will result in a +RuntimeError+. + # + # Read more about value objects on http://c2.com/cgi/wiki?ValueObject and on the dangers of not + # keeping value objects immutable on http://c2.com/cgi/wiki?ValueObjectsShouldBeImmutable + # + # == Custom constructors and converters + # + # By default value objects are initialized by calling the <tt>new</tt> constructor of the value + # class passing each of the mapped attributes, in the order specified by the <tt>:mapping</tt> + # option, as arguments. If the value class doesn't support this convention then #composed_of allows + # a custom constructor to be specified. + # + # When a new value is assigned to the value object, the default assumption is that the new value + # is an instance of the value class. Specifying a custom converter allows the new value to be automatically + # converted to an instance of value class if necessary. + # + # For example, the +NetworkResource+ model has +network_address+ and +cidr_range+ attributes that should be + # aggregated using the +NetAddr::CIDR+ value class (http://www.rubydoc.info/gems/netaddr/1.5.0/NetAddr/CIDR). + # The constructor for the value class is called +create+ and it expects a CIDR address string as a parameter. + # New values can be assigned to the value object using either another +NetAddr::CIDR+ object, a string + # or an array. The <tt>:constructor</tt> and <tt>:converter</tt> options can be used to meet + # these requirements: + # + # class NetworkResource < ActiveRecord::Base + # composed_of :cidr, + # class_name: 'NetAddr::CIDR', + # mapping: [ %w(network_address network), %w(cidr_range bits) ], + # allow_nil: true, + # constructor: Proc.new { |network_address, cidr_range| NetAddr::CIDR.create("#{network_address}/#{cidr_range}") }, + # converter: Proc.new { |value| NetAddr::CIDR.create(value.is_a?(Array) ? value.join('/') : value) } + # end + # + # # This calls the :constructor + # network_resource = NetworkResource.new(network_address: '192.168.0.1', cidr_range: 24) + # + # # These assignments will both use the :converter + # network_resource.cidr = [ '192.168.2.1', 8 ] + # network_resource.cidr = '192.168.0.1/24' + # + # # This assignment won't use the :converter as the value is already an instance of the value class + # network_resource.cidr = NetAddr::CIDR.create('192.168.2.1/8') + # + # # Saving and then reloading will use the :constructor on reload + # network_resource.save + # network_resource.reload + # + # == Finding records by a value object + # + # Once a #composed_of relationship is specified for a model, records can be loaded from the database + # by specifying an instance of the value object in the conditions hash. The following example + # finds all customers with +balance_amount+ equal to 20 and +balance_currency+ equal to "USD": + # + # Customer.where(balance: Money.new(20, "USD")) + # module ClassMethods # Adds reader and writer methods for manipulating a value object: # <tt>composed_of :address</tt> adds <tt>address</tt> and <tt>address=(new_address)</tt> methods. @@ -206,7 +206,7 @@ module ActiveRecord # or a Proc that is called when a new value is assigned to the value object. The converter is # passed the single value that is used in the assignment and is only called if the new value is # not an instance of <tt>:class_name</tt>. If <tt>:allow_nil</tt> is set to true, the converter - # can return nil to skip the assignment. + # can return +nil+ to skip the assignment. # # Option examples: # composed_of :temperature, mapping: %w(reading celsius) @@ -242,8 +242,8 @@ module ActiveRecord private def reader_method(name, class_name, mapping, allow_nil, constructor) define_method(name) do - if @aggregation_cache[name].nil? && (!allow_nil || mapping.any? {|key, _| !_read_attribute(key).nil? }) - attrs = mapping.collect {|key, _| _read_attribute(key)} + if @aggregation_cache[name].nil? && (!allow_nil || mapping.any? { |key, _| !_read_attribute(key).nil? }) + attrs = mapping.collect { |key, _| _read_attribute(key) } object = constructor.respond_to?(:call) ? constructor.call(*attrs) : class_name.constantize.send(constructor, *attrs) diff --git a/activerecord/lib/active_record/association_relation.rb b/activerecord/lib/active_record/association_relation.rb index 2da2d968b9..de2d03cd0b 100644 --- a/activerecord/lib/active_record/association_relation.rb +++ b/activerecord/lib/active_record/association_relation.rb @@ -29,7 +29,10 @@ module ActiveRecord private def exec_queries - super.each { |r| @association.set_inverse_instance r } + super do |r| + @association.set_inverse_instance r + yield r if block_given? + end end end end diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index 659e512ce2..f3e2189bcb 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -97,6 +97,16 @@ module ActiveRecord end end + class HasManyThroughOrderError < ActiveRecordError #:nodoc: + def initialize(owner_class_name = nil, reflection = nil, through_reflection = nil) + if owner_class_name && reflection && through_reflection + super("Cannot have a has_many :through association '#{owner_class_name}##{reflection.name}' which goes through '#{owner_class_name}##{through_reflection.name}' before the through association is defined.") + else + super("Cannot have a has_many :through association before the through association is defined.") + end + end + end + class ThroughCantAssociateThroughHasOneOrManyReflection < ActiveRecordError #:nodoc: def initialize(owner = nil, reflection = nil) if owner && reflection @@ -107,6 +117,21 @@ module ActiveRecord end end + class AmbiguousSourceReflectionForThroughAssociation < ActiveRecordError # :nodoc: + def initialize(klass, macro, association_name, options, possible_sources) + example_options = options.dup + example_options[:source] = possible_sources.first + + super("Ambiguous source reflection for through association. Please " \ + "specify a :source directive on your declaration like:\n" \ + "\n" \ + " class #{klass} < ActiveRecord::Base\n" \ + " #{macro} :#{association_name}, #{example_options}\n" \ + " end" + ) + end + end + class HasManyThroughCantAssociateThroughHasOneOrManyReflection < ThroughCantAssociateThroughHasOneOrManyReflection #:nodoc: end @@ -224,6 +249,11 @@ module ActiveRecord autoload :AliasTracker end + def self.eager_load! + super + Preloader.eager_load! + end + # Returns the association instance for the given name, instantiating it if it doesn't already exist def association(name) #:nodoc: association = association_instance_get(name) @@ -255,16 +285,16 @@ module ActiveRecord private # Clears out the association cache. - def clear_association_cache # :nodoc: + def clear_association_cache @association_cache.clear if persisted? end - def init_internals # :nodoc: + def init_internals @association_cache = {} super end - # Returns the specified association instance if it exists, nil otherwise. + # Returns the specified association instance if it exists, +nil+ otherwise. def association_instance_get(name) @association_cache[name] end @@ -274,882 +304,882 @@ module ActiveRecord @association_cache[name] = association end - # \Associations are a set of macro-like class methods for tying objects together through - # foreign keys. They express relationships like "Project has one Project Manager" - # or "Project belongs to a Portfolio". Each macro adds a number of methods to the - # class which are specialized according to the collection or association symbol and the - # options hash. It works much the same way as Ruby's own <tt>attr*</tt> - # methods. - # - # class Project < ActiveRecord::Base - # belongs_to :portfolio - # has_one :project_manager - # has_many :milestones - # has_and_belongs_to_many :categories - # end - # - # The project class now has the following methods (and more) to ease the traversal and - # manipulation of its relationships: - # * <tt>Project#portfolio, Project#portfolio=(portfolio), Project#portfolio.nil?</tt> - # * <tt>Project#project_manager, Project#project_manager=(project_manager), Project#project_manager.nil?,</tt> - # * <tt>Project#milestones.empty?, Project#milestones.size, Project#milestones, Project#milestones<<(milestone),</tt> - # <tt>Project#milestones.delete(milestone), Project#milestones.destroy(milestone), Project#milestones.find(milestone_id),</tt> - # <tt>Project#milestones.build, Project#milestones.create</tt> - # * <tt>Project#categories.empty?, Project#categories.size, Project#categories, Project#categories<<(category1),</tt> - # <tt>Project#categories.delete(category1), Project#categories.destroy(category1)</tt> - # - # === A word of warning - # - # Don't create associations that have the same name as {instance methods}[rdoc-ref:ActiveRecord::Core] of - # <tt>ActiveRecord::Base</tt>. Since the association adds a method with that name to - # its model, using an association with the same name as one provided by <tt>ActiveRecord::Base</tt> will override the method inherited through <tt>ActiveRecord::Base</tt> and will break things. - # For instance, +attributes+ and +connection+ would be bad choices for association names, because those names already exist in the list of <tt>ActiveRecord::Base</tt> instance methods. - # - # == Auto-generated methods - # See also Instance Public methods below for more details. - # - # === Singular associations (one-to-one) - # | | belongs_to | - # generated methods | belongs_to | :polymorphic | has_one - # ----------------------------------+------------+--------------+--------- - # other(force_reload=false) | X | X | X - # other=(other) | X | X | X - # build_other(attributes={}) | X | | X - # create_other(attributes={}) | X | | X - # create_other!(attributes={}) | X | | X - # - # === Collection associations (one-to-many / many-to-many) - # | | | has_many - # generated methods | habtm | has_many | :through - # ----------------------------------+-------+----------+---------- - # others(force_reload=false) | X | X | X - # others=(other,other,...) | X | X | X - # other_ids | X | X | X - # other_ids=(id,id,...) | X | X | X - # others<< | X | X | X - # others.push | X | X | X - # others.concat | X | X | X - # others.build(attributes={}) | X | X | X - # others.create(attributes={}) | X | X | X - # others.create!(attributes={}) | X | X | X - # others.size | X | X | X - # others.length | X | X | X - # others.count | X | X | X - # others.sum(*args) | X | X | X - # others.empty? | X | X | X - # others.clear | X | X | X - # others.delete(other,other,...) | X | X | X - # others.delete_all | X | X | X - # others.destroy(other,other,...) | X | X | X - # others.destroy_all | X | X | X - # others.find(*args) | X | X | X - # others.exists? | X | X | X - # others.distinct | X | X | X - # others.reset | X | X | X - # - # === Overriding generated methods - # - # Association methods are generated in a module that is included into the model class, - # which allows you to easily override with your own methods and call the original - # generated method with +super+. For example: - # - # class Car < ActiveRecord::Base - # belongs_to :owner - # belongs_to :old_owner - # def owner=(new_owner) - # self.old_owner = self.owner - # super - # end - # end - # - # If your model class is <tt>Project</tt>, then the module is - # named <tt>Project::GeneratedAssociationMethods</tt>. The +GeneratedAssociationMethods+ module is - # included in the model class immediately after the (anonymous) generated attributes methods - # module, meaning an association will override the methods for an attribute with the same name. - # - # == Cardinality and associations - # - # Active Record associations can be used to describe one-to-one, one-to-many and many-to-many - # relationships between models. Each model uses an association to describe its role in - # the relation. The #belongs_to association is always used in the model that has - # the foreign key. - # - # === One-to-one - # - # Use #has_one in the base, and #belongs_to in the associated model. - # - # class Employee < ActiveRecord::Base - # has_one :office - # end - # class Office < ActiveRecord::Base - # belongs_to :employee # foreign key - employee_id - # end - # - # === One-to-many - # - # Use #has_many in the base, and #belongs_to in the associated model. - # - # class Manager < ActiveRecord::Base - # has_many :employees - # end - # class Employee < ActiveRecord::Base - # belongs_to :manager # foreign key - manager_id - # end - # - # === Many-to-many - # - # There are two ways to build a many-to-many relationship. - # - # The first way uses a #has_many association with the <tt>:through</tt> option and a join model, so - # there are two stages of associations. - # - # class Assignment < ActiveRecord::Base - # belongs_to :programmer # foreign key - programmer_id - # belongs_to :project # foreign key - project_id - # end - # class Programmer < ActiveRecord::Base - # has_many :assignments - # has_many :projects, through: :assignments - # end - # class Project < ActiveRecord::Base - # has_many :assignments - # has_many :programmers, through: :assignments - # end - # - # For the second way, use #has_and_belongs_to_many in both models. This requires a join table - # that has no corresponding model or primary key. - # - # class Programmer < ActiveRecord::Base - # has_and_belongs_to_many :projects # foreign keys in the join table - # end - # class Project < ActiveRecord::Base - # has_and_belongs_to_many :programmers # foreign keys in the join table - # end - # - # Choosing which way to build a many-to-many relationship is not always simple. - # If you need to work with the relationship model as its own entity, - # use #has_many <tt>:through</tt>. Use #has_and_belongs_to_many when working with legacy schemas or when - # you never work directly with the relationship itself. - # - # == Is it a #belongs_to or #has_one association? - # - # Both express a 1-1 relationship. The difference is mostly where to place the foreign - # key, which goes on the table for the class declaring the #belongs_to relationship. - # - # class User < ActiveRecord::Base - # # I reference an account. - # belongs_to :account - # end - # - # class Account < ActiveRecord::Base - # # One user references me. - # has_one :user - # end - # - # The tables for these classes could look something like: - # - # CREATE TABLE users ( - # id int NOT NULL auto_increment, - # account_id int default NULL, - # name varchar default NULL, - # PRIMARY KEY (id) - # ) - # - # CREATE TABLE accounts ( - # id int NOT NULL auto_increment, - # name varchar default NULL, - # PRIMARY KEY (id) - # ) - # - # == Unsaved objects and associations - # - # You can manipulate objects and associations before they are saved to the database, but - # there is some special behavior you should be aware of, mostly involving the saving of - # associated objects. - # - # You can set the <tt>:autosave</tt> option on a #has_one, #belongs_to, - # #has_many, or #has_and_belongs_to_many association. Setting it - # to +true+ will _always_ save the members, whereas setting it to +false+ will - # _never_ save the members. More details about <tt>:autosave</tt> option is available at - # AutosaveAssociation. - # - # === One-to-one associations - # - # * Assigning an object to a #has_one association automatically saves that object and - # the object being replaced (if there is one), in order to update their foreign - # keys - except if the parent object is unsaved (<tt>new_record? == true</tt>). - # * If either of these saves fail (due to one of the objects being invalid), an - # ActiveRecord::RecordNotSaved exception is raised and the assignment is - # cancelled. - # * If you wish to assign an object to a #has_one association without saving it, - # use the <tt>#build_association</tt> method (documented below). The object being - # replaced will still be saved to update its foreign key. - # * Assigning an object to a #belongs_to association does not save the object, since - # the foreign key field belongs on the parent. It does not save the parent either. - # - # === Collections - # - # * Adding an object to a collection (#has_many or #has_and_belongs_to_many) automatically - # saves that object, except if the parent object (the owner of the collection) is not yet - # stored in the database. - # * If saving any of the objects being added to a collection (via <tt>push</tt> or similar) - # fails, then <tt>push</tt> returns +false+. - # * If saving fails while replacing the collection (via <tt>association=</tt>), an - # ActiveRecord::RecordNotSaved exception is raised and the assignment is - # cancelled. - # * You can add an object to a collection without automatically saving it by using the - # <tt>collection.build</tt> method (documented below). - # * All unsaved (<tt>new_record? == true</tt>) members of the collection are automatically - # saved when the parent is saved. - # - # == Customizing the query - # - # \Associations are built from <tt>Relation</tt> objects, and you can use the Relation syntax - # to customize them. For example, to add a condition: - # - # class Blog < ActiveRecord::Base - # has_many :published_posts, -> { where(published: true) }, class_name: 'Post' - # end - # - # Inside the <tt>-> { ... }</tt> block you can use all of the usual Relation methods. - # - # === Accessing the owner object - # - # Sometimes it is useful to have access to the owner object when building the query. The owner - # is passed as a parameter to the block. For example, the following association would find all - # events that occur on the user's birthday: - # - # class User < ActiveRecord::Base - # has_many :birthday_events, ->(user) { where(starts_on: user.birthday) }, class_name: 'Event' - # end - # - # Note: Joining, eager loading and preloading of these associations is not fully possible. - # These operations happen before instance creation and the scope will be called with a +nil+ argument. - # This can lead to unexpected behavior and is deprecated. - # - # == Association callbacks - # - # Similar to the normal callbacks that hook into the life cycle of an Active Record object, - # you can also define callbacks that get triggered when you add an object to or remove an - # object from an association collection. - # - # class Project - # has_and_belongs_to_many :developers, after_add: :evaluate_velocity - # - # def evaluate_velocity(developer) - # ... - # end - # end - # - # It's possible to stack callbacks by passing them as an array. Example: - # - # class Project - # has_and_belongs_to_many :developers, - # after_add: [:evaluate_velocity, Proc.new { |p, d| p.shipping_date = Time.now}] - # end - # - # Possible callbacks are: +before_add+, +after_add+, +before_remove+ and +after_remove+. - # - # If any of the +before_add+ callbacks throw an exception, the object will not be - # added to the collection. - # - # Similarly, if any of the +before_remove+ callbacks throw an exception, the object - # will not be removed from the collection. - # - # == Association extensions - # - # The proxy objects that control the access to associations can be extended through anonymous - # modules. This is especially beneficial for adding new finders, creators, and other - # factory-type methods that are only used as part of this association. - # - # class Account < ActiveRecord::Base - # has_many :people do - # def find_or_create_by_name(name) - # first_name, last_name = name.split(" ", 2) - # find_or_create_by(first_name: first_name, last_name: last_name) - # end - # end - # end - # - # person = Account.first.people.find_or_create_by_name("David Heinemeier Hansson") - # person.first_name # => "David" - # person.last_name # => "Heinemeier Hansson" - # - # If you need to share the same extensions between many associations, you can use a named - # extension module. - # - # module FindOrCreateByNameExtension - # def find_or_create_by_name(name) - # first_name, last_name = name.split(" ", 2) - # find_or_create_by(first_name: first_name, last_name: last_name) - # end - # end - # - # class Account < ActiveRecord::Base - # has_many :people, -> { extending FindOrCreateByNameExtension } - # end - # - # class Company < ActiveRecord::Base - # has_many :people, -> { extending FindOrCreateByNameExtension } - # end - # - # Some extensions can only be made to work with knowledge of the association's internals. - # Extensions can access relevant state using the following methods (where +items+ is the - # name of the association): - # - # * <tt>record.association(:items).owner</tt> - Returns the object the association is part of. - # * <tt>record.association(:items).reflection</tt> - Returns the reflection object that describes the association. - # * <tt>record.association(:items).target</tt> - Returns the associated object for #belongs_to and #has_one, or - # the collection of associated objects for #has_many and #has_and_belongs_to_many. - # - # However, inside the actual extension code, you will not have access to the <tt>record</tt> as - # above. In this case, you can access <tt>proxy_association</tt>. For example, - # <tt>record.association(:items)</tt> and <tt>record.items.proxy_association</tt> will return - # the same object, allowing you to make calls like <tt>proxy_association.owner</tt> inside - # association extensions. - # - # == Association Join Models - # - # Has Many associations can be configured with the <tt>:through</tt> option to use an - # explicit join model to retrieve the data. This operates similarly to a - # #has_and_belongs_to_many association. The advantage is that you're able to add validations, - # callbacks, and extra attributes on the join model. Consider the following schema: - # - # class Author < ActiveRecord::Base - # has_many :authorships - # has_many :books, through: :authorships - # end - # - # class Authorship < ActiveRecord::Base - # belongs_to :author - # belongs_to :book - # end - # - # @author = Author.first - # @author.authorships.collect { |a| a.book } # selects all books that the author's authorships belong to - # @author.books # selects all books by using the Authorship join model - # - # You can also go through a #has_many association on the join model: - # - # class Firm < ActiveRecord::Base - # has_many :clients - # has_many :invoices, through: :clients - # end - # - # class Client < ActiveRecord::Base - # belongs_to :firm - # has_many :invoices - # end - # - # class Invoice < ActiveRecord::Base - # belongs_to :client - # end - # - # @firm = Firm.first - # @firm.clients.flat_map { |c| c.invoices } # select all invoices for all clients of the firm - # @firm.invoices # selects all invoices by going through the Client join model - # - # Similarly you can go through a #has_one association on the join model: - # - # class Group < ActiveRecord::Base - # has_many :users - # has_many :avatars, through: :users - # end - # - # class User < ActiveRecord::Base - # belongs_to :group - # has_one :avatar - # end - # - # class Avatar < ActiveRecord::Base - # belongs_to :user - # end - # - # @group = Group.first - # @group.users.collect { |u| u.avatar }.compact # select all avatars for all users in the group - # @group.avatars # selects all avatars by going through the User join model. - # - # An important caveat with going through #has_one or #has_many associations on the - # join model is that these associations are *read-only*. For example, the following - # would not work following the previous example: - # - # @group.avatars << Avatar.new # this would work if User belonged_to Avatar rather than the other way around - # @group.avatars.delete(@group.avatars.last) # so would this - # - # == Setting Inverses - # - # If you are using a #belongs_to on the join model, it is a good idea to set the - # <tt>:inverse_of</tt> option on the #belongs_to, which will mean that the following example - # works correctly (where <tt>tags</tt> is a #has_many <tt>:through</tt> association): - # - # @post = Post.first - # @tag = @post.tags.build name: "ruby" - # @tag.save - # - # The last line ought to save the through record (a <tt>Tagging</tt>). This will only work if the - # <tt>:inverse_of</tt> is set: - # - # class Tagging < ActiveRecord::Base - # belongs_to :post - # belongs_to :tag, inverse_of: :taggings - # end - # - # If you do not set the <tt>:inverse_of</tt> record, the association will - # do its best to match itself up with the correct inverse. Automatic - # inverse detection only works on #has_many, #has_one, and - # #belongs_to associations. - # - # Extra options on the associations, as defined in the - # <tt>AssociationReflection::INVALID_AUTOMATIC_INVERSE_OPTIONS</tt> constant, will - # also prevent the association's inverse from being found automatically. - # - # The automatic guessing of the inverse association uses a heuristic based - # on the name of the class, so it may not work for all associations, - # especially the ones with non-standard names. - # - # You can turn off the automatic detection of inverse associations by setting - # the <tt>:inverse_of</tt> option to <tt>false</tt> like so: - # - # class Tagging < ActiveRecord::Base - # belongs_to :tag, inverse_of: false - # end - # - # == Nested \Associations - # - # You can actually specify *any* association with the <tt>:through</tt> option, including an - # association which has a <tt>:through</tt> option itself. For example: - # - # class Author < ActiveRecord::Base - # has_many :posts - # has_many :comments, through: :posts - # has_many :commenters, through: :comments - # end - # - # class Post < ActiveRecord::Base - # has_many :comments - # end - # - # class Comment < ActiveRecord::Base - # belongs_to :commenter - # end - # - # @author = Author.first - # @author.commenters # => People who commented on posts written by the author - # - # An equivalent way of setting up this association this would be: - # - # class Author < ActiveRecord::Base - # has_many :posts - # has_many :commenters, through: :posts - # end - # - # class Post < ActiveRecord::Base - # has_many :comments - # has_many :commenters, through: :comments - # end - # - # class Comment < ActiveRecord::Base - # belongs_to :commenter - # end - # - # When using a nested association, you will not be able to modify the association because there - # is not enough information to know what modification to make. For example, if you tried to - # add a <tt>Commenter</tt> in the example above, there would be no way to tell how to set up the - # intermediate <tt>Post</tt> and <tt>Comment</tt> objects. - # - # == Polymorphic \Associations - # - # Polymorphic associations on models are not restricted on what types of models they - # can be associated with. Rather, they specify an interface that a #has_many association - # must adhere to. - # - # class Asset < ActiveRecord::Base - # belongs_to :attachable, polymorphic: true - # end - # - # class Post < ActiveRecord::Base - # has_many :assets, as: :attachable # The :as option specifies the polymorphic interface to use. - # end - # - # @asset.attachable = @post - # - # This works by using a type column in addition to a foreign key to specify the associated - # record. In the Asset example, you'd need an +attachable_id+ integer column and an - # +attachable_type+ string column. - # - # Using polymorphic associations in combination with single table inheritance (STI) is - # a little tricky. In order for the associations to work as expected, ensure that you - # store the base model for the STI models in the type column of the polymorphic - # association. To continue with the asset example above, suppose there are guest posts - # and member posts that use the posts table for STI. In this case, there must be a +type+ - # column in the posts table. - # - # Note: The <tt>attachable_type=</tt> method is being called when assigning an +attachable+. - # The +class_name+ of the +attachable+ is passed as a String. - # - # class Asset < ActiveRecord::Base - # belongs_to :attachable, polymorphic: true - # - # def attachable_type=(class_name) - # super(class_name.constantize.base_class.to_s) - # end - # end - # - # class Post < ActiveRecord::Base - # # because we store "Post" in attachable_type now dependent: :destroy will work - # has_many :assets, as: :attachable, dependent: :destroy - # end - # - # class GuestPost < Post - # end - # - # class MemberPost < Post - # end - # - # == Caching - # - # All of the methods are built on a simple caching principle that will keep the result - # of the last query around unless specifically instructed not to. The cache is even - # shared across methods to make it even cheaper to use the macro-added methods without - # worrying too much about performance at the first go. - # - # 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 # uses the milestone cache - # - # == Eager loading of associations - # - # Eager loading is a way to find objects of a certain class and a number of named associations. - # It is one of the easiest ways to prevent the dreaded N+1 problem in which fetching 100 - # posts that each need to display their author triggers 101 database queries. Through the - # use of eager loading, the number of queries will be reduced from 101 to 2. - # - # class Post < ActiveRecord::Base - # belongs_to :author - # has_many :comments - # end - # - # Consider the following loop using the class above: - # - # Post.all.each do |post| - # puts "Post: " + post.title - # puts "Written by: " + post.author.name - # puts "Last comment on: " + post.comments.first.created_on - # end - # - # To iterate over these one hundred posts, we'll generate 201 database queries. Let's - # first just optimize it for retrieving the author: - # - # Post.includes(:author).each do |post| - # - # This references the name of the #belongs_to association that also used the <tt>:author</tt> - # symbol. After loading the posts, +find+ will collect the +author_id+ from each one and load - # all of the referenced authors with one query. Doing so will cut down the number of queries - # from 201 to 102. - # - # We can improve upon the situation further by referencing both associations in the finder with: - # - # Post.includes(:author, :comments).each do |post| - # - # This will load all comments with a single query. This reduces the total number of queries - # to 3. In general, the number of queries will be 1 plus the number of associations - # named (except if some of the associations are polymorphic #belongs_to - see below). - # - # To include a deep hierarchy of associations, use a hash: - # - # Post.includes(:author, { comments: { author: :gravatar } }).each do |post| - # - # The above code will load all the comments and all of their associated - # authors and gravatars. You can mix and match any combination of symbols, - # arrays, and hashes to retrieve the associations you want to load. - # - # All of this power shouldn't fool you into thinking that you can pull out huge amounts - # of data with no performance penalty just because you've reduced the number of queries. - # The database still needs to send all the data to Active Record and it still needs to - # be processed. So it's no catch-all for performance problems, but it's a great way to - # cut down on the number of queries in a situation as the one described above. - # - # Since only one table is loaded at a time, conditions or orders cannot reference tables - # other than the main one. If this is the case, Active Record falls back to the previously - # used <tt>LEFT OUTER JOIN</tt> based strategy. For example: - # - # Post.includes([:author, :comments]).where(['comments.approved = ?', true]) - # - # This will result in a single SQL query with joins along the lines of: - # <tt>LEFT OUTER JOIN comments ON comments.post_id = posts.id</tt> and - # <tt>LEFT OUTER JOIN authors ON authors.id = posts.author_id</tt>. Note that using conditions - # like this can have unintended consequences. - # In the above example, posts with no approved comments are not returned at all because - # the conditions apply to the SQL statement as a whole and not just to the association. - # - # You must disambiguate column references for this fallback to happen, for example - # <tt>order: "author.name DESC"</tt> will work but <tt>order: "name DESC"</tt> will not. - # - # If you want to load all posts (including posts with no approved comments), then write - # your own <tt>LEFT OUTER JOIN</tt> query using <tt>ON</tt>: - # - # Post.joins("LEFT OUTER JOIN comments ON comments.post_id = posts.id AND comments.approved = '1'") - # - # In this case, it is usually more natural to include an association which has conditions defined on it: - # - # class Post < ActiveRecord::Base - # has_many :approved_comments, -> { where(approved: true) }, class_name: 'Comment' - # end - # - # Post.includes(:approved_comments) - # - # This will load posts and eager load the +approved_comments+ association, which contains - # only those comments that have been approved. - # - # If you eager load an association with a specified <tt>:limit</tt> option, it will be ignored, - # returning all the associated objects: - # - # class Picture < ActiveRecord::Base - # has_many :most_recent_comments, -> { order('id DESC').limit(10) }, class_name: 'Comment' - # end - # - # Picture.includes(:most_recent_comments).first.most_recent_comments # => returns all associated comments. - # - # Eager loading is supported with polymorphic associations. - # - # class Address < ActiveRecord::Base - # belongs_to :addressable, polymorphic: true - # end - # - # A call that tries to eager load the addressable model - # - # Address.includes(:addressable) - # - # This will execute one query to load the addresses and load the addressables with one - # query per addressable type. - # For example, if all the addressables are either of class Person or Company, then a total - # of 3 queries will be executed. The list of addressable types to load is determined on - # the back of the addresses loaded. This is not supported if Active Record has to fallback - # to the previous implementation of eager loading and will raise ActiveRecord::EagerLoadPolymorphicError. - # The reason is that the parent model's type is a column value so its corresponding table - # name cannot be put in the +FROM+/+JOIN+ clauses of that query. - # - # == Table Aliasing - # - # Active Record uses table aliasing in the case that a table is referenced multiple times - # in a join. If a table is referenced only once, the standard table name is used. The - # second time, the table is aliased as <tt>#{reflection_name}_#{parent_table_name}</tt>. - # Indexes are appended for any more successive uses of the table name. - # - # Post.joins(:comments) - # # => SELECT ... FROM posts INNER JOIN comments ON ... - # Post.joins(:special_comments) # STI - # # => SELECT ... FROM posts INNER JOIN comments ON ... AND comments.type = 'SpecialComment' - # Post.joins(:comments, :special_comments) # special_comments is the reflection name, posts is the parent table name - # # => SELECT ... FROM posts INNER JOIN comments ON ... INNER JOIN comments special_comments_posts - # - # Acts as tree example: - # - # TreeMixin.joins(:children) - # # => SELECT ... FROM mixins INNER JOIN mixins childrens_mixins ... - # TreeMixin.joins(children: :parent) - # # => SELECT ... FROM mixins INNER JOIN mixins childrens_mixins ... - # INNER JOIN parents_mixins ... - # TreeMixin.joins(children: {parent: :children}) - # # => SELECT ... FROM mixins INNER JOIN mixins childrens_mixins ... - # INNER JOIN parents_mixins ... - # INNER JOIN mixins childrens_mixins_2 - # - # Has and Belongs to Many join tables use the same idea, but add a <tt>_join</tt> suffix: - # - # Post.joins(:categories) - # # => SELECT ... FROM posts INNER JOIN categories_posts ... INNER JOIN categories ... - # Post.joins(categories: :posts) - # # => SELECT ... FROM posts INNER JOIN categories_posts ... INNER JOIN categories ... - # INNER JOIN categories_posts posts_categories_join INNER JOIN posts posts_categories - # Post.joins(categories: {posts: :categories}) - # # => SELECT ... FROM posts INNER JOIN categories_posts ... INNER JOIN categories ... - # INNER JOIN categories_posts posts_categories_join INNER JOIN posts posts_categories - # INNER JOIN categories_posts categories_posts_join INNER JOIN categories categories_posts_2 - # - # If you wish to specify your own custom joins using ActiveRecord::QueryMethods#joins method, those table - # names will take precedence over the eager associations: - # - # Post.joins(:comments).joins("inner join comments ...") - # # => SELECT ... FROM posts INNER JOIN comments_posts ON ... INNER JOIN comments ... - # Post.joins(:comments, :special_comments).joins("inner join comments ...") - # # => SELECT ... FROM posts INNER JOIN comments comments_posts ON ... - # INNER JOIN comments special_comments_posts ... - # INNER JOIN comments ... - # - # Table aliases are automatically truncated according to the maximum length of table identifiers - # according to the specific database. - # - # == Modules - # - # By default, associations will look for objects within the current module scope. Consider: - # - # module MyApplication - # module Business - # class Firm < ActiveRecord::Base - # has_many :clients - # end - # - # class Client < ActiveRecord::Base; end - # end - # end - # - # When <tt>Firm#clients</tt> is called, it will in turn call - # <tt>MyApplication::Business::Client.find_all_by_firm_id(firm.id)</tt>. - # If you want to associate with a class in another module scope, this can be done by - # specifying the complete class name. - # - # module MyApplication - # module Business - # class Firm < ActiveRecord::Base; end - # end - # - # module Billing - # class Account < ActiveRecord::Base - # belongs_to :firm, class_name: "MyApplication::Business::Firm" - # end - # end - # end - # - # == Bi-directional associations - # - # When you specify an association, there is usually an association on the associated model - # that specifies the same relationship in reverse. For example, with the following models: - # - # class Dungeon < ActiveRecord::Base - # has_many :traps - # has_one :evil_wizard - # end - # - # class Trap < ActiveRecord::Base - # belongs_to :dungeon - # end - # - # class EvilWizard < ActiveRecord::Base - # belongs_to :dungeon - # end - # - # The +traps+ association on +Dungeon+ and the +dungeon+ association on +Trap+ are - # the inverse of each other, and the inverse of the +dungeon+ association on +EvilWizard+ - # is the +evil_wizard+ association on +Dungeon+ (and vice-versa). By default, - # Active Record can guess the inverse of the association based on the name - # of the class. The result is the following: - # - # d = Dungeon.first - # t = d.traps.first - # d.object_id == t.dungeon.object_id # => true - # - # The +Dungeon+ instances +d+ and <tt>t.dungeon</tt> in the above example refer to - # the same in-memory instance since the association matches the name of the class. - # The result would be the same if we added +:inverse_of+ to our model definitions: - # - # class Dungeon < ActiveRecord::Base - # has_many :traps, inverse_of: :dungeon - # has_one :evil_wizard, inverse_of: :dungeon - # end - # - # class Trap < ActiveRecord::Base - # belongs_to :dungeon, inverse_of: :traps - # end - # - # class EvilWizard < ActiveRecord::Base - # belongs_to :dungeon, inverse_of: :evil_wizard - # end - # - # There are limitations to <tt>:inverse_of</tt> support: - # - # * does not work with <tt>:through</tt> associations. - # * does not work with <tt>:polymorphic</tt> associations. - # * inverse associations for #belongs_to associations #has_many are ignored. - # - # For more information, see the documentation for the +:inverse_of+ option. - # - # == Deleting from associations - # - # === Dependent associations - # - # #has_many, #has_one, and #belongs_to associations support the <tt>:dependent</tt> option. - # This allows you to specify that associated records should be deleted when the owner is - # deleted. - # - # For example: - # - # class Author - # has_many :posts, dependent: :destroy - # end - # Author.find(1).destroy # => Will destroy all of the author's posts, too - # - # The <tt>:dependent</tt> option can have different values which specify how the deletion - # is done. For more information, see the documentation for this option on the different - # specific association types. When no option is given, the behavior is to do nothing - # with the associated records when destroying a record. - # - # Note that <tt>:dependent</tt> is implemented using Rails' callback - # system, which works by processing callbacks in order. Therefore, other - # callbacks declared either before or after the <tt>:dependent</tt> option - # can affect what it does. - # - # Note that <tt>:dependent</tt> option is ignored for #has_one <tt>:through</tt> associations. - # - # === Delete or destroy? - # - # #has_many and #has_and_belongs_to_many associations have the methods <tt>destroy</tt>, - # <tt>delete</tt>, <tt>destroy_all</tt> and <tt>delete_all</tt>. - # - # For #has_and_belongs_to_many, <tt>delete</tt> and <tt>destroy</tt> are the same: they - # cause the records in the join table to be removed. - # - # For #has_many, <tt>destroy</tt> and <tt>destroy_all</tt> will always call the <tt>destroy</tt> method of the - # record(s) being removed so that callbacks are run. However <tt>delete</tt> and <tt>delete_all</tt> will either - # do the deletion according to the strategy specified by the <tt>:dependent</tt> option, or - # if no <tt>:dependent</tt> option is given, then it will follow the default strategy. - # The default strategy is to do nothing (leave the foreign keys with the parent ids set), except for - # #has_many <tt>:through</tt>, where the default strategy is <tt>delete_all</tt> (delete - # the join records, without running their callbacks). - # - # There is also a <tt>clear</tt> method which is the same as <tt>delete_all</tt>, except that - # it returns the association rather than the records which have been deleted. - # - # === What gets deleted? - # - # There is a potential pitfall here: #has_and_belongs_to_many and #has_many <tt>:through</tt> - # associations have records in join tables, as well as the associated records. So when we - # call one of these deletion methods, what exactly should be deleted? - # - # The answer is that it is assumed that deletion on an association is about removing the - # <i>link</i> between the owner and the associated object(s), rather than necessarily the - # associated objects themselves. So with #has_and_belongs_to_many and #has_many - # <tt>:through</tt>, the join records will be deleted, but the associated records won't. - # - # This makes sense if you think about it: if you were to call <tt>post.tags.delete(Tag.find_by(name: 'food'))</tt> - # you would want the 'food' tag to be unlinked from the post, rather than for the tag itself - # to be removed from the database. - # - # However, there are examples where this strategy doesn't make sense. For example, suppose - # a person has many projects, and each project has many tasks. If we deleted one of a person's - # tasks, we would probably not want the project to be deleted. In this scenario, the delete method - # won't actually work: it can only be used if the association on the join model is a - # #belongs_to. In other situations you are expected to perform operations directly on - # either the associated records or the <tt>:through</tt> association. - # - # With a regular #has_many there is no distinction between the "associated records" - # and the "link", so there is only one choice for what gets deleted. - # - # With #has_and_belongs_to_many and #has_many <tt>:through</tt>, if you want to delete the - # associated records themselves, you can always do something along the lines of - # <tt>person.tasks.each(&:destroy)</tt>. - # - # == Type safety with ActiveRecord::AssociationTypeMismatch - # - # If you attempt to assign an object to an association that doesn't match the inferred - # or specified <tt>:class_name</tt>, you'll get an ActiveRecord::AssociationTypeMismatch. - # - # == Options - # - # All of the association macros can be specialized through options. This makes cases - # more complex than the simple and guessable ones possible. + # \Associations are a set of macro-like class methods for tying objects together through + # foreign keys. They express relationships like "Project has one Project Manager" + # or "Project belongs to a Portfolio". Each macro adds a number of methods to the + # class which are specialized according to the collection or association symbol and the + # options hash. It works much the same way as Ruby's own <tt>attr*</tt> + # methods. + # + # class Project < ActiveRecord::Base + # belongs_to :portfolio + # has_one :project_manager + # has_many :milestones + # has_and_belongs_to_many :categories + # end + # + # The project class now has the following methods (and more) to ease the traversal and + # manipulation of its relationships: + # * <tt>Project#portfolio, Project#portfolio=(portfolio), Project#portfolio.nil?</tt> + # * <tt>Project#project_manager, Project#project_manager=(project_manager), Project#project_manager.nil?,</tt> + # * <tt>Project#milestones.empty?, Project#milestones.size, Project#milestones, Project#milestones<<(milestone),</tt> + # <tt>Project#milestones.delete(milestone), Project#milestones.destroy(milestone), Project#milestones.find(milestone_id),</tt> + # <tt>Project#milestones.build, Project#milestones.create</tt> + # * <tt>Project#categories.empty?, Project#categories.size, Project#categories, Project#categories<<(category1),</tt> + # <tt>Project#categories.delete(category1), Project#categories.destroy(category1)</tt> + # + # === A word of warning + # + # Don't create associations that have the same name as {instance methods}[rdoc-ref:ActiveRecord::Core] of + # <tt>ActiveRecord::Base</tt>. Since the association adds a method with that name to + # its model, using an association with the same name as one provided by <tt>ActiveRecord::Base</tt> will override the method inherited through <tt>ActiveRecord::Base</tt> and will break things. + # For instance, +attributes+ and +connection+ would be bad choices for association names, because those names already exist in the list of <tt>ActiveRecord::Base</tt> instance methods. + # + # == Auto-generated methods + # See also Instance Public methods below for more details. + # + # === Singular associations (one-to-one) + # | | belongs_to | + # generated methods | belongs_to | :polymorphic | has_one + # ----------------------------------+------------+--------------+--------- + # other(force_reload=false) | X | X | X + # other=(other) | X | X | X + # build_other(attributes={}) | X | | X + # create_other(attributes={}) | X | | X + # create_other!(attributes={}) | X | | X + # + # === Collection associations (one-to-many / many-to-many) + # | | | has_many + # generated methods | habtm | has_many | :through + # ----------------------------------+-------+----------+---------- + # others(force_reload=false) | X | X | X + # others=(other,other,...) | X | X | X + # other_ids | X | X | X + # other_ids=(id,id,...) | X | X | X + # others<< | X | X | X + # others.push | X | X | X + # others.concat | X | X | X + # others.build(attributes={}) | X | X | X + # others.create(attributes={}) | X | X | X + # others.create!(attributes={}) | X | X | X + # others.size | X | X | X + # others.length | X | X | X + # others.count | X | X | X + # others.sum(*args) | X | X | X + # others.empty? | X | X | X + # others.clear | X | X | X + # others.delete(other,other,...) | X | X | X + # others.delete_all | X | X | X + # others.destroy(other,other,...) | X | X | X + # others.destroy_all | X | X | X + # others.find(*args) | X | X | X + # others.exists? | X | X | X + # others.distinct | X | X | X + # others.reset | X | X | X + # + # === Overriding generated methods + # + # Association methods are generated in a module included into the model + # class, making overrides easy. The original generated method can thus be + # called with +super+: + # + # class Car < ActiveRecord::Base + # belongs_to :owner + # belongs_to :old_owner + # + # def owner=(new_owner) + # self.old_owner = self.owner + # super + # end + # end + # + # The association methods module is included immediately after the + # generated attributes methods module, meaning an association will + # override the methods for an attribute with the same name. + # + # == Cardinality and associations + # + # Active Record associations can be used to describe one-to-one, one-to-many and many-to-many + # relationships between models. Each model uses an association to describe its role in + # the relation. The #belongs_to association is always used in the model that has + # the foreign key. + # + # === One-to-one + # + # Use #has_one in the base, and #belongs_to in the associated model. + # + # class Employee < ActiveRecord::Base + # has_one :office + # end + # class Office < ActiveRecord::Base + # belongs_to :employee # foreign key - employee_id + # end + # + # === One-to-many + # + # Use #has_many in the base, and #belongs_to in the associated model. + # + # class Manager < ActiveRecord::Base + # has_many :employees + # end + # class Employee < ActiveRecord::Base + # belongs_to :manager # foreign key - manager_id + # end + # + # === Many-to-many + # + # There are two ways to build a many-to-many relationship. + # + # The first way uses a #has_many association with the <tt>:through</tt> option and a join model, so + # there are two stages of associations. + # + # class Assignment < ActiveRecord::Base + # belongs_to :programmer # foreign key - programmer_id + # belongs_to :project # foreign key - project_id + # end + # class Programmer < ActiveRecord::Base + # has_many :assignments + # has_many :projects, through: :assignments + # end + # class Project < ActiveRecord::Base + # has_many :assignments + # has_many :programmers, through: :assignments + # end + # + # For the second way, use #has_and_belongs_to_many in both models. This requires a join table + # that has no corresponding model or primary key. + # + # class Programmer < ActiveRecord::Base + # has_and_belongs_to_many :projects # foreign keys in the join table + # end + # class Project < ActiveRecord::Base + # has_and_belongs_to_many :programmers # foreign keys in the join table + # end + # + # Choosing which way to build a many-to-many relationship is not always simple. + # If you need to work with the relationship model as its own entity, + # use #has_many <tt>:through</tt>. Use #has_and_belongs_to_many when working with legacy schemas or when + # you never work directly with the relationship itself. + # + # == Is it a #belongs_to or #has_one association? + # + # Both express a 1-1 relationship. The difference is mostly where to place the foreign + # key, which goes on the table for the class declaring the #belongs_to relationship. + # + # class User < ActiveRecord::Base + # # I reference an account. + # belongs_to :account + # end + # + # class Account < ActiveRecord::Base + # # One user references me. + # has_one :user + # end + # + # The tables for these classes could look something like: + # + # CREATE TABLE users ( + # id int NOT NULL auto_increment, + # account_id int default NULL, + # name varchar default NULL, + # PRIMARY KEY (id) + # ) + # + # CREATE TABLE accounts ( + # id int NOT NULL auto_increment, + # name varchar default NULL, + # PRIMARY KEY (id) + # ) + # + # == Unsaved objects and associations + # + # You can manipulate objects and associations before they are saved to the database, but + # there is some special behavior you should be aware of, mostly involving the saving of + # associated objects. + # + # You can set the <tt>:autosave</tt> option on a #has_one, #belongs_to, + # #has_many, or #has_and_belongs_to_many association. Setting it + # to +true+ will _always_ save the members, whereas setting it to +false+ will + # _never_ save the members. More details about <tt>:autosave</tt> option is available at + # AutosaveAssociation. + # + # === One-to-one associations + # + # * Assigning an object to a #has_one association automatically saves that object and + # the object being replaced (if there is one), in order to update their foreign + # keys - except if the parent object is unsaved (<tt>new_record? == true</tt>). + # * If either of these saves fail (due to one of the objects being invalid), an + # ActiveRecord::RecordNotSaved exception is raised and the assignment is + # cancelled. + # * If you wish to assign an object to a #has_one association without saving it, + # use the <tt>#build_association</tt> method (documented below). The object being + # replaced will still be saved to update its foreign key. + # * Assigning an object to a #belongs_to association does not save the object, since + # the foreign key field belongs on the parent. It does not save the parent either. + # + # === Collections + # + # * Adding an object to a collection (#has_many or #has_and_belongs_to_many) automatically + # saves that object, except if the parent object (the owner of the collection) is not yet + # stored in the database. + # * If saving any of the objects being added to a collection (via <tt>push</tt> or similar) + # fails, then <tt>push</tt> returns +false+. + # * If saving fails while replacing the collection (via <tt>association=</tt>), an + # ActiveRecord::RecordNotSaved exception is raised and the assignment is + # cancelled. + # * You can add an object to a collection without automatically saving it by using the + # <tt>collection.build</tt> method (documented below). + # * All unsaved (<tt>new_record? == true</tt>) members of the collection are automatically + # saved when the parent is saved. + # + # == Customizing the query + # + # \Associations are built from <tt>Relation</tt> objects, and you can use the Relation syntax + # to customize them. For example, to add a condition: + # + # class Blog < ActiveRecord::Base + # has_many :published_posts, -> { where(published: true) }, class_name: 'Post' + # end + # + # Inside the <tt>-> { ... }</tt> block you can use all of the usual Relation methods. + # + # === Accessing the owner object + # + # Sometimes it is useful to have access to the owner object when building the query. The owner + # is passed as a parameter to the block. For example, the following association would find all + # events that occur on the user's birthday: + # + # class User < ActiveRecord::Base + # has_many :birthday_events, ->(user) { where(starts_on: user.birthday) }, class_name: 'Event' + # end + # + # Note: Joining, eager loading and preloading of these associations is not fully possible. + # These operations happen before instance creation and the scope will be called with a +nil+ argument. + # This can lead to unexpected behavior and is deprecated. + # + # == Association callbacks + # + # Similar to the normal callbacks that hook into the life cycle of an Active Record object, + # you can also define callbacks that get triggered when you add an object to or remove an + # object from an association collection. + # + # class Project + # has_and_belongs_to_many :developers, after_add: :evaluate_velocity + # + # def evaluate_velocity(developer) + # ... + # end + # end + # + # It's possible to stack callbacks by passing them as an array. Example: + # + # class Project + # has_and_belongs_to_many :developers, + # after_add: [:evaluate_velocity, Proc.new { |p, d| p.shipping_date = Time.now}] + # end + # + # Possible callbacks are: +before_add+, +after_add+, +before_remove+ and +after_remove+. + # + # If any of the +before_add+ callbacks throw an exception, the object will not be + # added to the collection. + # + # Similarly, if any of the +before_remove+ callbacks throw an exception, the object + # will not be removed from the collection. + # + # == Association extensions + # + # The proxy objects that control the access to associations can be extended through anonymous + # modules. This is especially beneficial for adding new finders, creators, and other + # factory-type methods that are only used as part of this association. + # + # class Account < ActiveRecord::Base + # has_many :people do + # def find_or_create_by_name(name) + # first_name, last_name = name.split(" ", 2) + # find_or_create_by(first_name: first_name, last_name: last_name) + # end + # end + # end + # + # person = Account.first.people.find_or_create_by_name("David Heinemeier Hansson") + # person.first_name # => "David" + # person.last_name # => "Heinemeier Hansson" + # + # If you need to share the same extensions between many associations, you can use a named + # extension module. + # + # module FindOrCreateByNameExtension + # def find_or_create_by_name(name) + # first_name, last_name = name.split(" ", 2) + # find_or_create_by(first_name: first_name, last_name: last_name) + # end + # end + # + # class Account < ActiveRecord::Base + # has_many :people, -> { extending FindOrCreateByNameExtension } + # end + # + # class Company < ActiveRecord::Base + # has_many :people, -> { extending FindOrCreateByNameExtension } + # end + # + # Some extensions can only be made to work with knowledge of the association's internals. + # Extensions can access relevant state using the following methods (where +items+ is the + # name of the association): + # + # * <tt>record.association(:items).owner</tt> - Returns the object the association is part of. + # * <tt>record.association(:items).reflection</tt> - Returns the reflection object that describes the association. + # * <tt>record.association(:items).target</tt> - Returns the associated object for #belongs_to and #has_one, or + # the collection of associated objects for #has_many and #has_and_belongs_to_many. + # + # However, inside the actual extension code, you will not have access to the <tt>record</tt> as + # above. In this case, you can access <tt>proxy_association</tt>. For example, + # <tt>record.association(:items)</tt> and <tt>record.items.proxy_association</tt> will return + # the same object, allowing you to make calls like <tt>proxy_association.owner</tt> inside + # association extensions. + # + # == Association Join Models + # + # Has Many associations can be configured with the <tt>:through</tt> option to use an + # explicit join model to retrieve the data. This operates similarly to a + # #has_and_belongs_to_many association. The advantage is that you're able to add validations, + # callbacks, and extra attributes on the join model. Consider the following schema: + # + # class Author < ActiveRecord::Base + # has_many :authorships + # has_many :books, through: :authorships + # end + # + # class Authorship < ActiveRecord::Base + # belongs_to :author + # belongs_to :book + # end + # + # @author = Author.first + # @author.authorships.collect { |a| a.book } # selects all books that the author's authorships belong to + # @author.books # selects all books by using the Authorship join model + # + # You can also go through a #has_many association on the join model: + # + # class Firm < ActiveRecord::Base + # has_many :clients + # has_many :invoices, through: :clients + # end + # + # class Client < ActiveRecord::Base + # belongs_to :firm + # has_many :invoices + # end + # + # class Invoice < ActiveRecord::Base + # belongs_to :client + # end + # + # @firm = Firm.first + # @firm.clients.flat_map { |c| c.invoices } # select all invoices for all clients of the firm + # @firm.invoices # selects all invoices by going through the Client join model + # + # Similarly you can go through a #has_one association on the join model: + # + # class Group < ActiveRecord::Base + # has_many :users + # has_many :avatars, through: :users + # end + # + # class User < ActiveRecord::Base + # belongs_to :group + # has_one :avatar + # end + # + # class Avatar < ActiveRecord::Base + # belongs_to :user + # end + # + # @group = Group.first + # @group.users.collect { |u| u.avatar }.compact # select all avatars for all users in the group + # @group.avatars # selects all avatars by going through the User join model. + # + # An important caveat with going through #has_one or #has_many associations on the + # join model is that these associations are *read-only*. For example, the following + # would not work following the previous example: + # + # @group.avatars << Avatar.new # this would work if User belonged_to Avatar rather than the other way around + # @group.avatars.delete(@group.avatars.last) # so would this + # + # == Setting Inverses + # + # If you are using a #belongs_to on the join model, it is a good idea to set the + # <tt>:inverse_of</tt> option on the #belongs_to, which will mean that the following example + # works correctly (where <tt>tags</tt> is a #has_many <tt>:through</tt> association): + # + # @post = Post.first + # @tag = @post.tags.build name: "ruby" + # @tag.save + # + # The last line ought to save the through record (a <tt>Tagging</tt>). This will only work if the + # <tt>:inverse_of</tt> is set: + # + # class Tagging < ActiveRecord::Base + # belongs_to :post + # belongs_to :tag, inverse_of: :taggings + # end + # + # If you do not set the <tt>:inverse_of</tt> record, the association will + # do its best to match itself up with the correct inverse. Automatic + # inverse detection only works on #has_many, #has_one, and + # #belongs_to associations. + # + # Extra options on the associations, as defined in the + # <tt>AssociationReflection::INVALID_AUTOMATIC_INVERSE_OPTIONS</tt> constant, will + # also prevent the association's inverse from being found automatically. + # + # The automatic guessing of the inverse association uses a heuristic based + # on the name of the class, so it may not work for all associations, + # especially the ones with non-standard names. + # + # You can turn off the automatic detection of inverse associations by setting + # the <tt>:inverse_of</tt> option to <tt>false</tt> like so: + # + # class Tagging < ActiveRecord::Base + # belongs_to :tag, inverse_of: false + # end + # + # == Nested \Associations + # + # You can actually specify *any* association with the <tt>:through</tt> option, including an + # association which has a <tt>:through</tt> option itself. For example: + # + # class Author < ActiveRecord::Base + # has_many :posts + # has_many :comments, through: :posts + # has_many :commenters, through: :comments + # end + # + # class Post < ActiveRecord::Base + # has_many :comments + # end + # + # class Comment < ActiveRecord::Base + # belongs_to :commenter + # end + # + # @author = Author.first + # @author.commenters # => People who commented on posts written by the author + # + # An equivalent way of setting up this association this would be: + # + # class Author < ActiveRecord::Base + # has_many :posts + # has_many :commenters, through: :posts + # end + # + # class Post < ActiveRecord::Base + # has_many :comments + # has_many :commenters, through: :comments + # end + # + # class Comment < ActiveRecord::Base + # belongs_to :commenter + # end + # + # When using a nested association, you will not be able to modify the association because there + # is not enough information to know what modification to make. For example, if you tried to + # add a <tt>Commenter</tt> in the example above, there would be no way to tell how to set up the + # intermediate <tt>Post</tt> and <tt>Comment</tt> objects. + # + # == Polymorphic \Associations + # + # Polymorphic associations on models are not restricted on what types of models they + # can be associated with. Rather, they specify an interface that a #has_many association + # must adhere to. + # + # class Asset < ActiveRecord::Base + # belongs_to :attachable, polymorphic: true + # end + # + # class Post < ActiveRecord::Base + # has_many :assets, as: :attachable # The :as option specifies the polymorphic interface to use. + # end + # + # @asset.attachable = @post + # + # This works by using a type column in addition to a foreign key to specify the associated + # record. In the Asset example, you'd need an +attachable_id+ integer column and an + # +attachable_type+ string column. + # + # Using polymorphic associations in combination with single table inheritance (STI) is + # a little tricky. In order for the associations to work as expected, ensure that you + # store the base model for the STI models in the type column of the polymorphic + # association. To continue with the asset example above, suppose there are guest posts + # and member posts that use the posts table for STI. In this case, there must be a +type+ + # column in the posts table. + # + # Note: The <tt>attachable_type=</tt> method is being called when assigning an +attachable+. + # The +class_name+ of the +attachable+ is passed as a String. + # + # class Asset < ActiveRecord::Base + # belongs_to :attachable, polymorphic: true + # + # def attachable_type=(class_name) + # super(class_name.constantize.base_class.to_s) + # end + # end + # + # class Post < ActiveRecord::Base + # # because we store "Post" in attachable_type now dependent: :destroy will work + # has_many :assets, as: :attachable, dependent: :destroy + # end + # + # class GuestPost < Post + # end + # + # class MemberPost < Post + # end + # + # == Caching + # + # All of the methods are built on a simple caching principle that will keep the result + # of the last query around unless specifically instructed not to. The cache is even + # shared across methods to make it even cheaper to use the macro-added methods without + # worrying too much about performance at the first go. + # + # 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 # uses the milestone cache + # + # == Eager loading of associations + # + # Eager loading is a way to find objects of a certain class and a number of named associations. + # It is one of the easiest ways to prevent the dreaded N+1 problem in which fetching 100 + # posts that each need to display their author triggers 101 database queries. Through the + # use of eager loading, the number of queries will be reduced from 101 to 2. + # + # class Post < ActiveRecord::Base + # belongs_to :author + # has_many :comments + # end + # + # Consider the following loop using the class above: + # + # Post.all.each do |post| + # puts "Post: " + post.title + # puts "Written by: " + post.author.name + # puts "Last comment on: " + post.comments.first.created_on + # end + # + # To iterate over these one hundred posts, we'll generate 201 database queries. Let's + # first just optimize it for retrieving the author: + # + # Post.includes(:author).each do |post| + # + # This references the name of the #belongs_to association that also used the <tt>:author</tt> + # symbol. After loading the posts, +find+ will collect the +author_id+ from each one and load + # all of the referenced authors with one query. Doing so will cut down the number of queries + # from 201 to 102. + # + # We can improve upon the situation further by referencing both associations in the finder with: + # + # Post.includes(:author, :comments).each do |post| + # + # This will load all comments with a single query. This reduces the total number of queries + # to 3. In general, the number of queries will be 1 plus the number of associations + # named (except if some of the associations are polymorphic #belongs_to - see below). + # + # To include a deep hierarchy of associations, use a hash: + # + # Post.includes(:author, { comments: { author: :gravatar } }).each do |post| + # + # The above code will load all the comments and all of their associated + # authors and gravatars. You can mix and match any combination of symbols, + # arrays, and hashes to retrieve the associations you want to load. + # + # All of this power shouldn't fool you into thinking that you can pull out huge amounts + # of data with no performance penalty just because you've reduced the number of queries. + # The database still needs to send all the data to Active Record and it still needs to + # be processed. So it's no catch-all for performance problems, but it's a great way to + # cut down on the number of queries in a situation as the one described above. + # + # Since only one table is loaded at a time, conditions or orders cannot reference tables + # other than the main one. If this is the case, Active Record falls back to the previously + # used <tt>LEFT OUTER JOIN</tt> based strategy. For example: + # + # Post.includes([:author, :comments]).where(['comments.approved = ?', true]) + # + # This will result in a single SQL query with joins along the lines of: + # <tt>LEFT OUTER JOIN comments ON comments.post_id = posts.id</tt> and + # <tt>LEFT OUTER JOIN authors ON authors.id = posts.author_id</tt>. Note that using conditions + # like this can have unintended consequences. + # In the above example, posts with no approved comments are not returned at all because + # the conditions apply to the SQL statement as a whole and not just to the association. + # + # You must disambiguate column references for this fallback to happen, for example + # <tt>order: "author.name DESC"</tt> will work but <tt>order: "name DESC"</tt> will not. + # + # If you want to load all posts (including posts with no approved comments), then write + # your own <tt>LEFT OUTER JOIN</tt> query using <tt>ON</tt>: + # + # Post.joins("LEFT OUTER JOIN comments ON comments.post_id = posts.id AND comments.approved = '1'") + # + # In this case, it is usually more natural to include an association which has conditions defined on it: + # + # class Post < ActiveRecord::Base + # has_many :approved_comments, -> { where(approved: true) }, class_name: 'Comment' + # end + # + # Post.includes(:approved_comments) + # + # This will load posts and eager load the +approved_comments+ association, which contains + # only those comments that have been approved. + # + # If you eager load an association with a specified <tt>:limit</tt> option, it will be ignored, + # returning all the associated objects: + # + # class Picture < ActiveRecord::Base + # has_many :most_recent_comments, -> { order('id DESC').limit(10) }, class_name: 'Comment' + # end + # + # Picture.includes(:most_recent_comments).first.most_recent_comments # => returns all associated comments. + # + # Eager loading is supported with polymorphic associations. + # + # class Address < ActiveRecord::Base + # belongs_to :addressable, polymorphic: true + # end + # + # A call that tries to eager load the addressable model + # + # Address.includes(:addressable) + # + # This will execute one query to load the addresses and load the addressables with one + # query per addressable type. + # For example, if all the addressables are either of class Person or Company, then a total + # of 3 queries will be executed. The list of addressable types to load is determined on + # the back of the addresses loaded. This is not supported if Active Record has to fallback + # to the previous implementation of eager loading and will raise ActiveRecord::EagerLoadPolymorphicError. + # The reason is that the parent model's type is a column value so its corresponding table + # name cannot be put in the +FROM+/+JOIN+ clauses of that query. + # + # == Table Aliasing + # + # Active Record uses table aliasing in the case that a table is referenced multiple times + # in a join. If a table is referenced only once, the standard table name is used. The + # second time, the table is aliased as <tt>#{reflection_name}_#{parent_table_name}</tt>. + # Indexes are appended for any more successive uses of the table name. + # + # Post.joins(:comments) + # # => SELECT ... FROM posts INNER JOIN comments ON ... + # Post.joins(:special_comments) # STI + # # => SELECT ... FROM posts INNER JOIN comments ON ... AND comments.type = 'SpecialComment' + # Post.joins(:comments, :special_comments) # special_comments is the reflection name, posts is the parent table name + # # => SELECT ... FROM posts INNER JOIN comments ON ... INNER JOIN comments special_comments_posts + # + # Acts as tree example: + # + # TreeMixin.joins(:children) + # # => SELECT ... FROM mixins INNER JOIN mixins childrens_mixins ... + # TreeMixin.joins(children: :parent) + # # => SELECT ... FROM mixins INNER JOIN mixins childrens_mixins ... + # INNER JOIN parents_mixins ... + # TreeMixin.joins(children: {parent: :children}) + # # => SELECT ... FROM mixins INNER JOIN mixins childrens_mixins ... + # INNER JOIN parents_mixins ... + # INNER JOIN mixins childrens_mixins_2 + # + # Has and Belongs to Many join tables use the same idea, but add a <tt>_join</tt> suffix: + # + # Post.joins(:categories) + # # => SELECT ... FROM posts INNER JOIN categories_posts ... INNER JOIN categories ... + # Post.joins(categories: :posts) + # # => SELECT ... FROM posts INNER JOIN categories_posts ... INNER JOIN categories ... + # INNER JOIN categories_posts posts_categories_join INNER JOIN posts posts_categories + # Post.joins(categories: {posts: :categories}) + # # => SELECT ... FROM posts INNER JOIN categories_posts ... INNER JOIN categories ... + # INNER JOIN categories_posts posts_categories_join INNER JOIN posts posts_categories + # INNER JOIN categories_posts categories_posts_join INNER JOIN categories categories_posts_2 + # + # If you wish to specify your own custom joins using ActiveRecord::QueryMethods#joins method, those table + # names will take precedence over the eager associations: + # + # Post.joins(:comments).joins("inner join comments ...") + # # => SELECT ... FROM posts INNER JOIN comments_posts ON ... INNER JOIN comments ... + # Post.joins(:comments, :special_comments).joins("inner join comments ...") + # # => SELECT ... FROM posts INNER JOIN comments comments_posts ON ... + # INNER JOIN comments special_comments_posts ... + # INNER JOIN comments ... + # + # Table aliases are automatically truncated according to the maximum length of table identifiers + # according to the specific database. + # + # == Modules + # + # By default, associations will look for objects within the current module scope. Consider: + # + # module MyApplication + # module Business + # class Firm < ActiveRecord::Base + # has_many :clients + # end + # + # class Client < ActiveRecord::Base; end + # end + # end + # + # When <tt>Firm#clients</tt> is called, it will in turn call + # <tt>MyApplication::Business::Client.find_all_by_firm_id(firm.id)</tt>. + # If you want to associate with a class in another module scope, this can be done by + # specifying the complete class name. + # + # module MyApplication + # module Business + # class Firm < ActiveRecord::Base; end + # end + # + # module Billing + # class Account < ActiveRecord::Base + # belongs_to :firm, class_name: "MyApplication::Business::Firm" + # end + # end + # end + # + # == Bi-directional associations + # + # When you specify an association, there is usually an association on the associated model + # that specifies the same relationship in reverse. For example, with the following models: + # + # class Dungeon < ActiveRecord::Base + # has_many :traps + # has_one :evil_wizard + # end + # + # class Trap < ActiveRecord::Base + # belongs_to :dungeon + # end + # + # class EvilWizard < ActiveRecord::Base + # belongs_to :dungeon + # end + # + # The +traps+ association on +Dungeon+ and the +dungeon+ association on +Trap+ are + # the inverse of each other, and the inverse of the +dungeon+ association on +EvilWizard+ + # is the +evil_wizard+ association on +Dungeon+ (and vice-versa). By default, + # Active Record can guess the inverse of the association based on the name + # of the class. The result is the following: + # + # d = Dungeon.first + # t = d.traps.first + # d.object_id == t.dungeon.object_id # => true + # + # The +Dungeon+ instances +d+ and <tt>t.dungeon</tt> in the above example refer to + # the same in-memory instance since the association matches the name of the class. + # The result would be the same if we added +:inverse_of+ to our model definitions: + # + # class Dungeon < ActiveRecord::Base + # has_many :traps, inverse_of: :dungeon + # has_one :evil_wizard, inverse_of: :dungeon + # end + # + # class Trap < ActiveRecord::Base + # belongs_to :dungeon, inverse_of: :traps + # end + # + # class EvilWizard < ActiveRecord::Base + # belongs_to :dungeon, inverse_of: :evil_wizard + # end + # + # There are limitations to <tt>:inverse_of</tt> support: + # + # * does not work with <tt>:through</tt> associations. + # * does not work with <tt>:polymorphic</tt> associations. + # * inverse associations for #belongs_to associations #has_many are ignored. + # + # For more information, see the documentation for the +:inverse_of+ option. + # + # == Deleting from associations + # + # === Dependent associations + # + # #has_many, #has_one, and #belongs_to associations support the <tt>:dependent</tt> option. + # This allows you to specify that associated records should be deleted when the owner is + # deleted. + # + # For example: + # + # class Author + # has_many :posts, dependent: :destroy + # end + # Author.find(1).destroy # => Will destroy all of the author's posts, too + # + # The <tt>:dependent</tt> option can have different values which specify how the deletion + # is done. For more information, see the documentation for this option on the different + # specific association types. When no option is given, the behavior is to do nothing + # with the associated records when destroying a record. + # + # Note that <tt>:dependent</tt> is implemented using Rails' callback + # system, which works by processing callbacks in order. Therefore, other + # callbacks declared either before or after the <tt>:dependent</tt> option + # can affect what it does. + # + # Note that <tt>:dependent</tt> option is ignored for #has_one <tt>:through</tt> associations. + # + # === Delete or destroy? + # + # #has_many and #has_and_belongs_to_many associations have the methods <tt>destroy</tt>, + # <tt>delete</tt>, <tt>destroy_all</tt> and <tt>delete_all</tt>. + # + # For #has_and_belongs_to_many, <tt>delete</tt> and <tt>destroy</tt> are the same: they + # cause the records in the join table to be removed. + # + # For #has_many, <tt>destroy</tt> and <tt>destroy_all</tt> will always call the <tt>destroy</tt> method of the + # record(s) being removed so that callbacks are run. However <tt>delete</tt> and <tt>delete_all</tt> will either + # do the deletion according to the strategy specified by the <tt>:dependent</tt> option, or + # if no <tt>:dependent</tt> option is given, then it will follow the default strategy. + # The default strategy is to do nothing (leave the foreign keys with the parent ids set), except for + # #has_many <tt>:through</tt>, where the default strategy is <tt>delete_all</tt> (delete + # the join records, without running their callbacks). + # + # There is also a <tt>clear</tt> method which is the same as <tt>delete_all</tt>, except that + # it returns the association rather than the records which have been deleted. + # + # === What gets deleted? + # + # There is a potential pitfall here: #has_and_belongs_to_many and #has_many <tt>:through</tt> + # associations have records in join tables, as well as the associated records. So when we + # call one of these deletion methods, what exactly should be deleted? + # + # The answer is that it is assumed that deletion on an association is about removing the + # <i>link</i> between the owner and the associated object(s), rather than necessarily the + # associated objects themselves. So with #has_and_belongs_to_many and #has_many + # <tt>:through</tt>, the join records will be deleted, but the associated records won't. + # + # This makes sense if you think about it: if you were to call <tt>post.tags.delete(Tag.find_by(name: 'food'))</tt> + # you would want the 'food' tag to be unlinked from the post, rather than for the tag itself + # to be removed from the database. + # + # However, there are examples where this strategy doesn't make sense. For example, suppose + # a person has many projects, and each project has many tasks. If we deleted one of a person's + # tasks, we would probably not want the project to be deleted. In this scenario, the delete method + # won't actually work: it can only be used if the association on the join model is a + # #belongs_to. In other situations you are expected to perform operations directly on + # either the associated records or the <tt>:through</tt> association. + # + # With a regular #has_many there is no distinction between the "associated records" + # and the "link", so there is only one choice for what gets deleted. + # + # With #has_and_belongs_to_many and #has_many <tt>:through</tt>, if you want to delete the + # associated records themselves, you can always do something along the lines of + # <tt>person.tasks.each(&:destroy)</tt>. + # + # == Type safety with ActiveRecord::AssociationTypeMismatch + # + # If you attempt to assign an object to an association that doesn't match the inferred + # or specified <tt>:class_name</tt>, you'll get an ActiveRecord::AssociationTypeMismatch. + # + # == Options + # + # All of the association macros can be specialized through options. This makes cases + # more complex than the simple and guessable ones possible. module ClassMethods # Specifies a one-to-many association. The following methods for retrieval and query of # collections of associated objects will be added: @@ -1296,6 +1326,12 @@ module ActiveRecord # If using with the <tt>:through</tt> option, the association on the join model must be # a #belongs_to, and the records which get deleted are the join records, rather than # the associated records. + # + # If using <tt>dependent: :destroy</tt> on a scoped association, only the scoped objects are destroyed. + # For example, if a Post model defines + # <tt>has_many :comments, -> { where published: true }, dependent: :destroy</tt> and <tt>destroy</tt> is + # called on a post, only published comments are destroyed. This means that any unpublished comments in the + # database would still contain a foreign key pointing to the now deleted post. # [:counter_cache] # This option can be used to configure a custom named <tt>:counter_cache.</tt> You only need this option, # when you customized the name of your <tt>:counter_cache</tt> on the #belongs_to association. @@ -1786,12 +1822,7 @@ module ActiveRecord # has_and_belongs_to_many :nations, class_name: "Country" # has_and_belongs_to_many :categories, join_table: "prods_cats" # has_and_belongs_to_many :categories, -> { readonly } - def has_and_belongs_to_many(name, scope = nil, options = {}, &extension) - if scope.is_a?(Hash) - options = scope - scope = nil - end - + def has_and_belongs_to_many(name, scope = nil, **options, &extension) habtm_reflection = ActiveRecord::Reflection::HasAndBelongsToManyReflection.new(name, scope, options, self) builder = Builder::HasAndBelongsToMany.new name, self, options @@ -1809,12 +1840,12 @@ module ActiveRecord include Module.new { class_eval <<-RUBY, __FILE__, __LINE__ + 1 - def destroy_associations - association(:#{middle_reflection.name}).delete_all(:delete_all) - association(:#{name}).reset - super - end - RUBY + def destroy_associations + association(:#{middle_reflection.name}).delete_all(:delete_all) + association(:#{name}).reset + super + end + RUBY } hm_options = {} @@ -1826,7 +1857,7 @@ module ActiveRecord end has_many name, scope, hm_options, &extension - self._reflections[name.to_s].parent_reflection = habtm_reflection + _reflections[name.to_s].parent_reflection = habtm_reflection end end end diff --git a/activerecord/lib/active_record/associations/association.rb b/activerecord/lib/active_record/associations/association.rb index 8328286805..84d0493a60 100644 --- a/activerecord/lib/active_record/associations/association.rb +++ b/activerecord/lib/active_record/associations/association.rb @@ -112,6 +112,15 @@ module ActiveRecord record end + # Remove the inverse association, if possible + def remove_inverse_instance(record) + if invertible_for?(record) + inverse = record.association(inverse_reflection_for(record).name) + inverse.target = nil + inverse.inversed = false + end + end + # Returns the class of the target. belongs_to polymorphic overrides this to look at the # polymorphic_type field on the owner. def klass @@ -166,13 +175,21 @@ module ActiveRecord def initialize_attributes(record, except_from_scope_attributes = nil) #:nodoc: except_from_scope_attributes ||= {} skip_assign = [reflection.foreign_key, reflection.type].compact - assigned_keys = record.changed + 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) set_inverse_instance(record) end + def create(attributes = {}, &block) + _create_record(attributes, &block) + end + + def create!(attributes = {}, &block) + _create_record(attributes, true, &block) + end + private def find_target? @@ -246,7 +263,7 @@ module ActiveRecord # so that when stale_state is different from the value stored on the last find_target, # the target is stale. # - # This is only relevant to certain associations, which is why it returns nil by default. + # This is only relevant to certain associations, which is why it returns +nil+ by default. def stale_state end diff --git a/activerecord/lib/active_record/associations/association_scope.rb b/activerecord/lib/active_record/associations/association_scope.rb index 12f8c1ccd4..c6d204d3c2 100644 --- a/activerecord/lib/active_record/associations/association_scope.rb +++ b/activerecord/lib/active_record/associations/association_scope.rb @@ -49,6 +49,8 @@ module ActiveRecord binds 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 :value_transformation diff --git a/activerecord/lib/active_record/associations/builder/belongs_to.rb b/activerecord/lib/active_record/associations/builder/belongs_to.rb index 3121e70a04..a1609ab0fb 100644 --- a/activerecord/lib/active_record/associations/builder/belongs_to.rb +++ b/activerecord/lib/active_record/associations/builder/belongs_to.rb @@ -35,17 +35,17 @@ module ActiveRecord::Associations::Builder # :nodoc: @_after_create_counter_called = false elsif (@_after_replace_counter_called ||= false) @_after_replace_counter_called = false - elsif attribute_changed?(foreign_key) && !new_record? + elsif saved_change_to_attribute?(foreign_key) && !new_record? if reflection.polymorphic? - model = attribute(reflection.foreign_type).try(:constantize) - model_was = attribute_was(reflection.foreign_type).try(:constantize) + model = attribute_in_database(reflection.foreign_type).try(:constantize) + model_was = attribute_before_last_save(reflection.foreign_type).try(:constantize) else model = reflection.klass model_was = reflection.klass end - foreign_key_was = attribute_was foreign_key - foreign_key = attribute foreign_key + foreign_key_was = attribute_before_last_save foreign_key + foreign_key = attribute_in_database foreign_key if foreign_key && model.respond_to?(:increment_counter) model.increment_counter(cache_column, foreign_key) @@ -70,14 +70,16 @@ module ActiveRecord::Associations::Builder # :nodoc: klass.attr_readonly cache_column if klass && klass.respond_to?(:attr_readonly) end - def self.touch_record(o, foreign_key, name, touch, touch_method) # :nodoc: - old_foreign_id = o.changed_attributes[foreign_key] + def self.touch_record(o, changes, foreign_key, name, touch, touch_method) # :nodoc: + old_foreign_id = changes[foreign_key] && changes[foreign_key].first if old_foreign_id association = o.association(name) reflection = association.reflection if reflection.polymorphic? - klass = o.public_send("#{reflection.foreign_type}_was").constantize + foreign_type = reflection.foreign_type + klass = changes[foreign_type] && changes[foreign_type].first || o.public_send(foreign_type) + klass = klass.constantize else klass = association.klass end @@ -107,13 +109,13 @@ module ActiveRecord::Associations::Builder # :nodoc: n = reflection.name touch = reflection.options[:touch] - callback = lambda { |record| - BelongsTo.touch_record(record, foreign_key, n, touch, belongs_to_touch_method) - } + callback = lambda { |changes_method| lambda { |record| + BelongsTo.touch_record(record, record.send(changes_method), foreign_key, n, touch, belongs_to_touch_method) + }} - model.after_save callback, if: :changed? - model.after_touch callback - model.after_destroy callback + model.after_save callback.(:saved_changes), if: :saved_changes? + model.after_touch callback.(:changes_to_save) + model.after_destroy callback.(:changes_to_save) end def self.add_destroy_callbacks(model, reflection) 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 047292b2bd..6b71826431 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 @@ -28,7 +28,7 @@ module ActiveRecord::Associations::Builder # :nodoc: class_name = options.fetch(:class_name) { name.to_s.camelize.singularize } - KnownClass.new lhs_class, class_name + KnownClass.new lhs_class, class_name.to_s end end end @@ -78,9 +78,9 @@ module ActiveRecord::Associations::Builder # :nodoc: private - def self.suppress_composite_primary_key(pk) - pk unless pk.is_a?(Array) - end + def self.suppress_composite_primary_key(pk) + pk unless pk.is_a?(Array) + end } join_model.name = "HABTM_#{association_name.to_s.camelize}" diff --git a/activerecord/lib/active_record/associations/builder/singular_association.rb b/activerecord/lib/active_record/associations/builder/singular_association.rb index bb96202a22..7732b63af6 100644 --- a/activerecord/lib/active_record/associations/builder/singular_association.rb +++ b/activerecord/lib/active_record/associations/builder/singular_association.rb @@ -8,7 +8,16 @@ module ActiveRecord::Associations::Builder # :nodoc: def self.define_accessors(model, reflection) super - define_constructors(model.generated_association_methods, reflection.name) if reflection.constructable? + mixin = model.generated_association_methods + name = reflection.name + + define_constructors(mixin, name) if reflection.constructable? + + mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1 + def reload_#{name} + association(:#{name}).force_reload_reader + end + CODE end # Defines the (build|create)_association methods for belongs_to or has_one association diff --git a/activerecord/lib/active_record/associations/collection_association.rb b/activerecord/lib/active_record/associations/collection_association.rb index ab267f6897..0437a79b84 100644 --- a/activerecord/lib/active_record/associations/collection_association.rb +++ b/activerecord/lib/active_record/associations/collection_association.rb @@ -25,16 +25,8 @@ module ActiveRecord # +load_target+ and the +loaded+ flag are your friends. class CollectionAssociation < Association #:nodoc: # Implements the reader method, e.g. foo.items for Foo.has_many :items - def reader(force_reload = false) - if force_reload - ActiveSupport::Deprecation.warn(<<-MSG.squish) - Passing an argument to force an association to reload is now - deprecated and will be removed in Rails 5.1. Please call `reload` - on the result collection proxy instead. - MSG - - klass.uncached { reload } - elsif stale_target? + def reader + if stale_target? reload end @@ -55,9 +47,7 @@ module ActiveRecord # Implements the ids reader method, e.g. foo.item_ids for Foo.has_many :items def ids_reader if loaded? - load_target.map do |record| - record.send(reflection.association_primary_key) - end + target.pluck(reflection.association_primary_key) else @association_ids ||= ( column = "#{reflection.quoted_table_name}.#{reflection.association_primary_key}" @@ -68,13 +58,17 @@ module ActiveRecord # Implements the ids writer method, e.g. foo.item_ids= for Foo.has_many :items def ids_writer(ids) - pk_type = reflection.primary_key_type + pk_type = reflection.association_primary_key_type 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) - end.values_at(*ids) - replace(records) + 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) + else + replace(records) + end end def reset @@ -82,14 +76,6 @@ module ActiveRecord @target = [] end - def select(*fields) - if block_given? - load_target.select.each { |e| yield e } - else - scope.select(*fields) - end - end - def find(*args) if block_given? load_target.find(*args) { |*block_args| yield(*block_args) } @@ -111,50 +97,6 @@ module ActiveRecord end end - def first(*args) - first_nth_or_last(:first, *args) - end - - def second(*args) - first_nth_or_last(:second, *args) - end - - def third(*args) - first_nth_or_last(:third, *args) - end - - def fourth(*args) - first_nth_or_last(:fourth, *args) - end - - def fifth(*args) - first_nth_or_last(:fifth, *args) - end - - def forty_two(*args) - first_nth_or_last(:forty_two, *args) - end - - def third_to_last(*args) - first_nth_or_last(:third_to_last, *args) - end - - def second_to_last(*args) - first_nth_or_last(:second_to_last, *args) - end - - def last(*args) - first_nth_or_last(:last, *args) - end - - def take(n = nil) - if loaded? - n ? target.take(n) : target.first - else - scope.take(n) - end - end - def build(attributes = {}, &block) if attributes.is_a?(Array) attributes.collect { |attr| build(attr, &block) } @@ -165,14 +107,6 @@ module ActiveRecord end end - def create(attributes = {}, &block) - _create_record(attributes, &block) - end - - def create!(attributes = {}, &block) - _create_record(attributes, true, &block) - end - # Add +records+ to this association. Returns +self+ so method calls may # be chained. Since << flattens its argument list and inserts each record, # +push+ and +concat+ behave identically. @@ -243,31 +177,6 @@ module ActiveRecord end end - # Returns the number of records. If no arguments are given, it counts all - # columns using SQL. If one argument is given, it counts only the passed - # column using SQL. If a block is given, it counts the number of records - # yielding a true value. - def count(column_name = nil) - return super if block_given? - relation = scope - if association_scope.distinct_value - # This is needed because 'SELECT count(DISTINCT *)..' is not valid SQL. - column_name ||= reflection.klass.primary_key - relation = relation.distinct - end - - value = relation.count(column_name) - - limit = options[:limit] - offset = options[:offset] - - if limit || offset - [ [value - offset.to_i, 0].max, limit.to_i ].min - else - value - end - end - # Removes +records+ from this association calling +before_remove+ and # +after_remove+ callbacks. # @@ -277,11 +186,8 @@ module ActiveRecord # +delete_records+. They are in any case removed from the collection. def delete(*records) return if records.empty? - _options = records.extract_options! - dependent = _options[:dependent] || options[:dependent] - records = find(records) if records.any? { |record| record.kind_of?(Integer) || record.kind_of?(String) } - delete_or_destroy(records, dependent) + delete_or_destroy(records, options[:dependent]) end # Deletes the +records+ and removes them from this association calling @@ -307,14 +213,10 @@ module ActiveRecord # +count_records+, which is a method descendants have to provide. def size if !find_target? || loaded? - if association_scope.distinct_value - target.uniq.size - else - target.size - end - elsif !loaded? && !association_scope.group_values.empty? + target.size + elsif !association_scope.group_values.empty? load_target.size - elsif !loaded? && !association_scope.distinct_value && target.is_a?(Array) + elsif !association_scope.distinct_value && target.is_a?(Array) unsaved_records = target.select(&:new_record?) unsaved_records.size + count_records else @@ -322,15 +224,6 @@ module ActiveRecord end end - # Returns the size of the collection calling +size+ on the target. - # - # If the collection has been already loaded +length+ and +size+ are - # equivalent. If not and you are going to need the records anyway this - # method will take one less query. Otherwise +size+ is more efficient. - def length - load_target.size - end - # Returns true if the collection is empty. # # If the collection has been loaded @@ -347,36 +240,6 @@ module ActiveRecord end end - # Returns true if the collections is not empty. - # If block given, loads all records and checks for one or more matches. - # Otherwise, equivalent to +!collection.empty?+. - def any? - if block_given? - load_target.any? { |*block_args| yield(*block_args) } - else - !empty? - end - end - - # Returns true if the collection has more than 1 record. - # If block given, loads all records and checks for two or more matches. - # Otherwise, equivalent to +collection.size > 1+. - def many? - if block_given? - load_target.many? { |*block_args| yield(*block_args) } - else - size > 1 - end - end - - def distinct - seen = {} - load_target.find_all do |record| - seen[record.id] = true unless seen.key?(record.id) - end - end - alias uniq distinct - # Replace this collection with +other_array+. This will perform a diff # and delete/add only records that have changed. def replace(other_array) @@ -426,19 +289,28 @@ module ActiveRecord def replace_on_target(record, index, skip_callbacks) callback(:before_add, record) unless skip_callbacks - was_loaded = loaded? - yield(record) if block_given? + begin + if index + record_was = target[index] + target[index] = record + else + target << record + end + + set_inverse_instance(record) - unless !was_loaded && loaded? + yield(record) if block_given? + rescue if index - @target[index] = record + target[index] = record_was else - @target << record + target.delete(record) end + + raise end callback(:after_add, record) unless skip_callbacks - set_inverse_instance(record) record end @@ -453,6 +325,12 @@ module ActiveRecord owner.new_record? && !foreign_key_present? end + def find_from_target? + loaded? || + owner.new_record? || + target.any? { |record| record.new_record? || record.changed? } + end + private def find_target @@ -467,9 +345,9 @@ module ActiveRecord end binds = AssociationScope.get_bind_values(owner, reflection.chain) - records = sc.execute(binds, klass, conn) - records.each { |record| set_inverse_instance(record) } - records + sc.execute(binds, klass, conn) do |record| + set_inverse_instance(record) + end end # We have some records loaded from the database (persisted) and some that are @@ -489,7 +367,7 @@ module ActiveRecord persisted.map! do |record| if mem_record = memory.delete(record) - ((record.attribute_names & mem_record.attribute_names) - mem_record.changes.keys).each do |name| + ((record.attribute_names & mem_record.attribute_names) - mem_record.changed_attribute_names_to_save).each do |name| mem_record[name] = record[name] end @@ -549,8 +427,9 @@ module ActiveRecord records.each { |record| callback(:after_remove, record) } end - # Delete the given records from the association, using one of the methods :destroy, - # :delete_all or :nullify (or nil, in which case a default is used). + # Delete the given records from the association, + # using one of the methods +:destroy+, +:delete_all+ + # or +:nullify+ (or +nil+, in which case a default is used). def delete_records(records, method) raise NotImplementedError end @@ -599,25 +478,6 @@ module ActiveRecord owner.class.send(full_callback_name) end - # Should we deal with assoc.first or assoc.last by issuing an independent query to - # the database, or by getting the target, and then taking the first/last item from that? - # - # If the args is just a non-empty options hash, go to the database. - # - # Otherwise, go to the database only if none of the following are true: - # * target already loaded - # * owner is new record - # * target contains new or changed record(s) - def fetch_first_nth_or_last_using_find?(args) - if args.first.is_a?(Hash) - true - else - !(loaded? || - owner.new_record? || - target.any? { |record| record.new_record? || record.changed? }) - end - end - def include_in_memory?(record) if reflection.is_a?(ActiveRecord::Reflection::ThroughReflection) assoc = owner.association(reflection.through_reflection.name) @@ -644,14 +504,6 @@ module ActiveRecord load_target.select { |r| ids.include?(r.id.to_s) } end end - - # Fetches the first/last using SQL if possible, otherwise from the target array. - def first_nth_or_last(type, *args) - args.shift if args.first.is_a?(Hash) && args.first.empty? - - collection = fetch_first_nth_or_last_using_find?(args) ? scope : load_target - collection.send(type, *args) - end end end end diff --git a/activerecord/lib/active_record/associations/collection_proxy.rb b/activerecord/lib/active_record/associations/collection_proxy.rb index 806a905323..0d84805b4d 100644 --- a/activerecord/lib/active_record/associations/collection_proxy.rb +++ b/activerecord/lib/active_record/associations/collection_proxy.rb @@ -28,7 +28,6 @@ module ActiveRecord # is computed directly through SQL and does not trigger by itself the # instantiation of the actual post records. class CollectionProxy < Relation - delegate(*(ActiveRecord::Calculations.public_instance_methods - [:count]), to: :scope) delegate :exists?, :update_all, :arel, to: :scope def initialize(klass, association) #:nodoc: @@ -54,6 +53,12 @@ module ActiveRecord @association.loaded? end + ## + # :method: select + # + # :call-seq: + # select(*fields, &block) + # # Works in two ways. # # *First:* Specify a subset of fields to be selected from the result set. @@ -101,15 +106,6 @@ module ActiveRecord # # #<Pet id: 2, name: "Spook", person_id: 1>, # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> # # ] - # - # person.pets.select(:name) { |pet| pet.name =~ /oo/ } - # # => [ - # # #<Pet id: 2, name: "Spook">, - # # #<Pet id: 3, name: "Choo-Choo"> - # # ] - def select(*fields, &block) - @association.select(*fields, &block) - end # Finds an object in the collection responding to the +id+. Uses the same # rules as ActiveRecord::Base.find. Returns ActiveRecord::RecordNotFound @@ -141,6 +137,12 @@ module ActiveRecord @association.find(*args, &block) end + ## + # :method: first + # + # :call-seq: + # first(limit = nil) + # # Returns the first record, or the first +n+ records, from the collection. # If the collection is empty, the first form returns +nil+, and the second # form returns an empty array. @@ -167,45 +169,63 @@ module ActiveRecord # another_person_without.pets # => [] # another_person_without.pets.first # => nil # another_person_without.pets.first(3) # => [] - def first(*args) - @association.first(*args) - end + ## + # :method: second + # + # :call-seq: + # second() + # # Same as #first except returns only the second record. - def second(*args) - @association.second(*args) - end + ## + # :method: third + # + # :call-seq: + # third() + # # Same as #first except returns only the third record. - def third(*args) - @association.third(*args) - end + ## + # :method: fourth + # + # :call-seq: + # fourth() + # # Same as #first except returns only the fourth record. - def fourth(*args) - @association.fourth(*args) - end + ## + # :method: fifth + # + # :call-seq: + # fifth() + # # Same as #first except returns only the fifth record. - def fifth(*args) - @association.fifth(*args) - end + ## + # :method: forty_two + # + # :call-seq: + # forty_two() + # # Same as #first except returns only the forty second record. # Also known as accessing "the reddit". - def forty_two(*args) - @association.forty_two(*args) - end + ## + # :method: third_to_last + # + # :call-seq: + # third_to_last() + # # Same as #first except returns only the third-to-last record. - def third_to_last(*args) - @association.third_to_last(*args) - end + ## + # :method: second_to_last + # + # :call-seq: + # second_to_last() + # # Same as #first except returns only the second-to-last record. - def second_to_last(*args) - @association.second_to_last(*args) - end # Returns the last record, or the last +n+ records, from the collection. # If the collection is empty, the first form returns +nil+, and the second @@ -233,8 +253,9 @@ module ActiveRecord # another_person_without.pets # => [] # another_person_without.pets.last # => nil # another_person_without.pets.last(3) # => [] - def last(*args) - @association.last(*args) + def last(limit = nil) + load_target if find_from_target? + super end # Gives a record (or N records if a parameter is supplied) from the collection @@ -262,8 +283,9 @@ module ActiveRecord # another_person_without.pets # => [] # another_person_without.pets.take # => nil # another_person_without.pets.take(2) # => [] - def take(n = nil) - @association.take(n) + def take(limit = nil) + load_target if find_from_target? + super end # Returns a new object of the collection type that has been instantiated @@ -696,6 +718,12 @@ module ActiveRecord @association.destroy(*records) end + ## + # :method: distinct + # + # :call-seq: + # distinct(value = true) + # # Specifies whether the records should be unique or not. # # class Person < ActiveRecord::Base @@ -710,11 +738,32 @@ module ActiveRecord # # person.pets.select(:name).distinct # # => [#<Pet name: "Fancy-Fancy">] - def distinct - @association.distinct + # + # person.pets.select(:name).distinct.distinct(false) + # # => [ + # # #<Pet name: "Fancy-Fancy">, + # # #<Pet name: "Fancy-Fancy"> + # # ] + + #-- + def uniq + load_target.uniq end - alias uniq distinct + def calculate(operation, column_name) + null_scope? ? scope.calculate(operation, column_name) : super + end + + def pluck(*column_names) + null_scope? ? scope.pluck(*column_names) : super + end + + ## + # :method: count + # + # :call-seq: + # count(column_name = nil, &block) + # # Count all records. # # class Person < ActiveRecord::Base @@ -734,9 +783,6 @@ module ActiveRecord # perform the count using Ruby. # # person.pets.count { |pet| pet.name.include?('-') } # => 2 - def count(column_name = nil, &block) - @association.count(column_name, &block) - end # Returns the size of the collection. If the collection hasn't been loaded, # it executes a <tt>SELECT COUNT(*)</tt> query. Else it calls <tt>collection.size</tt>. @@ -766,6 +812,12 @@ module ActiveRecord @association.size end + ## + # :method: length + # + # :call-seq: + # length() + # # Returns the size of the collection calling +size+ on the target. # If the collection has been already loaded, +length+ and +size+ are # equivalent. If not and you are going to need the records anyway this @@ -786,9 +838,6 @@ module ActiveRecord # # #<Pet id: 2, name: "Spook", person_id: 1>, # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> # # ] - def length - @association.length - end # Returns +true+ if the collection is empty. If the collection has been # loaded it is equivalent @@ -812,6 +861,12 @@ module ActiveRecord @association.empty? end + ## + # :method: any? + # + # :call-seq: + # any?() + # # Returns +true+ if the collection is not empty. # # class Person < ActiveRecord::Base @@ -841,10 +896,13 @@ module ActiveRecord # pet.group == 'dogs' # end # # => true - def any?(&block) - @association.any?(&block) - end + ## + # :method: many? + # + # :call-seq: + # many?() + # # Returns true if the collection has more than one record. # Equivalent to <tt>collection.size > 1</tt>. # @@ -879,9 +937,6 @@ module ActiveRecord # pet.group == 'cats' # end # # => true - def many?(&block) - @association.many?(&block) - end # Returns +true+ if the given +record+ is present in the collection. # @@ -1073,6 +1128,24 @@ module ActiveRecord private + def find_nth_with_limit(index, limit) + load_target if find_from_target? + super + end + + def find_nth_from_last(index) + load_target if find_from_target? + super + end + + def null_scope? + @association.null_scope? + end + + def find_from_target? + @association.find_from_target? + end + def exec_queries load_target end diff --git a/activerecord/lib/active_record/associations/has_many_association.rb b/activerecord/lib/active_record/associations/has_many_association.rb index 6720578b2c..b413eb3f9c 100644 --- a/activerecord/lib/active_record/associations/has_many_association.rb +++ b/activerecord/lib/active_record/associations/has_many_association.rb @@ -16,14 +16,7 @@ module ActiveRecord when :restrict_with_error unless empty? record = owner.class.human_attribute_name(reflection.name).downcase - message = owner.errors.generate_message(:base, :'restrict_dependent_destroy.many', record: record, raise: true) rescue nil - if message - ActiveSupport::Deprecation.warn(<<-MESSAGE.squish) - The error key `:'restrict_dependent_destroy.many'` has been deprecated and will be removed in Rails 5.1. - Please use `:'restrict_dependent_destroy.has_many'` instead. - MESSAGE - end - owner.errors.add(:base, message || :'restrict_dependent_destroy.has_many', record: record) + owner.errors.add(:base, :'restrict_dependent_destroy.has_many', record: record) throw(:abort) end @@ -38,7 +31,6 @@ module ActiveRecord def insert_record(record, validate = true, raise = false) set_owner_attributes(record) - set_inverse_instance(record) if raise record.save!(validate: validate) @@ -72,7 +64,7 @@ module ActiveRecord # the loaded flag is set to true as well. def count_records count = if reflection.has_cached_counter? - owner._read_attribute reflection.counter_cache_column + owner._read_attribute(reflection.counter_cache_column).to_i else scope.count end @@ -80,7 +72,7 @@ module ActiveRecord # If there's nothing in the database and @target has no new records # we are certain the current target is an empty array. This is a # documented side-effect of the method that may avoid an extra SELECT. - @target ||= [] and loaded! if count == 0 + (@target ||= []) && loaded! if count == 0 [association_scope.limit_value, count].compact.min end @@ -108,7 +100,7 @@ module ActiveRecord end def delete_or_nullify_all_records(method) - count = delete_count(method, self.scope) + count = delete_count(method, scope) update_counter(-count) end diff --git a/activerecord/lib/active_record/associations/has_many_through_association.rb b/activerecord/lib/active_record/associations/has_many_through_association.rb index d258eac0ed..c4a7fe4432 100644 --- a/activerecord/lib/active_record/associations/has_many_through_association.rb +++ b/activerecord/lib/active_record/associations/has_many_through_association.rb @@ -38,10 +38,12 @@ module ActiveRecord def insert_record(record, validate = true, raise = false) ensure_not_nested - if raise - record.save!(validate: validate) - else - return unless record.save(validate: validate) + if record.new_record? || record.has_changes_to_save? + if raise + record.save!(validate: validate) + else + return unless record.save(validate: validate) + end end save_through_record(record) @@ -86,7 +88,10 @@ module ActiveRecord end def save_through_record(record) - build_through_record(record).save! + association = build_through_record(record) + if association.changed? + association.save! + end ensure @through_records.delete(record.object_id) end diff --git a/activerecord/lib/active_record/associations/has_one_association.rb b/activerecord/lib/active_record/associations/has_one_association.rb index 5ea9577301..b624154def 100644 --- a/activerecord/lib/active_record/associations/has_one_association.rb +++ b/activerecord/lib/active_record/associations/has_one_association.rb @@ -12,14 +12,7 @@ module ActiveRecord when :restrict_with_error if load_target record = owner.class.human_attribute_name(reflection.name).downcase - message = owner.errors.generate_message(:base, :'restrict_dependent_destroy.one', record: record, raise: true) rescue nil - if message - ActiveSupport::Deprecation.warn(<<-MESSAGE.squish) - The error key `:'restrict_dependent_destroy.one'` has been deprecated and will be removed in Rails 5.1. - Please use `:'restrict_dependent_destroy.has_one'` instead. - MESSAGE - end - owner.errors.add(:base, message || :'restrict_dependent_destroy.has_one', record: record) + owner.errors.add(:base, :'restrict_dependent_destroy.has_one', record: record) throw(:abort) end @@ -35,7 +28,7 @@ module ActiveRecord return target unless target || record assigning_another_record = target != record - if assigning_another_record || record.changed? + if assigning_another_record || record.has_changes_to_save? save &&= owner.persisted? transaction_if(save) do @@ -86,8 +79,9 @@ module ActiveRecord target.delete when :destroy target.destroy - else + else nullify_owner_attributes(target) + remove_inverse_instance(target) if target.persisted? && owner.persisted? && !target.save set_owner_attributes(target) diff --git a/activerecord/lib/active_record/associations/join_dependency.rb b/activerecord/lib/active_record/associations/join_dependency.rb index 3946d5baa4..4cd1e64c3d 100644 --- a/activerecord/lib/active_record/associations/join_dependency.rb +++ b/activerecord/lib/active_record/associations/join_dependency.rb @@ -7,12 +7,12 @@ module ActiveRecord class Aliases # :nodoc: def initialize(tables) @tables = tables - @alias_cache = tables.each_with_object({}) { |table,h| - h[table.node] = table.columns.each_with_object({}) { |column,i| + @alias_cache = tables.each_with_object({}) { |table, h| + h[table.node] = table.columns.each_with_object({}) { |column, i| i[column.name] = column.alias } } - @name_and_alias_cache = tables.each_with_object({}) { |table,h| + @name_and_alias_cache = tables.each_with_object({}) { |table, h| h[table.node] = table.columns.map { |column| [column.name, column.alias] } @@ -62,7 +62,7 @@ module ActiveRecord walk_tree assoc, hash end when Hash - associations.each do |k,v| + associations.each do |k, v| cache = hash[k] ||= {} walk_tree v, cache end @@ -126,8 +126,8 @@ module ActiveRecord end def 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.new join_root.each_with_index.map { |join_part, i| + columns = join_part.column_names.each_with_index.map { |column_name, j| Aliases::Column.new column_name, "t#{i}_r#{j}" } Aliases::Table.new(join_part, columns) @@ -143,7 +143,7 @@ module ActiveRecord } } - model_cache = Hash.new { |h,klass| h[klass] = {} } + model_cache = Hash.new { |h, klass| h[klass] = {} } parents = model_cache[join_root] column_aliases = aliases.column_aliases join_root @@ -223,13 +223,13 @@ module ActiveRecord [left.children.find { |node2| node1.match? node2 }, node1] }.partition(&:first) - ojs = missing.flat_map { |_,n| make_outer_joins left, n } - intersection.flat_map { |l,r| walk l, r }.concat ojs + ojs = missing.flat_map { |_, n| make_outer_joins left, n } + intersection.flat_map { |l, r| walk l, r }.concat ojs end def find_reflection(klass, name) - klass._reflect_on_association(name) or - raise ConfigurationError, "Can't join '#{ klass.name }' to association named '#{ name }'; perhaps you misspelled it?" + klass._reflect_on_association(name) || + raise(ConfigurationError, "Can't join '#{klass.name}' to association named '#{name}'; perhaps you misspelled it?") end def build(associations, base_klass) @@ -274,7 +274,11 @@ module ActiveRecord construct(model, node, row, rs, seen, model_cache, aliases) else model = construct_model(ar_parent, node, row, model_cache, id, aliases) - model.readonly! + + if node.reflection.scope_for(node.base_klass).readonly_value + model.readonly! + end + seen[ar_parent.object_id][node.base_klass][id] = model construct(model, node, row, rs, seen, model_cache, aliases) end @@ -282,17 +286,19 @@ module ActiveRecord end def construct_model(record, node, row, model_cache, id, aliases) - model = model_cache[node][id] ||= node.instantiate(row, - aliases.column_aliases(node)) other = record.association(node.reflection.name) + model = model_cache[node][id] ||= + node.instantiate(row, aliases.column_aliases(node)) do |m| + other.set_inverse_instance(m) + end + if node.reflection.collection? other.target.push(model) else other.target = model end - other.set_inverse_instance(model) model 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 551087f822..61cec5403a 100644 --- a/activerecord/lib/active_record/associations/join_dependency/join_part.rb +++ b/activerecord/lib/active_record/associations/join_dependency/join_part.rb @@ -62,8 +62,8 @@ module ActiveRecord hash end - def instantiate(row, aliases) - base_klass.instantiate(extract_record(row, aliases)) + def instantiate(row, aliases, &block) + base_klass.instantiate(extract_record(row, aliases), &block) end end end diff --git a/activerecord/lib/active_record/associations/preloader.rb b/activerecord/lib/active_record/associations/preloader.rb index a81860e40f..9f77f38b35 100644 --- a/activerecord/lib/active_record/associations/preloader.rb +++ b/activerecord/lib/active_record/associations/preloader.rb @@ -106,7 +106,7 @@ module ActiveRecord private - # Loads all the given data into +records+ for the +association+. + # Loads all the given data into +records+ for the +association+. def preloaders_on(association, records, scope) case association when Hash @@ -132,18 +132,18 @@ module ActiveRecord } end - # Loads all the given data into +records+ for a singular +association+. - # - # Functions by instantiating a preloader class such as Preloader::HasManyThrough and - # call the +run+ method for each passed in class in the +records+ argument. - # - # Not all records have the same class, so group then preload group on the reflection - # itself so that if various subclass share the same association then we do not split - # them unnecessarily - # - # Additionally, polymorphic belongs_to associations can have multiple associated - # classes, depending on the polymorphic_type field. So we group by the classes as - # well. + # Loads all the given data into +records+ for a singular +association+. + # + # Functions by instantiating a preloader class such as Preloader::HasManyThrough and + # call the +run+ method for each passed in class in the +records+ argument. + # + # Not all records have the same class, so group then preload group on the reflection + # itself so that if various subclass share the same association then we do not split + # them unnecessarily + # + # Additionally, polymorphic belongs_to associations can have multiple associated + # classes, depending on the polymorphic_type field. So we group by the classes as + # well. def preloaders_for_one(association, records, scope) grouped_records(association, records).flat_map do |reflection, klasses| klasses.map do |rhs_klass, rs| @@ -187,10 +187,10 @@ module ActiveRecord def self.owners; []; end 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 - # that accepts a preloader. + # 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 + # that accepts a preloader. def preloader_for(reflection, owners, rhs_klass) return NullPreloader unless rhs_klass diff --git a/activerecord/lib/active_record/associations/preloader/association.rb b/activerecord/lib/active_record/associations/preloader/association.rb index a8afa48865..4072d19380 100644 --- a/activerecord/lib/active_record/associations/preloader/association.rb +++ b/activerecord/lib/active_record/associations/preloader/association.rb @@ -28,10 +28,6 @@ module ActiveRecord end def records_for(ids) - query_scope(ids) - end - - def query_scope(ids) scope.where(association_key_name => ids) end @@ -62,7 +58,12 @@ module ActiveRecord private def associated_records_by_owner(preloader) - records = load_records + 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 + owners.each_with_object({}) do |owner, result| result[owner] = records[convert_key(owner[owner_key_name])] || [] end @@ -79,6 +80,15 @@ module ActiveRecord @owner_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 + end + end + @owners_by_key + end + def key_conversion_required? @key_conversion_required ||= association_key_type != owner_key_type end @@ -99,13 +109,13 @@ module ActiveRecord @model.type_for_attribute(owner_key_name.to_s).type end - def load_records + def load_records(&block) return {} if owner_keys.empty? # Some databases impose a limit on the number of ids in a list (in Oracle it's 1000) # Make several smaller queries if necessary or make one query if the adapter supports it - slices = owner_keys.each_slice(klass.connection.in_clause_length || owner_keys.size) + slices = owner_keys.each_slice(klass.connection.in_clause_length || owner_keys.size) @preloaded_records = slices.flat_map do |slice| - records_for(slice) + records_for(slice).load(&block) end @preloaded_records.group_by do |record| convert_key(record[association_key_name]) @@ -113,7 +123,7 @@ module ActiveRecord end def reflection_scope - @reflection_scope ||= reflection.scope ? klass.unscoped.instance_exec(nil, &reflection.scope) : klass.unscoped + @reflection_scope ||= reflection.scope_for(klass) end def build_scope diff --git a/activerecord/lib/active_record/associations/preloader/collection_association.rb b/activerecord/lib/active_record/associations/preloader/collection_association.rb index 24b8e01029..26690bf16d 100644 --- a/activerecord/lib/active_record/associations/preloader/collection_association.rb +++ b/activerecord/lib/active_record/associations/preloader/collection_association.rb @@ -9,7 +9,6 @@ module ActiveRecord association = owner.association(reflection.name) association.loaded! association.target.concat(records) - records.each { |record| association.set_inverse_instance(record) } 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 index 0888d383a6..5c5828262e 100644 --- a/activerecord/lib/active_record/associations/preloader/singular_association.rb +++ b/activerecord/lib/active_record/associations/preloader/singular_association.rb @@ -10,7 +10,6 @@ module ActiveRecord association = owner.association(reflection.name) association.target = record - association.set_inverse_instance(record) if record 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 be9dfe7686..9d44a02021 100644 --- a/activerecord/lib/active_record/associations/preloader/through_association.rb +++ b/activerecord/lib/active_record/associations/preloader/through_association.rb @@ -24,7 +24,7 @@ module ActiveRecord reset_association owners, through_reflection.name - middle_records = through_records.flat_map { |(_,rec)| rec } + middle_records = through_records.flat_map { |(_, rec)| rec } preloaders = preloader.preload(middle_records, source_reflection.name, @@ -32,13 +32,13 @@ module ActiveRecord @preloaded_records = preloaders.flat_map(&:preloaded_records) - middle_to_pl = preloaders.each_with_object({}) do |pl,h| + 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| + 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| diff --git a/activerecord/lib/active_record/associations/singular_association.rb b/activerecord/lib/active_record/associations/singular_association.rb index 1fe9a23263..91580a28d0 100644 --- a/activerecord/lib/active_record/associations/singular_association.rb +++ b/activerecord/lib/active_record/associations/singular_association.rb @@ -2,16 +2,8 @@ module ActiveRecord module Associations class SingularAssociation < Association #:nodoc: # Implements the reader method, e.g. foo.bar for Foo.has_one :bar - def reader(force_reload = false) - if force_reload && klass - ActiveSupport::Deprecation.warn(<<-MSG.squish) - Passing an argument to force an association to reload is now - deprecated and will be removed in Rails 5.1. Please call `reload` - on the parent object instead. - MSG - - klass.uncached { reload } - elsif !loaded? || stale_target? + def reader + if !loaded? || stale_target? reload end @@ -23,14 +15,6 @@ module ActiveRecord replace(record) end - def create(attributes = {}, &block) - _create_record(attributes, &block) - end - - def create!(attributes = {}, &block) - _create_record(attributes, true, &block) - end - def build(attributes = {}) record = build_record(attributes) yield(record) if block_given? @@ -38,6 +22,13 @@ module ActiveRecord record end + # Implements the reload reader method, e.g. foo.reload_bar for + # Foo.has_one :bar + def force_reload_reader + klass.uncached { reload } + target + end + private def create_scope @@ -56,9 +47,11 @@ module ActiveRecord end binds = AssociationScope.get_bind_values(owner, reflection.chain) - if record = sc.execute(binds, klass, conn).first + sc.execute(binds, klass, conn) do |record| set_inverse_instance record - end + end.first + rescue ::RangeError + nil end def replace(record) diff --git a/activerecord/lib/active_record/associations/through_association.rb b/activerecord/lib/active_record/associations/through_association.rb index f4129edc5a..6b87993ba3 100644 --- a/activerecord/lib/active_record/associations/through_association.rb +++ b/activerecord/lib/active_record/associations/through_association.rb @@ -4,7 +4,7 @@ module ActiveRecord module ThroughAssociation #:nodoc: delegate :source_reflection, :through_reflection, to: :reflection - protected + private # We merge in these scopes for two reasons: # @@ -21,8 +21,6 @@ module ActiveRecord scope end - private - # Construct attributes for :through pointing to owner and associate. This is used by the # methods which create and delete records on the association. # diff --git a/activerecord/lib/active_record/attribute.rb b/activerecord/lib/active_record/attribute.rb index 380593e809..38281158d8 100644 --- a/activerecord/lib/active_record/attribute.rb +++ b/activerecord/lib/active_record/attribute.rb @@ -128,11 +128,22 @@ module ActiveRecord 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 @@ -143,14 +154,6 @@ module ActiveRecord assigned? && type.changed?(original_value, value, value_before_type_cast) end - def original_value_for_database - if assigned? - original_attribute.original_value_for_database - else - _original_value_for_database - end - end - def _original_value_for_database type.serialize(original_value) end @@ -187,7 +190,7 @@ module ActiveRecord class Null < Attribute # :nodoc: def initialize(name) - super(name, nil, Type::Value.new) + super(name, nil, Type.default_value) end def type_cast(*) diff --git a/activerecord/lib/active_record/attribute/user_provided_default.rb b/activerecord/lib/active_record/attribute/user_provided_default.rb index a4e2c2ec85..57f8bbed76 100644 --- a/activerecord/lib/active_record/attribute/user_provided_default.rb +++ b/activerecord/lib/active_record/attribute/user_provided_default.rb @@ -20,6 +20,8 @@ module ActiveRecord 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 diff --git a/activerecord/lib/active_record/attribute_assignment.rb b/activerecord/lib/active_record/attribute_assignment.rb index f3ce52fdfe..d0dfca0cac 100644 --- a/activerecord/lib/active_record/attribute_assignment.rb +++ b/activerecord/lib/active_record/attribute_assignment.rb @@ -12,7 +12,7 @@ module ActiveRecord private - def _assign_attributes(attributes) # :nodoc: + def _assign_attributes(attributes) multi_parameter_attributes = {} nested_parameter_attributes = {} @@ -29,17 +29,17 @@ module ActiveRecord assign_multiparameter_attributes(multi_parameter_attributes) unless multi_parameter_attributes.empty? end - # Assign any deferred nested attributes after the base attributes have been set. + # Assign any deferred nested attributes after the base attributes have been set. def assign_nested_parameter_attributes(pairs) pairs.each { |k, v| _assign_attribute(k, v) } end - # Instantiates objects for all attribute classes that needs more than one constructor parameter. This is done - # by calling new on the column type or aggregation type (through composed_of) object with these parameters. - # So having the pairs written_on(1) = "2004", written_on(2) = "6", written_on(3) = "24", will instantiate - # written_on (a date type) with Date.new("2004", "6", "24"). You can also specify a typecast character in the - # parentheses to have the parameters typecasted before they're used in the constructor. Use i for Integer and - # f for Float. If all the values for a given attribute are empty, the attribute will be set to +nil+. + # Instantiates objects for all attribute classes that needs more than one constructor parameter. This is done + # by calling new on the column type or aggregation type (through composed_of) object with these parameters. + # So having the pairs written_on(1) = "2004", written_on(2) = "6", written_on(3) = "24", will instantiate + # written_on (a date type) with Date.new("2004", "6", "24"). You can also specify a typecast character in the + # parentheses to have the parameters typecasted before they're used in the constructor. Use i for Integer and + # f for Float. If all the values for a given attribute are empty, the attribute will be set to +nil+. def assign_multiparameter_attributes(pairs) execute_callstack_for_multiparameter_attributes( extract_callstack_for_multiparameter_attributes(pairs) diff --git a/activerecord/lib/active_record/attribute_decorators.rb b/activerecord/lib/active_record/attribute_decorators.rb index 340dfe11cf..c39e9ce4c5 100644 --- a/activerecord/lib/active_record/attribute_decorators.rb +++ b/activerecord/lib/active_record/attribute_decorators.rb @@ -8,12 +8,34 @@ module ActiveRecord end module ClassMethods # :nodoc: + # This method is an internal API used to create class macros such as + # +serialize+, and features like time zone aware attributes. + # + # Used to wrap the type of an attribute in a new type. + # When the schema for a model is loaded, attributes with the same name as + # +column_name+ will have their type yielded to the given block. The + # return value of that block will be used instead. + # + # Subsequent calls where +column_name+ and +decorator_name+ are the same + # will override the previous decorator, not decorate twice. This can be + # used to create idempotent class macros like +serialize+ def decorate_attribute_type(column_name, decorator_name, &block) matcher = ->(name, _) { name == column_name.to_s } key = "_#{column_name}_#{decorator_name}" decorate_matching_attribute_types(matcher, key, &block) end + # This method is an internal API used to create higher level features like + # time zone aware attributes. + # + # When the schema for a model is loaded, +matcher+ will be called for each + # attribute with its name and type. If the matcher returns a truthy value, + # the type will then be yielded to the given block, and the return value + # of that block will replace the type. + # + # Subsequent calls to this method with the same value for +decorator_name+ + # will replace the previous decorator, not decorate twice. This can be + # used to ensure that class macros are idempotent. def decorate_matching_attribute_types(matcher, decorator_name, &block) reload_schema_from_cache decorator_name = decorator_name.to_s diff --git a/activerecord/lib/active_record/attribute_methods.rb b/activerecord/lib/active_record/attribute_methods.rb index ba26a11b39..ebe06566cc 100644 --- a/activerecord/lib/active_record/attribute_methods.rb +++ b/activerecord/lib/active_record/attribute_methods.rb @@ -394,30 +394,23 @@ module ActiveRecord protected - def clone_attribute_value(reader_method, attribute_name) # :nodoc: - value = send(reader_method, attribute_name) - value.duplicable? ? value.clone : value - rescue TypeError, NoMethodError - value + def attribute_method?(attr_name) # :nodoc: + # We check defined? because Syck calls respond_to? before actually calling initialize. + defined?(@attributes) && @attributes.key?(attr_name) end - def arel_attributes_with_values_for_create(attribute_names) # :nodoc: + private + + def arel_attributes_with_values_for_create(attribute_names) arel_attributes_with_values(attributes_for_create(attribute_names)) end - def arel_attributes_with_values_for_update(attribute_names) # :nodoc: + def arel_attributes_with_values_for_update(attribute_names) arel_attributes_with_values(attributes_for_update(attribute_names)) end - def attribute_method?(attr_name) # :nodoc: - # We check defined? because Syck calls respond_to? before actually calling initialize. - defined?(@attributes) && @attributes.key?(attr_name) - end - - private - - # Returns a Hash of the Arel::Attributes and attribute values that have been - # typecasted for use in an Arel insert/update method. + # Returns a Hash of the Arel::Attributes and attribute values that have been + # typecasted for use in an Arel insert/update method. def arel_attributes_with_values(attribute_names) attrs = {} arel_table = self.class.arel_table @@ -428,15 +421,15 @@ module ActiveRecord attrs end - # Filters the primary keys and readonly attributes from the attribute names. + # Filters the primary keys and readonly attributes from the attribute names. def attributes_for_update(attribute_names) attribute_names.reject do |name| readonly_attribute?(name) end end - # Filters out the primary keys, from the attribute names, when the primary - # key is to be generated (e.g. the id attribute has no value). + # Filters out the primary keys, from the attribute names, when the primary + # key is to be generated (e.g. the id attribute has no value). def attributes_for_create(attribute_names) attribute_names.reject do |name| pk_attribute?(name) && id.nil? 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 92f124078c..115eb1ef3f 100644 --- a/activerecord/lib/active_record/attribute_methods/before_type_cast.rb +++ b/activerecord/lib/active_record/attribute_methods/before_type_cast.rb @@ -63,7 +63,7 @@ module ActiveRecord private - # Handle *_before_type_cast for method_missing. + # Handle *_before_type_cast for method_missing. def attribute_before_type_cast(attribute_name) read_attribute_before_type_cast(attribute_name) end diff --git a/activerecord/lib/active_record/attribute_methods/dirty.rb b/activerecord/lib/active_record/attribute_methods/dirty.rb index c9638bf70b..b0e1391cb9 100644 --- a/activerecord/lib/active_record/attribute_methods/dirty.rb +++ b/activerecord/lib/active_record/attribute_methods/dirty.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true require "active_support/core_ext/module/attribute_accessors" require "active_record/attribute_mutation_tracker" @@ -15,6 +16,18 @@ module ActiveRecord class_attribute :partial_writes, instance_writer: false self.partial_writes = true + + after_create { changes_internally_applied } + after_update { changes_internally_applied } + + # Attribute methods for "changed in last call to save?" + attribute_method_affix(prefix: "saved_change_to_", suffix: "?") + attribute_method_prefix("saved_change_to_") + attribute_method_suffix("_before_last_save") + + # Attribute methods for "will change if I call save?" + attribute_method_affix(prefix: "will_save_change_to_", suffix: "?") + attribute_method_suffix("_change_to_be_saved", "_in_database") end # Attempts to +save+ the record and clears changed attributes if successful. @@ -35,8 +48,8 @@ module ActiveRecord # <tt>reload</tt> the record and clears changed attributes. def reload(*) super.tap do - @mutation_tracker = nil @previous_mutation_tracker = nil + clear_mutation_trackers @changed_attributes = HashWithIndifferentAccess.new end end @@ -46,19 +59,26 @@ module ActiveRecord @attributes = self.class._default_attributes.map do |attr| attr.with_value_from_user(@attributes.fetch_value(attr.name)) end - @mutation_tracker = nil + 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 = HashWithIndifferentAccess.new - store_original_attributes + clear_mutation_trackers end def clear_changes_information @previous_mutation_tracker = nil @changed_attributes = HashWithIndifferentAccess.new - store_original_attributes + forget_attribute_assignments + clear_mutation_trackers end def raw_write_attribute(attr_name, *) @@ -80,17 +100,27 @@ module ActiveRecord 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 + end previous_mutation_tracker.changes end @@ -98,6 +128,114 @@ module ActiveRecord 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")`. + # Behaves similarly to +attribute_changed?+. This method is useful in + # after callbacks to determine if the call to save changed a certain + # attribute. + # + # ==== Options + # + # +from+ When passed, this method will return false unless the original + # value is equal to the given option + # + # +to+ When passed, this method will return false unless the value was + # changed to the given value + def saved_change_to_attribute?(attr_name, **options) + mutations_before_last_save.changed?(attr_name, **options) + end + + # Returns the change to an attribute during the last save. If the + # attribute was changed, the result will be an array containing the + # original value and the saved value. + # + # Behaves similarly to +attribute_change+. This method is useful in after + # callbacks, to see the change in an attribute that just occurred + # + # This method can be invoked as `saved_change_to_name` in instead of + # `saved_change_to_attribute("name")` + def saved_change_to_attribute(attr_name) + mutations_before_last_save.change_to_attribute(attr_name) + end + + # Returns the original value of an attribute before the last save. + # Behaves similarly to +attribute_was+. This method is useful in after + # callbacks to get the original value of an attribute before the save that + # just occurred + def attribute_before_last_save(attr_name) + mutations_before_last_save.original_value(attr_name) + end + + # Did the last call to `save` have any changes to change? + def saved_changes? + mutations_before_last_save.any_changes? + end + + # Returns a hash containing all the changes that were just saved. + def saved_changes + mutations_before_last_save.changes + end + + # 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` + def attribute_change_to_be_saved(attr_name) + mutations_from_database.change_to_attribute(attr_name) + end + + # Alias for `attribute_was` + def attribute_in_database(attr_name) + mutations_from_database.original_value(attr_name) + end + + # Alias for `changed?` + def has_changes_to_save? + mutations_from_database.any_changes? + end + + # Alias for `changes` + def changes_to_save + mutations_from_database.changes + end + + # Alias for `changed` + def changed_attribute_names_to_save + changes_to_save.keys + end + + # 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 @@ -107,12 +245,37 @@ module ActiveRecord @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 + mutations_from_database.force_change(attr_name) end def _update_record(*) @@ -124,18 +287,27 @@ module ActiveRecord end def keys_for_partial_write - changed & self.class.column_names + changed_attribute_names_to_save & self.class.column_names end - def store_original_attributes + 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 diff --git a/activerecord/lib/active_record/attribute_methods/primary_key.rb b/activerecord/lib/active_record/attribute_methods/primary_key.rb index 9e99ed8ac1..2f32caa257 100644 --- a/activerecord/lib/active_record/attribute_methods/primary_key.rb +++ b/activerecord/lib/active_record/attribute_methods/primary_key.rb @@ -9,7 +9,7 @@ module ActiveRecord # available. def to_key sync_with_transaction_state - key = self.id + key = id [key] if key end @@ -45,7 +45,12 @@ module ActiveRecord attribute_was(self.class.primary_key) end - protected + def id_in_database + sync_with_transaction_state + attribute_in_database(self.class.primary_key) + end + + private def attribute_method?(attr_name) attr_name == "id" || super @@ -60,7 +65,7 @@ module ActiveRecord end end - ID_ATTRIBUTE_METHODS = %w(id id= id? id_before_type_cast id_was).to_set + ID_ATTRIBUTE_METHODS = %w(id id= id? id_before_type_cast id_was id_in_database).to_set def dangerous_attribute_method?(method_name) super && !ID_ATTRIBUTE_METHODS.include?(method_name) @@ -130,10 +135,10 @@ module ActiveRecord return pk unless pk.is_a?(Array) warn <<-WARNING.strip_heredoc - WARNING: Active Record does not support composite primary key. + WARNING: Active Record does not support composite primary key. - #{table_name} has composite primary key. Composite primary key is ignored. - WARNING + #{table_name} has composite primary key. Composite primary key is ignored. + WARNING end end end diff --git a/activerecord/lib/active_record/attribute_methods/read.rb b/activerecord/lib/active_record/attribute_methods/read.rb index 58f82cfd30..369a6e35aa 100644 --- a/activerecord/lib/active_record/attribute_methods/read.rb +++ b/activerecord/lib/active_record/attribute_methods/read.rb @@ -4,26 +4,26 @@ module ActiveRecord extend ActiveSupport::Concern module ClassMethods - protected + private - # We want to generate the methods via module_eval rather than - # define_method, because define_method is slower on dispatch. - # Evaluating many similar methods may use more memory as the instruction - # sequences are duplicated and cached (in MRI). define_method may - # be slower on dispatch, but if you're careful about the closure - # created, then define_method will consume much less memory. - # - # But sometimes the database might return columns with - # characters that are not allowed in normal method names (like - # 'my_column(omg)'. So to work around this we first define with - # the __temp__ identifier, and then use alias method to rename - # it to what we want. - # - # We are also defining a constant to hold the frozen string of - # the attribute name. Using a constant means that we do not have - # to allocate an object on each call to the attribute method. - # Making it frozen means that it doesn't get duped when used to - # key the @attributes in read_attribute. + # We want to generate the methods via module_eval rather than + # define_method, because define_method is slower on dispatch. + # Evaluating many similar methods may use more memory as the instruction + # sequences are duplicated and cached (in MRI). define_method may + # be slower on dispatch, but if you're careful about the closure + # created, then define_method will consume much less memory. + # + # But sometimes the database might return columns with + # characters that are not allowed in normal method names (like + # 'my_column(omg)'. So to work around this we first define with + # the __temp__ identifier, and then use alias method to rename + # it to what we want. + # + # We are also defining a constant to hold the frozen string of + # the attribute name. Using a constant means that we do not have + # to allocate an object on each call to the attribute method. + # Making it frozen means that it doesn't get duped when used to + # key the @attributes in read_attribute. def define_method_attribute(name) safe_name = name.unpack("h*".freeze).first temp_method = "__temp__#{safe_name}" @@ -31,11 +31,11 @@ module ActiveRecord ActiveRecord::AttributeMethods::AttrNames.set_name_cache safe_name, name generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ + 1 - def #{temp_method} - name = ::ActiveRecord::AttributeMethods::AttrNames::ATTR_#{safe_name} - _read_attribute(name) { |n| missing_attribute(n, caller) } - end - STR + def #{temp_method} + name = ::ActiveRecord::AttributeMethods::AttrNames::ATTR_#{safe_name} + _read_attribute(name) { |n| missing_attribute(n, caller) } + end + STR generated_attribute_methods.module_eval do alias_method name, temp_method @@ -48,7 +48,12 @@ module ActiveRecord # it has been typecast (for example, "2004-12-12" in a date column is cast # to a date object, like Date.new(2004, 12, 12)). def read_attribute(attr_name, &block) - name = attr_name.to_s + name = if self.class.attribute_alias?(attr_name) + self.class.attribute_alias(attr_name).to_s + else + attr_name.to_s + end + name = self.class.primary_key if name == "id".freeze _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 c70178cd2c..945192fe04 100644 --- a/activerecord/lib/active_record/attribute_methods/serialization.rb +++ b/activerecord/lib/active_record/attribute_methods/serialization.rb @@ -26,7 +26,7 @@ module ActiveRecord # ==== Parameters # # * +attr_name+ - The field name that should be serialized. - # * +class_name_or_coder+ - Optional, a coder object, which responds to `.load` / `.dump` + # * +class_name_or_coder+ - Optional, a coder object, which responds to +.load+ and +.dump+ # or a class name that the object type should be equal to. # # ==== Example 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 2c8e86fbb2..df1231ad47 100644 --- a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb +++ b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb @@ -39,7 +39,7 @@ module ActiveRecord end def set_time_zone_without_conversion(value) - ::Time.zone.local_to_utc(value).in_time_zone if value + ::Time.zone.local_to_utc(value).try(:in_time_zone) if value end def map_avoiding_infinite_recursion(value) @@ -63,13 +63,14 @@ module ActiveRecord self.skip_time_zone_conversion_for_attributes = [] class_attribute :time_zone_aware_types, instance_writer: false - self.time_zone_aware_types = [:datetime, :not_explicitly_configured] + self.time_zone_aware_types = [:datetime, :time] end module ClassMethods private def inherited(subclass) + super # We need to apply this decorator here, rather than on module inclusion. The closure # created by the matcher would otherwise evaluate for `ActiveRecord::Base`, not the # sub class being decorated. As such, changes to `time_zone_aware_attributes`, or @@ -80,35 +81,13 @@ module ActiveRecord TimeZoneConverter.new(type) end end - super end def create_time_zone_conversion_attribute?(name, cast_type) enabled_for_column = time_zone_aware_attributes && !skip_time_zone_conversion_for_attributes.include?(name.to_sym) - result = enabled_for_column && - time_zone_aware_types.include?(cast_type.type) - - if enabled_for_column && - !result && - cast_type.type == :time && - time_zone_aware_types.include?(:not_explicitly_configured) - ActiveSupport::Deprecation.warn(<<-MESSAGE.strip_heredoc) - Time columns will become time zone aware in Rails 5.1. This - still causes `String`s to be parsed as if they were in `Time.zone`, - and `Time`s to be converted to `Time.zone`. - - To keep the old behavior, you must add the following to your initializer: - - config.active_record.time_zone_aware_types = [:datetime] - - To silence this deprecation warning, add the following: - - config.active_record.time_zone_aware_types = [:datetime, :time] - MESSAGE - end - result + enabled_for_column && time_zone_aware_types.include?(cast_type.type) end end end diff --git a/activerecord/lib/active_record/attribute_methods/write.rb b/activerecord/lib/active_record/attribute_methods/write.rb index 5822414129..fe0e01db28 100644 --- a/activerecord/lib/active_record/attribute_methods/write.rb +++ b/activerecord/lib/active_record/attribute_methods/write.rb @@ -8,20 +8,20 @@ module ActiveRecord end module ClassMethods - protected + private def define_method_attribute=(name) safe_name = name.unpack("h*".freeze).first ActiveRecord::AttributeMethods::AttrNames.set_name_cache safe_name, name 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) - end - alias_method #{(name + '=').inspect}, :__temp__#{safe_name}= - undef_method :__temp__#{safe_name}= - STR + def __temp__#{safe_name}=(value) + name = ::ActiveRecord::AttributeMethods::AttrNames::ATTR_#{safe_name} + write_attribute(name, value) + end + alias_method #{(name + '=').inspect}, :__temp__#{safe_name}= + undef_method :__temp__#{safe_name}= + STR end end @@ -29,7 +29,13 @@ module ActiveRecord # specified +value+. Empty strings for Integer and Float columns are # turned into +nil+. def write_attribute(attr_name, value) - write_attribute_with_type_cast(attr_name, value, true) + name = if self.class.attribute_alias?(attr_name) + self.class.attribute_alias(attr_name).to_s + else + attr_name.to_s + end + + write_attribute_with_type_cast(name, value, true) end def raw_write_attribute(attr_name, value) # :nodoc: @@ -37,7 +43,7 @@ module ActiveRecord end private - # Handle *= for method_missing. + # Handle *= for method_missing. def attribute=(attribute_name, value) write_attribute(attribute_name, value) end diff --git a/activerecord/lib/active_record/attribute_mutation_tracker.rb b/activerecord/lib/active_record/attribute_mutation_tracker.rb index c257aef52f..3417090830 100644 --- a/activerecord/lib/active_record/attribute_mutation_tracker.rb +++ b/activerecord/lib/active_record/attribute_mutation_tracker.rb @@ -1,7 +1,10 @@ module ActiveRecord class AttributeMutationTracker # :nodoc: + OPTION_NOT_GIVEN = Object.new + def initialize(attributes) @attributes = attributes + @forced_changes = Set.new end def changed_values @@ -14,15 +17,29 @@ module ActiveRecord def changes attr_names.each_with_object({}.with_indifferent_access) do |attr_name, result| - if changed?(attr_name) - result[attr_name] = [attributes[attr_name].original_value, attributes.fetch_value(attr_name)] + change = change_to_attribute(attr_name) + if change + result[attr_name] = change end end end - def changed?(attr_name) + def change_to_attribute(attr_name) + 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) } + end + + def changed?(attr_name, from: OPTION_NOT_GIVEN, to: OPTION_NOT_GIVEN) attr_name = attr_name.to_s - attributes[attr_name].changed? + 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) @@ -32,11 +49,22 @@ module ActiveRecord 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].original_value + end + + def force_change(attr_name) + 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 + attr_reader :attributes, :forced_changes private @@ -48,14 +76,21 @@ module ActiveRecord class NullMutationTracker # :nodoc: include Singleton - def changed_values + def changed_values(*) {} end - def changes + def changes(*) {} end + def change_to_attribute(attr_name) + end + + def any_changes?(*) + false + end + def changed?(*) false end @@ -66,5 +101,8 @@ module ActiveRecord 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 index 5bde1f107c..66b278219a 100644 --- a/activerecord/lib/active_record/attribute_set.rb +++ b/activerecord/lib/active_record/attribute_set.rb @@ -98,6 +98,8 @@ module ActiveRecord 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 diff --git a/activerecord/lib/active_record/attribute_set/builder.rb b/activerecord/lib/active_record/attribute_set/builder.rb index 661f996e1a..2f624d32af 100644 --- a/activerecord/lib/active_record/attribute_set/builder.rb +++ b/activerecord/lib/active_record/attribute_set/builder.rb @@ -90,6 +90,8 @@ module ActiveRecord @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 diff --git a/activerecord/lib/active_record/attribute_set/yaml_encoder.rb b/activerecord/lib/active_record/attribute_set/yaml_encoder.rb index c86cfc4263..899de14792 100644 --- a/activerecord/lib/active_record/attribute_set/yaml_encoder.rb +++ b/activerecord/lib/active_record/attribute_set/yaml_encoder.rb @@ -31,6 +31,8 @@ module ActiveRecord end end + # TODO Change this to private once we've dropped Ruby 2.2 support. + # Workaround for Ruby 2.2 "private attribute?" warning. protected attr_reader :default_types diff --git a/activerecord/lib/active_record/attributes.rb b/activerecord/lib/active_record/attributes.rb index 4b92e5835f..75f5ba3a96 100644 --- a/activerecord/lib/active_record/attributes.rb +++ b/activerecord/lib/active_record/attributes.rb @@ -116,7 +116,7 @@ module ActiveRecord # Users may also define their own custom types, as long as they respond # to the methods defined on the value type. The method +deserialize+ or # +cast+ will be called on your type object, with raw input from the - # database or from your controllers. See ActiveRecord::Type::Value for the + # database or from your controllers. See ActiveModel::Type::Value for the # expected API. It is recommended that your type objects inherit from an # existing type, or from ActiveRecord::Type::Value # @@ -143,7 +143,7 @@ module ActiveRecord # store_listing.price_in_cents # => 1000 # # For more details on creating custom types, see the documentation for - # ActiveRecord::Type::Value. For more details on registering your types + # ActiveModel::Type::Value. For more details on registering your types # to be referenced by a symbol, see ActiveRecord::Type.register. You can # also pass a type object directly, in place of a symbol. # @@ -190,8 +190,8 @@ module ActiveRecord # The type of an attribute is given the opportunity to change how dirty # tracking is performed. The methods +changed?+ and +changed_in_place?+ # will be called from ActiveModel::Dirty. See the documentation for those - # methods in ActiveRecord::Type::Value for more details. - def attribute(name, cast_type, **options) + # methods in ActiveModel::Type::Value for more details. + def attribute(name, cast_type = Type::Value.new, **options) name = name.to_s reload_schema_from_cache diff --git a/activerecord/lib/active_record/autosave_association.rb b/activerecord/lib/active_record/autosave_association.rb index 6e6620aad5..6bccbc06cd 100644 --- a/activerecord/lib/active_record/autosave_association.rb +++ b/activerecord/lib/active_record/autosave_association.rb @@ -154,10 +154,10 @@ module ActiveRecord # Loop prevention for validation of associations unless @_already_called[name] begin - @_already_called[name]=true + @_already_called[name] = true result = instance_eval(&block) ensure - @_already_called[name]=false + @_already_called[name] = false end end @@ -267,7 +267,7 @@ module ActiveRecord # Returns whether or not this record has been changed in any way (including whether # any of its nested autosave associations are likewise changed) def changed_for_autosave? - new_record? || changed? || marked_for_destruction? || nested_records_changed_for_autosave? + new_record? || has_changes_to_save? || marked_for_destruction? || nested_records_changed_for_autosave? end private @@ -325,30 +325,24 @@ module ActiveRecord # Returns whether or not the association is valid and applies any errors to # the parent, <tt>self</tt>, if it wasn't. Skips any <tt>:autosave</tt> # enabled records if they're marked_for_destruction? or destroyed. - def association_valid?(reflection, record, index=nil) + def association_valid?(reflection, record, index = nil) return true if record.destroyed? || (reflection.options[:autosave] && record.marked_for_destruction?) - validation_context = self.validation_context unless [:create, :update].include?(self.validation_context) - unless valid = record.valid?(validation_context) + context = validation_context unless [:create, :update].include?(validation_context) + + unless valid = record.valid?(context) if reflection.options[:autosave] indexed_attribute = !index.nil? && (reflection.options[:index_errors] || ActiveRecord::Base.index_nested_attribute_errors) record.errors.each do |attribute, message| - if indexed_attribute - attribute = "#{reflection.name}[#{index}].#{attribute}" - else - attribute = "#{reflection.name}.#{attribute}" - end + attribute = normalize_reflection_attribute(indexed_attribute, reflection, index, attribute) errors[attribute] << message errors[attribute].uniq! end record.errors.details.each_key do |attribute| - if indexed_attribute - reflection_attribute = "#{reflection.name}[#{index}].#{attribute}" - else - reflection_attribute = "#{reflection.name}.#{attribute}" - end + reflection_attribute = + normalize_reflection_attribute(indexed_attribute, reflection, index, attribute).to_sym record.errors.details[attribute].each do |error| errors.details[reflection_attribute] << error @@ -362,6 +356,14 @@ module ActiveRecord valid end + def normalize_reflection_attribute(indexed_attribute, reflection, index, attribute) + if indexed_attribute + "#{reflection.name}[#{index}].#{attribute}" + else + "#{reflection.name}.#{attribute}" + end + end + # Is used as a before_save callback to check while saving a collection # association whether or not the parent was a new record before saving. def before_save_collection_association @@ -381,6 +383,9 @@ module ActiveRecord if association = association_instance_get(reflection.name) autosave = reflection.options[:autosave] + # reconstruct the scope now that we know the owner's id + association.reset_scope if association.respond_to?(:reset_scope) + if records = associated_records_to_validate_or_save(association, @new_record_before_save, autosave) if autosave records_to_destroy = records.select(&:marked_for_destruction?) @@ -406,9 +411,6 @@ module ActiveRecord raise ActiveRecord::Rollback unless saved end end - - # reconstruct the scope now that we know the owner's id - association.reset_scope if association.respond_to?(:reset_scope) end end @@ -449,7 +451,7 @@ module ActiveRecord def record_changed?(reflection, record, key) record.new_record? || (record.has_attribute?(reflection.foreign_key) && record[reflection.foreign_key] != key) || - record.attribute_changed?(reflection.foreign_key) + record.will_save_change_to_attribute?(reflection.foreign_key) end # Saves the associated record if it's new or <tt>:autosave</tt> is enabled. @@ -457,7 +459,9 @@ module ActiveRecord # In addition, it will destroy the association if it was marked for destruction. def save_belongs_to_association(reflection) association = association_instance_get(reflection.name) - record = association && association.load_target + return unless association && association.loaded? && !association.stale_target? + + record = association.load_target if record && !record.destroyed? autosave = reflection.options[:autosave] diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index 01d7886406..ac1aa2df45 100644 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -14,6 +14,7 @@ require "active_support/core_ext/module/introspection" require "active_support/core_ext/object/duplicable" require "active_support/core_ext/class/subclasses" require "active_record/attribute_decorators" +require "active_record/define_callbacks" require "active_record/errors" require "active_record/log_subscriber" require "active_record/explain_subscriber" @@ -303,6 +304,7 @@ module ActiveRecord #:nodoc: include AttributeDecorators include Locking::Optimistic include Locking::Pessimistic + include DefineCallbacks include AttributeMethods include Callbacks include Timestamp @@ -312,8 +314,8 @@ module ActiveRecord #:nodoc: include NestedAttributes include Aggregations include Transactions - include NoTouching include TouchLater + include NoTouching include Reflection include Serialization include Store diff --git a/activerecord/lib/active_record/callbacks.rb b/activerecord/lib/active_record/callbacks.rb index c616733aa4..eb44887e18 100644 --- a/activerecord/lib/active_record/callbacks.rb +++ b/activerecord/lib/active_record/callbacks.rb @@ -225,6 +225,55 @@ module ActiveRecord # # This way, the +before_destroy+ gets executed before the <tt>dependent: :destroy</tt> is called, and the data is still available. # + # Also, there are cases when you want several callbacks of the same type to + # be executed in order. + # + # For example: + # + # class Topic + # has_many :children + # + # after_save :log_children + # after_save :do_something_else + # + # private + # + # def log_chidren + # # Child processing + # end + # + # def do_something_else + # # Something else + # end + # end + # + # In this case the +log_children+ gets executed before +do_something_else+. + # The same applies to all non-transactional callbacks. + # + # In case there are multiple transactional callbacks as seen below, the order + # is reversed. + # + # For example: + # + # class Topic + # has_many :children + # + # after_commit :log_children + # after_commit :do_something_else + # + # private + # + # def log_chidren + # # Child processing + # end + # + # def do_something_else + # # Something else + # end + # end + # + # In this case the +do_something_else+ gets executed before +log_children+. + # # == \Transactions # # The entire callback chain of a {#save}[rdoc-ref:Persistence#save], {#save!}[rdoc-ref:Persistence#save!], @@ -265,17 +314,6 @@ module ActiveRecord :before_destroy, :around_destroy, :after_destroy, :after_commit, :after_rollback ] - module ClassMethods # :nodoc: - include ActiveModel::Callbacks - end - - included do - include ActiveModel::Validations::Callbacks - - define_model_callbacks :initialize, :find, :touch, only: :after - define_model_callbacks :save, :create, :update, :destroy - end - def destroy #:nodoc: @_destroy_callback_already_called ||= false return if @_destroy_callback_already_called @@ -294,15 +332,15 @@ module ActiveRecord private - def create_or_update(*) #:nodoc: + def create_or_update(*) _run_save_callbacks { super } end - def _create_record #:nodoc: + def _create_record _run_create_callbacks { super } end - def _update_record(*) #:nodoc: + def _update_record(*) _run_update_callbacks { super } end end diff --git a/activerecord/lib/active_record/coders/yaml_column.rb b/activerecord/lib/active_record/coders/yaml_column.rb index 1c8c9fa272..3a04a10fc9 100644 --- a/activerecord/lib/active_record/coders/yaml_column.rb +++ b/activerecord/lib/active_record/coders/yaml_column.rb @@ -1,5 +1,4 @@ require "yaml" -require "active_support/core_ext/regexp" module ActiveRecord module Coders # :nodoc: 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 535d79b525..5ec2fc073e 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb @@ -69,7 +69,7 @@ module ActiveRecord # 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). + # blocking wait. (Default +nil+, which means don't schedule the Reaper). # #-- # Synchronization policy: @@ -116,7 +116,7 @@ module ActiveRecord end end - # If +element+ is in the queue, remove and return it, or nil. + # If +element+ is in the queue, remove and return it, or +nil+. def delete(element) synchronize do @queue.delete(element) @@ -135,7 +135,7 @@ module ActiveRecord # If +timeout+ is not given, remove and return the head the # queue if the number of available elements is strictly # greater than the number of threads currently waiting (that - # is, don't jump ahead in line). Otherwise, return nil. + # is, don't jump ahead in line). Otherwise, return +nil+. # # If +timeout+ is given, block if there is no element # available, waiting up to +timeout+ seconds for an element to @@ -158,33 +158,33 @@ module ActiveRecord @lock.synchronize(&block) end - # Test if the queue currently contains any elements. + # Test if the queue currently contains any elements. def any? !@queue.empty? end - # A thread can remove an element from the queue without - # waiting if and only if the number of currently available - # connections is strictly greater than the number of waiting - # threads. + # A thread can remove an element from the queue without + # waiting if and only if the number of currently available + # connections is strictly greater than the number of waiting + # threads. def can_remove_no_wait? @queue.size > @num_waiting end - # Removes and returns the head of the queue if possible, or nil. + # Removes and returns the head of the queue if possible, or +nil+. def remove @queue.shift end - # Remove and return the head the queue if the number of - # available elements is strictly greater than the number of - # threads currently waiting. Otherwise, return nil. + # Remove and return the head the queue if the number of + # available elements is strictly greater than the number of + # threads currently waiting. Otherwise, return +nil+. def no_wait_poll remove if can_remove_no_wait? end - # Waits on the queue up to +timeout+ seconds, then removes and - # returns the head of the queue. + # Waits on the queue up to +timeout+ seconds, then removes and + # returns the head of the queue. def wait_poll(timeout) @num_waiting += 1 @@ -282,7 +282,7 @@ module ActiveRecord end # Every +frequency+ seconds, the reaper will call +reap+ on +pool+. - # A reaper instantiated with a nil frequency will never reap the + # A reaper instantiated with a +nil+ frequency will never reap the # connection pool. # # Configure the frequency by setting "reaping_frequency" in your @@ -307,6 +307,7 @@ module ActiveRecord end include MonitorMixin + include QueryCache::ConnectionPoolConfiguration attr_accessor :automatic_reconnect, :checkout_timeout, :schema_cache attr_reader :spec, :connections, :size, :reaper @@ -349,8 +350,7 @@ module ActiveRecord # currently in the process of independently establishing connections to the DB. @now_connecting = 0 - # A boolean toggle that allows/disallows new connections. - @new_cons_enabled = true + @threads_blocking_new_connections = 0 @available = ConnectionLeasingQueue.new self end @@ -445,8 +445,6 @@ module ActiveRecord # connections in the pool within a timeout interval (default duration is # <tt>spec.config[:checkout_timeout] * 2</tt> seconds). def clear_reloadable_connections(raise_on_acquisition_timeout = true) - num_new_conns_required = 0 - with_exclusively_acquired_all_connections(raise_on_acquisition_timeout) do synchronize do @connections.each do |conn| @@ -457,24 +455,9 @@ module ActiveRecord conn.disconnect! if conn.requires_reloading? end @connections.delete_if(&:requires_reloading?) - @available.clear - - if @connections.size < @size - # because of the pruning done by this method, we might be running - # low on connections, while threads stuck in queue are helpless - # (not being able to establish new connections for themselves), - # see also more detailed explanation in +remove+ - num_new_conns_required = num_waiting_in_queue - @connections.size - end - - @connections.each do |conn| - @available.add conn - end end end - - bulk_make_new_connections(num_new_conns_required) if num_new_conns_required > 0 end # Clears the cache which maps classes and re-connects connections that @@ -581,9 +564,27 @@ module ActiveRecord @available.num_waiting end + # Return connection pool's usage statistic + # Example: + # + # ActiveRecord::Base.connection_pool.stat # => { size: 15, connections: 1, busy: 1, dead: 0, idle: 0, waiting: 0, checkout_timeout: 5 } + def stat + synchronize do + { + size: size, + connections: @connections.size, + busy: @connections.count { |c| c.in_use? && c.owner.alive? }, + dead: @connections.count { |c| c.in_use? && !c.owner.alive? }, + idle: @connections.count { |c| !c.in_use? }, + waiting: num_waiting_in_queue, + checkout_timeout: checkout_timeout + } + end + end + private - #-- - # this is unfortunately not concurrent + #-- + # this is unfortunately not concurrent def bulk_make_new_connections(num_new_conns_needed) num_new_conns_needed.times do # try_to_checkout_new_connection will not exceed pool's @size limit @@ -594,19 +595,19 @@ module ActiveRecord end end - #-- - # From the discussion on GitHub: - # https://github.com/rails/rails/pull/14938#commitcomment-6601951 - # This hook-in method allows for easier monkey-patching fixes needed by - # JRuby users that use Fibers. + #-- + # From the discussion on GitHub: + # https://github.com/rails/rails/pull/14938#commitcomment-6601951 + # This hook-in method allows for easier monkey-patching fixes needed by + # JRuby users that use Fibers. def connection_cache_key(thread) thread end - # Take control of all existing connections so a "group" action such as - # reload/disconnect can be performed safely. It is no longer enough to - # wrap it in +synchronize+ because some pool's actions are allowed - # to be performed outside of the main +synchronize+ block. + # Take control of all existing connections so a "group" action such as + # reload/disconnect can be performed safely. It is no longer enough to + # wrap it in +synchronize+ because some pool's actions are allowed + # to be performed outside of the main +synchronize+ block. def with_exclusively_acquired_all_connections(raise_on_acquisition_timeout = true) with_new_connections_blocked do attempt_to_checkout_all_existing_connections(raise_on_acquisition_timeout) @@ -617,7 +618,7 @@ module ActiveRecord def attempt_to_checkout_all_existing_connections(raise_on_acquisition_timeout = true) collected_conns = synchronize do # account for our own connections - @connections.select {|conn| conn.owner == Thread.current} + @connections.select { |conn| conn.owner == Thread.current } end newly_checked_out = [] @@ -654,12 +655,12 @@ module ActiveRecord if release_newly_checked_out && newly_checked_out # releasing only those conns that were checked out in this method, conns # checked outside this method (before it was called) are not for us to release - newly_checked_out.each {|conn| checkin(conn)} + newly_checked_out.each { |conn| checkin(conn) } end end - #-- - # Must be called in a synchronize block. + #-- + # Must be called in a synchronize block. def checkout_for_exclusive_access(checkout_timeout) checkout(checkout_timeout) rescue ConnectionTimeoutError @@ -681,26 +682,45 @@ module ActiveRecord end def with_new_connections_blocked - previous_value = nil synchronize do - previous_value, @new_cons_enabled = @new_cons_enabled, false + @threads_blocking_new_connections += 1 end + yield ensure - synchronize { @new_cons_enabled = previous_value } + num_new_conns_required = 0 + + synchronize do + @threads_blocking_new_connections -= 1 + + if @threads_blocking_new_connections.zero? + @available.clear + + num_new_conns_required = num_waiting_in_queue + + @connections.each do |conn| + next if conn.in_use? + + @available.add conn + num_new_conns_required -= 1 + end + end + end + + bulk_make_new_connections(num_new_conns_required) if num_new_conns_required > 0 end - # Acquire a connection by one of 1) immediately removing one - # from the queue of available connections, 2) creating a new - # connection if the pool is not at capacity, 3) waiting on the - # queue for a connection to become available. - # - # Raises: - # - ActiveRecord::ConnectionTimeoutError if a connection could not be acquired - # - #-- - # Implementation detail: the connection returned by +acquire_connection+ - # will already be "+connection.lease+ -ed" to the current thread. + # Acquire a connection by one of 1) immediately removing one + # from the queue of available connections, 2) creating a new + # connection if the pool is not at capacity, 3) waiting on the + # queue for a connection to become available. + # + # Raises: + # - ActiveRecord::ConnectionTimeoutError if a connection could not be acquired + # + #-- + # 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 # +conn.lease+ the returned connection (and to do this in a +synchronized+ @@ -716,8 +736,8 @@ module ActiveRecord end end - #-- - # if owner_thread param is omitted, this must be called in synchronize block + #-- + # if owner_thread param is omitted, this must be called in synchronize block def remove_connection_from_thread_cache(conn, owner_thread = conn.owner) @thread_cached_conns.delete_pair(connection_cache_key(owner_thread), conn) end @@ -729,17 +749,17 @@ module ActiveRecord end end - # If the pool is not at a +@size+ limit, establish new connection. Connecting - # to the DB is done outside main synchronized section. - #-- - # Implementation constraint: a newly established connection returned by this - # method must be in the +.leased+ state. + # If the pool is not at a +@size+ limit, establish new connection. Connecting + # to the DB is done outside main synchronized section. + #-- + # Implementation constraint: a newly established connection returned by this + # method must be in the +.leased+ state. def try_to_checkout_new_connection # first in synchronized section check if establishing new conns is allowed # and increment @now_connecting, to prevent overstepping this pool's @size # constraint do_checkout = synchronize do - if @new_cons_enabled && (@connections.size + @now_connecting) < @size + if @threads_blocking_new_connections.zero? && (@connections.size + @now_connecting) < @size @now_connecting += 1 end end @@ -833,7 +853,7 @@ module ActiveRecord class ConnectionHandler def initialize # These caches are keyed by spec.name (ConnectionSpecification#name). - @owner_to_pool = Concurrent::Map.new(initial_capacity: 2) do |h,k| + @owner_to_pool = Concurrent::Map.new(initial_capacity: 2) do |h, k| h[k] = Concurrent::Map.new(initial_capacity: 2) end end 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 95c72f1e20..407e019326 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/database_limits.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/database_limits.rb @@ -46,7 +46,7 @@ module ActiveRecord end # Returns the maximum number of elements in an IN (x,y,z) clause. - # nil means no limit. + # +nil+ means no limit. def in_clause_length nil end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb index 452e78a40b..769f488469 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb @@ -10,9 +10,9 @@ module ActiveRecord def to_sql(arel, binds = []) if arel.respond_to?(:ast) collected = visitor.accept(arel.ast, collector) - collected.compile(binds, self) + collected.compile(binds, self).freeze else - arel + arel.dup.freeze end end @@ -51,8 +51,7 @@ module ActiveRecord # Returns a single value from a record def select_value(arel, name = nil, binds = []) - arel, binds = binds_from_relation arel, binds - if result = select_rows(to_sql(arel, binds), name, binds).first + if result = select_rows(arel, name, binds).first result.first end end @@ -60,14 +59,13 @@ module ActiveRecord # Returns an array of the values of the first column in a select: # select_values("SELECT id FROM companies LIMIT 3") => [1,2,3] def select_values(arel, name = nil, binds = []) - arel, binds = binds_from_relation arel, binds - select_rows(to_sql(arel, binds), name, binds).map(&:first) + select_rows(arel, name, binds).map(&:first) end # Returns an array of arrays containing the field values. # Order is the same as that returned by +columns+. - def select_rows(sql, name = nil, binds = []) - exec_query(sql, name, binds).rows + def select_rows(arel, name = nil, binds = []) + select_all(arel, name, binds).rows end # Executes the SQL statement in the context of this connection and returns @@ -90,6 +88,7 @@ module ActiveRecord # +binds+ as the bind substitutes. +name+ is logged along with # the executed +sql+ statement. def exec_insert(sql, name = nil, binds = [], pk = nil, sequence_name = nil) + sql, binds = sql_for_insert(sql, pk, nil, sequence_name, binds) exec_query(sql, name, binds) end @@ -114,34 +113,27 @@ module ActiveRecord # Executes an INSERT query and returns the new record's ID # - # +id_value+ will be returned unless the value is nil, in + # +id_value+ will be returned unless the value is +nil+, in # which case the database will attempt to calculate the last inserted # id and return that value. # # 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 = []) - sql, binds, pk, sequence_name = sql_for_insert(to_sql(arel, binds), pk, id_value, sequence_name, binds) - value = exec_insert(sql, name, binds, pk, sequence_name) + value = exec_insert(to_sql(arel, binds), name, binds, pk, sequence_name) id_value || last_inserted_id(value) end alias create insert - alias insert_sql insert - deprecate insert_sql: :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) end - alias update_sql update - deprecate update_sql: :update # 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) end - alias delete_sql delete - deprecate delete_sql: :delete # Returns +true+ when the connection adapter supports prepared statement # caching, otherwise returns +false+ @@ -245,7 +237,7 @@ module ActiveRecord end def reset_transaction #:nodoc: - @transaction_manager = TransactionManager.new(self) + @transaction_manager = ConnectionAdapters::TransactionManager.new(self) end # Register a record with the current transaction so that its after_commit and after_rollback callbacks @@ -334,17 +326,12 @@ module ActiveRecord # Sanitizes the given LIMIT parameter in order to prevent SQL injection. # # The +limit+ may be anything that can evaluate to a string via #to_s. It - # should look like an integer, or a comma-delimited list of integers, or - # an Arel SQL literal. + # should look like an integer, or an Arel SQL literal. # # Returns Integer and Arel::Nodes::SqlLiteral limits as is. - # Returns the sanitized limit parameter, either as an integer, or as a - # string which contains a comma-delimited list of integers. def sanitize_limit(limit) if limit.is_a?(Integer) || limit.is_a?(Arel::Nodes::SqlLiteral) limit - elsif limit.to_s.include?(",") - Arel.sql limit.to_s.split(",").map{ |i| Integer(i) }.join(",") else Integer(limit) end @@ -360,7 +347,7 @@ module ActiveRecord end alias join_to_delete join_to_update - protected + private # Returns a subquery for the given key using the join information. def subquery_for(key, select) @@ -379,7 +366,7 @@ module ActiveRecord end def sql_for_insert(sql, pk, id_value, sequence_name, binds) - [sql, binds, pk, sequence_name] + [sql, binds] end def last_inserted_id(result) 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 10c60080d5..7eab7de5d3 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb @@ -4,6 +4,9 @@ module ActiveRecord class << self def included(base) #:nodoc: dirties_query_cache base, :insert, :update, :delete, :rollback_to_savepoint, :rollback_db_transaction + + base.set_callback :checkout, :after, :configure_query_cache! + base.set_callback :checkin, :after, :disable_query_cache! end def dirties_query_cache(base, *method_names) @@ -18,11 +21,32 @@ module ActiveRecord end end + module ConnectionPoolConfiguration + def initialize(*) + super + @query_cache_enabled = Concurrent::Map.new { false } + end + + def enable_query_cache! + @query_cache_enabled[connection_cache_key(Thread.current)] = true + connection.enable_query_cache! if active_connection? + end + + def disable_query_cache! + @query_cache_enabled.delete connection_cache_key(Thread.current) + connection.disable_query_cache! if active_connection? + end + + def query_cache_enabled + @query_cache_enabled[connection_cache_key(Thread.current)] + end + end + attr_reader :query_cache, :query_cache_enabled def initialize(*) super - @query_cache = Hash.new { |h,sql| h[sql] = {} } + @query_cache = Hash.new { |h, sql| h[sql] = {} } @query_cache_enabled = false end @@ -41,6 +65,7 @@ module ActiveRecord def disable_query_cache! @query_cache_enabled = false + clear_query_cache end # Disable the query cache within the block. @@ -65,7 +90,7 @@ module ActiveRecord if @query_cache_enabled && !locked?(arel) arel, binds = binds_from_relation arel, binds sql = to_sql(arel, binds) - cache_sql(sql, binds) { super(sql, name, binds, preparable: preparable) } + cache_sql(sql, name, binds) { super(sql, name, binds, preparable: preparable) } else super end @@ -73,11 +98,17 @@ module ActiveRecord private - def cache_sql(sql, binds) + def cache_sql(sql, name, binds) result = if @query_cache[sql].key?(binds) - ActiveSupport::Notifications.instrument("sql.active_record", - sql: sql, binds: binds, name: "CACHE", connection_id: object_id) + ActiveSupport::Notifications.instrument( + "sql.active_record", + sql: sql, + binds: binds, + name: name, + connection_id: object_id, + cached: true, + ) @query_cache[sql][binds] else @query_cache[sql][binds] = yield @@ -85,11 +116,15 @@ module ActiveRecord result.dup end - # If arel is locked this is a SELECT ... FOR UPDATE or somesuch. Such - # queries should not be cached. + # If arel is locked this is a SELECT ... FOR UPDATE or somesuch. Such + # queries should not be cached. def locked?(arel) arel.respond_to?(:locked) && arel.locked end + + def configure_query_cache! + enable_query_cache! if pool.query_cache_enabled + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb index bbd52b8a91..0c6bc16e6f 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb @@ -5,20 +5,10 @@ module ActiveRecord module Quoting # Quotes the column value to help prevent # {SQL injection attacks}[http://en.wikipedia.org/wiki/SQL_injection]. - def quote(value, column = nil) + def quote(value) # records are quoted as their primary key return value.quoted_id if value.respond_to?(:quoted_id) - if column - ActiveSupport::Deprecation.warn(<<-MSG.squish) - Passing a column to `quote` has been deprecated. It is only used - for type casting, which should be handled elsewhere. See - https://github.com/rails/arel/commit/6160bfbda1d1781c3b08a33ec4955f170e95be11 - for more information. - MSG - value = type_cast_from_column(column, value) - end - _quote(value) end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb index c10f45937e..9b324c090b 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb @@ -71,7 +71,7 @@ module ActiveRecord polymorphic: false, index: true, foreign_key: false, - type: :integer, + type: :bigint, **options ) @name = name @@ -100,6 +100,8 @@ module ActiveRecord end end + # TODO Change this to private once we've dropped Ruby 2.2 support. + # Workaround for Ruby 2.2 "private attribute?" warning. protected attr_reader :name, :polymorphic, :index, :foreign_key, :type, :options @@ -211,7 +213,7 @@ module ActiveRecord def initialize(name, temporary = false, options = nil, as = nil, comment: nil) @columns_hash = {} - @indexes = {} + @indexes = [] @foreign_keys = [] @primary_keys = nil @temporary = temporary @@ -303,7 +305,7 @@ module ActiveRecord # end def column(name, type, options = {}) name = name.to_s - type = type.to_sym + type = type.to_sym if type options = options.dup if @columns_hash[name] && @columns_hash[name].primary_key? @@ -327,7 +329,7 @@ module ActiveRecord # # index(:account_id, name: 'index_projects_on_account_id') def index(column_name, options = {}) - indexes[column_name] = options + indexes << [column_name, options] end def foreign_key(table_name, options = {}) # :nodoc: @@ -341,9 +343,7 @@ module ActiveRecord # <tt>:updated_at</tt> to the table. See {connection.add_timestamps}[rdoc-ref:SchemaStatements#add_timestamps] # # t.timestamps null: false - def timestamps(*args) - options = args.extract_options! - + def timestamps(**options) options[:null] = false if options[:null].nil? column(:created_at, :datetime, options) @@ -477,7 +477,7 @@ module ActiveRecord # Checks to see if a column exists. # - # t.string(:name) unless t.column_exists?(:name, :string) + # t.string(:name) unless t.column_exists?(:name, :string) # # See {connection.column_exists?}[rdoc-ref:SchemaStatements#column_exists?] def column_exists?(column_name, type = nil, options = {}) @@ -498,9 +498,9 @@ module ActiveRecord # Checks to see if an index exists. # - # unless t.index_exists?(:branch_id) - # t.index(:branch_id) - # end + # unless t.index_exists?(:branch_id) + # t.index(:branch_id) + # end # # See {connection.index_exists?}[rdoc-ref:SchemaStatements#index_exists?] def index_exists?(column_name, options = {}) 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 8bb7362c2e..b912d24626 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb @@ -7,10 +7,7 @@ module ActiveRecord # Adapter level by over-writing this code inside the database specific adapters module ColumnDumper def column_spec(column) - spec = Hash[prepare_column_options(column).map { |k, v| [k, "#{k}: #{v}"] }] - spec[:name] = column.name.inspect - spec[:type] = schema_type(column).to_s - spec + [schema_type(column), prepare_column_options(column)] end def column_spec_for_primary_key(column) @@ -38,7 +35,7 @@ module ActiveRecord end default = schema_default(column) if column.has_default? - spec[:default] = default unless default.nil? + spec[:default] = default unless default.nil? spec[:null] = "false" unless column.null @@ -53,13 +50,13 @@ module ActiveRecord # Lists the valid migration options def migration_keys - [:name, :limit, :precision, :scale, :default, :null, :collation, :comment] + [:limit, :precision, :scale, :default, :null, :collation, :comment] end private def default_primary_key?(column) - schema_type(column) == :integer + schema_type(column) == :bigint end def schema_type(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 d0aefcef68..1bdc086380 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb @@ -43,7 +43,7 @@ module ActiveRecord end # Returns an array of table names defined in the database. - def tables(name = nil) + def tables raise NotImplementedError, "#tables is not implemented" end @@ -69,7 +69,9 @@ module ActiveRecord end # Returns an array of indexes for the given table. - # def indexes(table_name, name = nil) end + def indexes(table_name, name = nil) + raise NotImplementedError, "#indexes is not implemented" + end # Checks to see if an index exists on a table for a given index definition. # @@ -120,7 +122,7 @@ module ActiveRecord checks = [] checks << lambda { |c| c.name == column_name } checks << lambda { |c| c.type == type } if type - (migration_keys - [:name]).each do |attr| + migration_keys.each do |attr| checks << lambda { |c| c.send(attr) == options[attr] } if options.key?(attr) end @@ -278,16 +280,16 @@ module ActiveRecord result = execute schema_creation.accept td unless supports_indexes_in_create? - td.indexes.each_pair do |column_name, index_options| + td.indexes.each do |column_name, index_options| add_index(table_name, column_name, index_options) end end if supports_comments? && !supports_comments_in_create? - change_table_comment(table_name, comment) if comment + change_table_comment(table_name, comment) if comment.present? td.columns.each do |column| - change_column_comment(table_name, column.name, column.comment) if column.comment + change_column_comment(table_name, column.name, column.comment) if column.comment.present? end end @@ -339,7 +341,7 @@ module ActiveRecord column_options.reverse_merge!(null: false) type = column_options.delete(:type) || :integer - t1_column, t2_column = [table_1, table_2].map{ |t| t.to_s.singularize.foreign_key } + t1_column, t2_column = [table_1, table_2].map { |t| t.to_s.singularize.foreign_key } create_table(join_table_name, options.merge!(id: false)) do |td| td.send type, t1_column, column_options @@ -511,7 +513,7 @@ module ActiveRecord # Default is (38,0). # * DB2: <tt>:precision</tt> [1..63], <tt>:scale</tt> [0..62]. # Default unknown. - # * SqlServer?: <tt>:precision</tt> [1..38], <tt>:scale</tt> [0..38]. + # * SqlServer: <tt>:precision</tt> [1..38], <tt>:scale</tt> [0..38]. # Default (38,0). # # == Examples @@ -533,6 +535,10 @@ module ActiveRecord # add_column(:measurements, :huge_integer, :decimal, precision: 30) # # ALTER TABLE "measurements" ADD "huge_integer" decimal(30) # + # # Defines a column that stores an array of a type. + # add_column(:users, :skills, :text, array: true) + # # ALTER TABLE "users" ADD "skills" text[] + # # # Defines a column with a database-specific type. # add_column(:shapes, :triangle, 'polygon') # # ALTER TABLE "shapes" ADD "triangle" polygon @@ -962,12 +968,12 @@ module ActiveRecord def foreign_key_for(from_table, options_or_to_table = {}) # :nodoc: return unless supports_foreign_keys? - foreign_keys(from_table).detect {|fk| fk.defined_for? options_or_to_table } + foreign_keys(from_table).detect { |fk| fk.defined_for? options_or_to_table } end def foreign_key_for!(from_table, options_or_to_table = {}) # :nodoc: - foreign_key_for(from_table, options_or_to_table) or \ - raise ArgumentError, "Table '#{from_table}' has no foreign key for #{options_or_to_table}" + foreign_key_for(from_table, options_or_to_table) || \ + raise(ArgumentError, "Table '#{from_table}' has no foreign key for #{options_or_to_table}") end def foreign_key_column_for(table_name) # :nodoc: @@ -990,17 +996,15 @@ module ActiveRecord end def insert_versions_sql(versions) # :nodoc: - sm_table = ActiveRecord::Migrator.schema_migrations_table_name + sm_table = quote_table_name(ActiveRecord::Migrator.schema_migrations_table_name) - if supports_multi_insert? + if versions.is_a?(Array) sql = "INSERT INTO #{sm_table} (version) VALUES\n" - sql << versions.map {|v| "('#{v}')" }.join(",\n") + sql << versions.map { |v| "(#{quote(v)})" }.join(",\n") sql << ";\n\n" sql else - versions.map { |version| - "INSERT INTO #{sm_table} (version) VALUES ('#{version}');" - }.join "\n\n" + "INSERT INTO #{sm_table} (version) VALUES (#{quote(versions)});" end end @@ -1024,26 +1028,33 @@ module ActiveRecord sm_table = quote_table_name(ActiveRecord::Migrator.schema_migrations_table_name) migrated = select_values("SELECT version FROM #{sm_table}").map(&:to_i) - paths = migrations_paths.map {|p| "#{p}/[0-9]*_*.rb" } + paths = migrations_paths.map { |p| "#{p}/[0-9]*_*.rb" } versions = Dir[*paths].map do |filename| filename.split("/").last.split("_").first.to_i end unless migrated.include?(version) - execute "INSERT INTO #{sm_table} (version) VALUES ('#{version}')" + execute "INSERT INTO #{sm_table} (version) VALUES (#{quote(version)})" end - inserting = (versions - migrated).select {|v| v < version} + inserting = (versions - migrated).select { |v| v < version } if inserting.any? - if (duplicate = inserting.detect {|v| inserting.count(v) > 1}) + if (duplicate = inserting.detect { |v| inserting.count(v) > 1 }) raise "Duplicate migration #{duplicate}. Please renumber your migrations to resolve the conflict." end - execute insert_versions_sql(inserting) + if supports_multi_insert? + execute insert_versions_sql(inserting) + else + inserting.each do |v| + execute insert_versions_sql(v) + end + end end end def type_to_sql(type, limit = nil, precision = nil, scale = nil) #:nodoc: - if native = native_database_types[type.to_sym] + type = type.to_sym if type + if native = native_database_types[type] column_type_sql = (native.is_a?(Hash) ? native[:name] : native).dup if type == :decimal # ignore limit, use precision and scale @@ -1111,7 +1122,7 @@ module ActiveRecord end def add_index_options(table_name, column_name, comment: nil, **options) # :nodoc: - if column_name.is_a?(String) && /\W/ === column_name + if column_name.is_a?(String) && /\W/.match?(column_name) column_names = column_name else column_names = Array(column_name) @@ -1160,41 +1171,41 @@ module ActiveRecord raise NotImplementedError, "#{self.class} does not support changing column comments" end - protected - def add_index_sort_order(option_strings, column_names, options = {}) - if options.is_a?(Hash) && order = options[:order] + private + + def add_index_sort_order(quoted_columns, **options) + if order = options[:order] case order when Hash - column_names.each {|name| option_strings[name] += " #{order[name].upcase}" if order.has_key?(name)} + order = order.symbolize_keys + quoted_columns.each { |name, column| column << " #{order[name].upcase}" if order[name].present? } when String - column_names.each {|name| option_strings[name] += " #{order.upcase}"} + quoted_columns.each { |name, column| column << " #{order.upcase}" if order.present? } end end - return option_strings + quoted_columns end # Overridden by the MySQL adapter for supporting index lengths - def quoted_columns_for_index(column_names, options = {}) - return [column_names] if column_names.is_a?(String) - - option_strings = Hash[column_names.map {|name| [name, ""]}] - - # add index sort order if supported + def add_options_for_index_columns(quoted_columns, **options) if supports_index_sort_order? - option_strings = add_index_sort_order(option_strings, column_names, options) + quoted_columns = add_index_sort_order(quoted_columns, options) end - column_names.map {|name| quote_column_name(name) + option_strings[name]} + quoted_columns + end + + def quoted_columns_for_index(column_names, **options) + return [column_names] if column_names.is_a?(String) + + quoted_columns = Hash[column_names.map { |name| [name.to_sym, quote_column_name(name).dup] }] + add_options_for_index_columns(quoted_columns, options).values end def index_name_for_remove(table_name, options = {}) return options[:name] if can_remove_index_by_name?(options) - # if the adapter doesn't support the indexes call the best we can do - # is return the default index name for the options provided - return index_name(table_name, options) unless respond_to?(:indexes) - checks = [] if options.is_a?(Hash) @@ -1244,7 +1255,6 @@ module ActiveRecord end end - private def create_table_definition(*args) TableDefinition.new(*args) end @@ -1253,7 +1263,7 @@ module ActiveRecord AlterTable.new create_table_definition(name) end - def index_name_options(column_names) # :nodoc: + def index_name_options(column_names) if column_names.is_a?(String) column_names = column_names.scan(/\w+/).join("_") end @@ -1261,7 +1271,7 @@ module ActiveRecord { column: column_names } end - def foreign_key_name(table_name, options) # :nodoc: + def foreign_key_name(table_name, options) identifier = "#{table_name}_#{options.fetch(:column)}_fk" hashed_identifier = Digest::SHA256.hexdigest(identifier).first(10) options.fetch(:name) do @@ -1269,7 +1279,7 @@ module ActiveRecord end end - def validate_index_length!(table_name, new_name, internal = false) # :nodoc: + def validate_index_length!(table_name, new_name, internal = false) max_index_length = internal ? index_name_length : allowed_index_name_length if new_name.length > max_index_length diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb index 0c7197a002..4046b3829d 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb @@ -62,17 +62,17 @@ module ActiveRecord # notably, the instance methods provided by SchemaStatements are very useful. class AbstractAdapter ADAPTER_NAME = "Abstract".freeze + include ActiveSupport::Callbacks + define_callbacks :checkout, :checkin + include Quoting, DatabaseStatements, SchemaStatements include DatabaseLimits include QueryCache - include ActiveSupport::Callbacks include ColumnDumper include Savepoints SIMPLE_INT = /\A\d+\z/ - define_callbacks :checkout, :checkin - attr_accessor :visitor, :pool attr_reader :schema_cache, :owner, :logger alias :in_use? :owner @@ -106,7 +106,7 @@ module ActiveRecord @pool = nil @schema_cache = SchemaCache.new self @quoted_column_names, @quoted_table_names = {}, {} - @visitor = arel_visitor + @visitor = arel_visitor if self.class.type_cast_config_to_boolean(config.fetch(:prepared_statements) { true }) @prepared_statements = true @@ -161,6 +161,14 @@ module ActiveRecord SchemaCreation.new self end + # Returns an array of +Column+ objects for the table specified by +table_name+. + def columns(table_name) # :nodoc: + table_name = table_name.to_s + column_definitions(table_name).map do |field| + new_column_from_field(table_name, field) + end + end + # this method must only be called while holding connection pool's mutex def lease if in_use? @@ -491,9 +499,9 @@ module ActiveRecord result end - protected + private - def initialize_type_map(m) # :nodoc: + def initialize_type_map(m) 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 @@ -524,37 +532,37 @@ module ActiveRecord end end - def reload_type_map # :nodoc: + def reload_type_map type_map.clear initialize_type_map(type_map) end - def register_class_with_limit(mapping, key, klass) # :nodoc: + def register_class_with_limit(mapping, key, klass) mapping.register_type(key) do |*args| limit = extract_limit(args.last) klass.new(limit: limit) end end - def register_class_with_precision(mapping, key, klass) # :nodoc: + def register_class_with_precision(mapping, key, klass) mapping.register_type(key) do |*args| precision = extract_precision(args.last) klass.new(precision: precision) end end - def extract_scale(sql_type) # :nodoc: + def extract_scale(sql_type) case sql_type when /\((\d+)\)/ then 0 when /\((\d+)(,(\d+))\)/ then $3.to_i end end - def extract_precision(sql_type) # :nodoc: + def extract_precision(sql_type) $1.to_i if sql_type =~ /\((\d+)(,\d+)?\)/ end - def extract_limit(sql_type) # :nodoc: + def extract_limit(sql_type) case sql_type when /^bigint/i 8 @@ -575,7 +583,7 @@ module ActiveRecord exception end - def log(sql, name = "SQL", binds = [], type_casted_binds = [], statement_name = nil) + def log(sql, name = "SQL", binds = [], type_casted_binds = [], statement_name = nil) # :doc: @instrumenter.instrument( "sql.active_record", sql: sql, @@ -590,14 +598,19 @@ module ActiveRecord def translate_exception(exception, message) # override in derived class - ActiveRecord::StatementInvalid.new(message) + case exception + when RuntimeError + exception + else + ActiveRecord::StatementInvalid.new(message) + end end def without_prepared_statement?(binds) !prepared_statements || binds.empty? end - def column_for(table_name, column_name) # :nodoc: + def column_for(table_name, column_name) column_name = column_name.to_s columns(table_name).detect { |c| c.name == column_name } || raise(ActiveRecordError, "No such column: #{table_name}.#{column_name}") 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 cc820036ca..aedf4581f5 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb @@ -9,7 +9,6 @@ require "active_record/connection_adapters/mysql/schema_dumper" require "active_record/connection_adapters/mysql/type_metadata" require "active_support/core_ext/string/strip" -require "active_support/core_ext/regexp" module ActiveRecord module ConnectionAdapters @@ -40,7 +39,7 @@ module ActiveRecord self.emulate_booleans = true NATIVE_DATABASE_TYPES = { - primary_key: "int auto_increment PRIMARY KEY", + primary_key: "bigint auto_increment PRIMARY KEY", string: { name: "varchar", limit: 255 }, text: { name: "text", limit: 65535 }, integer: { name: "int", limit: 4 }, @@ -68,8 +67,8 @@ module ActiveRecord @statements = StatementPool.new(self.class.type_cast_config_to_integer(config[:statement_limit])) - if version < "5.0.0" - raise "Your version of MySQL (#{full_version.match(/^\d+\.\d+\.\d+/)[0]}) is too old. Active Record supports MySQL >= 5.0." + 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." end end @@ -216,7 +215,11 @@ module ActiveRecord # Executes the SQL statement in the context of this connection. def execute(sql, name = nil) - log(sql, name) { @connection.query(sql) } + log(sql, name) do + ActiveSupport::Dependencies.interlock.permit_concurrent_loads do + @connection.query(sql) + end + end end # Mysql2Adapter doesn't have to free a result after using it, but we use this method @@ -307,45 +310,36 @@ module ActiveRecord show_variable "collation_database" end - def tables(name = nil) # :nodoc: - ActiveSupport::Deprecation.warn(<<-MSG.squish) - #tables currently returns both tables and views. - This behavior is deprecated and will be changed with Rails 5.1 to only return tables. - Use #data_sources instead. - MSG + def tables # :nodoc: + sql = "SELECT table_name FROM information_schema.tables WHERE table_type = 'BASE TABLE'" + sql << " AND table_schema = #{quote(@config[:database])}" - if name - ActiveSupport::Deprecation.warn(<<-MSG.squish) - Passing arguments to #tables is deprecated without replacement. - MSG - end + select_values(sql, "SCHEMA") + end - data_sources + def views # :nodoc: + select_values("SHOW FULL TABLES WHERE table_type = 'VIEW'", "SCHEMA") end - def data_sources + def data_sources # :nodoc: sql = "SELECT table_name FROM information_schema.tables " sql << "WHERE table_schema = #{quote(@config[:database])}" select_values(sql, "SCHEMA") end - def truncate(table_name, name = nil) - execute "TRUNCATE TABLE #{quote_table_name(table_name)}", name - end + def table_exists?(table_name) # :nodoc: + return false unless table_name.present? - def table_exists?(table_name) - # Update lib/active_record/internal_metadata.rb when this gets removed - ActiveSupport::Deprecation.warn(<<-MSG.squish) - #table_exists? currently checks both tables and views. - This behavior is deprecated and will be changed with Rails 5.1 to only check tables. - Use #data_source_exists? instead. - MSG + schema, name = extract_schema_qualified_name(table_name) + + sql = "SELECT table_name FROM information_schema.tables WHERE table_type = 'BASE TABLE'" + sql << " AND table_schema = #{quote(schema)} AND table_name = #{quote(name)}" - data_source_exists?(table_name) + select_values(sql, "SCHEMA").any? end - def data_source_exists?(table_name) + def data_source_exists?(table_name) # :nodoc: return false unless table_name.present? schema, name = extract_schema_qualified_name(table_name) @@ -356,10 +350,6 @@ module ActiveRecord select_values(sql, "SCHEMA").any? end - def views # :nodoc: - select_values("SHOW FULL TABLES WHERE table_type = 'VIEW'", "SCHEMA") - end - def view_exists?(view_name) # :nodoc: return false unless view_name.present? @@ -371,8 +361,18 @@ module ActiveRecord select_values(sql, "SCHEMA").any? end + def truncate(table_name, name = nil) + execute "TRUNCATE TABLE #{quote_table_name(table_name)}", name + 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 + indexes = [] current_index = nil execute_and_free("SHOW KEYS FROM #{quote_table_name(table_name)}", "SCHEMA") do |result| @@ -384,29 +384,25 @@ module ActiveRecord mysql_index_type = row[:Index_type].downcase.to_sym index_type = INDEX_TYPES.include?(mysql_index_type) ? mysql_index_type : nil index_using = INDEX_USINGS.include?(mysql_index_type) ? mysql_index_type : nil - indexes << IndexDefinition.new(row[:Table], row[:Key_name], row[:Non_unique].to_i == 0, [], [], nil, nil, index_type, index_using, row[:Index_comment].presence) + indexes << IndexDefinition.new(row[:Table], row[:Key_name], row[:Non_unique].to_i == 0, [], {}, nil, nil, index_type, index_using, row[:Index_comment].presence) end indexes.last.columns << row[:Column_name] - indexes.last.lengths << row[:Sub_part] + indexes.last.lengths.merge!(row[:Column_name] => row[:Sub_part].to_i) if row[:Sub_part] end end indexes end - # Returns an array of +Column+ objects for the table specified by +table_name+. - def columns(table_name) # :nodoc: - table_name = table_name.to_s - column_definitions(table_name).map do |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] - else - default, default_function = field[:Default], nil - end - new_column(field[:Field], default, type_metadata, field[:Null] == "YES", table_name, default_function, field[:Collation], comment: field[:Comment].presence) + def new_column_from_field(table_name, field) # :nodoc: + type_metadata = fetch_type_metadata(field[:Type], field[:Extra]) + if type_metadata.type == :datetime && field[:Default] == "CURRENT_TIMESTAMP" + default, default_function = nil, field[:Default] + else + default, default_function = field[:Default], nil end + new_column(field[:Field], default, type_metadata, field[:Null] == "YES", table_name, default_function, field[:Collation], comment: field[:Comment].presence) end def table_comment(table_name) # :nodoc: @@ -509,7 +505,7 @@ module ActiveRecord end def add_sql_comment!(sql, comment) # :nodoc: - sql << " COMMENT #{quote(comment)}" if comment + sql << " COMMENT #{quote(comment)}" if comment.present? sql end @@ -531,6 +527,7 @@ module ActiveRecord WHERE fk.referenced_column_name IS NOT NULL AND fk.table_schema = #{quote(schema)} AND fk.table_name = #{quote(name)} + AND rc.table_name = #{quote(name)} SQL fk_info.map do |row| @@ -570,22 +567,23 @@ module ActiveRecord # Maps logical Rails types to MySQL-specific data types. def type_to_sql(type, limit = nil, precision = nil, scale = nil, unsigned = nil) - sql = case type.to_s - when "integer" - integer_to_sql(limit) - when "text" - text_to_sql(limit) - when "blob" - binary_to_sql(limit) - when "binary" - if (0..0xfff) === limit - "varbinary(#{limit})" - else - binary_to_sql(limit) - end - else - super(type, limit, precision, scale) - end + sql = \ + case type.to_s + when "integer" + integer_to_sql(limit) + when "text" + text_to_sql(limit) + when "blob" + binary_to_sql(limit) + when "binary" + if (0..0xfff) === limit + "varbinary(#{limit})" + else + binary_to_sql(limit) + end + else + super(type, limit, precision, scale) + end sql << " unsigned" if unsigned && type != :primary_key sql @@ -649,9 +647,9 @@ module ActiveRecord !native_database_types[type].nil? end - protected + private - def initialize_type_map(m) # :nodoc: + def initialize_type_map(m) super register_class_with_limit m, %r(char)i, MysqlString @@ -680,20 +678,20 @@ module ActiveRecord m.register_type(%r(enum)i) do |sql_type| limit = sql_type[/^enum\((.+)\)/i, 1] - .split(",").map{|enum| enum.strip.length - 2}.max + .split(",").map { |enum| enum.strip.length - 2 }.max MysqlString.new(limit: limit) end m.register_type(%r(^set)i) do |sql_type| limit = sql_type[/^set\((.+)\)/i, 1] - .split(",").map{|set| set.strip.length - 1}.sum - 1 + .split(",").map { |set| set.strip.length - 1 }.sum - 1 MysqlString.new(limit: limit) end end - def register_integer_type(mapping, key, options) # :nodoc: + def register_integer_type(mapping, key, options) mapping.register_type(key) do |sql_type| - if /\bunsigned\z/ === sql_type + if /\bunsigned\b/.match?(sql_type) Type::UnsignedInteger.new(options) else Type::Integer.new(options) @@ -702,7 +700,7 @@ module ActiveRecord end def extract_precision(sql_type) - if /time/ === sql_type + if /time/.match?(sql_type) super || 0 else super @@ -710,39 +708,38 @@ module ActiveRecord end def fetch_type_metadata(sql_type, extra = "") - MySQL::TypeMetadata.new(super(sql_type), extra: extra, strict: strict_mode?) + MySQL::TypeMetadata.new(super(sql_type), extra: extra) end - def add_index_length(option_strings, column_names, options = {}) - if options.is_a?(Hash) && length = options[:length] + def add_index_length(quoted_columns, **options) + if length = options[:length] case length when Hash - column_names.each {|name| option_strings[name] += "(#{length[name]})" if length.has_key?(name) && length[name].present?} + length = length.symbolize_keys + quoted_columns.each { |name, column| column << "(#{length[name]})" if length[name].present? } when Integer - column_names.each {|name| option_strings[name] += "(#{length})"} + quoted_columns.each { |name, column| column << "(#{length})" } end end - return option_strings + quoted_columns end - def quoted_columns_for_index(column_names, options = {}) - option_strings = Hash[column_names.map {|name| [name, ""]}] - - # add index length - option_strings = add_index_length(option_strings, column_names, options) - - # add index sort order - option_strings = add_index_sort_order(option_strings, column_names, options) - - column_names.map {|name| quote_column_name(name) + option_strings[name]} + 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 + # See https://dev.mysql.com/doc/refman/5.7/en/error-messages-server.html ER_DUP_ENTRY = 1062 + ER_NOT_NULL_VIOLATION = 1048 + ER_DO_NOT_HAVE_DEFAULT = 1364 ER_NO_REFERENCED_ROW_2 = 1452 ER_DATA_TOO_LONG = 1406 + ER_OUT_OF_RANGE = 1264 ER_LOCK_DEADLOCK = 1213 + ER_CANNOT_ADD_FOREIGN = 1215 + ER_CANNOT_CREATE_TABLE = 1005 def translate_exception(exception, message) case error_number(exception) @@ -750,8 +747,20 @@ module ActiveRecord RecordNotUnique.new(message) when ER_NO_REFERENCED_ROW_2 InvalidForeignKey.new(message) + when ER_CANNOT_ADD_FOREIGN + mismatched_foreign_key(message) + when ER_CANNOT_CREATE_TABLE + if message.include?("errno: 150") + mismatched_foreign_key(message) + else + super + end when ER_DATA_TOO_LONG ValueTooLong.new(message) + when ER_OUT_OF_RANGE + RangeError.new(message) + when ER_NOT_NULL_VIOLATION, ER_DO_NOT_HAVE_DEFAULT + NotNullViolation.new(message) when ER_LOCK_DEADLOCK Deadlocked.new(message) else @@ -776,6 +785,10 @@ module ActiveRecord options[:null] = column.null end + unless options.key?(:comment) + options[:comment] = column.comment + end + td = create_table_definition(table_name) cd = td.new_column_definition(column.name, type, options) schema_creation.accept(ChangeColumnDefinition.new(cd, column.name)) @@ -800,7 +813,7 @@ module ActiveRecord end def remove_columns_sql(table_name, *column_names) - column_names.map {|column_name| remove_column_sql(table_name, column_name) } + column_names.map { |column_name| remove_column_sql(table_name, column_name) } end def add_index_sql(table_name, column_name, options = {}) @@ -822,10 +835,8 @@ module ActiveRecord [remove_column_sql(table_name, :updated_at), remove_column_sql(table_name, :created_at)] end - private - - # MySQL is too stupid to create a temporary table for use subquery, so we have - # to give it some prompting in the form of a subsubquery. Ugh! + # MySQL is too stupid to create a temporary table for use subquery, so we have + # to give it some prompting in the form of a subsubquery. Ugh! def subquery_for(key, select) subsubselect = select.clone subsubselect.projections = [key] @@ -893,7 +904,7 @@ module ActiveRecord end.compact.join(", ") # ...and send them all in one query - @connection.query "SET #{encoding} #{sql_mode_assignment} #{variable_assignments}" + execute "SET #{encoding} #{sql_mode_assignment} #{variable_assignments}" end def column_definitions(table_name) # :nodoc: @@ -917,6 +928,18 @@ module ActiveRecord MySQL::TableDefinition.new(*args) end + def mismatched_foreign_key(message) + parts = message.scan(/`(\w+)`[ $)]/).flatten + MismatchedForeignKey.new( + self, + message: message, + table: parts[0], + foreign_key: parts[1], + target_table: parts[2], + primary_key: parts[3], + ) + end + def extract_schema_qualified_name(string) # :nodoc: schema, name = string.to_s.scan(/[^`.\s]+|`[^`]*`/) schema, name = @config[:database], schema unless name @@ -930,7 +953,7 @@ module ActiveRecord when 3; "mediumint" when nil, 4; "int" when 5..8; "bigint" - else raise(ActiveRecordError, "No integer type has byte size #{limit}") + else raise(ActiveRecordError, "No integer type has byte size #{limit}. Use a decimal with scale 0 instead.") end end diff --git a/activerecord/lib/active_record/connection_adapters/column.rb b/activerecord/lib/active_record/connection_adapters/column.rb index 1808173592..61cd7ae4cc 100644 --- a/activerecord/lib/active_record/connection_adapters/column.rb +++ b/activerecord/lib/active_record/connection_adapters/column.rb @@ -29,7 +29,7 @@ module ActiveRecord end def bigint? - /\Abigint\b/ === sql_type + /\Abigint\b/.match?(sql_type) end # Returns the human name of the column name. @@ -40,6 +40,28 @@ module ActiveRecord Base.human_attribute_name(@name) end + def init_with(coder) + @name = coder["name"] + @table_name = coder["table_name"] + @sql_type_metadata = coder["sql_type_metadata"] + @null = coder["null"] + @default = coder["default"] + @default_function = coder["default_function"] + @collation = coder["collation"] + @comment = coder["comment"] + end + + def encode_with(coder) + coder["name"] = @name + coder["table_name"] = @table_name + coder["sql_type_metadata"] = @sql_type_metadata + coder["null"] = @null + coder["default"] = @default + coder["default_function"] = @default_function + coder["collation"] = @collation + coder["comment"] = @comment + end + def ==(other) other.is_a?(Column) && attributes_for_hash == other.attributes_for_hash diff --git a/activerecord/lib/active_record/connection_adapters/connection_specification.rb b/activerecord/lib/active_record/connection_adapters/connection_specification.rb index be6b55e53c..dcf56997db 100644 --- a/activerecord/lib/active_record/connection_adapters/connection_specification.rb +++ b/activerecord/lib/active_record/connection_adapters/connection_specification.rb @@ -48,8 +48,8 @@ module ActiveRecord # Converts the given URL to a full connection hash. def to_hash - config = raw_config.reject { |_,value| value.blank? } - config.map { |key,value| config[key] = uri_parser.unescape(value) if value.is_a? String } + config = raw_config.reject { |_, value| value.blank? } + config.map { |key, value| config[key] = uri_parser.unescape(value) if value.is_a? String } config end @@ -63,15 +63,15 @@ module ActiveRecord @uri_parser ||= URI::Parser.new end - # Converts the query parameters of the URI into a hash. - # - # "localhost?pool=5&reaping_frequency=2" - # # => { "pool" => "5", "reaping_frequency" => "2" } - # - # returns empty hash if no query present. - # - # "localhost" - # # => {} + # Converts the query parameters of the URI into a hash. + # + # "localhost?pool=5&reaping_frequency=2" + # # => { "pool" => "5", "reaping_frequency" => "2" } + # + # returns empty hash if no query present. + # + # "localhost" + # # => {} def query_hash Hash[(@query || "").split("&").map { |pair| pair.split("=") }] end @@ -92,7 +92,7 @@ module ActiveRecord end end - # Returns name of the database. + # Returns name of the database. def database_from_path if @adapter == "sqlite3" # 'sqlite3:/foo' is absolute, because that makes sense. The @@ -192,26 +192,26 @@ module ActiveRecord private - # Returns fully resolved connection, accepts hash, string or symbol. - # Always returns a hash. - # - # == Examples - # - # Symbol representing current environment. - # - # Resolver.new("production" => {}).resolve_connection(:production) - # # => {} - # - # One layer deep hash of connection values. - # - # Resolver.new({}).resolve_connection("adapter" => "sqlite3") - # # => { "adapter" => "sqlite3" } - # - # Connection URL. - # - # Resolver.new({}).resolve_connection("postgresql://localhost/foo") - # # => { "host" => "localhost", "database" => "foo", "adapter" => "postgresql" } - # + # Returns fully resolved connection, accepts hash, string or symbol. + # Always returns a hash. + # + # == Examples + # + # Symbol representing current environment. + # + # Resolver.new("production" => {}).resolve_connection(:production) + # # => {} + # + # One layer deep hash of connection values. + # + # Resolver.new({}).resolve_connection("adapter" => "sqlite3") + # # => { "adapter" => "sqlite3" } + # + # Connection URL. + # + # Resolver.new({}).resolve_connection("postgresql://localhost/foo") + # # => { "host" => "localhost", "database" => "foo", "adapter" => "postgresql" } + # def resolve_connection(spec) case spec when Symbol @@ -223,13 +223,13 @@ module ActiveRecord end end - # Takes the environment such as +:production+ or +:development+. - # This requires that the @configurations was initialized with a key that - # matches. - # - # Resolver.new("production" => {}).resolve_symbol_connection(:production) - # # => {} - # + # Takes the environment such as +:production+ or +:development+. + # This requires that the @configurations was initialized with a key that + # matches. + # + # Resolver.new("production" => {}).resolve_symbol_connection(:production) + # # => {} + # def resolve_symbol_connection(spec) if config = configurations[spec.to_s] resolve_connection(config).merge("name" => spec.to_s) @@ -238,10 +238,10 @@ module ActiveRecord end end - # Accepts a hash. Expands the "url" key that contains a - # URL database connection to a full connection - # hash and merges with the rest of the hash. - # Connection details inside of the "url" key win any merge conflicts + # Accepts a hash. Expands the "url" key that contains a + # URL database connection to a full connection + # hash and merges with the rest of the hash. + # Connection details inside of the "url" key win any merge conflicts def resolve_hash_connection(spec) if spec["url"] && spec["url"] !~ /^jdbc:/ connection_hash = resolve_url_connection(spec.delete("url")) @@ -250,11 +250,11 @@ module ActiveRecord spec end - # Takes a connection URL. - # - # Resolver.new({}).resolve_url_connection("postgresql://localhost/foo") - # # => { "host" => "localhost", "database" => "foo", "adapter" => "postgresql" } - # + # Takes a connection URL. + # + # Resolver.new({}).resolve_url_connection("postgresql://localhost/foo") + # # => { "host" => "localhost", "database" => "foo", "adapter" => "postgresql" } + # def resolve_url_connection(url) ConnectionUrlResolver.new(url).to_hash end diff --git a/activerecord/lib/active_record/connection_adapters/mysql/column.rb b/activerecord/lib/active_record/connection_adapters/mysql/column.rb index 5452e44c84..1499c1681f 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql/column.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql/column.rb @@ -2,41 +2,19 @@ module ActiveRecord module ConnectionAdapters module MySQL class Column < ConnectionAdapters::Column # :nodoc: - delegate :strict, :extra, to: :sql_type_metadata, allow_nil: true - - def initialize(*) - super - extract_default - end - - def has_default? - return false if blob_or_text_column? # MySQL forbids defaults on blob and text columns - super - end - - def blob_or_text_column? - /\A(?:tiny|medium|long)?blob\b/ === sql_type || type == :text - end + delegate :extra, to: :sql_type_metadata, allow_nil: true def unsigned? - /\bunsigned\z/ === sql_type + /\bunsigned(?: zerofill)?\z/.match?(sql_type) end def case_sensitive? - collation && collation !~ /_ci\z/ + collation && !/_ci\z/.match?(collation) end def auto_increment? extra == "auto_increment" end - - private - - def extract_default - if blob_or_text_column? - @default = null || strict ? nil : "" - end - end end end end 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 c8238eb266..8c67a7a80b 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql/database_statements.rb @@ -3,7 +3,7 @@ module ActiveRecord module MySQL module DatabaseStatements # Returns an ActiveRecord::Result instance. - def select_all(arel, name = nil, binds = [], preparable: nil) + def select_all(arel, name = nil, binds = [], preparable: nil) # :nodoc: result = if ExplainRegistry.collect? && prepared_statements unprepared_statement { super } else @@ -15,8 +15,8 @@ module ActiveRecord # Returns an array of arrays containing the field values. # Order is the same as that returned by +columns+. - def select_rows(sql, name = nil, binds = []) - select_result(sql, name, binds) do |result| + def select_rows(arel, name = nil, binds = []) # :nodoc: + select_result(arel, name, binds) do |result| @connection.next_result while @connection.more_results? result.to_a end @@ -24,11 +24,9 @@ module ActiveRecord # Executes the SQL statement in the context of this connection. def execute(sql, name = nil) - if @connection - # make sure we carry over any changes to ActiveRecord::Base.default_timezone that have been - # made since we established the connection - @connection.query_options[:database_timezone] = ActiveRecord::Base.default_timezone - end + # make sure we carry over any changes to ActiveRecord::Base.default_timezone that have been + # made since we established the connection + @connection.query_options[:database_timezone] = ActiveRecord::Base.default_timezone super end @@ -54,15 +52,15 @@ module ActiveRecord end alias :exec_update :exec_delete - protected + private def last_inserted_id(result) @connection.last_id end - private - - def select_result(sql, name = nil, binds = []) + def select_result(arel, name, binds) + arel, binds = binds_from_relation(arel, binds) + sql = to_sql(arel, binds) if without_prepared_statement?(binds) execute_and_free(sql, name) { |result| yield result } else @@ -71,11 +69,9 @@ module ActiveRecord end def exec_stmt_and_free(sql, name, binds, cache_stmt: false) - if @connection - # make sure we carry over any changes to ActiveRecord::Base.default_timezone that have been - # made since we established the connection - @connection.query_options[:database_timezone] = ActiveRecord::Base.default_timezone - end + # make sure we carry over any changes to ActiveRecord::Base.default_timezone that have been + # made since we established the connection + @connection.query_options[:database_timezone] = ActiveRecord::Base.default_timezone type_casted_binds = type_casted_binds(binds) @@ -90,7 +86,9 @@ module ActiveRecord end begin - result = stmt.execute(*type_casted_binds) + result = ActiveSupport::Dependencies.interlock.permit_concurrent_loads do + stmt.execute(*type_casted_binds) + end rescue Mysql2::Error => e if cache_stmt @statements.delete(sql) 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 0b7dea232f..9691060cd3 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 @@ -39,7 +39,7 @@ module ActiveRecord def compute_column_widths(result) [].tap do |widths| result.columns.each_with_index do |column, i| - cells_in_column = [column] + result.rows.map {|r| r[i].nil? ? "NULL" : r[i].to_s} + cells_in_column = [column] + result.rows.map { |r| r[i].nil? ? "NULL" : r[i].to_s } widths << cells_in_column.map(&:length).max end end @@ -47,7 +47,7 @@ module ActiveRecord def build_separator(widths) padding = 1 - "+" + widths.map {|w| "-" * (w + (padding*2))}.join("+") + "+" + "+" + widths.map { |w| "-" * (w + (padding * 2)) }.join("+") + "+" end def build_cells(items, widths) 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 ce773ed75b..0cf40de70f 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql/schema_definitions.rb @@ -3,7 +3,10 @@ module ActiveRecord module MySQL module ColumnMethods def primary_key(name, type = :primary_key, **options) - options[:auto_increment] = true if type == :bigint && !options.key?(:default) + if type == :primary_key && !options.key?(:default) + options[:auto_increment] = true + options[:limit] = 8 + end super end 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 39221eeb0c..2065816501 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql/schema_dumper.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql/schema_dumper.rb @@ -3,11 +3,9 @@ module ActiveRecord module MySQL module ColumnDumper def column_spec_for_primary_key(column) - if column.bigint? - spec = { id: :bigint.inspect } - spec[:default] = schema_default(column) || "nil" unless column.auto_increment? - else - spec = super + spec = super + if column.type == :integer && !column.auto_increment? + spec[:default] = schema_default(column) || "nil" end spec[:unsigned] = "true" if column.unsigned? spec @@ -38,7 +36,7 @@ module ActiveRecord end def schema_precision(column) - super unless /time/ === column.sql_type && column.precision == 0 + super unless /time/.match?(column.sql_type) && column.precision == 0 end def schema_collation(column) 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 1be5cb4740..24dcf852e1 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql/type_metadata.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql/type_metadata.rb @@ -2,13 +2,12 @@ module ActiveRecord module ConnectionAdapters module MySQL class TypeMetadata < DelegateClass(SqlTypeMetadata) # :nodoc: - attr_reader :extra, :strict + attr_reader :extra - def initialize(type_metadata, extra: "", strict: false) + def initialize(type_metadata, extra: "") super(type_metadata) @type_metadata = type_metadata @extra = extra - @strict = strict end def ==(other) @@ -24,7 +23,7 @@ module ActiveRecord protected def attributes_for_hash - [self.class, @type_metadata, extra, strict] + [self.class, @type_metadata, extra] end end end diff --git a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb index 0130b4ef62..45e400b75b 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb @@ -14,12 +14,10 @@ module ActiveRecord config[:username] = "root" if config[:username].nil? config[:flags] ||= 0 - if Mysql2::Client.const_defined? :FOUND_ROWS - if config[:flags].kind_of? Array - config[:flags].push "FOUND_ROWS".freeze - else - config[:flags] |= Mysql2::Client::FOUND_ROWS - end + if config[:flags].kind_of? Array + config[:flags].push "FOUND_ROWS".freeze + else + config[:flags] |= Mysql2::Client::FOUND_ROWS end client = Mysql2::Client.new(config) @@ -90,7 +88,6 @@ module ActiveRecord #++ def active? - return false unless @connection @connection.ping end @@ -105,10 +102,7 @@ module ActiveRecord # Otherwise, this method does nothing. def disconnect! super - unless @connection.nil? - @connection.close - @connection = nil - end + @connection.close end private 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 87338986b9..705e6063dc 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb @@ -7,18 +7,14 @@ module ActiveRecord PostgreSQL::ExplainPrettyPrinter.new.pp(exec_query(sql, "EXPLAIN", binds)) end - def select_value(arel, name = nil, binds = []) - arel, binds = binds_from_relation arel, binds - sql = to_sql(arel, binds) - execute_and_clear(sql, name, binds) do |result| + def select_value(arel, name = nil, binds = []) # :nodoc: + select_result(arel, name, binds) do |result| result.getvalue(0, 0) if result.ntuples > 0 && result.nfields > 0 end end - def select_values(arel, name = nil, binds = []) - arel, binds = binds_from_relation arel, binds - sql = to_sql(arel, binds) - execute_and_clear(sql, name, binds) do |result| + def select_values(arel, name = nil, binds = []) # :nodoc: + select_result(arel, name, binds) do |result| if result.nfields > 0 result.column_values(0) else @@ -29,8 +25,8 @@ module ActiveRecord # Executes a SELECT query and returns an array of rows. Each row is an # array of field values. - def select_rows(sql, name = nil, binds = []) - execute_and_clear(sql, name, binds) do |result| + def select_rows(arel, name = nil, binds = []) # :nodoc: + select_result(arel, name, binds) do |result| result.values end end @@ -85,17 +81,21 @@ module ActiveRecord # Queries the database and returns the results in an Array-like object def query(sql, name = nil) #:nodoc: log(sql, name) do - result_as_array @connection.async_exec(sql) + ActiveSupport::Dependencies.interlock.permit_concurrent_loads do + result_as_array @connection.async_exec(sql) + end end end - # Executes an SQL statement, returning a PGresult object on success - # or raising a PGError exception otherwise. - # Note: the PGresult object is manually memory managed; if you don't - # need it specifically, you many want consider the exec_query wrapper. + # Executes an SQL statement, returning a PG::Result object on success + # or raising a PG::Error exception otherwise. + # Note: the PG::Result object is manually memory managed; if you don't + # need it specifically, you may want consider the <tt>exec_query</tt> wrapper. def execute(sql, name = nil) log(sql, name) do - @connection.async_exec(sql) + ActiveSupport::Dependencies.interlock.permit_concurrent_loads do + @connection.async_exec(sql) + end end end @@ -113,7 +113,7 @@ module ActiveRecord end def exec_delete(sql, name = nil, binds = []) - execute_and_clear(sql, name, binds) {|result| result.cmd_tuples } + execute_and_clear(sql, name, binds) { |result| result.cmd_tuples } end alias :exec_update :exec_delete @@ -124,26 +124,29 @@ module ActiveRecord pk = primary_key(table_ref) if table_ref end - pk = suppress_composite_primary_key(pk) - - if pk && use_insert_returning? + if pk = suppress_composite_primary_key(pk) sql = "#{sql} RETURNING #{quote_column_name(pk)}" end super end + private :sql_for_insert def exec_insert(sql, name = nil, binds = [], pk = nil, sequence_name = nil) - val = exec_query(sql, name, binds) - if !use_insert_returning? && pk + if use_insert_returning? || pk == false + super + else + result = exec_query(sql, name, binds) unless sequence_name table_ref = extract_table_ref_from_insert_sql(sql) - sequence_name = default_sequence_name(table_ref, pk) - return val unless sequence_name + if table_ref + pk = primary_key(table_ref) if pk.nil? + pk = suppress_composite_primary_key(pk) + sequence_name = default_sequence_name(table_ref, pk) + end + return result unless sequence_name end last_insert_id_result(sequence_name) - else - val end end @@ -172,6 +175,14 @@ module ActiveRecord def suppress_composite_primary_key(pk) pk unless pk.is_a?(Array) end + + def select_result(arel, name, binds) + arel, binds = binds_from_relation(arel, binds) + sql = to_sql(arel, binds) + execute_and_clear(sql, name, binds) do |result| + yield result + end + end end end end 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 f95a63968c..99f3a5bbdf 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 @@ -28,7 +28,7 @@ module ActiveRecord pp << header.center(width).rstrip pp << "-" * width - pp += lines.map {|line| " #{line}"} + pp += lines.map { |line| " #{line}" } nrows = result.rows.length rows_label = nrows == 1 ? "row" : "rows" diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb index 8c318886cf..0e526f6201 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb @@ -12,7 +12,7 @@ require "active_record/connection_adapters/postgresql/oid/json" require "active_record/connection_adapters/postgresql/oid/jsonb" require "active_record/connection_adapters/postgresql/oid/money" require "active_record/connection_adapters/postgresql/oid/point" -require "active_record/connection_adapters/postgresql/oid/rails_5_1_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" 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 1a66afb23a..e1a75f8e5e 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb @@ -5,8 +5,10 @@ module ActiveRecord class Array < Type::Value # :nodoc: include Type::Helpers::Mutable + Data = Struct.new(:encoder, :values) # :nodoc: + attr_reader :subtype, :delimiter - delegate :type, :user_input_in_time_zone, :limit, to: :subtype + delegate :type, :user_input_in_time_zone, :limit, :precision, :scale, to: :subtype def initialize(subtype, delimiter = ",") @subtype = subtype @@ -17,8 +19,11 @@ module ActiveRecord end def deserialize(value) - if value.is_a?(::String) + case value + when ::String type_cast_array(@pg_decoder.decode(value), :deserialize) + when Data + deserialize(value.values) else super end @@ -33,7 +38,8 @@ module ActiveRecord def serialize(value) if value.is_a?(::Array) - @pg_encoder.encode(type_cast_array(value, :serialize)) + casted_values = type_cast_array(value, :serialize) + Data.new(@pg_encoder, casted_values) else super end @@ -54,6 +60,10 @@ module ActiveRecord value.map(&block) end + def changed_in_place?(raw_old_value, new_value) + deserialize(raw_old_value) != new_value + end + private def type_cast_array(value, method) diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit.rb index a6b5a89ec0..0a505f46a7 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit.rb @@ -7,7 +7,7 @@ module ActiveRecord :bit end - def cast(value) + def cast_value(value) if ::String === value case value when /^0x/i @@ -16,7 +16,7 @@ module ActiveRecord value # Bit-string notation end else - value + value.to_s end end @@ -34,13 +34,15 @@ module ActiveRecord end def binary? - /\A[01]*\Z/ === value + /\A[01]*\Z/.match?(value) end def hex? - /\A[0-9A-F]*\Z/i === value + /\A[0-9A-F]*\Z/i.match?(value) end + # TODO Change this to private once we've dropped Ruby 2.2 support. + # Workaround for Ruby 2.2 "private attribute?" warning. protected attr_reader :value 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 2d3e6a925d..49dd4fc73f 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/hstore.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/hstore.rb @@ -12,8 +12,8 @@ module ActiveRecord def deserialize(value) if value.is_a?(::String) ::Hash[value.scan(HstorePair).map { |k, v| - v = v.upcase == "NULL" ? nil : v.gsub(/\A"(.*)"\Z/m,'\1').gsub(/\\(.)/, '\1') - k = k.gsub(/\A"(.*)"\Z/m,'\1').gsub(/\\(.)/, '\1') + v = v.upcase == "NULL" ? nil : v.gsub(/\A"(.*)"\Z/m, '\1').gsub(/\\(.)/, '\1') + k = k.gsub(/\A"(.*)"\Z/m, '\1').gsub(/\\(.)/, '\1') [k, v] }] else @@ -24,6 +24,8 @@ module ActiveRecord def serialize(value) if value.is_a?(::Hash) value.map { |k, v| "#{escape_hstore(k)}=>#{escape_hstore(v)}" }.join(", ") + elsif value.respond_to?(:to_unsafe_h) + serialize(value.to_unsafe_h) else value end @@ -33,6 +35,14 @@ module ActiveRecord ActiveRecord::Store::StringKeyedHashAccessor end + # Will compare the Hash equivalents of +raw_old_value+ and +new_value+. + # By comparing hashes, this avoids an edge case where the order of + # the keys change between the two hashes, and they would not be marked + # as equal. + def changed_in_place?(raw_old_value, new_value) + deserialize(raw_old_value) != new_value + end + private HstorePair = begin diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/rails_5_1_point.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/legacy_point.rb index 845ff5b6a9..775eecaf85 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/rails_5_1_point.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/legacy_point.rb @@ -1,10 +1,8 @@ module ActiveRecord - Point = Struct.new(:x, :y) - module ConnectionAdapters module PostgreSQL module OID # :nodoc: - class Rails51Point < Type::Value # :nodoc: + class LegacyPoint < Type::Value # :nodoc: include Type::Helpers::Mutable def type @@ -14,23 +12,20 @@ module ActiveRecord def cast(value) case value when ::String - return if value.blank? - if value[0] == "(" && value[-1] == ")" value = value[1...-1] end - x, y = value.split(",") - build_point(x, y) + cast(value.split(",")) when ::Array - build_point(*value) + value.map { |v| Float(v) } else value end end def serialize(value) - if value.is_a?(ActiveRecord::Point) - "(#{number_for_point(value.x)},#{number_for_point(value.y)})" + if value.is_a?(::Array) + "(#{number_for_point(value[0])},#{number_for_point(value[1])})" else super end @@ -41,10 +36,6 @@ module ActiveRecord def number_for_point(number) number.to_s.gsub(/\.0$/, "") end - - def build_point(x, y) - ActiveRecord::Point.new(Float(x), Float(y)) - end end end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/point.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/point.rb index bb4db2564c..7c764e7287 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/point.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/point.rb @@ -1,4 +1,6 @@ module ActiveRecord + Point = Struct.new(:x, :y) + module ConnectionAdapters module PostgreSQL module OID # :nodoc: @@ -12,20 +14,34 @@ module ActiveRecord def cast(value) case value when ::String + return if value.blank? + if value[0] == "(" && value[-1] == ")" value = value[1...-1] end - cast(value.split(",")) + x, y = value.split(",") + build_point(x, y) when ::Array - value.map { |v| Float(v) } + build_point(*value) else value end end def serialize(value) - if value.is_a?(::Array) - "(#{number_for_point(value[0])},#{number_for_point(value[1])})" + case value + when ActiveRecord::Point + "(#{number_for_point(value.x)},#{number_for_point(value.y)})" + when ::Array + serialize(build_point(*value)) + else + super + end + end + + def type_cast_for_schema(value) + if ActiveRecord::Point === value + [value.x, value.y] else super end @@ -36,6 +52,10 @@ module ActiveRecord def number_for_point(number) number.to_s.gsub(/\.0$/, "") end + + def build_point(x, y) + ActiveRecord::Point.new(Float(x), Float(y)) + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb index 2c714f4018..54d5d0902e 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb @@ -1,5 +1,3 @@ -require "active_support/core_ext/string/filters" - 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 b5031d890f..3783925954 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb @@ -92,6 +92,8 @@ module ActiveRecord else super end + when OID::Array::Data + _quote(encode_array(value)) else super end @@ -106,10 +108,37 @@ module ActiveRecord { value: value.to_s, format: 1 } when OID::Xml::Data, OID::Bit::Data value.to_s + when OID::Array::Data + encode_array(value) else super end end + + def encode_array(array_data) + encoder = array_data.encoder + values = type_cast_array(array_data.values) + + result = encoder.encode(values) + if encoding = determine_encoding_of_strings_in_array(values) + result.force_encoding(encoding) + end + result + end + + def determine_encoding_of_strings_in_array(value) + case value + when ::Array then determine_encoding_of_strings_in_array(value.first) + when ::String then value.encoding + end + end + + def type_cast_array(values) + case values + when ::Array then values.map { |item| type_cast_array(item) } + else _type_cast(values) + end + end end end end 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 a11dbe7dce..4afb4733eb 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb @@ -11,11 +11,22 @@ module ActiveRecord # t.timestamps # end # - # By default, this will use the +uuid_generate_v4()+ function from the - # +uuid-ossp+ extension, which MUST be enabled on your database. To enable - # the +uuid-ossp+ extension, you can use the +enable_extension+ method in your - # migrations. To use a UUID primary key without +uuid-ossp+ enabled, you can - # set the +:default+ option to +nil+: + # By default, this will use the +gen_random_uuid()+ function from the + # +pgcrypto+ extension. As that extension is only available in + # PostgreSQL 9.4+, for earlier versions an explicit default can be set + # to use +uuid_generate_v4()+ from the +uuid-ossp+ extension instead: + # + # create_table :stuffs, id: false do |t| + # t.primary_key :id, :uuid, default: "uuid_generate_v4()" + # t.uuid :foo_id + # t.timestamps + # end + # + # To enable the appropriate extension, which is a requirement, use + # the +enable_extension+ method in your migrations. + # + # To use a UUID primary key without any of the extensions, set the + # +:default+ option to +nil+: # # create_table :stuffs, id: false do |t| # t.primary_key :id, :uuid, default: nil @@ -23,15 +34,24 @@ module ActiveRecord # t.timestamps # end # - # You may also pass a different UUID generation function from +uuid-ossp+ - # or another library. + # You may also pass a custom stored procedure that returns a UUID or use a + # different UUID generation function from another library. # # Note that setting the UUID primary key default value to +nil+ will # require you to assure that you always provide a UUID value before saving # a record (as primary keys cannot be +nil+). This might be done via the # +SecureRandom.uuid+ method and a +before_save+ callback, for instance. def primary_key(name, type = :primary_key, **options) - options[:default] = options.fetch(:default, "uuid_generate_v4()") if type == :uuid + 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 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 c20baf655c..7808d37deb 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_dumper.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_dumper.rb @@ -25,7 +25,7 @@ module ActiveRecord private def default_primary_key?(column) - schema_type(column) == :serial + schema_type(column) == :bigserial end def schema_type(column) 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 531d323a55..bfda113e40 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb @@ -71,13 +71,7 @@ module ActiveRecord end # Returns the list of all tables in the schema search path. - def tables(name = nil) - if name - ActiveSupport::Deprecation.warn(<<-MSG.squish) - Passing arguments to #tables is deprecated without replacement. - MSG - end - + def tables select_values("SELECT tablename FROM pg_tables WHERE schemaname = ANY(current_schemas(false))", "SCHEMA") end @@ -86,7 +80,17 @@ module ActiveRecord SELECT c.relname FROM pg_class c LEFT JOIN pg_namespace n ON n.oid = c.relnamespace - WHERE c.relkind IN ('r', 'v','m') -- (r)elation/table, (v)iew, (m)aterialized view + WHERE c.relkind IN ('r','v','m') -- (r)elation/table, (v)iew, (m)aterialized view + AND n.nspname = ANY (current_schemas(false)) + SQL + end + + def views # :nodoc: + select_values(<<-SQL, "SCHEMA") + SELECT c.relname + FROM pg_class c + LEFT JOIN pg_namespace n ON n.oid = c.relnamespace + WHERE c.relkind IN ('v','m') -- (v)iew, (m)aterialized view AND n.nspname = ANY (current_schemas(false)) SQL end @@ -95,36 +99,28 @@ module ActiveRecord # If the schema is not specified as part of +name+ then it will only find tables within # the current schema search path (regardless of permissions to access tables in other schemas) def table_exists?(name) - ActiveSupport::Deprecation.warn(<<-MSG.squish) - #table_exists? currently checks both tables and views. - This behavior is deprecated and will be changed with Rails 5.1 to only check tables. - Use #data_source_exists? instead. - MSG - - data_source_exists?(name) - end - - def data_source_exists?(name) name = Utils.extract_schema_qualified_name(name.to_s) return false unless name.identifier - select_value(<<-SQL, "SCHEMA").to_i > 0 - SELECT COUNT(*) - FROM pg_class c - LEFT JOIN pg_namespace n ON n.oid = c.relnamespace - WHERE c.relkind IN ('r','v','m') -- (r)elation/table, (v)iew, (m)aterialized view - AND c.relname = '#{name.identifier}' - AND n.nspname = #{name.schema ? "'#{name.schema}'" : 'ANY (current_schemas(false))'} + select_values(<<-SQL, "SCHEMA").any? + SELECT tablename + FROM pg_tables + WHERE tablename = #{quote(name.identifier)} + AND schemaname = #{name.schema ? quote(name.schema) : "ANY (current_schemas(false))"} SQL end - def views # :nodoc: - select_values(<<-SQL, "SCHEMA") + def data_source_exists?(name) # :nodoc: + name = Utils.extract_schema_qualified_name(name.to_s) + return false unless name.identifier + + select_values(<<-SQL, "SCHEMA").any? SELECT c.relname FROM pg_class c LEFT JOIN pg_namespace n ON n.oid = c.relnamespace - WHERE c.relkind IN ('v','m') -- (v)iew, (m)aterialized view - AND n.nspname = ANY (current_schemas(false)) + WHERE c.relkind IN ('r','v','m') -- (r)elation/table, (v)iew, (m)aterialized view + AND c.relname = #{quote(name.identifier)} + AND n.nspname = #{name.schema ? quote(name.schema) : "ANY (current_schemas(false))"} SQL end @@ -137,8 +133,8 @@ module ActiveRecord FROM pg_class c LEFT JOIN pg_namespace n ON n.oid = c.relnamespace WHERE c.relkind IN ('v','m') -- (v)iew, (m)aterialized view - AND c.relname = '#{name.identifier}' - AND n.nspname = #{name.schema ? "'#{name.schema}'" : 'ANY (current_schemas(false))'} + AND c.relname = #{quote(name.identifier)} + AND n.nspname = #{name.schema ? quote(name.schema) : "ANY (current_schemas(false))"} SQL end @@ -170,7 +166,13 @@ module ActiveRecord end # Returns an array of indexes for the given table. - def indexes(table_name, name = nil) + def indexes(table_name, name = nil) # :nodoc: + if name + ActiveSupport::Deprecation.warn(<<-MSG.squish) + Passing name to #indexes is deprecated without replacement. + MSG + end + table = Utils.extract_schema_qualified_name(table_name.to_s) result = query(<<-SQL, "SCHEMA") @@ -221,25 +223,29 @@ module ActiveRecord end.compact end - # Returns the list of all column definitions for a table. - def columns(table_name) # :nodoc: - table_name = table_name.to_s - column_definitions(table_name).map do |column_name, type, default, notnull, oid, fmod, collation, comment| - oid = oid.to_i - fmod = fmod.to_i - type_metadata = fetch_type_metadata(column_name, type, oid, fmod) - default_value = extract_value_from_default(default) - default_function = extract_default_function(default_value, default) - new_column(column_name, default_value, type_metadata, !notnull, table_name, default_function, collation, comment: comment.presence) - end - end - - def new_column(*args) # :nodoc: - PostgreSQLColumn.new(*args) + def new_column_from_field(table_name, field) # :nondoc: + column_name, type, default, notnull, oid, fmod, collation, comment = field + oid = oid.to_i + fmod = fmod.to_i + type_metadata = fetch_type_metadata(column_name, type, oid, fmod) + default_value = extract_value_from_default(default) + default_function = extract_default_function(default_value, default) + PostgreSQLColumn.new( + column_name, + default_value, + type_metadata, + !notnull, + table_name, + default_function, + collation, + comment: comment.presence + ) end def table_options(table_name) # :nodoc: - { comment: table_comment(table_name) } + if comment = table_comment(table_name) + { comment: comment } + end end # Returns a comment stored in database for given table @@ -360,7 +366,7 @@ module ActiveRecord # Resets the sequence of a table's primary key to the maximum value. def reset_pk_sequence!(table, pk = nil, sequence = nil) #:nodoc: - unless pk and sequence + unless pk && sequence default_pk, default_sequence = pk_and_sequence_for(table) pk ||= default_pk @@ -403,7 +409,7 @@ module ActiveRecord AND dep.refobjid = '#{quote_table_name(table)}'::regclass end_sql - if result.nil? or result.empty? + if result.nil? || result.empty? result = query(<<-end_sql, "SCHEMA")[0] SELECT attr.attname, nsp.nspname, CASE @@ -439,7 +445,7 @@ module ActiveRecord WITH pk_constraint AS ( SELECT conrelid, unnest(conkey) AS connum FROM pg_constraint WHERE contype = 'p' - AND conrelid = '#{quote_table_name(table_name)}'::regclass + AND conrelid = #{quote(quote_table_name(table_name))}::regclass ), cons AS ( SELECT conrelid, connum, row_number() OVER() AS rownum FROM pk_constraint ) @@ -625,31 +631,32 @@ module ActiveRecord # Maps logical Rails types to PostgreSQL-specific data types. def type_to_sql(type, limit = nil, precision = nil, scale = nil, array = nil) - sql = case type.to_s - when "binary" - # PostgreSQL doesn't support limits on binary (bytea) columns. - # The hard limit is 1GB, because of a 32-bit size field, and TOAST. - case limit - when nil, 0..0x3fffffff; super(type) - else raise(ActiveRecordError, "No binary type has byte size #{limit}.") - end - when "text" - # PostgreSQL doesn't support limits on text columns. - # The hard limit is 1GB, according to section 8.3 in the manual. - case limit - when nil, 0..0x3fffffff; super(type) - else raise(ActiveRecordError, "The limit on text can be at most 1GB - 1byte.") - end - when "integer" - case limit - when 1, 2; "smallint" - when nil, 3, 4; "integer" - when 5..8; "bigint" - else raise(ActiveRecordError, "No integer type has byte size #{limit}. Use a numeric with scale 0 instead.") - end - else - super(type, limit, precision, scale) - end + sql = \ + case type.to_s + when "binary" + # PostgreSQL doesn't support limits on binary (bytea) columns. + # The hard limit is 1GB, because of a 32-bit size field, and TOAST. + case limit + when nil, 0..0x3fffffff; super(type) + else raise(ActiveRecordError, "No binary type has byte size #{limit}.") + end + when "text" + # PostgreSQL doesn't support limits on text columns. + # The hard limit is 1GB, according to section 8.3 in the manual. + case limit + when nil, 0..0x3fffffff; super(type) + else raise(ActiveRecordError, "The limit on text can be at most 1GB - 1byte.") + end + when "integer" + case limit + when 1, 2; "smallint" + when nil, 3, 4; "integer" + when 5..8; "bigint" + else raise(ActiveRecordError, "No integer type has byte size #{limit}. Use a numeric with scale 0 instead.") + end + else + super(type, limit, precision, scale) + end sql << "[]" if array && type != :primary_key sql @@ -658,7 +665,7 @@ module ActiveRecord # PostgreSQL requires the ORDER BY columns in the select list for distinct queries, and # requires that the ORDER BY include the distinct column. def columns_for_distinct(columns, orders) #:nodoc: - order_columns = orders.reject(&:blank?).map{ |s| + order_columns = orders.reject(&:blank?).map { |s| # Convert Arel node to string s = s.to_sql unless s.is_a?(String) # Remove any ASC/DESC modifiers 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 bcef8ac715..311988625f 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/type_metadata.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/type_metadata.rb @@ -8,7 +8,7 @@ module ActiveRecord @type_metadata = type_metadata @oid = oid @fmod = fmod - @array = /\[\]$/ === type_metadata.sql_type + @array = /\[\]$/.match?(type_metadata.sql_type) end def sql_type diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/utils.rb b/activerecord/lib/active_record/connection_adapters/postgresql/utils.rb index 9a0b80d7d3..a3f9ce6d64 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/utils.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/utils.rb @@ -35,6 +35,12 @@ module ActiveRecord end protected + + def parts + @parts ||= [@schema, @identifier].compact + end + + private def unquote(part) if part && part.start_with?('"') part[1..-2] @@ -42,10 +48,6 @@ module ActiveRecord part end end - - def parts - @parts ||= [@schema, @identifier].compact - end end module Utils # :nodoc: @@ -53,7 +55,7 @@ module ActiveRecord # Returns an instance of <tt>ActiveRecord::ConnectionAdapters::PostgreSQL::Name</tt> # extracted from +string+. - # +schema+ is nil if not specified in +string+. + # +schema+ is +nil+ if not specified in +string+. # +schema+ and +identifier+ exclude surrounding quotes (regardless of whether provided in +string+) # +string+ supports the range of schema/table references understood by PostgreSQL, for example: # diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index bd53123511..0ebd907cc0 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -70,7 +70,7 @@ module ActiveRecord ADAPTER_NAME = "PostgreSQL".freeze NATIVE_DATABASE_TYPES = { - primary_key: "serial primary key", + primary_key: "bigserial primary key", string: { name: "character varying" }, text: { name: "text" }, integer: { name: "integer" }, @@ -315,6 +315,10 @@ module ActiveRecord postgresql_version >= 90300 end + def supports_pgcrypto_uuid? + postgresql_version >= 90400 + end + 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") @@ -400,10 +404,12 @@ module ActiveRecord @connection.server_version end - protected + private # See http://www.postgresql.org/docs/current/static/errcodes-appendix.html VALUE_LIMIT_VIOLATION = "22001" + NUMERIC_VALUE_OUT_OF_RANGE = "22003" + NOT_NULL_VIOLATION = "23502" FOREIGN_KEY_VIOLATION = "23503" UNIQUE_VIOLATION = "23505" SERIALIZATION_FAILURE = "40001" @@ -419,6 +425,10 @@ module ActiveRecord InvalidForeignKey.new(message) when VALUE_LIMIT_VIOLATION ValueTooLong.new(message) + when NUMERIC_VALUE_OUT_OF_RANGE + RangeError.new(message) + when NOT_NULL_VIOLATION + NotNullViolation.new(message) when SERIALIZATION_FAILURE SerializationFailure.new(message) when DEADLOCK_DETECTED @@ -428,22 +438,20 @@ module ActiveRecord end end - private - - def get_oid_type(oid, fmod, column_name, sql_type = "") # :nodoc: + def get_oid_type(oid, fmod, column_name, sql_type = "") if !type_map.key?(oid) load_additional_types(type_map, [oid]) end type_map.fetch(oid, fmod, sql_type) { warn "unknown OID #{oid}: failed to recognize type of '#{column_name}'. It will be treated as String." - Type::Value.new.tap do |cast_type| + Type.default_value.tap do |cast_type| type_map.register_type(oid, cast_type) end } end - def initialize_type_map(m) # :nodoc: + 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 @@ -511,7 +519,7 @@ module ActiveRecord load_additional_types(m) end - def extract_limit(sql_type) # :nodoc: + def extract_limit(sql_type) case sql_type when /^bigint/i, /^int8/i 8 @@ -523,11 +531,11 @@ module ActiveRecord end # Extracts the value from a PostgreSQL column default definition. - def extract_value_from_default(default) # :nodoc: + def extract_value_from_default(default) case default # Quoted types when /\A[\(B]?'(.*)'.*::"?([\w. ]+)"?(?:\[\])?\z/m - # The default 'now'::date is CURRENT_DATE + # The default 'now'::date is CURRENT_DATE if $1 == "now".freeze && $2 == "date".freeze nil else @@ -542,22 +550,22 @@ module ActiveRecord # Object identifier types when /\A-?\d+\z/ $1 - else - # Anything else is blank, some user type, or some function - # and we can't know the value of that, so return nil. + else + # Anything else is blank, some user type, or some function + # and we can't know the value of that, so return nil. nil end end - def extract_default_function(default_value, default) # :nodoc: + def extract_default_function(default_value, default) default if has_default_function?(default_value, default) end - def has_default_function?(default_value, default) # :nodoc: + def has_default_function?(default_value, default) !default_value && (%r{\w+\(.*\)|\(.*\)::\w+} === default) end - def load_additional_types(type_map, oids = nil) # :nodoc: + def load_additional_types(type_map, oids = nil) initializer = OID::TypeMapInitializer.new(type_map) if supports_ranges? @@ -601,7 +609,11 @@ module ActiveRecord def exec_no_cache(sql, name, binds) type_casted_binds = type_casted_binds(binds) - log(sql, name, binds, type_casted_binds) { @connection.async_exec(sql, type_casted_binds) } + log(sql, name, binds, type_casted_binds) do + ActiveSupport::Dependencies.interlock.permit_concurrent_loads do + @connection.async_exec(sql, type_casted_binds) + end + end end def exec_cache(sql, name, binds) @@ -609,7 +621,9 @@ module ActiveRecord type_casted_binds = type_casted_binds(binds) log(sql, name, binds, type_casted_binds, stmt_key) do - @connection.exec_prepared(stmt_key, type_casted_binds) + ActiveSupport::Dependencies.interlock.permit_concurrent_loads do + @connection.exec_prepared(stmt_key, type_casted_binds) + end end rescue ActiveRecord::StatementInvalid => e raise unless is_cached_plan_failure?(e) @@ -719,7 +733,7 @@ module ActiveRecord end # Returns the current ID of a table's sequence. - def last_insert_id_result(sequence_name) # :nodoc: + def last_insert_id_result(sequence_name) exec_query("SELECT currval('#{sequence_name}')", "SQL") end @@ -741,7 +755,7 @@ module ActiveRecord # Query implementation notes: # - format_type includes the column size constraint, e.g. varchar(50) # - ::regclass is a function that gives the id for a table name - def column_definitions(table_name) # :nodoc: + def column_definitions(table_name) query(<<-end_sql, "SCHEMA") SELECT a.attname, format_type(a.atttypid, a.atttypmod), pg_get_expr(d.adbin, d.adrelid), a.attnotnull, a.atttypid, a.atttypmod, @@ -750,18 +764,18 @@ module ActiveRecord col_description(a.attrelid, a.attnum) AS comment FROM pg_attribute a LEFT JOIN pg_attrdef d ON a.attrelid = d.adrelid AND a.attnum = d.adnum - WHERE a.attrelid = '#{quote_table_name(table_name)}'::regclass + WHERE a.attrelid = #{quote(quote_table_name(table_name))}::regclass AND a.attnum > 0 AND NOT a.attisdropped ORDER BY a.attnum end_sql end - def extract_table_ref_from_insert_sql(sql) # :nodoc: + def extract_table_ref_from_insert_sql(sql) sql[/into\s("[A-Za-z0-9_."\[\]\s]+"|[A-Za-z0-9_."\[\]]+)\s*/im] $1.strip if $1 end - def create_table_definition(*args) # :nodoc: + def create_table_definition(*args) PostgreSQL::TableDefinition.new(*args) end @@ -771,10 +785,14 @@ module ActiveRecord sql = <<-end_sql SELECT exists( SELECT * FROM pg_proc + WHERE proname = 'lower' + AND proargtypes = ARRAY[#{quote column.sql_type}::regtype]::oidvector + ) OR exists( + SELECT * FROM pg_proc INNER JOIN pg_cast - ON casttarget::text::oidvector = proargtypes + ON ARRAY[casttarget]::oidvector = proargtypes WHERE proname = 'lower' - AND castsource = '#{column.sql_type}'::regtype::oid + AND castsource = #{quote column.sql_type}::regtype ) end_sql execute_and_clear(sql, "SCHEMA", []) do |result| @@ -788,7 +806,6 @@ module ActiveRecord map[Integer] = PG::TextEncoder::Integer.new map[TrueClass] = PG::TextEncoder::Boolean.new map[FalseClass] = PG::TextEncoder::Boolean.new - map[Float] = PG::TextEncoder::Float.new @connection.type_map_for_queries = map end @@ -838,8 +855,8 @@ module ActiveRecord ActiveRecord::Type.register(:json, OID::Json, adapter: :postgresql) ActiveRecord::Type.register(:jsonb, OID::Jsonb, adapter: :postgresql) ActiveRecord::Type.register(:money, OID::Money, adapter: :postgresql) - ActiveRecord::Type.register(:point, OID::Rails51Point, adapter: :postgresql) - ActiveRecord::Type.register(:legacy_point, OID::Point, adapter: :postgresql) + ActiveRecord::Type.register(:point, OID::Point, adapter: :postgresql) + ActiveRecord::Type.register(:legacy_point, OID::LegacyPoint, adapter: :postgresql) ActiveRecord::Type.register(:uuid, OID::Uuid, adapter: :postgresql) ActiveRecord::Type.register(:vector, OID::Vector, adapter: :postgresql) ActiveRecord::Type.register(:xml, OID::Xml, adapter: :postgresql) diff --git a/activerecord/lib/active_record/connection_adapters/schema_cache.rb b/activerecord/lib/active_record/connection_adapters/schema_cache.rb index 8219f132c3..4d339b0a8c 100644 --- a/activerecord/lib/active_record/connection_adapters/schema_cache.rb +++ b/activerecord/lib/active_record/connection_adapters/schema_cache.rb @@ -21,6 +21,22 @@ module ActiveRecord @data_sources = @data_sources.dup end + def encode_with(coder) + coder["columns"] = @columns + coder["columns_hash"] = @columns_hash + coder["primary_keys"] = @primary_keys + coder["data_sources"] = @data_sources + coder["version"] = ActiveRecord::Migrator.current_version + end + + def init_with(coder) + @columns = coder["columns"] + @columns_hash = coder["columns_hash"] + @primary_keys = coder["primary_keys"] + @data_sources = coder["data_sources"] + @version = coder["version"] + end + def primary_keys(table_name) @primary_keys[table_name] ||= data_source_exists?(table_name) ? connection.primary_key(table_name) : nil end @@ -32,8 +48,6 @@ module ActiveRecord @data_sources[name] = connection.data_source_exists?(name) end - alias table_exists? data_source_exists? - deprecate table_exists?: "use #data_source_exists? instead" # Add internal cache for table with +table_name+. def add(table_name) @@ -47,8 +61,6 @@ module ActiveRecord def data_sources(name) @data_sources[name] end - alias tables data_sources - deprecate tables: "use #data_sources instead" # Get the columns for a table def columns(table_name) @@ -83,8 +95,6 @@ module ActiveRecord @primary_keys.delete name @data_sources.delete name end - alias clear_table_cache! clear_data_source_cache! - deprecate clear_table_cache!: "use #clear_data_source_cache! instead" def marshal_dump # if we get current version during initialization, it happens stack over flow. diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/sqlite3/schema_definitions.rb new file mode 100644 index 0000000000..d0b38dff4c --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/sqlite3/schema_definitions.rb @@ -0,0 +1,23 @@ +module ActiveRecord + module ConnectionAdapters + module SQLite3 + module ColumnMethods + def primary_key(name, type = :primary_key, **options) + if options.delete(:auto_increment) == true && %i(integer bigint).include?(type) + type = :primary_key + end + + super + end + end + + class TableDefinition < ActiveRecord::ConnectionAdapters::TableDefinition + include ColumnMethods + end + + class Table < ActiveRecord::ConnectionAdapters::Table + include ColumnMethods + 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 new file mode 100644 index 0000000000..c027fef83c --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/sqlite3/schema_dumper.rb @@ -0,0 +1,13 @@ +module ActiveRecord + module ConnectionAdapters + module SQLite3 + module ColumnDumper + private + + def default_primary_key?(column) + schema_type(column) == :integer + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb index 57699badba..ec44d020c2 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb @@ -3,6 +3,8 @@ 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" gem "sqlite3", "~> 1.3.6" require "sqlite3" @@ -52,6 +54,7 @@ module ActiveRecord ADAPTER_NAME = "SQLite".freeze include SQLite3::Quoting + include SQLite3::ColumnDumper NATIVE_DATABASE_TYPES = { primary_key: "INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL", @@ -75,6 +78,10 @@ module ActiveRecord end end + def update_table_definition(table_name, base) # :nodoc: + SQLite3::Table.new(table_name, base) + end + def schema_creation # :nodoc: SQLite3::SchemaCreation.new self end @@ -191,30 +198,32 @@ module ActiveRecord type_casted_binds = type_casted_binds(binds) log(sql, name, binds, type_casted_binds) do - # Don't cache statements if they are not prepared - unless prepare - stmt = @connection.prepare(sql) - begin - cols = stmt.columns - unless without_prepared_statement?(binds) - stmt.bind_params(type_casted_binds) + ActiveSupport::Dependencies.interlock.permit_concurrent_loads do + # Don't cache statements if they are not prepared + unless prepare + stmt = @connection.prepare(sql) + begin + cols = stmt.columns + unless without_prepared_statement?(binds) + stmt.bind_params(type_casted_binds) + end + records = stmt.to_a + ensure + stmt.close end + else + cache = @statements[sql] ||= { + stmt: @connection.prepare(sql) + } + stmt = cache[:stmt] + cols = cache[:cols] ||= stmt.columns + stmt.reset! + stmt.bind_params(type_casted_binds) records = stmt.to_a - ensure - stmt.close end - else - cache = @statements[sql] ||= { - stmt: @connection.prepare(sql) - } - stmt = cache[:stmt] - cols = cache[:cols] ||= stmt.columns - stmt.reset! - stmt.bind_params(type_casted_binds) - records = stmt.to_a - end - ActiveRecord::Result.new(cols, records) + ActiveRecord::Result.new(cols, records) + end end end @@ -229,64 +238,55 @@ module ActiveRecord end def execute(sql, name = nil) #:nodoc: - log(sql, name) { @connection.execute(sql) } + log(sql, name) do + ActiveSupport::Dependencies.interlock.permit_concurrent_loads do + @connection.execute(sql) + end + end end def begin_db_transaction #:nodoc: - log("begin transaction",nil) { @connection.transaction } + log("begin transaction", nil) { @connection.transaction } end def commit_db_transaction #:nodoc: - log("commit transaction",nil) { @connection.commit } + log("commit transaction", nil) { @connection.commit } end def exec_rollback_db_transaction #:nodoc: - log("rollback transaction",nil) { @connection.rollback } + log("rollback transaction", nil) { @connection.rollback } end # SCHEMA STATEMENTS ======================================== - def tables(name = nil) # :nodoc: - ActiveSupport::Deprecation.warn(<<-MSG.squish) - #tables currently returns both tables and views. - This behavior is deprecated and will be changed with Rails 5.1 to only return tables. - Use #data_sources instead. - MSG - - if name - ActiveSupport::Deprecation.warn(<<-MSG.squish) - Passing arguments to #tables is deprecated without replacement. - MSG - end - - data_sources + def tables # :nodoc: + select_values("SELECT name FROM sqlite_master WHERE type = 'table' AND name <> 'sqlite_sequence'", "SCHEMA") end - def data_sources + def data_sources # :nodoc: select_values("SELECT name FROM sqlite_master WHERE type IN ('table','view') AND name <> 'sqlite_sequence'", "SCHEMA") end - def table_exists?(table_name) - ActiveSupport::Deprecation.warn(<<-MSG.squish) - #table_exists? currently checks both tables and views. - This behavior is deprecated and will be changed with Rails 5.1 to only check tables. - Use #data_source_exists? instead. - MSG - - data_source_exists?(table_name) + def views # :nodoc: + select_values("SELECT name FROM sqlite_master WHERE type = 'view' AND name <> 'sqlite_sequence'", "SCHEMA") end - def data_source_exists?(table_name) + def table_exists?(table_name) # :nodoc: return false unless table_name.present? - sql = "SELECT name FROM sqlite_master WHERE type IN ('table','view') AND name <> 'sqlite_sequence'" + sql = "SELECT name FROM sqlite_master WHERE type = 'table' AND name <> 'sqlite_sequence'" sql << " AND name = #{quote(table_name)}" select_values(sql, "SCHEMA").any? end - def views # :nodoc: - select_values("SELECT name FROM sqlite_master WHERE type = 'view' AND name <> 'sqlite_sequence'", "SCHEMA") + def data_source_exists?(table_name) # :nodoc: + return false unless table_name.present? + + sql = "SELECT name FROM sqlite_master WHERE type IN ('table','view') AND name <> 'sqlite_sequence'" + sql << " AND name = #{quote(table_name)}" + + select_values(sql, "SCHEMA").any? end def view_exists?(view_name) # :nodoc: @@ -298,28 +298,30 @@ module ActiveRecord select_values(sql, "SCHEMA").any? end - # Returns an array of +Column+ objects for the table specified by +table_name+. - def columns(table_name) # :nodoc: - table_name = table_name.to_s - table_structure(table_name).map do |field| - case field["dflt_value"] - when /^null$/i - field["dflt_value"] = nil - when /^'(.*)'$/m - field["dflt_value"] = $1.gsub("''", "'") - when /^"(.*)"$/m - field["dflt_value"] = $1.gsub('""', '"') - end - - collation = field["collation"] - sql_type = field["type"] - type_metadata = fetch_type_metadata(sql_type) - new_column(field["name"], field["dflt_value"], type_metadata, field["notnull"].to_i == 0, table_name, nil, collation) + def new_column_from_field(table_name, field) # :nondoc: + case field["dflt_value"] + when /^null$/i + field["dflt_value"] = nil + when /^'(.*)'$/m + field["dflt_value"] = $1.gsub("''", "'") + when /^"(.*)"$/m + field["dflt_value"] = $1.gsub('""', '"') end + + collation = field["collation"] + sql_type = field["type"] + type_metadata = fetch_type_metadata(sql_type) + new_column(field["name"], field["dflt_value"], type_metadata, field["notnull"].to_i == 0, table_name, nil, collation) 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 + exec_query("PRAGMA index_list(#{quote_table_name(table_name)})", "SCHEMA").map do |row| sql = <<-SQL SELECT sql @@ -410,7 +412,7 @@ module ActiveRecord self.default = options[:default] if include_default self.null = options[:null] if options.include?(:null) self.precision = options[:precision] if options.include?(:precision) - self.scale = options[:scale] if options.include?(:scale) + self.scale = options[:scale] if options.include?(:scale) self.collation = options[:collation] if options.include?(:collation) end end @@ -418,21 +420,22 @@ module ActiveRecord def rename_column(table_name, column_name, new_column_name) #:nodoc: column = column_for(table_name, column_name) - alter_table(table_name, rename: {column.name => new_column_name.to_s}) + alter_table(table_name, rename: { column.name => new_column_name.to_s }) rename_column_indexes(table_name, column.name, new_column_name) end - protected + private def table_structure(table_name) structure = exec_query("PRAGMA table_info(#{quote_table_name(table_name)})", "SCHEMA") raise(ActiveRecord::StatementInvalid, "Could not find table '#{table_name}'") if structure.empty? table_structure_with_collation(table_name, structure) end + alias column_definitions table_structure - def alter_table(table_name, options = {}) #:nodoc: + def alter_table(table_name, options = {}) altered_table_name = "a#{table_name}" - caller = lambda {|definition| yield definition if block_given?} + caller = lambda { |definition| yield definition if block_given? } transaction do move_table(table_name, altered_table_name, @@ -441,12 +444,12 @@ module ActiveRecord end end - def move_table(from, to, options = {}, &block) #:nodoc: + def move_table(from, to, options = {}, &block) copy_table(from, to, options, &block) drop_table(from) end - def copy_table(from, to, options = {}) #:nodoc: + def copy_table(from, to, options = {}) from_primary_key = primary_key(from) options[:id] = false create_table(to, options) do |definition| @@ -472,7 +475,7 @@ module ActiveRecord options[:rename] || {}) end - def copy_table_indexes(from, to, rename = {}) #:nodoc: + def copy_table_indexes(from, to, rename = {}) indexes(from).each do |index| name = index.name if to == "a#{from}" @@ -482,7 +485,7 @@ module ActiveRecord end to_column_names = columns(to).map(&:name) - columns = index.columns.map {|c| rename[c] || c }.select do |column| + columns = index.columns.map { |c| rename[c] || c }.select do |column| to_column_names.include?(column) end @@ -495,11 +498,11 @@ module ActiveRecord end end - def copy_table_contents(from, to, columns, rename = {}) #:nodoc: - column_mappings = Hash[columns.map {|name| [name, name]}] + def copy_table_contents(from, to, columns, rename = {}) + column_mappings = Hash[columns.map { |name| [name, name] }] rename.each { |a| column_mappings[a.last] = a.first } from_columns = columns(from).collect(&:name) - columns = columns.find_all{|col| from_columns.include?(column_mappings[col])} + columns = columns.find_all { |col| from_columns.include?(column_mappings[col]) } from_columns_to_copy = columns.map { |col| column_mappings[col] } quoted_columns = columns.map { |col| quote_column_name(col) } * "," quoted_from_columns = from_columns_to_copy.map { |col| quote_column_name(col) } * "," @@ -520,20 +523,23 @@ module ActiveRecord # column *column_name* is not unique when /column(s)? .* (is|are) not unique/, /UNIQUE constraint failed: .*/ RecordNotUnique.new(message) + when /.* may not be NULL/, /NOT NULL constraint failed: .*/ + NotNullViolation.new(message) else super end end - private COLLATE_REGEX = /.*\"(\w+)\".*collate\s+\"(\w+)\".*/i.freeze def table_structure_with_collation(table_name, basic_structure) collation_hash = {} - sql = "SELECT sql FROM - (SELECT * FROM sqlite_master UNION ALL - SELECT * FROM sqlite_temp_master) - WHERE type='table' and name='#{ table_name }' \;" + sql = <<-SQL + SELECT sql FROM + (SELECT * FROM sqlite_master UNION ALL + SELECT * FROM sqlite_temp_master) + WHERE type = 'table' AND name = #{quote(table_name)} + SQL # Result will have following sample string # CREATE TABLE "users" ("id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, @@ -564,6 +570,10 @@ module ActiveRecord basic_structure.to_hash end end + + def create_table_definition(*args) + SQLite3::TableDefinition.new(*args) + end end 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 273b1b0b5c..790db56185 100644 --- a/activerecord/lib/active_record/connection_adapters/statement_pool.rb +++ b/activerecord/lib/active_record/connection_adapters/statement_pool.rb @@ -6,7 +6,7 @@ module ActiveRecord DEFAULT_STATEMENT_LIMIT = 1000 def initialize(statement_limit = nil) - @cache = Hash.new { |h,pid| h[pid] = {} } + @cache = Hash.new { |h, pid| h[pid] = {} } @statement_limit = statement_limit || DEFAULT_STATEMENT_LIMIT end diff --git a/activerecord/lib/active_record/connection_handling.rb b/activerecord/lib/active_record/connection_handling.rb index b4cd6cdd38..2ede92feff 100644 --- a/activerecord/lib/active_record/connection_handling.rb +++ b/activerecord/lib/active_record/connection_handling.rb @@ -109,7 +109,7 @@ module ActiveRecord end def connection_pool - connection_handler.retrieve_connection_pool(connection_specification_name) or raise ConnectionNotEstablished + connection_handler.retrieve_connection_pool(connection_specification_name) || raise(ConnectionNotEstablished) end def retrieve_connection diff --git a/activerecord/lib/active_record/core.rb b/activerecord/lib/active_record/core.rb index cb11cdefd9..4d30bdf196 100644 --- a/activerecord/lib/active_record/core.rb +++ b/activerecord/lib/active_record/core.rb @@ -171,41 +171,37 @@ module ActiveRecord return super if block_given? || primary_key.nil? || scope_attributes? || - columns_hash.include?(inheritance_column) || - ids.first.kind_of?(Array) - - id = ids.first - if ActiveRecord::Base === id - id = id.id - ActiveSupport::Deprecation.warn(<<-MSG.squish) - You are passing an instance of ActiveRecord::Base to `find`. - Please pass the id of the object by calling `.id`. - MSG - end + columns_hash.include?(inheritance_column) + + id = ids.first + + return super if id.kind_of?(Array) || + id.is_a?(ActiveRecord::Base) key = primary_key statement = cached_find_by_statement(key) { |params| where(key => params.bind).limit(1) } + record = statement.execute([id], self, connection).first unless record raise RecordNotFound.new("Couldn't find #{name} with '#{primary_key}'=#{id}", name, primary_key, id) end record - rescue RangeError + rescue ::RangeError raise RecordNotFound.new("Couldn't find #{name} with an out of range value for '#{primary_key}'", name, primary_key) end def find_by(*args) # :nodoc: - return super if scope_attributes? || !(Hash === args.first) || reflect_on_all_aggregations.any? + return super if scope_attributes? || reflect_on_all_aggregations.any? hash = args.first - return super if hash.values.any? { |v| - v.nil? || Array === v || Hash === v || Relation === v + return super if !(Hash === hash) || hash.values.any? { |v| + v.nil? || Array === v || Hash === v || Relation === v || Base === v } # We can't cache Post.find_by(author: david) ...yet @@ -223,13 +219,13 @@ module ActiveRecord statement.execute(hash.values, self, connection).first rescue TypeError raise ActiveRecord::StatementInvalid - rescue RangeError + rescue ::RangeError nil end end def find_by!(*args) # :nodoc: - find_by(*args) or raise RecordNotFound.new("Couldn't find #{name}", name) + find_by(*args) || raise(RecordNotFound.new("Couldn't find #{name}", name)) end def initialize_generated_modules # :nodoc: @@ -239,7 +235,9 @@ module ActiveRecord def generated_association_methods @generated_association_methods ||= begin mod = const_set(:GeneratedAssociationMethods, Module.new) + private_constant :GeneratedAssociationMethods include mod + mod end end @@ -299,14 +297,14 @@ module ActiveRecord private - def cached_find_by_statement(key, &block) # :nodoc: + 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) } end - def relation # :nodoc: + def relation relation = Relation.create(self, arel_table, predicate_builder) if finder_needs_type_condition? && !ignore_default_scope? @@ -316,7 +314,7 @@ module ActiveRecord end end - def table_metadata # :nodoc: + def table_metadata TableMetadata.new(self, arel_table) end end @@ -330,8 +328,8 @@ module ActiveRecord # # Instantiates a single new object # User.new(first_name: 'Jamie') def initialize(attributes = nil) - @attributes = self.class._default_attributes.deep_dup self.class.define_attribute_methods + @attributes = self.class._default_attributes.deep_dup init_internals initialize_internals_callback @@ -366,6 +364,8 @@ module ActiveRecord self.class.define_attribute_methods + yield self if block_given? + _run_find_callbacks _run_initialize_callbacks @@ -450,7 +450,7 @@ module ActiveRecord # [ Person.find(1), Person.find(2), Person.find(3) ] & [ Person.find(1), Person.find(4) ] # => [ Person.find(1) ] def hash if id - [self.class, id].hash + self.class.hash ^ id.hash else super end @@ -472,7 +472,7 @@ module ActiveRecord # Allows sort on objects def <=>(other_object) if other_object.is_a?(self.class) - self.to_key <=> other_object.to_key + to_key <=> other_object.to_key else super end @@ -498,7 +498,7 @@ module ActiveRecord # We check defined?(@attributes) not to issue warnings if the object is # allocated but not initialized. inspection = if defined?(@attributes) && @attributes - self.class.column_names.collect do |name| + self.class.attribute_names.collect do |name| if has_attribute?(name) "#{name}: #{attribute_for_inspect(name)}" end @@ -536,20 +536,20 @@ module ActiveRecord # Returns a hash of the given methods with their names as keys and returned values as values. def slice(*methods) - Hash[methods.map! { |method| [method, public_send(method)] }].with_indifferent_access + Hash[methods.flatten.map! { |method| [method, public_send(method)] }].with_indifferent_access end private - # Under Ruby 1.9, Array#flatten will call #to_ary (recursively) on each of the elements - # of the array, and then rescues from the possible NoMethodError. If those elements are - # ActiveRecord::Base's, then this triggers the various method_missing's that we have, - # which significantly impacts upon performance. - # - # So we can avoid the method_missing hit by explicitly defining #to_ary as nil here. - # - # See also http://tenderlovemaking.com/2011/06/28/til-its-ok-to-return-nil-from-to_ary.html - def to_ary # :nodoc: + # +Array#flatten+ will call +#to_ary+ (recursively) on each of the elements of + # the array, and then rescues from the possible +NoMethodError+. If those elements are + # +ActiveRecord::Base+'s, then this triggers the various +method_missing+'s that we have, + # which significantly impacts upon performance. + # + # So we can avoid the +method_missing+ hit by explicitly defining +#to_ary+ as +nil+ here. + # + # See also http://tenderlovemaking.com/2011/06/28/til-its-ok-to-return-nil-from-to_ary.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 e2da512813..93b9371206 100644 --- a/activerecord/lib/active_record/counter_cache.rb +++ b/activerecord/lib/active_record/counter_cache.rb @@ -12,13 +12,21 @@ module ActiveRecord # # * +id+ - The id of the object you wish to reset a counter on. # * +counters+ - One or more association counters to reset. Association name or counter name can be given. + # * <tt>:touch</tt> - Touch timestamp columns when updating. + # Pass +true+ to touch +updated_at+ and/or +updated_on+. Pass a symbol to + # touch that column or an array of symbols to touch just those ones. # # ==== Examples # - # # For Post with id #1 records reset the comments_count + # # For the Post with id #1, reset the comments_count # Post.reset_counters(1, :comments) - def reset_counters(id, *counters) + # + # # Like above, but also touch the +updated_at+ and/or +updated_on+ + # # attributes. + # Post.reset_counters(1, :comments, touch: true) + def reset_counters(id, *counters, touch: nil) object = find(id) + counters.each do |counter_association| has_many_association = _reflect_on_association(counter_association) unless has_many_association @@ -37,10 +45,12 @@ module ActiveRecord reflection = child_class._reflections.values.find { |e| e.belongs_to? && e.foreign_key.to_s == foreign_key && e.options[:counter_cache].present? } counter_name = reflection.counter_cache_column - unscoped.where(primary_key => object.id).update_all( - counter_name => object.send(counter_association).count(:all) - ) + updates = { counter_name.to_sym => object.send(counter_association).count(:all) } + updates.merge!(touch_updates(touch)) if touch + + unscoped.where(primary_key => object.id).update_all(updates) end + return true end @@ -55,6 +65,9 @@ module ActiveRecord # * +id+ - The id of the object you wish to update a counter on or an array of ids. # * +counters+ - A Hash containing the names of the fields # to update as keys and the amount to update the field by as values. + # * <tt>:touch</tt> option - Touch timestamp columns when updating. + # Pass +true+ to touch +updated_at+ and/or +updated_on+. Pass a symbol to + # touch that column or an array of symbols to touch just those ones. # # ==== Examples # @@ -73,13 +86,28 @@ module ActiveRecord # # UPDATE posts # # SET comment_count = COALESCE(comment_count, 0) + 1 # # WHERE id IN (10, 15) + # + # # For the Posts with id of 10 and 15, increment the comment_count by 1 + # # and update the updated_at value for each counter. + # Post.update_counters [10, 15], comment_count: 1, touch: true + # # Executes the following SQL: + # # UPDATE posts + # # SET comment_count = COALESCE(comment_count, 0) + 1, + # # `updated_at` = '2016-10-13T09:59:23-05:00' + # # WHERE id IN (10, 15) def update_counters(id, counters) + touch = counters.delete(:touch) + updates = counters.map do |counter_name, value| operator = value < 0 ? "-" : "+" quoted_column = connection.quote_column_name(counter_name) "#{quoted_column} = COALESCE(#{quoted_column}, 0) #{operator} #{value.abs}" end + if touch + updates << sanitize_sql_for_assignment(touch_updates(touch)) + end + unscoped.where(primary_key => id).update_all updates.join(", ") end @@ -94,13 +122,20 @@ module ActiveRecord # # * +counter_name+ - The name of the field that should be incremented. # * +id+ - The id of the object that should be incremented or an array of ids. + # * <tt>:touch</tt> - Touch timestamp columns when updating. + # Pass +true+ to touch +updated_at+ and/or +updated_on+. Pass a symbol to + # touch that column or an array of symbols to touch just those ones. # # ==== Examples # # # Increment the posts_count column for the record with an id of 5 # DiscussionBoard.increment_counter(:posts_count, 5) - def increment_counter(counter_name, id) - update_counters(id, counter_name => 1) + # + # # Increment the posts_count column for the record with an id of 5 + # # and update the updated_at value. + # DiscussionBoard.increment_counter(:posts_count, 5, touch: true) + def increment_counter(counter_name, id, touch: nil) + update_counters(id, counter_name => 1, touch: touch) end # Decrement a numeric field by one, via a direct SQL update. @@ -112,14 +147,28 @@ module ActiveRecord # # * +counter_name+ - The name of the field that should be decremented. # * +id+ - The id of the object that should be decremented or an array of ids. + # * <tt>:touch</tt> - Touch timestamp columns when updating. + # Pass +true+ to touch +updated_at+ and/or +updated_on+. Pass a symbol to + # touch that column or an array of symbols to touch just those ones. # # ==== Examples # # # Decrement the posts_count column for the record with an id of 5 # DiscussionBoard.decrement_counter(:posts_count, 5) - def decrement_counter(counter_name, id) - update_counters(id, counter_name => -1) + # + # # Decrement the posts_count column for the record with an id of 5 + # # and update the updated_at value. + # DiscussionBoard.decrement_counter(:posts_count, 5, touch: true) + def decrement_counter(counter_name, id, touch: nil) + update_counters(id, counter_name => -1, touch: touch) end + + private + def touch_updates(touch) + touch = timestamp_attributes_for_update_in_model if touch == true + touch_time = current_time_from_proper_timezone + Array(touch).map { |column| [ column, touch_time ] }.to_h + end end private diff --git a/activerecord/lib/active_record/define_callbacks.rb b/activerecord/lib/active_record/define_callbacks.rb new file mode 100644 index 0000000000..7d955a24be --- /dev/null +++ b/activerecord/lib/active_record/define_callbacks.rb @@ -0,0 +1,20 @@ +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 + # of deprecated code removes this need. + module DefineCallbacks + extend ActiveSupport::Concern + + module ClassMethods # :nodoc: + include ActiveModel::Callbacks + end + + included do + include ActiveModel::Validations::Callbacks + + define_model_callbacks :initialize, :find, :touch, only: :after + define_model_callbacks :save, :create, :update, :destroy + end + end +end diff --git a/activerecord/lib/active_record/dynamic_matchers.rb b/activerecord/lib/active_record/dynamic_matchers.rb index fd2fa7410a..08d42f3dd4 100644 --- a/activerecord/lib/active_record/dynamic_matchers.rb +++ b/activerecord/lib/active_record/dynamic_matchers.rb @@ -1,8 +1,7 @@ -require "active_support/core_ext/regexp" module ActiveRecord module DynamicMatchers #:nodoc: - def respond_to?(name, include_private = false) + def respond_to_missing?(name, include_private = false) if self == Base super else @@ -63,10 +62,10 @@ module ActiveRecord def define model.class_eval <<-CODE, __FILE__, __LINE__ + 1 - def self.#{name}(#{signature}) - #{body} - end - CODE + def self.#{name}(#{signature}) + #{body} + end + CODE end private @@ -75,14 +74,14 @@ module ActiveRecord "#{finder}(#{attributes_hash})" end - # The parameters in the signature may have reserved Ruby words, in order - # to prevent errors, we start each param name with `_`. + # The parameters in the signature may have reserved Ruby words, in order + # to prevent errors, we start each param name with `_`. def signature attribute_names.map { |name| "_#{name}" }.join(", ") end - # Given that the parameters starts with `_`, the finder needs to use the - # same parameter name. + # Given that the parameters starts with `_`, the finder needs to use the + # same parameter name. def attributes_hash "{" + attribute_names.map { |name| ":#{name} => _#{name}" }.join(",") + "}" end diff --git a/activerecord/lib/active_record/enum.rb b/activerecord/lib/active_record/enum.rb index 0a94ab58dd..0ab03b2ab3 100644 --- a/activerecord/lib/active_record/enum.rb +++ b/activerecord/lib/active_record/enum.rb @@ -140,6 +140,8 @@ module ActiveRecord end end + # TODO Change this to private once we've dropped Ruby 2.2 support. + # Workaround for Ruby 2.2 "private attribute?" warning. protected attr_reader :name, :mapping, :subtype diff --git a/activerecord/lib/active_record/errors.rb b/activerecord/lib/active_record/errors.rb index 8fbe43e3ec..18fac5af1b 100644 --- a/activerecord/lib/active_record/errors.rb +++ b/activerecord/lib/active_record/errors.rb @@ -43,7 +43,7 @@ module ActiveRecord # Raised when connection to the database could not been established (for example when # {ActiveRecord::Base.connection=}[rdoc-ref:ConnectionHandling#connection] - # is given a nil object). + # is given a +nil+ object). class ConnectionNotEstablished < ActiveRecordError end @@ -95,19 +95,9 @@ module ActiveRecord # # Wraps the underlying database error as +cause+. class StatementInvalid < ActiveRecordError - def initialize(message = nil, original_exception = nil) - if original_exception - ActiveSupport::Deprecation.warn("Passing #original_exception is deprecated and has no effect. " \ - "Exceptions will automatically capture the original exception.", caller) - end - + def initialize(message = nil) super(message || $!.try(:message)) end - - def original_exception - ActiveSupport::Deprecation.warn("#original_exception is deprecated. Use #cause instead.", caller) - cause - end end # Defunct wrapper class kept for compatibility. @@ -123,10 +113,46 @@ module ActiveRecord class InvalidForeignKey < WrappedDatabaseException end + # Raised when a foreign key constraint cannot be added because the column type does not match the referenced column type. + class MismatchedForeignKey < StatementInvalid + def initialize(adapter = nil, message: nil, table: nil, foreign_key: nil, target_table: nil, primary_key: nil) + @adapter = adapter + if table + msg = <<-EOM.strip_heredoc + Column `#{foreign_key}` on table `#{table}` has a type of `#{column_type(table, foreign_key)}`. + This does not match column `#{primary_key}` on `#{target_table}`, which has type `#{column_type(target_table, primary_key)}`. + To resolve this issue, change the type of the `#{foreign_key}` column on `#{table}` to be :integer. (For example `t.integer #{foreign_key}`). + EOM + else + msg = <<-EOM + There is a mismatch between the foreign key and primary key column types. + Verify that the foreign key column type and the primary key of the associated table match types. + EOM + end + if message + msg << "\nOriginal message: #{message}" + end + super(msg) + end + + private + def column_type(table, column) + @adapter.columns(table).detect { |c| c.name == column }.sql_type + end + end + + # Raised when a record cannot be inserted or updated because it would violate a not null constraint. + class NotNullViolation < StatementInvalid + end + # Raised when a record cannot be inserted or updated because a value too long for a column type. class ValueTooLong < StatementInvalid end + # Raised when values that executed are out of range. + class RangeError < StatementInvalid + end + # Raised when number of bind variables in statement given to +:condition+ key # (for example, when using {ActiveRecord::Base.find}[rdoc-ref:FinderMethods#find] method) # does not match number of expected values supplied. diff --git a/activerecord/lib/active_record/explain.rb b/activerecord/lib/active_record/explain.rb index 980b8e1baa..8f7ae2c33c 100644 --- a/activerecord/lib/active_record/explain.rb +++ b/activerecord/lib/active_record/explain.rb @@ -1,4 +1,3 @@ -require "active_support/lazy_load_hooks" require "active_record/explain_registry" module ActiveRecord diff --git a/activerecord/lib/active_record/explain_subscriber.rb b/activerecord/lib/active_record/explain_subscriber.rb index 706b57842f..abd8cfc8f2 100644 --- a/activerecord/lib/active_record/explain_subscriber.rb +++ b/activerecord/lib/active_record/explain_subscriber.rb @@ -18,10 +18,13 @@ module ActiveRecord # # On the other hand, we want to monitor the performance of our real database # queries, not the performance of the access to the query cache. - IGNORED_PAYLOADS = %w(SCHEMA EXPLAIN CACHE) + IGNORED_PAYLOADS = %w(SCHEMA EXPLAIN) EXPLAINED_SQLS = /\A\s*(with|select|update|delete|insert)\b/i def ignore_payload?(payload) - payload[:exception] || IGNORED_PAYLOADS.include?(payload[:name]) || payload[:sql] !~ EXPLAINED_SQLS + payload[:exception] || + payload[:cached] || + IGNORED_PAYLOADS.include?(payload[:name]) || + payload[:sql] !~ EXPLAINED_SQLS end ActiveSupport::Notifications.subscribe("sql.active_record", new) diff --git a/activerecord/lib/active_record/fixture_set/file.rb b/activerecord/lib/active_record/fixture_set/file.rb index 0888208709..6cf2e01179 100644 --- a/activerecord/lib/active_record/fixture_set/file.rb +++ b/activerecord/lib/active_record/fixture_set/file.rb @@ -38,7 +38,7 @@ module ActiveRecord if row row.last else - {'model_class': nil} + { 'model_class': nil } end end end @@ -66,10 +66,13 @@ module ActiveRecord # Validate our unmarshalled data. def validate(data) unless Hash === data || YAML::Omap === data - raise Fixture::FormatError, "fixture is not a hash" + raise Fixture::FormatError, "fixture is not a hash: #{@file}" end - raise Fixture::FormatError unless data.all? { |name, row| Hash === row } + invalid = data.reject { |_, row| Hash === row } + if invalid.any? + raise Fixture::FormatError, "fixture key is not a hash: #{@file}, keys: #{invalid.keys.inspect}" + end data end end diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb index 40a9aa2783..91d8054ef2 100644 --- a/activerecord/lib/active_record/fixtures.rb +++ b/activerecord/lib/active_record/fixtures.rb @@ -88,7 +88,7 @@ module ActiveRecord # assert_equal "Ruby on Rails", @rubyonrails.name # end # - # In order to use these methods to access fixtured data within your testcases, you must specify one of the + # In order to use these methods to access fixtured data within your test cases, you must specify one of the # following in your ActiveSupport::TestCase-derived class: # # - to fully enable instantiated fixtures (enable alternate methods #1 and #2 above) @@ -103,7 +103,7 @@ module ActiveRecord # # = Dynamic fixtures with ERB # - # Some times you don't care about the content of the fixtures as much as you care about the volume. + # Sometimes you don't care about the content of the fixtures as much as you care about the volume. # In these cases, you can mix ERB in with your YAML fixtures to create a bunch of fixtures for load # testing, like: # @@ -415,9 +415,9 @@ module ActiveRecord # possibly in a folder with the same name. #++ - MAX_ID = 2 ** 30 - 1 + MAX_ID = 2**30 - 1 - @@all_cached_fixtures = Hash.new { |h,k| h[k] = {} } + @@all_cached_fixtures = Hash.new { |h, k| h[k] = {} } def self.default_fixture_model_name(fixture_set_name, config = ActiveRecord::Base) # :nodoc: config.pluralize_table_names ? @@ -536,16 +536,16 @@ module ActiveRecord update_all_loaded_fixtures fixtures_map connection.transaction(requires_new: true) do - deleted_tables = Set.new + deleted_tables = Hash.new { |h, k| h[k] = Set.new } fixture_sets.each do |fs| conn = fs.model_class.respond_to?(:connection) ? fs.model_class.connection : connection table_rows = fs.table_rows table_rows.each_key do |table| - unless deleted_tables.include? table + unless deleted_tables[conn].include? table conn.delete "DELETE FROM #{conn.quote_table_name(table)}", "Fixture Delete" end - deleted_tables << table + deleted_tables[conn] << table end table_rows.each do |fixture_set_name, rows| @@ -597,18 +597,18 @@ module ActiveRecord @fixtures = read_fixture_files(path) - @connection = connection + @connection = connection - @table_name = ( model_class.respond_to?(:table_name) ? + @table_name = (model_class.respond_to?(:table_name) ? model_class.table_name : - self.class.default_fixture_table_name(name, config) ) + self.class.default_fixture_table_name(name, config)) end def [](x) fixtures[x] end - def []=(k,v) + def []=(k, v) fixtures[k] = v end @@ -629,7 +629,7 @@ module ActiveRecord fixtures.delete("DEFAULTS") # track any join tables we need to insert later - rows = Hash.new { |h,table| h[table] = [] } + rows = Hash.new { |h, table| h[table] = [] } rows[table_name] = fixtures.map do |label, fixture| row = fixture.to_hash @@ -862,29 +862,17 @@ module ActiveRecord class_attribute :fixture_table_names class_attribute :fixture_class_names class_attribute :use_transactional_tests - class_attribute :use_transactional_fixtures class_attribute :use_instantiated_fixtures # true, false, or :no_instances class_attribute :pre_loaded_fixtures class_attribute :config - singleton_class.deprecate "use_transactional_fixtures=" => "use use_transactional_tests= instead" - self.fixture_table_names = [] self.use_instantiated_fixtures = false self.pre_loaded_fixtures = false self.config = ActiveRecord::Base self.fixture_class_names = {} - - silence_warnings do - define_singleton_method :use_transactional_tests do - if use_transactional_fixtures.nil? - true - else - use_transactional_fixtures - end - end - end + self.use_transactional_tests = true end module ClassMethods @@ -897,12 +885,12 @@ module ActiveRecord # # The keys must be the fixture names, that coincide with the short paths to the fixture files. def set_fixture_class(class_names = {}) - self.fixture_class_names = self.fixture_class_names.merge(class_names.stringify_keys) + self.fixture_class_names = fixture_class_names.merge(class_names.stringify_keys) end def fixtures(*fixture_set_names) if fixture_set_names.first == :all - fixture_set_names = Dir["#{fixture_path}/{**,*}/*.{yml}"] + fixture_set_names = Dir["#{fixture_path}/{**,*}/*.{yml}"].uniq fixture_set_names.map! { |f| f[(fixture_path.to_s.size + 1)..-5] } else fixture_set_names = fixture_set_names.flatten.map(&:to_s) diff --git a/activerecord/lib/active_record/inheritance.rb b/activerecord/lib/active_record/inheritance.rb index 4adcd7e65c..fbdaeaae51 100644 --- a/activerecord/lib/active_record/inheritance.rb +++ b/activerecord/lib/active_record/inheritance.rb @@ -130,16 +130,26 @@ module ActiveRecord store_full_sti_class ? name : name.demodulize end + def inherited(subclass) + subclass.instance_variable_set(:@_type_candidates_cache, Concurrent::Map.new) + super + end + protected - # Returns the class type of the record using the current module as a prefix. So descendants of - # MyApp::Business::Account would appear as MyApp::Business::AccountSubclass. + # Returns the class type of the record using the current module as a prefix. So descendants of + # MyApp::Business::Account would appear as MyApp::Business::AccountSubclass. def compute_type(type_name) - if type_name.match(/^::/) + if type_name.start_with?("::".freeze) # If the type is prefixed with a scope operator then we assume that # the type_name is an absolute reference. ActiveSupport::Dependencies.constantize(type_name) else + type_candidate = @_type_candidates_cache[type_name] + if type_candidate && type_constant = ActiveSupport::Dependencies.safe_constantize(type_candidate) + return type_constant + end + # Build a list of candidates to search for candidates = [] name.scan(/::|$/) { candidates.unshift "#{$`}::#{type_name}" } @@ -147,7 +157,10 @@ module ActiveRecord candidates.each do |candidate| constant = ActiveSupport::Dependencies.safe_constantize(candidate) - return constant if candidate == constant.to_s + if candidate == constant.to_s + @_type_candidates_cache[type_name] = candidate + return constant + end end raise NameError.new("uninitialized constant #{candidates.first}", candidates.first) @@ -156,9 +169,9 @@ module ActiveRecord private - # Called by +instantiate+ to decide which class to use for a new - # record instance. For single-table inheritance, we check the record - # for a +type+ column and return the corresponding class. + # Called by +instantiate+ to decide which class to use for a new + # record instance. For single-table inheritance, we check the record + # for a +type+ column and return the corresponding class. def discriminate_class_for_record(record) if using_single_table_inheritance?(record) find_sti_class(record[inheritance_column]) @@ -199,8 +212,8 @@ module ActiveRecord sti_column.in(sti_names) end - # Detect the subclass from the inheritance column of attrs. If the inheritance column value - # is not self or a valid subclass, raises ActiveRecord::SubclassNotFound + # Detect the subclass from the inheritance column of attrs. If the inheritance column value + # is not self or a valid subclass, raises ActiveRecord::SubclassNotFound def subclass_from_attributes(attrs) attrs = attrs.to_h if attrs.respond_to?(:permitted?) if attrs.is_a?(Hash) @@ -225,11 +238,11 @@ module ActiveRecord ensure_proper_type end - # Sets the attribute used for single table inheritance to this class name if this is not the - # ActiveRecord::Base descendant. - # Considering the hierarchy Reply < Message < ActiveRecord::Base, this makes it possible to - # do Reply.new without having to set <tt>Reply[Reply.inheritance_column] = "Reply"</tt> yourself. - # No such attribute would be set for objects of the Message class in that example. + # Sets the attribute used for single table inheritance to this class name if this is not the + # ActiveRecord::Base descendant. + # Considering the hierarchy Reply < Message < ActiveRecord::Base, this makes it possible to + # do Reply.new without having to set <tt>Reply[Reply.inheritance_column] = "Reply"</tt> yourself. + # No such attribute would be set for objects of the Message class in that example. def ensure_proper_type klass = self.class if klass.finder_needs_type_condition? diff --git a/activerecord/lib/active_record/integration.rb b/activerecord/lib/active_record/integration.rb index e4c7a55541..8e71b60b29 100644 --- a/activerecord/lib/active_record/integration.rb +++ b/activerecord/lib/active_record/integration.rb @@ -15,9 +15,9 @@ module ActiveRecord self.cache_timestamp_format = :usec end - # Returns a String, which Action Pack uses for constructing a URL to this - # object. The default implementation returns this record's id as a String, - # or nil if this record's unsaved. + # Returns a +String+, which Action Pack uses for constructing a URL to this + # object. The default implementation returns this record's id as a +String+, + # or +nil+ if this record's unsaved. # # For example, suppose that you have a User model, and that you have a # <tt>resources :users</tt> route. Normally, +user_path+ will @@ -53,18 +53,21 @@ module ActiveRecord # # Person.find(5).cache_key(:updated_at, :last_reviewed_at) def cache_key(*timestamp_names) - case - when new_record? + if new_record? "#{model_name.cache_key}/new" - when timestamp_names.any? - timestamp = max_updated_column_timestamp(timestamp_names) - timestamp = timestamp.utc.to_s(cache_timestamp_format) - "#{model_name.cache_key}/#{id}-#{timestamp}" - when timestamp = max_updated_column_timestamp - timestamp = timestamp.utc.to_s(cache_timestamp_format) - "#{model_name.cache_key}/#{id}-#{timestamp}" else - "#{model_name.cache_key}/#{id}" + timestamp = if timestamp_names.any? + max_updated_column_timestamp(timestamp_names) + else + max_updated_column_timestamp + end + + if timestamp + timestamp = timestamp.utc.to_s(cache_timestamp_format) + "#{model_name.cache_key}/#{id}-#{timestamp}" + else + "#{model_name.cache_key}/#{id}" + end end end diff --git a/activerecord/lib/active_record/internal_metadata.rb b/activerecord/lib/active_record/internal_metadata.rb index 20d61dba67..25ee9d6bfe 100644 --- a/activerecord/lib/active_record/internal_metadata.rb +++ b/activerecord/lib/active_record/internal_metadata.rb @@ -23,7 +23,7 @@ module ActiveRecord end def table_exists? - ActiveSupport::Deprecation.silence { connection.table_exists?(table_name) } + connection.table_exists?(table_name) end # Creates an internal metadata table with columns +key+ and +value+ diff --git a/activerecord/lib/active_record/locking/optimistic.rb b/activerecord/lib/active_record/locking/optimistic.rb index 1b6cda3861..2659c60f1f 100644 --- a/activerecord/lib/active_record/locking/optimistic.rb +++ b/activerecord/lib/active_record/locking/optimistic.rb @@ -47,6 +47,8 @@ module ActiveRecord # self.locking_column = :lock_person # end # + # Please note that the optimistic locking will be ignored if you update the + # locking column's value. module Optimistic extend ActiveSupport::Concern @@ -60,13 +62,14 @@ module ActiveRecord end private + def increment_lock lock_col = self.class.locking_column previous_lock_value = send(lock_col).to_i send(lock_col + "=", previous_lock_value + 1) end - def _create_record(attribute_names = self.attribute_names, *) # :nodoc: + def _create_record(attribute_names = self.attribute_names, *) if locking_enabled? # We always want to persist the locking version, even if we don't detect # a change from the default, since the database might have no default @@ -75,23 +78,26 @@ module ActiveRecord super end - def _update_record(attribute_names = self.attribute_names) #:nodoc: + def _update_record(attribute_names = self.attribute_names) return super unless locking_enabled? - return 0 if attribute_names.empty? lock_col = self.class.locking_column - previous_lock_value = send(lock_col).to_i - increment_lock - attribute_names += [lock_col] - attribute_names.uniq! + return super if attribute_names.include?(lock_col) + return 0 if attribute_names.empty? begin + previous_lock_value = read_attribute_before_type_cast(lock_col) + + increment_lock + + attribute_names.push(lock_col) + relation = self.class.unscoped affected_rows = relation.where( self.class.primary_key => id, - lock_col => previous_lock_value, + lock_col => previous_lock_value ).update_all( attributes_for_update(attribute_names).map do |name| [name, _read_attribute(name)] @@ -104,9 +110,9 @@ module ActiveRecord affected_rows - # If something went wrong, revert the version. + # If something went wrong, revert the locking_column value. rescue Exception - send(lock_col + "=", previous_lock_value) + send(lock_col + "=", previous_lock_value.to_i) raise end end @@ -168,10 +174,10 @@ module ActiveRecord private - # We need to apply this decorator here, rather than on module inclusion. The closure - # created by the matcher would otherwise evaluate for `ActiveRecord::Base`, not the - # sub class being decorated. As such, changes to `lock_optimistically`, or - # `locking_column` would not be picked up. + # We need to apply this decorator here, rather than on module inclusion. The closure + # created by the matcher would otherwise evaluate for `ActiveRecord::Base`, not the + # sub class being decorated. As such, changes to `lock_optimistically`, or + # `locking_column` would not be picked up. def inherited(subclass) subclass.class_eval do is_lock_column = ->(name, _) { lock_optimistically && name == locking_column } diff --git a/activerecord/lib/active_record/log_subscriber.rb b/activerecord/lib/active_record/log_subscriber.rb index 20910fbb15..4b8d8d9105 100644 --- a/activerecord/lib/active_record/log_subscriber.rb +++ b/activerecord/lib/active_record/log_subscriber.rb @@ -15,36 +15,22 @@ module ActiveRecord rt end - def initialize - super - @odd = false - end - - def render_bind(attr, type_casted_value) - value = if attr.type.binary? && attr.value - "<#{attr.value_for_database.to_s.bytesize} bytes of binary data>" - else - type_casted_value - end - - [attr.name, value] - end - def sql(event) - return unless logger.debug? - self.class.runtime += event.duration + return unless logger.debug? payload = event.payload return if IGNORE_PAYLOAD_NAMES.include?(payload[:name]) name = "#{payload[:name]} (#{event.duration.round(1)}ms)" + name = "CACHE #{name}" if payload[:cached] sql = payload[:sql] binds = nil unless (payload[:binds] || []).empty? - binds = " " + payload[:binds].zip(payload[:type_casted_binds]).map { |attr, value| + casted_params = type_casted_binds(payload[:binds], payload[:type_casted_binds]) + binds = " " + payload[:binds].zip(casted_params).map { |attr, value| render_bind(attr, value) }.inspect end @@ -57,6 +43,20 @@ module ActiveRecord private + def type_casted_binds(binds, casted_binds) + casted_binds || binds.map { |attr| type_cast attr.value_for_database } + end + + def render_bind(attr, type_casted_value) + value = if attr.type.binary? && attr.value + "<#{attr.value_for_database.to_s.bytesize} bytes of binary data>" + else + type_casted_value + end + + [attr.name, value] + end + def colorize_payload_name(name, payload_name) if payload_name.blank? || payload_name == "SQL" # SQL vs Model Load/Exists color(name, MAGENTA, true) @@ -89,6 +89,10 @@ module ActiveRecord def logger ActiveRecord::Base.logger end + + def type_cast(value) + ActiveRecord::Base.connection.type_cast(value) + end end end diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb index 9206547acf..6e5f5fa2a7 100644 --- a/activerecord/lib/active_record/migration.rb +++ b/activerecord/lib/active_record/migration.rb @@ -1,6 +1,6 @@ require "set" +require "zlib" require "active_support/core_ext/module/attribute_accessors" -require "active_support/core_ext/regexp" module ActiveRecord class MigrationError < ActiveRecordError#:nodoc: @@ -277,8 +277,10 @@ module ActiveRecord # # * <tt>change_column(table_name, column_name, type, options)</tt>: Changes # the column to a different type using the same parameters as add_column. - # * <tt>change_column_default(table_name, column_name, default)</tt>: Sets a - # default value for +column_name+ defined by +default+ on +table_name+. + # * <tt>change_column_default(table_name, column_name, default_or_changes)</tt>: + # Sets a default value for +column_name+ defined by +default_or_changes+ on + # +table_name+. Passing a hash containing <tt>:from</tt> and <tt>:to</tt> + # as +default_or_changes+ will make this change reversible in the migration. # * <tt>change_column_null(table_name, column_name, null, default = nil)</tt>: # Sets or removes a +NOT NULL+ constraint on +column_name+. The +null+ flag # indicates whether the value can be +NULL+. See @@ -520,7 +522,10 @@ module ActiveRecord def self.inherited(subclass) # :nodoc: super if subclass.superclass == Migration - subclass.include Compatibility::Legacy + raise StandardError, "Directly inheriting from ActiveRecord::Migration is not supported. " \ + "Please specify the Rails release the migration was written for:\n" \ + "\n" \ + " class #{self.class.name} < ActiveRecord::Migration[4.2]" end end @@ -725,7 +730,7 @@ module ActiveRecord # end def reversible helper = ReversibleBlockHelper.new(reverting?) - execute_block{ yield helper } + execute_block { yield helper } end # Runs the given migration classes. @@ -767,7 +772,7 @@ module ActiveRecord when :down then announce "reverting" end - time = nil + time = nil ActiveRecord::Base.connection_pool.with_connection do |conn| time = Benchmark.measure do exec_migration(conn, direction) @@ -795,7 +800,7 @@ module ActiveRecord @connection = nil end - def write(text="") + def write(text = "") puts(text) if verbose end @@ -805,7 +810,7 @@ module ActiveRecord write "== %s %s" % [text, "=" * length] end - def say(message, subitem=false) + def say(message, subitem = false) write "#{subitem ? " ->" : "--"} #{message}" end @@ -989,11 +994,11 @@ module ActiveRecord end end - def rollback(migrations_paths, steps=1) + def rollback(migrations_paths, steps = 1) move(:down, migrations_paths, steps) end - def forward(migrations_paths, steps=1) + def forward(migrations_paths, steps = 1) move(:up, migrations_paths, steps) end @@ -1024,12 +1029,10 @@ module ActiveRecord end def get_all_versions(connection = Base.connection) - ActiveSupport::Deprecation.silence do - if connection.table_exists?(schema_migrations_table_name) - SchemaMigration.all.map { |x| x.version.to_i }.sort - else - [] - end + if connection.table_exists?(schema_migrations_table_name) + SchemaMigration.all.map { |x| x.version.to_i }.sort + else + [] end end @@ -1163,29 +1166,31 @@ module ActiveRecord private - # Used for running a specific migration. + # Used for running a specific migration. def run_without_lock migration = migrations.detect { |m| m.version == @target_version } raise UnknownMigrationVersionError.new(@target_version) if migration.nil? - execute_migration_in_transaction(migration, @direction) + result = execute_migration_in_transaction(migration, @direction) record_environment + result end - # Used for running multiple migrations up to or down to a certain value. + # Used for running multiple migrations up to or down to a certain value. def migrate_without_lock if invalid_target? raise UnknownMigrationVersionError.new(@target_version) end - runnable.each do |migration| + result = runnable.each do |migration| execute_migration_in_transaction(migration, @direction) end record_environment + result end - # Stores the current environment in the database. + # Stores the current environment in the database. def record_environment return if down? ActiveRecord::InternalMetadata[:environment] = ActiveRecord::Migrator.current_environment @@ -1195,7 +1200,7 @@ module ActiveRecord migrated.include?(migration.version.to_i) end - # Return true if a valid version is not provided. + # Return true if a valid version is not provided. def invalid_target? !target && @target_version && @target_version > 0 end @@ -1230,10 +1235,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 @@ -1272,7 +1277,7 @@ module ActiveRecord @direction == :down end - # Wrap the migration in a transaction only if supported by the adapter. + # Wrap the migration in a transaction only if supported by the adapter. def ddl_transaction(migration) if use_transaction?(migration) Base.transaction { yield } diff --git a/activerecord/lib/active_record/migration/command_recorder.rb b/activerecord/lib/active_record/migration/command_recorder.rb index 6b2c5d8da5..03103bba98 100644 --- a/activerecord/lib/active_record/migration/command_recorder.rb +++ b/activerecord/lib/active_record/migration/command_recorder.rb @@ -92,7 +92,7 @@ module ActiveRecord send(method, args, &block) end - def respond_to?(*args) # :nodoc: + def respond_to_missing?(*args) # :nodoc: super || delegate.respond_to?(*args) end @@ -125,10 +125,10 @@ module ActiveRecord }.each do |cmd, inv| [[inv, cmd], [cmd, inv]].uniq.each do |method, inverse| class_eval <<-EOV, __FILE__, __LINE__ + 1 - def invert_#{method}(args, &block) # def invert_create_table(args, &block) - [:#{inverse}, args, block] # [:drop_table, args, block] - end # end - EOV + def invert_#{method}(args, &block) # def invert_create_table(args, &block) + [:#{inverse}, args, block] # [:drop_table, args, block] + end # end + EOV end end end @@ -225,7 +225,7 @@ module ActiveRecord [:add_foreign_key, reversed_args] end - # Forwards any missing method call to the \target. + # Forwards any missing method call to the \target. def method_missing(method, *args, &block) if @delegate.respond_to?(method) @delegate.send(method, *args, &block) diff --git a/activerecord/lib/active_record/migration/compatibility.rb b/activerecord/lib/active_record/migration/compatibility.rb index a79251f908..2904634eb7 100644 --- a/activerecord/lib/active_record/migration/compatibility.rb +++ b/activerecord/lib/active_record/migration/compatibility.rb @@ -13,7 +13,27 @@ module ActiveRecord V5_1 = Current - module FourTwoShared + class V5_0 < V5_1 + def create_table(table_name, options = {}) + if adapter_name == "PostgreSQL" + if options[:id] == :uuid && !options[:default] + options[:default] = "uuid_generate_v4()" + end + end + + # Since 5.1 Postgres 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. + if options[:id].blank? + options[:id] = :integer + options[:auto_increment] = true + end + + super + end + end + + class V4_2 < V5_0 module TableDefinition def references(*, **options) options[:index] ||= false @@ -21,7 +41,7 @@ module ActiveRecord end alias :belongs_to :references - def timestamps(*, **options) + def timestamps(**options) options[:null] = true if options[:null].nil? super end @@ -59,7 +79,7 @@ module ActiveRecord end alias :add_belongs_to :add_reference - def add_timestamps(*, **options) + def add_timestamps(_, **options) options[:null] = true if options[:null].nil? super end @@ -101,29 +121,6 @@ module ActiveRecord index_name end end - - class V5_0 < V5_1 - end - - class V4_2 < V5_0 - # 4.2 is defined as a module because it needs to be shared with - # Legacy. When the time comes, V5_0 should be defined straight - # in its class. - include FourTwoShared - end - - module Legacy - include FourTwoShared - - def migrate(*) - ActiveSupport::Deprecation.warn \ - "Directly inheriting from ActiveRecord::Migration is deprecated. " \ - "Please specify the Rails release the migration was written for:\n" \ - "\n" \ - " class #{self.class.name} < ActiveRecord::Migration[4.2]" - super - end - end end end end diff --git a/activerecord/lib/active_record/model_schema.rb b/activerecord/lib/active_record/model_schema.rb index 480734669d..2a28c6bf6d 100644 --- a/activerecord/lib/active_record/model_schema.rb +++ b/activerecord/lib/active_record/model_schema.rb @@ -2,71 +2,150 @@ module ActiveRecord module ModelSchema extend ActiveSupport::Concern + ## + # :singleton-method: primary_key_prefix_type + # :call-seq: primary_key_prefix_type + # + # The prefix type that will be prepended to every primary key column name. + # The options are +:table_name+ and +:table_name_with_underscore+. If the first is specified, + # the Product class will look for "productid" instead of "id" as the primary column. If the + # latter is specified, the Product class will look for "product_id" instead of "id". Remember + # that this is a global setting for all Active Records. + + ## + # :singleton-method: primary_key_prefix_type= + # :call-seq: primary_key_prefix_type=(prefix_type) + # + # Sets the prefix type that will be prepended to every primary key column name. + # The options are +:table_name+ and +:table_name_with_underscore+. If the first is specified, + # the Product class will look for "productid" instead of "id" as the primary column. If the + # latter is specified, the Product class will look for "product_id" instead of "id". Remember + # that this is a global setting for all Active Records. + + ## + # :singleton-method: table_name_prefix + # :call-seq: table_name_prefix + # + # The prefix string to prepend to every table name. + + ## + # :singleton-method: table_name_prefix= + # :call-seq: table_name_prefix=(prefix) + # + # Sets the prefix string to prepend to every table name. So if set to "basecamp_", all table + # names will be named like "basecamp_projects", "basecamp_people", etc. This is a convenient + # way of creating a namespace for tables in a shared database. By default, the prefix is the + # empty string. + # + # If you are organising your models within modules you can add a prefix to the models within + # a namespace by defining a singleton method in the parent module called table_name_prefix which + # returns your chosen prefix. + + ## + # :singleton-method: table_name_suffix + # :call-seq: table_name_suffix + # + # The suffix string to append to every table name. + + ## + # :singleton-method: table_name_suffix= + # :call-seq: table_name_suffix=(suffix) + # + # Works like +table_name_prefix=+, but appends instead of prepends (set to "_basecamp" gives "projects_basecamp", + # "people_basecamp"). By default, the suffix is the empty string. + # + # If you are organising your models within modules, you can add a suffix to the models within + # a namespace by defining a singleton method in the parent module called table_name_suffix which + # returns your chosen suffix. + + ## + # :singleton-method: schema_migrations_table_name + # :call-seq: schema_migrations_table_name + # + # The name of the schema migrations table. By default, the value is <tt>"schema_migrations"</tt>. + + ## + # :singleton-method: schema_migrations_table_name= + # :call-seq: schema_migrations_table_name=(table_name) + # + # Sets the name of the schema migrations table. + + ## + # :singleton-method: internal_metadata_table_name + # :call-seq: internal_metadata_table_name + # + # The name of the internal metadata table. By default, the value is <tt>"ar_internal_metadata"</tt>. + + ## + # :singleton-method: internal_metadata_table_name= + # :call-seq: internal_metadata_table_name=(table_name) + # + # 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 + # + # Indicates whether table names should be the pluralized versions of the corresponding class names. + # If true, the default table name for a Product class will be "products". If false, it would just be "product". + # See table_name for the full rules on table/class naming. This is true, by default. + + ## + # :singleton-method: pluralize_table_names= + # :call-seq: pluralize_table_names=(value) + # + # Set whether table names should be the pluralized versions of the corresponding class names. + # If true, the default table name for a Product class will be "products". If false, it would just be "product". + # See table_name for the full rules on table/class naming. This is true, by default. + + ## + # :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 - ## - # :singleton-method: - # Accessor for the prefix type that will be prepended to every primary key column name. - # The options are :table_name and :table_name_with_underscore. If the first is specified, - # the Product class will look for "productid" instead of "id" as the primary column. If the - # latter is specified, the Product class will look for "product_id" instead of "id". Remember - # that this is a global setting for all Active Records. mattr_accessor :primary_key_prefix_type, instance_writer: false - ## - # :singleton-method: - # Accessor for the name of the prefix string to prepend to every table name. So if set - # to "basecamp_", all table names will be named like "basecamp_projects", "basecamp_people", - # etc. This is a convenient way of creating a namespace for tables in a shared database. - # By default, the prefix is the empty string. - # - # If you are organising your models within modules you can add a prefix to the models within - # a namespace by defining a singleton method in the parent module called table_name_prefix which - # returns your chosen prefix. class_attribute :table_name_prefix, instance_writer: false self.table_name_prefix = "" - ## - # :singleton-method: - # Works like +table_name_prefix+, but appends instead of prepends (set to "_basecamp" gives "projects_basecamp", - # "people_basecamp"). By default, the suffix is the empty string. - # - # If you are organising your models within modules, you can add a suffix to the models within - # a namespace by defining a singleton method in the parent module called table_name_suffix which - # returns your chosen suffix. class_attribute :table_name_suffix, instance_writer: false self.table_name_suffix = "" - ## - # :singleton-method: - # Accessor for the name of the schema migrations table. By default, the value is "schema_migrations" class_attribute :schema_migrations_table_name, instance_accessor: false self.schema_migrations_table_name = "schema_migrations" - ## - # :singleton-method: - # Accessor for the name of the internal metadata table. By default, the value is "ar_internal_metadata" class_attribute :internal_metadata_table_name, instance_accessor: false self.internal_metadata_table_name = "ar_internal_metadata" - ## - # :singleton-method: - # Accessor for an array of names of environments where destructive actions should be prohibited. By default, - # the value is ["production"] class_attribute :protected_environments, instance_accessor: false self.protected_environments = ["production"] - ## - # :singleton-method: - # Indicates whether table names should be the pluralized versions of the corresponding class names. - # If true, the default table name for a Product class will be +products+. If false, it would just be +product+. - # See table_name for the full rules on table/class naming. This is true, by default. class_attribute :pluralize_table_names, instance_writer: false self.pluralize_table_names = true - ## - # :singleton-method: - # Accessor for 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. class_attribute :ignored_columns, instance_accessor: false self.ignored_columns = [].freeze @@ -173,11 +252,11 @@ module ActiveRecord end def full_table_name_prefix #:nodoc: - (parents.detect{ |p| p.respond_to?(:table_name_prefix) } || self).table_name_prefix + (parents.detect { |p| p.respond_to?(:table_name_prefix) } || self).table_name_prefix end def full_table_name_suffix #:nodoc: - (parents.detect {|p| p.respond_to?(:table_name_suffix) } || self).table_name_suffix + (parents.detect { |p| p.respond_to?(:table_name_suffix) } || self).table_name_suffix end # Defines the name of the table column which will store the class name on single-table @@ -213,7 +292,7 @@ module ActiveRecord end # Sets the name of the sequence to use when generating ids to the given - # value, or (if the value is nil or false) to the value returned by the + # value, or (if the value is +nil+ or +false+) to the value returned by the # given block. This is required for Oracle and is useful for any # database which relies on sequences for primary key generation. # @@ -268,7 +347,7 @@ module ActiveRecord def attribute_types # :nodoc: load_schema - @attribute_types ||= Hash.new(Type::Value.new) + @attribute_types ||= Hash.new(Type.default_value) end def yaml_encoder # :nodoc: @@ -370,7 +449,6 @@ module ActiveRecord def load_schema! @columns_hash = connection.schema_cache.columns_hash(table_name).except(*ignored_columns) @columns_hash.each do |name, column| - warn_if_deprecated_type(column) define_attribute( name, connection.lookup_cast_type_from_column(column), @@ -398,13 +476,13 @@ module ActiveRecord end end - # Guesses the table name, but does not decorate it with prefix and suffix information. + # Guesses the table name, but does not decorate it with prefix and suffix information. def undecorated_table_name(class_name = base_class.name) table_name = class_name.to_s.demodulize.underscore pluralize_table_names ? table_name.pluralize : table_name end - # Computes and returns a table name according to default conventions. + # Computes and returns a table name according to default conventions. def compute_table_name base = base_class if self == base @@ -421,28 +499,6 @@ module ActiveRecord base.table_name end end - - def warn_if_deprecated_type(column) - return if attributes_to_define_after_schema_loads.key?(column.name) - if column.respond_to?(:oid) && column.sql_type.start_with?("point") - if column.array? - array_arguments = ", array: true" - else - array_arguments = "" - end - ActiveSupport::Deprecation.warn(<<-WARNING.strip_heredoc) - The behavior of the `:point` type will be changing in Rails 5.1 to - return a `Point` object, rather than an `Array`. If you'd like to - keep the old behavior, you can add this line to #{name}: - - attribute :#{column.name}, :legacy_point#{array_arguments} - - If you'd like the new behavior today, you can add this line: - - attribute :#{column.name}, :point#{array_arguments} - WARNING - end - end end end end diff --git a/activerecord/lib/active_record/nested_attributes.rb b/activerecord/lib/active_record/nested_attributes.rb index 4873db7270..01ecd79b8f 100644 --- a/activerecord/lib/active_record/nested_attributes.rb +++ b/activerecord/lib/active_record/nested_attributes.rb @@ -341,26 +341,26 @@ module ActiveRecord private - # Generates a writer method for this association. Serves as a point for - # accessing the objects in the association. For example, this method - # could generate the following: - # - # def pirate_attributes=(attributes) - # assign_nested_attributes_for_one_to_one_association(:pirate, attributes) - # end - # - # This redirects the attempts to write objects in an association through - # the helper methods defined below. Makes it seem like the nested - # associations are just regular associations. + # Generates a writer method for this association. Serves as a point for + # accessing the objects in the association. For example, this method + # could generate the following: + # + # def pirate_attributes=(attributes) + # assign_nested_attributes_for_one_to_one_association(:pirate, attributes) + # end + # + # This redirects the attempts to write objects in an association through + # the helper methods defined below. Makes it seem like the nested + # 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 - def #{association_name}_attributes=(attributes) - assign_nested_attributes_for_#{type}_association(:#{association_name}, attributes) - end - eoruby + if method_defined?(:#{association_name}_attributes=) + remove_method(:#{association_name}_attributes=) + end + def #{association_name}_attributes=(attributes) + assign_nested_attributes_for_#{type}_association(:#{association_name}, attributes) + end + eoruby end end @@ -375,25 +375,25 @@ module ActiveRecord private - # Attribute hash keys that should not be assigned as normal attributes. - # These hash keys are nested attributes implementation details. + # Attribute hash keys that should not be assigned as normal attributes. + # These hash keys are nested attributes implementation details. UNASSIGNABLE_KEYS = %w( id _destroy ) - # Assigns the given attributes to the association. - # - # If an associated record does not yet exist, one will be instantiated. If - # an associated record already exists, the method's behavior depends on - # the value of the update_only option. If update_only is +false+ and the - # given attributes include an <tt>:id</tt> that matches the existing record's - # id, then the existing record will be modified. If no <tt>:id</tt> is provided - # it will be replaced with a new record. If update_only is +true+ the existing - # record will be modified regardless of whether an <tt>:id</tt> is provided. - # - # If the given attributes include a matching <tt>:id</tt> attribute, or - # update_only is true, and a <tt>:_destroy</tt> key set to a truthy value, - # then the existing record will be marked for destruction. + # Assigns the given attributes to the association. + # + # If an associated record does not yet exist, one will be instantiated. If + # an associated record already exists, the method's behavior depends on + # the value of the update_only option. If update_only is +false+ and the + # given attributes include an <tt>:id</tt> that matches the existing record's + # id, then the existing record will be modified. If no <tt>:id</tt> is provided + # it will be replaced with a new record. If update_only is +true+ the existing + # record will be modified regardless of whether an <tt>:id</tt> is provided. + # + # If the given attributes include a matching <tt>:id</tt> attribute, or + # update_only is true, and a <tt>:_destroy</tt> key set to a truthy value, + # then the existing record will be marked for destruction. def assign_nested_attributes_for_one_to_one_association(association_name, attributes) - options = self.nested_attributes_options[association_name] + options = nested_attributes_options[association_name] if attributes.respond_to?(:permitted?) attributes = attributes.to_h end @@ -424,35 +424,35 @@ module ActiveRecord end end - # Assigns the given attributes to the collection association. - # - # Hashes with an <tt>:id</tt> value matching an existing associated record - # will update that record. Hashes without an <tt>:id</tt> value will build - # a new record for the association. Hashes with a matching <tt>:id</tt> - # value and a <tt>:_destroy</tt> key set to a truthy value will mark the - # matched record for destruction. - # - # For example: - # - # assign_nested_attributes_for_collection_association(:people, { - # '1' => { id: '1', name: 'Peter' }, - # '2' => { name: 'John' }, - # '3' => { id: '2', _destroy: true } - # }) - # - # Will update the name of the Person with ID 1, build a new associated - # person with the name 'John', and mark the associated Person with ID 2 - # for destruction. - # - # Also accepts an Array of attribute hashes: - # - # assign_nested_attributes_for_collection_association(:people, [ - # { id: '1', name: 'Peter' }, - # { name: 'John' }, - # { id: '2', _destroy: true } - # ]) + # Assigns the given attributes to the collection association. + # + # Hashes with an <tt>:id</tt> value matching an existing associated record + # will update that record. Hashes without an <tt>:id</tt> value will build + # a new record for the association. Hashes with a matching <tt>:id</tt> + # value and a <tt>:_destroy</tt> key set to a truthy value will mark the + # matched record for destruction. + # + # For example: + # + # assign_nested_attributes_for_collection_association(:people, { + # '1' => { id: '1', name: 'Peter' }, + # '2' => { name: 'John' }, + # '3' => { id: '2', _destroy: true } + # }) + # + # Will update the name of the Person with ID 1, build a new associated + # person with the name 'John', and mark the associated Person with ID 2 + # for destruction. + # + # Also accepts an Array of attribute hashes: + # + # assign_nested_attributes_for_collection_association(:people, [ + # { id: '1', name: 'Peter' }, + # { name: 'John' }, + # { id: '2', _destroy: true } + # ]) def assign_nested_attributes_for_collection_association(association_name, attributes_collection) - options = self.nested_attributes_options[association_name] + options = nested_attributes_options[association_name] if attributes_collection.respond_to?(:permitted?) attributes_collection = attributes_collection.to_h end @@ -477,7 +477,7 @@ module ActiveRecord existing_records = if association.loaded? association.target else - attribute_ids = attributes_collection.map {|a| a["id"] || a[:id] }.compact + attribute_ids = attributes_collection.map { |a| a["id"] || a[:id] }.compact attribute_ids.empty? ? [] : association.scope.where(association.klass.primary_key => attribute_ids) end @@ -511,22 +511,23 @@ module ActiveRecord end end - # Takes in a limit and checks if the attributes_collection has too many - # records. It accepts limit in the form of symbol, proc, or - # number-like object (anything that can be compared with an integer). - # - # Raises TooManyRecords error if the attributes_collection is - # larger than the limit. + # Takes in a limit and checks if the attributes_collection has too many + # records. It accepts limit in the form of symbol, proc, or + # number-like object (anything that can be compared with an integer). + # + # Raises TooManyRecords error if the attributes_collection is + # larger than the limit. def check_record_limit!(limit, attributes_collection) if limit - limit = case limit - when Symbol - send(limit) - when Proc - limit.call - else - limit - end + limit = \ + case limit + when Symbol + send(limit) + when Proc + limit.call + else + limit + end if limit && attributes_collection.size > limit raise TooManyRecords, "Maximum #{limit} records are allowed. Got #{attributes_collection.size} records instead." @@ -534,34 +535,34 @@ module ActiveRecord end end - # Updates a record with the +attributes+ or marks it for destruction if - # +allow_destroy+ is +true+ and has_destroy_flag? returns +true+. + # Updates a record with the +attributes+ or marks it for destruction if + # +allow_destroy+ is +true+ and has_destroy_flag? returns +true+. def assign_to_or_mark_for_destruction(record, attributes, allow_destroy) record.assign_attributes(attributes.except(*UNASSIGNABLE_KEYS)) record.mark_for_destruction if has_destroy_flag?(attributes) && allow_destroy end - # Determines if a hash contains a truthy _destroy key. + # Determines if a hash contains a truthy _destroy key. def has_destroy_flag?(hash) Type::Boolean.new.cast(hash["_destroy"]) end - # Determines if a new record should be rejected by checking - # has_destroy_flag? or if a <tt>:reject_if</tt> proc exists for this - # association and evaluates to +true+. + # Determines if a new record should be rejected by checking + # has_destroy_flag? or if a <tt>:reject_if</tt> proc exists for this + # association and evaluates to +true+. def reject_new_record?(association_name, attributes) will_be_destroyed?(association_name, attributes) || call_reject_if(association_name, attributes) end - # Determines if a record with the particular +attributes+ should be - # rejected by calling the reject_if Symbol or Proc (if defined). - # The reject_if option is defined by +accepts_nested_attributes_for+. - # - # Returns false if there is a +destroy_flag+ on the attributes. + # Determines if a record with the particular +attributes+ should be + # rejected by calling the reject_if Symbol or Proc (if defined). + # The reject_if option is defined by +accepts_nested_attributes_for+. + # + # Returns false if there is a +destroy_flag+ on the attributes. def call_reject_if(association_name, attributes) return false if will_be_destroyed?(association_name, attributes) - case callback = self.nested_attributes_options[association_name][:reject_if] + case callback = nested_attributes_options[association_name][:reject_if] when Symbol method(callback).arity == 0 ? send(callback) : send(callback, attributes) when Proc @@ -569,7 +570,7 @@ module ActiveRecord end end - # Only take into account the destroy flag if <tt>:allow_destroy</tt> is true + # Only take into account the destroy flag if <tt>:allow_destroy</tt> is true def will_be_destroyed?(association_name, attributes) allow_destroy?(association_name) && has_destroy_flag?(attributes) end diff --git a/activerecord/lib/active_record/no_touching.rb b/activerecord/lib/active_record/no_touching.rb index edb5066fa0..4059020e25 100644 --- a/activerecord/lib/active_record/no_touching.rb +++ b/activerecord/lib/active_record/no_touching.rb @@ -45,6 +45,10 @@ module ActiveRecord NoTouching.applied_to?(self.class) end + def touch_later(*) # :nodoc: + super unless no_touching? + end + def touch(*) # :nodoc: super unless no_touching? end diff --git a/activerecord/lib/active_record/null_relation.rb b/activerecord/lib/active_record/null_relation.rb index 254550c378..2bb7ed6d5e 100644 --- a/activerecord/lib/active_record/null_relation.rb +++ b/activerecord/lib/active_record/null_relation.rb @@ -41,12 +41,11 @@ module ActiveRecord end def calculate(operation, _column_name) - if [:count, :sum].include? operation + case operation + when :count, :sum group_values.any? ? Hash.new : 0 - elsif [:average, :minimum, :maximum].include?(operation) && group_values.any? - Hash.new - else - nil + when :average, :minimum, :maximum + group_values.any? ? Hash.new : nil end end diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb index 4123671430..19fe9632ca 100644 --- a/activerecord/lib/active_record/persistence.rb +++ b/activerecord/lib/active_record/persistence.rb @@ -63,10 +63,10 @@ module ActiveRecord # # See <tt>ActiveRecord::Inheritance#discriminate_class_for_record</tt> to see # how this "single-table" inheritance mapping is implemented. - def instantiate(attributes, column_types = {}) + def instantiate(attributes, column_types = {}, &block) klass = discriminate_class_for_record(attributes) attributes = klass.attributes_builder.build_from_database(attributes, column_types) - klass.allocate.init_with("attributes" => attributes, "new_record" => false) + klass.allocate.init_with("attributes" => attributes, "new_record" => false, &block) end private @@ -107,7 +107,7 @@ module ActiveRecord # # By default, save always runs validations. If any of them fail the action # is cancelled and #save returns +false+, and the record won't be saved. However, if you supply - # validate: false, validations are bypassed altogether. See + # <tt>validate: false</tt>, validations are bypassed altogether. See # ActiveRecord::Validations for more information. # # By default, #save also sets the +updated_at+/+updated_on+ attributes to @@ -134,7 +134,7 @@ module ActiveRecord # # By default, #save! always runs validations. If any of them fail # ActiveRecord::RecordInvalid gets raised, and the record won't be saved. However, if you supply - # validate: false, validations are bypassed altogether. See + # <tt>validate: false</tt>, validations are bypassed altogether. See # ActiveRecord::Validations for more information. # # By default, #save! also sets the +updated_at+/+updated_on+ attributes to @@ -178,10 +178,14 @@ module ActiveRecord # and #destroy returns +false+. # See ActiveRecord::Callbacks for further details. def destroy - raise ReadOnlyRecord, "#{self.class} is marked as readonly" if readonly? + _raise_readonly_record_error if readonly? destroy_associations self.class.connection.add_transaction_record(self) - destroy_row if persisted? + @_trigger_destroy_callback = if persisted? + destroy_row > 0 + else + true + end @destroyed = true freeze end @@ -252,7 +256,12 @@ module ActiveRecord name = name.to_s verify_readonly_attribute(name) public_send("#{name}=", value) - save(validate: false) if changed? + + if has_changes_to_save? + save(validate: false) + else + true + end end # Updates the attributes of the model from the passed-in hash and saves the @@ -335,7 +344,7 @@ module ActiveRecord # Validations and callbacks are skipped. Returns +self+. def increment!(attribute, by = 1) increment(attribute, by) - change = public_send(attribute) - (attribute_was(attribute.to_s) || 0) + change = public_send(attribute) - (attribute_in_database(attribute.to_s) || 0) self.class.update_counters(id, attribute => change) clear_attribute_change(attribute) # eww self @@ -383,8 +392,8 @@ module ActiveRecord # Reloads the record from the database. # - # This method finds record by its primary key (which could be assigned manually) and - # modifies the receiver in-place: + # This method finds the record by its primary key (which could be assigned + # manually) and modifies the receiver in-place: # # account = Account.new # # => #<Account id: nil, email: nil> @@ -498,7 +507,6 @@ module ActiveRecord changes[column] = write_attribute(column, time) end - clear_attribute_changes(changes.keys) primary_key = self.class.primary_key scope = self.class.unscoped.where(primary_key => _read_attribute(primary_key)) @@ -508,12 +516,14 @@ module ActiveRecord changes[locking_column] = increment_lock end + clear_attribute_changes(changes.keys) result = scope.update_all(changes) == 1 if !result && locking_enabled? raise ActiveRecord::StaleObjectError.new(self, "touch") end + @_trigger_update_callback = result result else true @@ -535,7 +545,7 @@ module ActiveRecord end def create_or_update(*args) - raise ReadOnlyRecord, "#{self.class} is marked as readonly" if readonly? + _raise_readonly_record_error if readonly? result = new_record? ? _create_record : _update_record(*args) result != false end @@ -545,10 +555,13 @@ module ActiveRecord def _update_record(attribute_names = self.attribute_names) attributes_values = arel_attributes_with_values_for_update(attribute_names) if attributes_values.empty? - 0 + rows_affected = 0 + @_trigger_update_callback = true else - self.class.unscoped._update_record attributes_values, id, id_was + rows_affected = self.class.unscoped._update_record attributes_values, id, id_in_database + @_trigger_update_callback = rows_affected > 0 end + rows_affected end # Creates a record with values matching those of the instance attributes @@ -577,5 +590,9 @@ module ActiveRecord def belongs_to_touch_method :touch end + + def _raise_readonly_record_error + raise ReadOnlyRecord, "#{self.class} is marked as readonly" + end end end diff --git a/activerecord/lib/active_record/query_cache.rb b/activerecord/lib/active_record/query_cache.rb index 387dd8e9bd..ec246e97bc 100644 --- a/activerecord/lib/active_record/query_cache.rb +++ b/activerecord/lib/active_record/query_cache.rb @@ -24,26 +24,24 @@ module ActiveRecord end def self.run - connection = ActiveRecord::Base.connection - enabled = connection.query_cache_enabled - connection.enable_query_cache! + caching_pool = ActiveRecord::Base.connection_pool + caching_was_enabled = caching_pool.query_cache_enabled - enabled + caching_pool.enable_query_cache! + + [caching_pool, caching_was_enabled] end - def self.complete(enabled) - ActiveRecord::Base.connection.clear_query_cache - ActiveRecord::Base.connection.disable_query_cache! unless enabled + def self.complete((caching_pool, caching_was_enabled)) + caching_pool.disable_query_cache! unless caching_was_enabled + + ActiveRecord::Base.connection_handler.connection_pool_list.each do |pool| + pool.release_connection if pool.active_connection? && !pool.connection.transaction_open? + end end def self.install_executor_hooks(executor = ActiveSupport::Executor) executor.register_hook(self) - - executor.to_complete do - unless ActiveRecord::Base.connected? && ActiveRecord::Base.connection.transaction_open? - ActiveRecord::Base.clear_active_connections! - end - end end end end diff --git a/activerecord/lib/active_record/querying.rb b/activerecord/lib/active_record/querying.rb index 7b4b8c60f8..36689f6559 100644 --- a/activerecord/lib/active_record/querying.rb +++ b/activerecord/lib/active_record/querying.rb @@ -35,7 +35,7 @@ module ActiveRecord # # Post.find_by_sql ["SELECT title FROM posts WHERE author = ? AND created > ?", author_id, start_date] # Post.find_by_sql ["SELECT body FROM comments WHERE author = :user_id OR approved_by = :user_id", { :user_id => user_id }] - def find_by_sql(sql, binds = [], preparable: nil) + def find_by_sql(sql, binds = [], preparable: nil, &block) result_set = connection.select_all(sanitize_sql(sql), "#{name} Load", binds, preparable: preparable) column_types = result_set.column_types.dup columns_hash.each_key { |k| column_types.delete k } @@ -47,7 +47,7 @@ module ActiveRecord } message_bus.instrument("instantiation.active_record", payload) do - result_set.map { |record| instantiate(record, column_types) } + result_set.map { |record| instantiate(record, column_types, &block) } end end @@ -62,8 +62,7 @@ module ActiveRecord # # * +sql+ - An SQL statement which should return a count query from the database, see the example above. def count_by_sql(sql) - sql = sanitize_conditions(sql) - connection.select_value(sql, "#{name} Count").to_i + connection.select_value(sanitize_sql(sql), "#{name} Count").to_i end end end diff --git a/activerecord/lib/active_record/railtie.rb b/activerecord/lib/active_record/railtie.rb index 989d23bc37..0276d41494 100644 --- a/activerecord/lib/active_record/railtie.rb +++ b/activerecord/lib/active_record/railtie.rb @@ -82,15 +82,15 @@ module ActiveRecord if config.active_record.delete(:use_schema_cache_dump) config.after_initialize do |app| ActiveSupport.on_load(:active_record) do - filename = File.join(app.config.paths["db"].first, "schema_cache.dump") + filename = File.join(app.config.paths["db"].first, "schema_cache.yml") if File.file?(filename) - cache = Marshal.load File.binread filename + cache = YAML.load(File.read(filename)) if cache.version == ActiveRecord::Migrator.current_version - self.connection.schema_cache = cache - self.connection_pool.schema_cache = cache.dup + connection.schema_cache = cache + connection_pool.schema_cache = cache.dup else - warn "Ignoring db/schema_cache.dump 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 #{ActiveRecord::Migrator.current_version}, but the one in the cache is #{cache.version}." end end end @@ -108,7 +108,7 @@ module ActiveRecord initializer "active_record.set_configs" do |app| ActiveSupport.on_load(:active_record) do - app.config.active_record.each do |k,v| + app.config.active_record.each do |k, v| send "#{k}=", v end end diff --git a/activerecord/lib/active_record/railties/controller_runtime.rb b/activerecord/lib/active_record/railties/controller_runtime.rb index adb3c6c4e6..8658188623 100644 --- a/activerecord/lib/active_record/railties/controller_runtime.rb +++ b/activerecord/lib/active_record/railties/controller_runtime.rb @@ -6,10 +6,14 @@ module ActiveRecord module ControllerRuntime #:nodoc: extend ActiveSupport::Concern + # TODO Change this to private once we've dropped Ruby 2.2 support. + # Workaround for Ruby 2.2 "private attribute?" warning. protected attr_internal :db_runtime + private + def process_action(action, *args) # We also need to reset the runtime before each action # because of queries in middleware or in cases we are streaming diff --git a/activerecord/lib/active_record/railties/databases.rake b/activerecord/lib/active_record/railties/databases.rake index 1d52f77622..246d330b76 100644 --- a/activerecord/lib/active_record/railties/databases.rake +++ b/activerecord/lib/active_record/railties/databases.rake @@ -213,7 +213,7 @@ db_namespace = namespace :db do ENV["FIXTURES"].split(",") else # The use of String#[] here is to support namespaced fixtures. - Dir["#{fixtures_dir}/**/*.yml"].map {|f| f[(fixtures_dir.size + 1)..-5] } + Dir["#{fixtures_dir}/**/*.yml"].map { |f| f[(fixtures_dir.size + 1)..-5] } end ActiveRecord::FixtureSet.create_fixtures(fixtures_dir, fixture_files) @@ -265,19 +265,16 @@ db_namespace = namespace :db do end namespace :cache do - desc "Creates a db/schema_cache.dump file." + desc "Creates a db/schema_cache.yml file." task dump: [:environment, :load_config] do - con = ActiveRecord::Base.connection - filename = File.join(ActiveRecord::Tasks::DatabaseTasks.db_dir, "schema_cache.dump") - - con.schema_cache.clear! - con.data_sources.each { |table| con.schema_cache.add(table) } - open(filename, "wb") { |f| f.write(Marshal.dump(con.schema_cache)) } + 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.dump file." + desc "Clears a db/schema_cache.yml file." task clear: [:environment, :load_config] do - filename = File.join(ActiveRecord::Tasks::DatabaseTasks.db_dir, "schema_cache.dump") + filename = File.join(ActiveRecord::Tasks::DatabaseTasks.db_dir, "schema_cache.yml") rm_f filename, verbose: false end end @@ -312,14 +309,6 @@ db_namespace = namespace :db do end namespace :test do - - task :deprecated do - Rake.application.top_level_tasks.grep(/^db:test:/).each do |task| - $stderr.puts "WARNING: #{task} is deprecated. The Rails test helper now maintains " \ - "your test schema automatically, see the release notes for details." - end - end - # desc "Recreate the test database from the current schema" task load: %w(db:test:purge) do case ActiveRecord::Base.schema_format @@ -348,22 +337,6 @@ db_namespace = namespace :db do ActiveRecord::Tasks::DatabaseTasks.load_schema ActiveRecord::Base.configurations["test"], :sql, ENV["SCHEMA"] end - # desc "Recreate the test database from a fresh schema" - task clone: %w(db:test:deprecated environment) do - case ActiveRecord::Base.schema_format - when :ruby - db_namespace["test:clone_schema"].invoke - when :sql - db_namespace["test:clone_structure"].invoke - end - end - - # desc "Recreate the test database from a fresh schema.rb file" - task clone_schema: %w(db:test:deprecated db:schema:dump db:test:load_schema) - - # desc "Recreate the test database from a fresh structure.sql file" - task clone_structure: %w(db:test:deprecated db:structure:dump db:test:load_structure) - # desc "Empty the test database" task purge: %w(environment load_config check_protected_environments) do ActiveRecord::Tasks::DatabaseTasks.purge ActiveRecord::Base.configurations["test"] diff --git a/activerecord/lib/active_record/readonly_attributes.rb b/activerecord/lib/active_record/readonly_attributes.rb index 8ff265bdfa..6274996ab8 100644 --- a/activerecord/lib/active_record/readonly_attributes.rb +++ b/activerecord/lib/active_record/readonly_attributes.rb @@ -11,7 +11,7 @@ module ActiveRecord # Attributes listed as readonly will be used to create a new record but update operations will # ignore these fields. def attr_readonly(*attributes) - self._attr_readonly = Set.new(attributes.map(&:to_s)) + (self._attr_readonly || []) + self._attr_readonly = Set.new(attributes.map(&:to_s)) + (_attr_readonly || []) end # Returns an array of all the attributes that have been specified as readonly. diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb index 3553ff4da3..72f1ac4896 100644 --- a/activerecord/lib/active_record/reflection.rb +++ b/activerecord/lib/active_record/reflection.rb @@ -14,18 +14,19 @@ module ActiveRecord end def self.create(macro, name, scope, options, ar) - klass = case macro - when :composed_of - AggregateReflection - when :has_many - HasManyReflection - when :has_one - HasOneReflection - when :belongs_to - BelongsToReflection - else - raise "Unsupported Macro: #{macro}" - end + klass = \ + case macro + when :composed_of + AggregateReflection + when :has_many + HasManyReflection + when :has_one + HasOneReflection + when :belongs_to + BelongsToReflection + else + raise "Unsupported Macro: #{macro}" + end reflection = klass.new(name, scope, options, ar) options[:through] ? ThroughReflection.new(reflection) : reflection @@ -135,8 +136,8 @@ module ActiveRecord # BelongsToReflection # HasAndBelongsToManyReflection # ThroughReflection - # PolymorphicReflection - # RuntimeReflection + # PolymorphicReflection + # RuntimeReflection class AbstractReflection # :nodoc: def through_reflection? false @@ -281,7 +282,6 @@ module ActiveRecord end def autosave=(autosave) - @automatic_inverse_of = false @options[:autosave] = autosave parent_reflection = self.parent_reflection if parent_reflection @@ -311,6 +311,10 @@ module ActiveRecord active_record == other_aggregation.active_record end + def scope_for(klass) + scope ? klass.unscoped.instance_exec(nil, &scope) : klass.unscoped + end + private def derive_class_name name.to_s.camelize @@ -393,6 +397,10 @@ module ActiveRecord options[:primary_key] || primary_key(klass || self.klass) end + def association_primary_key_type + klass.type_for_attribute(association_primary_key) + end + def active_record_primary_key @active_record_primary_key ||= options[:primary_key] || primary_key(active_record) end @@ -532,14 +540,10 @@ module ActiveRecord # Attempts to find the inverse association name automatically. # If it cannot find a suitable inverse association name, it returns - # nil. + # +nil+. def inverse_name options.fetch(:inverse_of) do - if @automatic_inverse_of == false - nil - else - @automatic_inverse_of ||= automatic_inverse_of - end + @automatic_inverse_of ||= automatic_inverse_of end end @@ -703,7 +707,7 @@ module ActiveRecord def initialize(delegate_reflection) @delegate_reflection = delegate_reflection - @klass = delegate_reflection.options[:anonymous_class] + @klass = delegate_reflection.options[:anonymous_class] @source_reflection_name = delegate_reflection.options[:source] end @@ -846,6 +850,10 @@ 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) + end + # Gets an array of possible <tt>:through</tt> source reflection names in both singular and plural form. # # class Post < ActiveRecord::Base @@ -870,15 +878,13 @@ module ActiveRecord } if names.length > 1 - example_options = options.dup - example_options[:source] = source_reflection_names.first - ActiveSupport::Deprecation.warn \ - "Ambiguous source reflection for through association. Please " \ - "specify a :source directive on your declaration like:\n" \ - "\n" \ - " class #{active_record.name} < ActiveRecord::Base\n" \ - " #{macro} :#{name}, #{example_options}\n" \ - " end" + raise AmbiguousSourceReflectionForThroughAssociation.new( + active_record.name, + macro, + name, + options, + source_reflection_names + ) end @source_reflection_name = names.first @@ -925,6 +931,14 @@ module ActiveRecord raise HasOneThroughCantAssociateThroughCollection.new(active_record.name, self, through_reflection) end + if parent_reflection.nil? + reflections = active_record.reflections.keys.map(&:to_sym) + + if reflections.index(through_reflection.name) > reflections.index(name) + raise HasManyThroughOrderError.new(active_record.name, self, through_reflection) + end + end + check_validity_of_inverse! end @@ -955,8 +969,7 @@ module ActiveRecord end end - protected - + private def actual_source_reflection # FIXME: this is a horrible name source_reflection.send(:actual_source_reflection) end @@ -967,7 +980,6 @@ module ActiveRecord def inverse_name; delegate_reflection.send(:inverse_name); end - private def derive_class_name # get the class_name of the belongs_to association of the through reflection options[:source_type] || source_reflection.class_name @@ -979,7 +991,7 @@ module ActiveRecord delegate(*delegate_methods, to: :delegate_reflection) end - class PolymorphicReflection < ThroughReflection # :nodoc: + class PolymorphicReflection < AbstractReflection # :nodoc: def initialize(reflection, previous_reflection) @reflection = reflection @previous_reflection = previous_reflection diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb index 3983065d7a..61ee09bcc8 100644 --- a/activerecord/lib/active_record/relation.rb +++ b/activerecord/lib/active_record/relation.rb @@ -29,9 +29,7 @@ module ActiveRecord end def initialize_copy(other) - # This method is a hot spot, so for now, use Hash[] to dup the hash. - # https://bugs.ruby-lang.org/issues/7166 - @values = Hash[@values] + @values = @values.dup reset end @@ -245,7 +243,6 @@ module ActiveRecord # Please see further details in the # {Active Record Query Interface guide}[http://guides.rubyonrails.org/active_record_querying.html#running-explain]. def explain - #TODO: Fix for binds. exec_explain(collecting_queries_for_explain { exec_queries }) end @@ -365,6 +362,9 @@ module ActiveRecord # # # Update all books that match conditions, but limit it to 5 ordered by date # Book.where('title LIKE ?', '%Rails%').order(:created_at).limit(5).update_all(author: 'David') + # + # # Update all invoices and set the number column to its id value. + # Invoice.update_all('number = id') def update_all(updates) raise ArgumentError, "Empty list of attributes to change" if updates.blank? @@ -373,7 +373,7 @@ module ActiveRecord stmt.set Arel.sql(@klass.send(:sanitize_sql_for_assignment, updates)) stmt.table(table) - if joins_values.any? + if has_join_values? @klass.connection.join_to_update(stmt, arel, arel_attribute(primary_key)) else stmt.key = arel_attribute(primary_key) @@ -418,8 +418,7 @@ module ActiveRecord records.each { |record| record.update(attributes) } else if ActiveRecord::Base === id - id = id.id - ActiveSupport::Deprecation.warn(<<-MSG.squish) + 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 @@ -446,16 +445,8 @@ module ActiveRecord # ==== Examples # # Person.where(age: 0..18).destroy_all - def destroy_all(conditions = nil) - if conditions - ActiveSupport::Deprecation.warn(<<-MESSAGE.squish) - Passing conditions to destroy_all is deprecated and will be removed in Rails 5.1. - To achieve the same use where(conditions).destroy_all. - MESSAGE - where(conditions).destroy_all - else - records.each(&:destroy).tap { reset } - end + def destroy_all + records.each(&:destroy).tap { reset } end # Destroy an object (or multiple objects) that has the given id. The object is instantiated first, @@ -503,41 +494,28 @@ module ActiveRecord # # Post.limit(100).delete_all # # => ActiveRecord::ActiveRecordError: delete_all doesn't support limit - def delete_all(conditions = nil) - invalid_methods = INVALID_METHODS_FOR_DELETE_ALL.select { |method| - if MULTI_VALUE_METHODS.include?(method) - send("#{method}_values").any? - elsif SINGLE_VALUE_METHODS.include?(method) - send("#{method}_value") - elsif CLAUSE_METHODS.include?(method) - send("#{method}_clause").any? - end - } + def delete_all + invalid_methods = INVALID_METHODS_FOR_DELETE_ALL.select do |method| + value = get_value(method) + SINGLE_VALUE_METHODS.include?(method) ? value : value.any? + end if invalid_methods.any? raise ActiveRecordError.new("delete_all doesn't support #{invalid_methods.join(', ')}") end - if conditions - ActiveSupport::Deprecation.warn(<<-MESSAGE.squish) - Passing conditions to delete_all is deprecated and will be removed in Rails 5.1. - To achieve the same use where(conditions).delete_all. - MESSAGE - where(conditions).delete_all - else - stmt = Arel::DeleteManager.new - stmt.from(table) + stmt = Arel::DeleteManager.new + stmt.from(table) - if joins_values.any? - @klass.connection.join_to_delete(stmt, arel, arel_attribute(primary_key)) - else - stmt.wheres = arel.constraints - end + if has_join_values? + @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, "SQL", bound_attributes) - reset - affected - end + reset + affected end # Deletes the row with a primary key matching the +id+ argument, using a @@ -569,8 +547,8 @@ module ActiveRecord # return value is the relation itself, not the records. # # Post.where(published: true).load # => #<ActiveRecord::Relation> - def load - exec_queries unless loaded? + def load(&block) + exec_queries(&block) unless loaded? self end @@ -635,15 +613,6 @@ module ActiveRecord includes_values & joins_values end - # {#uniq}[rdoc-ref:QueryMethods#uniq] and - # {#uniq!}[rdoc-ref:QueryMethods#uniq!] are silently deprecated. - # #uniq_value delegates to #distinct_value to maintain backwards compatibility. - # Use #distinct_value instead. - def uniq_value - distinct_value - end - deprecate uniq_value: :distinct_value - # Compares two relations for equality. def ==(other) case other @@ -666,7 +635,7 @@ module ActiveRecord end def values - Hash[@values] + @values.dup end def inspect @@ -685,13 +654,18 @@ module ActiveRecord private - def exec_queries - @records = eager_loading? ? find_with_associations.freeze : @klass.find_by_sql(arel, bound_attributes).freeze + def has_join_values? + joins_values.any? || left_outer_joins_values.any? + 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 = build_preloader + preload += includes_values unless eager_loading? + preloader = nil preload.each do |associations| + preloader ||= build_preloader preloader.preload @records, associations end diff --git a/activerecord/lib/active_record/relation/batches.rb b/activerecord/lib/active_record/relation/batches.rb index 4b2987ac6d..76031515fd 100644 --- a/activerecord/lib/active_record/relation/batches.rb +++ b/activerecord/lib/active_record/relation/batches.rb @@ -260,7 +260,7 @@ module ActiveRecord end def act_on_ignored_order(error_on_ignore) - raise_error = (error_on_ignore.nil? ? self.klass.error_on_ignored_order : error_on_ignore) + raise_error = (error_on_ignore.nil? ? klass.error_on_ignored_order : error_on_ignore) if raise_error raise ArgumentError.new(ORDER_IGNORE_MESSAGE) diff --git a/activerecord/lib/active_record/relation/batches/batch_enumerator.rb b/activerecord/lib/active_record/relation/batches/batch_enumerator.rb index 333b3a63cf..3555779ec2 100644 --- a/activerecord/lib/active_record/relation/batches/batch_enumerator.rb +++ b/activerecord/lib/active_record/relation/batches/batch_enumerator.rb @@ -7,7 +7,7 @@ module ActiveRecord @of = of @relation = relation @start = start - @finish = finish + @finish = finish end # Looping through a collection of records from the database (using the diff --git a/activerecord/lib/active_record/relation/calculations.rb b/activerecord/lib/active_record/relation/calculations.rb index ecf3700aab..35c670f1a1 100644 --- a/activerecord/lib/active_record/relation/calculations.rb +++ b/activerecord/lib/active_record/relation/calculations.rb @@ -112,12 +112,11 @@ module ActiveRecord # ... # end def calculate(operation, column_name) - if column_name.is_a?(Symbol) && attribute_alias?(column_name) - column_name = attribute_alias(column_name) - end - if has_include?(column_name) - construct_relation_for_association_calculations.calculate(operation, column_name) + relation = construct_relation_for_association_calculations + relation = relation.distinct if operation.to_s.downcase == "count" + + relation.calculate(operation, column_name) else perform_calculation(operation, column_name) end @@ -160,7 +159,7 @@ module ActiveRecord # def pluck(*column_names) if loaded? && (column_names.map(&:to_s) - @klass.attribute_names - @klass.attribute_aliases.keys).empty? - return @records.pluck(*column_names) + return records.pluck(*column_names) end if has_include?(column_names.first) @@ -194,15 +193,10 @@ module ActiveRecord # If #count is used with #distinct (i.e. `relation.distinct.count`) it is # considered distinct. - distinct = self.distinct_value + distinct = distinct_value if operation == "count" column_name ||= select_for_count - - unless arel.ast.grep(Arel::Nodes::OuterJoin).empty? - distinct = true - end - column_name = primary_key if column_name == :all && distinct distinct = nil if column_name =~ /\s*DISTINCT[\s(]+/i end @@ -217,8 +211,8 @@ module ActiveRecord def aggregate_column(column_name) return column_name if Arel::Expressions === column_name - if @klass.column_names.include?(column_name.to_s) - Arel::Attribute.new(@klass.unscoped.table, column_name) + if @klass.has_attribute?(column_name.to_s) || @klass.attribute_alias?(column_name.to_s) + @klass.arel_attribute(column_name) else Arel.sql(column_name == :all ? "*" : column_name.to_s) end @@ -229,17 +223,17 @@ module ActiveRecord end def execute_simple_calculation(operation, column_name, distinct) #:nodoc: - # PostgreSQL doesn't like ORDER BY when there are no GROUP BY - relation = unscope(:order) - column_alias = column_name - if operation == "count" && (relation.limit_value || relation.offset_value) + if operation == "count" && (limit_value || offset_value) # Shortcut when limit is zero. - return 0 if relation.limit_value == 0 + return 0 if limit_value == 0 - query_builder = build_count_subquery(relation, column_name, distinct) + query_builder = build_count_subquery(spawn, column_name, distinct) else + # PostgreSQL doesn't like ORDER BY when there are no GROUP BY + relation = unscope(:order) + column = aggregate_column(column_name) select_value = operation_over_aggregate_column(column, operation, distinct) @@ -254,11 +248,11 @@ module ActiveRecord result = @klass.connection.select_all(query_builder, nil, bound_attributes) row = result.first value = row && row.values.first - column = result.column_types.fetch(column_alias) do + type = result.column_types.fetch(column_alias) do type_for(column_name) end - type_cast_calculated_value(value, column, operation) + type_cast_calculated_value(value, type, operation) end def execute_grouped_calculation(operation, column_name, distinct) #:nodoc: @@ -312,28 +306,26 @@ module ActiveRecord Hash[calculated_data.map do |row| key = group_columns.map { |aliaz, col_name| - column = type_for(col_name) do - calculated_data.column_types.fetch(aliaz) do - Type::Value.new - end + type = type_for(col_name) do + calculated_data.column_types.fetch(aliaz, Type.default_value) end - type_cast_calculated_value(row[aliaz], column) + type_cast_calculated_value(row[aliaz], type) } key = key.first if key.size == 1 key = key_records[key] if associated - column_type = calculated_data.column_types.fetch(aggregate_alias) { type_for(column_name) } - [key, type_cast_calculated_value(row[aggregate_alias], column_type, operation)] + type = calculated_data.column_types.fetch(aggregate_alias) { type_for(column_name) } + [key, type_cast_calculated_value(row[aggregate_alias], type, operation)] end] end - # Converts the given keys to the value that the database adapter returns as - # a usable column name: - # - # column_alias_for("users.id") # => "users_id" - # column_alias_for("sum(id)") # => "sum_id" - # column_alias_for("count(distinct users.id)") # => "count_distinct_users_id" - # column_alias_for("count(*)") # => "count_all" + # Converts the given keys to the value that the database adapter returns as + # a usable column name: + # + # column_alias_for("users.id") # => "users_id" + # column_alias_for("sum(id)") # => "sum_id" + # column_alias_for("count(distinct users.id)") # => "count_distinct_users_id" + # column_alias_for("count(*)") # => "count_all" def column_alias_for(keys) if keys.respond_to? :name keys = "#{keys.relation.name}.#{keys.name}" @@ -358,7 +350,7 @@ module ActiveRecord when "count" then value.to_i when "sum" then type.deserialize(value || 0) when "average" then value.respond_to?(:to_d) ? value.to_d : value - else type.deserialize(value) + else type.deserialize(value) end end diff --git a/activerecord/lib/active_record/relation/delegation.rb b/activerecord/lib/active_record/relation/delegation.rb index e1c36982dd..f965818ed2 100644 --- a/activerecord/lib/active_record/relation/delegation.rb +++ b/activerecord/lib/active_record/relation/delegation.rb @@ -1,6 +1,3 @@ -require "active_support/concern" -require "active_support/core_ext/regexp" - module ActiveRecord module Delegation # :nodoc: module DelegateCache # :nodoc: @@ -18,7 +15,10 @@ module ActiveRecord delegate = Class.new(klass) { include ClassSpecificRelation } - const_set klass.name.gsub("::".freeze, "_".freeze), delegate + mangled_name = klass.name.gsub("::".freeze, "_".freeze) + const_set mangled_name, delegate + private_constant mangled_name + cache[klass] = delegate end end @@ -81,7 +81,7 @@ module ActiveRecord end end - protected + private def method_missing(method, *args, &block) if @klass.respond_to?(method) @@ -108,12 +108,12 @@ module ActiveRecord end end - def respond_to?(method, include_private = false) + def respond_to_missing?(method, include_private = false) super || @klass.respond_to?(method, include_private) || arel.respond_to?(method, include_private) end - protected + private def method_missing(method, *args, &block) if @klass.respond_to?(method) diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb index 7bf84c2f15..6663bdb244 100644 --- a/activerecord/lib/active_record/relation/finder_methods.rb +++ b/activerecord/lib/active_record/relation/finder_methods.rb @@ -76,7 +76,7 @@ module ActiveRecord # Post.find_by "published_at < ?", 2.weeks.ago def find_by(arg, *args) where(arg, *args).take - rescue RangeError + rescue ::RangeError nil end @@ -84,7 +84,7 @@ module ActiveRecord # an ActiveRecord::RecordNotFound error. def find_by!(arg, *args) where(arg, *args).take! - rescue RangeError + rescue ::RangeError raise RecordNotFound.new("Couldn't find #{@klass.name} with an out of range value", @klass.name) end @@ -97,13 +97,13 @@ module ActiveRecord # Person.take(5) # returns 5 objects fetched by SELECT * FROM people LIMIT 5 # Person.where(["name LIKE '%?'", name]).take def take(limit = nil) - limit ? limit(limit).to_a : find_take + limit ? find_take_with_limit(limit) : find_take end # Same as #take but raises ActiveRecord::RecordNotFound if no record # is found. Note that #take! accepts no arguments. def take! - take or raise RecordNotFound.new("Couldn't find #{@klass.name} with [#{arel.where_sql(@klass.arel_engine)}]") + take || raise_record_not_found_exception! end # Find the first record (or first N records if a parameter is supplied). @@ -117,7 +117,7 @@ module ActiveRecord # def first(limit = nil) if limit - find_nth_with_limit_and_offset(0, limit, offset: offset_index) + find_nth_with_limit(0, limit) else find_nth 0 end @@ -126,7 +126,7 @@ module ActiveRecord # Same as #first but raises ActiveRecord::RecordNotFound if no record # is found. Note that #first! accepts no arguments. def first! - find_nth! 0 + first || raise_record_not_found_exception! end # Find the last record (or last N records if a parameter is supplied). @@ -152,20 +152,12 @@ module ActiveRecord result = result.reverse_order! limit ? result.reverse : result.first - rescue ActiveRecord::IrreversibleOrderError - ActiveSupport::Deprecation.warn(<<-WARNING.squish) - Finding a last element by loading the relation when SQL ORDER - can not be reversed is deprecated. - Rails 5.1 will raise ActiveRecord::IrreversibleOrderError in this case. - Please call `to_a.last` if you still want to load the relation. - WARNING - find_last(limit) end # Same as #last but raises ActiveRecord::RecordNotFound if no record # is found. Note that #last! accepts no arguments. def last! - last or raise RecordNotFound.new("Couldn't find #{@klass.name} with [#{arel.where_sql(@klass.arel_engine)}]") + last || raise_record_not_found_exception! end # Find the second record. @@ -181,7 +173,7 @@ module ActiveRecord # Same as #second but raises ActiveRecord::RecordNotFound if no record # is found. def second! - find_nth! 1 + second || raise_record_not_found_exception! end # Find the third record. @@ -197,7 +189,7 @@ module ActiveRecord # Same as #third but raises ActiveRecord::RecordNotFound if no record # is found. def third! - find_nth! 2 + third || raise_record_not_found_exception! end # Find the fourth record. @@ -213,7 +205,7 @@ module ActiveRecord # Same as #fourth but raises ActiveRecord::RecordNotFound if no record # is found. def fourth! - find_nth! 3 + fourth || raise_record_not_found_exception! end # Find the fifth record. @@ -229,7 +221,7 @@ module ActiveRecord # Same as #fifth but raises ActiveRecord::RecordNotFound if no record # is found. def fifth! - find_nth! 4 + fifth || raise_record_not_found_exception! end # Find the forty-second record. Also known as accessing "the reddit". @@ -245,7 +237,7 @@ module ActiveRecord # Same as #forty_two but raises ActiveRecord::RecordNotFound if no record # is found. def forty_two! - find_nth! 41 + forty_two || raise_record_not_found_exception! end # Find the third-to-last record. @@ -261,7 +253,7 @@ module ActiveRecord # Same as #third_to_last but raises ActiveRecord::RecordNotFound if no record # is found. def third_to_last! - find_nth_from_last 3 or raise RecordNotFound.new("Couldn't find #{@klass.name} with [#{arel.where_sql(@klass.arel_engine)}]") + third_to_last || raise_record_not_found_exception! end # Find the second-to-last record. @@ -277,7 +269,7 @@ module ActiveRecord # Same as #second_to_last but raises ActiveRecord::RecordNotFound if no record # is found. def second_to_last! - find_nth_from_last 2 or raise RecordNotFound.new("Couldn't find #{@klass.name} with [#{arel.where_sql(@klass.arel_engine)}]") + second_to_last || raise_record_not_found_exception! end # Returns true if a record exists in the table that matches the +id+ or @@ -309,8 +301,7 @@ module ActiveRecord # Person.exists? def exists?(conditions = :none) if Base === conditions - conditions = conditions.id - ActiveSupport::Deprecation.warn(<<-MSG.squish) + raise ArgumentError, <<-MSG.squish You are passing an instance of ActiveRecord::Base to `exists?`. Please pass the id of the object by calling `.id`. MSG @@ -321,7 +312,7 @@ module ActiveRecord relation = apply_join_dependency(self, construct_join_dependency(eager_loading: false)) return false if ActiveRecord::NullRelation === relation - relation = relation.except(:select, :order).select(ONE_AS_ONE).limit(1) + relation = relation.except(:select, :distinct).select(ONE_AS_ONE).limit(1) case conditions when Array, Hash @@ -333,7 +324,7 @@ module ActiveRecord end connection.select_value(relation, "#{name} Exists", relation.bound_attributes) ? true : false - rescue RangeError + rescue ::RangeError false end @@ -345,18 +336,24 @@ 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, result_size, expected_size) #:nodoc: + 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) conditions = " [#{conditions}]" if conditions - - if Array(ids).size == 1 - error = "Couldn't find #{@klass.name} with '#{primary_key}'=#{ids}#{conditions}" + name = @klass.name + + if ids.nil? + error = "Couldn't find #{name}" + error << " with#{conditions}" if conditions + raise RecordNotFound.new(error, name) + 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 #{@klass.name.pluralize} with '#{primary_key}': " + error = "Couldn't find all #{name.pluralize} with '#{key}': " error << "(#{ids.join(", ")})#{conditions} (found #{result_size} results, but was looking for #{expected_size})" - end - raise RecordNotFound, error + raise RecordNotFound.new(error, name, primary_key, ids) + end end private @@ -426,167 +423,147 @@ module ActiveRecord arel = relation.arel id_rows = @klass.connection.select_all(arel, "SQL", relation.bound_attributes) - id_rows.map {|row| row[primary_key]} + id_rows.map { |row| row[primary_key] } end def using_limitable_reflections?(reflections) reflections.none?(&:collection?) end - protected + private - def find_with_ids(*ids) - raise UnknownPrimaryKey.new(@klass) if primary_key.nil? + def find_with_ids(*ids) + raise UnknownPrimaryKey.new(@klass) if primary_key.nil? - expects_array = ids.first.kind_of?(Array) - return ids.first if expects_array && ids.first.empty? + expects_array = ids.first.kind_of?(Array) + return ids.first if expects_array && ids.first.empty? - ids = ids.flatten.compact.uniq + ids = ids.flatten.compact.uniq - case ids.size - when 0 - raise RecordNotFound, "Couldn't find #{@klass.name} without an ID" - when 1 - result = find_one(ids.first) - expects_array ? [ result ] : result - else - find_some(ids) + case ids.size + when 0 + raise RecordNotFound, "Couldn't find #{@klass.name} without an ID" + when 1 + result = find_one(ids.first) + expects_array ? [ result ] : result + else + find_some(ids) + end + rescue ::RangeError + raise RecordNotFound, "Couldn't find #{@klass.name} with an out of range ID" end - rescue RangeError - raise RecordNotFound, "Couldn't find #{@klass.name} with an out of range ID" - end - def find_one(id) - if ActiveRecord::Base === id - id = id.id - ActiveSupport::Deprecation.warn(<<-MSG.squish) - You are passing an instance of ActiveRecord::Base to `find`. - Please pass the id of the object by calling `.id`. - MSG - end + def find_one(id) + if ActiveRecord::Base === id + raise ArgumentError, <<-MSG.squish + You are passing an instance of ActiveRecord::Base to `find`. + Please pass the id of the object by calling `.id`. + MSG + end - relation = where(primary_key => id) - record = relation.take + relation = where(primary_key => id) + record = relation.take - raise_record_not_found_exception!(id, 0, 1) unless record + raise_record_not_found_exception!(id, 0, 1) unless record - record - end + record + end - def find_some(ids) - return find_some_ordered(ids) unless order_values.present? + def find_some(ids) + return find_some_ordered(ids) unless order_values.present? - result = where(primary_key => ids).to_a + result = where(primary_key => ids).to_a - expected_size = - if limit_value && ids.size > limit_value - limit_value - else - ids.size + expected_size = + if limit_value && ids.size > limit_value + limit_value + else + ids.size + end + + # 11 ids with limit 3, offset 9 should give 2 results. + if offset_value && (ids.size - offset_value < expected_size) + expected_size = ids.size - offset_value end - # 11 ids with limit 3, offset 9 should give 2 results. - if offset_value && (ids.size - offset_value < expected_size) - expected_size = ids.size - offset_value + if result.size == expected_size + result + else + raise_record_not_found_exception!(ids, result.size, expected_size) + end end - if result.size == expected_size - result - else - raise_record_not_found_exception!(ids, result.size, expected_size) - end - end + def find_some_ordered(ids) + ids = ids.slice(offset_value || 0, limit_value || ids.size) || [] - def find_some_ordered(ids) - ids = ids.slice(offset_value || 0, limit_value || ids.size) || [] + result = except(:limit, :offset).where(primary_key => ids).records - result = except(:limit, :offset).where(primary_key => ids).records + if result.size == ids.size + pk_type = @klass.type_for_attribute(primary_key) - if result.size == ids.size - pk_type = @klass.type_for_attribute(primary_key) - - records_by_id = result.index_by(&:id) - ids.map { |id| records_by_id.fetch(pk_type.cast(id)) } - else - raise_record_not_found_exception!(ids, result.size, ids.size) + records_by_id = result.index_by(&:id) + ids.map { |id| records_by_id.fetch(pk_type.cast(id)) } + else + raise_record_not_found_exception!(ids, result.size, ids.size) + end end - end - def find_take - if loaded? - records.first - else - @take ||= limit(1).records.first + def find_take + if loaded? + records.first + else + @take ||= limit(1).records.first + end end - end - def find_nth(index, offset = nil) - # TODO: once the offset argument is removed we rely on offset_index - # within find_nth_with_limit, rather than pass it in via - # find_nth_with_limit_and_offset - if offset - ActiveSupport::Deprecation.warn(<<-MSG.squish) - Passing an offset argument to find_nth is deprecated, - please use Relation#offset instead. - MSG - end - if loaded? - records[index] - else - offset ||= offset_index - @offsets[offset + index] ||= find_nth_with_limit_and_offset(index, 1, offset: offset).first + def find_take_with_limit(limit) + if loaded? + records.take(limit) + else + limit(limit).to_a + end end - end - - def find_nth!(index) - find_nth(index) or raise RecordNotFound.new("Couldn't find #{@klass.name} with [#{arel.where_sql(@klass.arel_engine)}]") - end - def find_nth_with_limit(index, limit) - # TODO: once the offset argument is removed from find_nth, - # find_nth_with_limit_and_offset can be merged into this method. - relation = if order_values.empty? && primary_key - order(arel_attribute(primary_key).asc) - else - self + def find_nth(index) + @offsets[offset_index + index] ||= find_nth_with_limit(index, 1).first end - relation = relation.offset(index) unless index.zero? - relation.limit(limit).to_a - end - - def find_nth_from_last(index) - if loaded? - records[-index] - else - relation = if order_values.empty? && primary_key - order(arel_attribute(primary_key).asc) + def find_nth_with_limit(index, limit) + if loaded? + records[index, limit] || [] else - self + relation = if order_values.empty? && primary_key + order(arel_attribute(primary_key).asc) + else + self + end + + relation = relation.offset(offset_index + index) unless index.zero? + relation.limit(limit).to_a end - - 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 end - end - - private - def find_nth_with_limit_and_offset(index, limit, offset:) # :nodoc: - if loaded? - records[index, limit] - else - index += offset - find_nth_with_limit(index, limit) + def find_nth_from_last(index) + if loaded? + records[-index] + else + relation = if order_values.empty? && primary_key + order(arel_attribute(primary_key).asc) + else + self + end + + 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 + end end - end - def find_last(limit) - limit ? records.last(limit) : records.last - end + def find_last(limit) + limit ? records.last(limit) : records.last + end end end diff --git a/activerecord/lib/active_record/relation/merger.rb b/activerecord/lib/active_record/relation/merger.rb index df43b5c64b..5dac00724a 100644 --- a/activerecord/lib/active_record/relation/merger.rb +++ b/activerecord/lib/active_record/relation/merger.rb @@ -151,15 +151,11 @@ module ActiveRecord end end - CLAUSE_METHOD_NAMES = CLAUSE_METHODS.map do |name| - ["#{name}_clause", "#{name}_clause="] - end - def merge_clauses - CLAUSE_METHOD_NAMES.each do |(reader, writer)| - clause = relation.send(reader) - other_clause = other.send(reader) - relation.send(writer, clause.merge(other_clause)) + CLAUSE_METHODS.each do |method| + clause = relation.get_value(method) + other_clause = other.get_value(method) + relation.set_value(method, clause.merge(other_clause)) end end end diff --git a/activerecord/lib/active_record/relation/predicate_builder.rb b/activerecord/lib/active_record/relation/predicate_builder.rb index 505a78cb78..18ae10a652 100644 --- a/activerecord/lib/active_record/relation/predicate_builder.rb +++ b/activerecord/lib/active_record/relation/predicate_builder.rb @@ -4,7 +4,6 @@ module ActiveRecord require "active_record/relation/predicate_builder/association_query_handler" require "active_record/relation/predicate_builder/base_handler" require "active_record/relation/predicate_builder/basic_object_handler" - require "active_record/relation/predicate_builder/class_handler" require "active_record/relation/predicate_builder/polymorphic_array_handler" require "active_record/relation/predicate_builder/range_handler" require "active_record/relation/predicate_builder/relation_handler" @@ -16,7 +15,6 @@ module ActiveRecord @handlers = [] register_handler(BasicObject, BasicObjectHandler.new) - register_handler(Class, ClassHandler.new(self)) register_handler(Base, BaseHandler.new(self)) register_handler(Range, RangeHandler.new) register_handler(RangeHandler::RangeWithBinds, RangeHandler.new) @@ -66,6 +64,8 @@ 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. protected attr_reader :table @@ -74,10 +74,10 @@ module ActiveRecord return ["1=0"] if attributes.empty? attributes.flat_map do |key, value| - if value.is_a?(Hash) + if value.is_a?(Hash) && !table.has_column?(key) associated_predicate_builder(key).expand_from_hash(value) else - expand(key, value) + build(table.arel_attribute(key), value) end end end @@ -87,14 +87,15 @@ module ActiveRecord binds = [] attributes.each do |column_name, value| - case value - when Hash + 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 Relation + next + when value.is_a?(Relation) binds += value.bound_attributes - when Range + 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? @@ -113,6 +114,15 @@ module ActiveRecord binds << build_bind_param(column_name, value) end end + + # 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) + if table.associated_with?(column_name) + result[column_name] = AssociationQueryHandler.value_for(table, column_name, value) + end end [result, binds] @@ -120,16 +130,6 @@ module ActiveRecord private - def expand(column, value) - # 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) - value = AssociationQueryHandler.value_for(table, column, value) if table.associated_with?(column) - build(table.arel_attribute(column), value) - end - def associated_predicate_builder(association_name) self.class.new(table.associated_table(association_name)) end @@ -155,9 +155,13 @@ module ActiveRecord end def can_be_bound?(column_name, value) - !value.nil? && - handler_for(value).is_a?(BasicObjectHandler) && - !table.associated_with?(column_name) + return if table.associated_with?(column_name) + 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_param(column_name, 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 6400caba06..88b6c37d43 100644 --- a/activerecord/lib/active_record/relation/predicate_builder/array_handler.rb +++ b/activerecord/lib/active_record/relation/predicate_builder/array_handler.rb @@ -29,6 +29,8 @@ module ActiveRecord array_predicates.inject { |composite, predicate| composite.or(predicate) } 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/association_query_handler.rb b/activerecord/lib/active_record/relation/predicate_builder/association_query_handler.rb index 7e20cb2c63..29860ec677 100644 --- a/activerecord/lib/active_record/relation/predicate_builder/association_query_handler.rb +++ b/activerecord/lib/active_record/relation/predicate_builder/association_query_handler.rb @@ -28,6 +28,8 @@ module ActiveRecord predicate_builder.build_from_hash(queries) 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 @@ -68,9 +70,6 @@ module ActiveRecord case value when Relation value.klass.base_class - when Array - val = value.compact.first - val.class.base_class if val.is_a?(Base) when Base value.class.base_class end 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 65c5159704..3bb1037885 100644 --- a/activerecord/lib/active_record/relation/predicate_builder/base_handler.rb +++ b/activerecord/lib/active_record/relation/predicate_builder/base_handler.rb @@ -9,6 +9,8 @@ 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/class_handler.rb b/activerecord/lib/active_record/relation/predicate_builder/class_handler.rb deleted file mode 100644 index 73ad864765..0000000000 --- a/activerecord/lib/active_record/relation/predicate_builder/class_handler.rb +++ /dev/null @@ -1,27 +0,0 @@ -module ActiveRecord - class PredicateBuilder - class ClassHandler # :nodoc: - def initialize(predicate_builder) - @predicate_builder = predicate_builder - end - - def call(attribute, value) - print_deprecation_warning - predicate_builder.build(attribute, value.name) - end - - protected - - attr_reader :predicate_builder - - private - - def print_deprecation_warning - ActiveSupport::Deprecation.warn(<<-MSG.squish) - Passing a class as a value in an Active Record query is deprecated and - will be removed. Pass a string instead. - MSG - end - end - end -end diff --git a/activerecord/lib/active_record/relation/predicate_builder/polymorphic_array_handler.rb b/activerecord/lib/active_record/relation/predicate_builder/polymorphic_array_handler.rb index 0c7f92b3d0..335124c952 100644 --- a/activerecord/lib/active_record/relation/predicate_builder/polymorphic_array_handler.rb +++ b/activerecord/lib/active_record/relation/predicate_builder/polymorphic_array_handler.rb @@ -21,6 +21,8 @@ module ActiveRecord end end + # TODO Change this to private once we've dropped Ruby 2.2 support. + # Workaround for Ruby 2.2 "private attribute?" warning. protected attr_reader :predicate_builder diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb index 1a59b3a146..4ee413c805 100644 --- a/activerecord/lib/active_record/relation/query_methods.rb +++ b/activerecord/lib/active_record/relation/query_methods.rb @@ -3,8 +3,6 @@ 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" -require "active_support/core_ext/string/filters" -require "active_support/core_ext/regexp" module ActiveRecord module QueryMethods @@ -56,62 +54,39 @@ module ActiveRecord end FROZEN_EMPTY_ARRAY = [].freeze - Relation::MULTI_VALUE_METHODS.each do |name| - class_eval <<-CODE, __FILE__, __LINE__ + 1 - def #{name}_values - @values[:#{name}] || FROZEN_EMPTY_ARRAY - end + FROZEN_EMPTY_HASH = {}.freeze - def #{name}_values=(values) - assert_mutability! - @values[:#{name}] = values + Relation::VALUE_METHODS.each do |name| + method_name = \ + case name + when *Relation::MULTI_VALUE_METHODS then "#{name}_values" + when *Relation::SINGLE_VALUE_METHODS then "#{name}_value" + when *Relation::CLAUSE_METHODS then "#{name}_clause" end - CODE - end - - (Relation::SINGLE_VALUE_METHODS - [:create_with]).each do |name| class_eval <<-CODE, __FILE__, __LINE__ + 1 - def #{name}_value # def readonly_value - @values[:#{name}] # @values[:readonly] + def #{method_name} # def includes_values + get_value(#{name.inspect}) # get_value(:includes) end # end - CODE - end - Relation::SINGLE_VALUE_METHODS.each do |name| - class_eval <<-CODE, __FILE__, __LINE__ + 1 - def #{name}_value=(value) # def readonly_value=(value) - assert_mutability! # assert_mutability! - @values[:#{name}] = value # @values[:readonly] = value + def #{method_name}=(value) # def includes_values=(value) + set_value(#{name.inspect}, value) # set_value(:includes, value) end # end CODE end - Relation::CLAUSE_METHODS.each do |name| - class_eval <<-CODE, __FILE__, __LINE__ + 1 - def #{name}_clause # def where_clause - @values[:#{name}] || new_#{name}_clause # @values[:where] || new_where_clause - end # end - # - def #{name}_clause=(value) # def where_clause=(value) - assert_mutability! # assert_mutability! - @values[:#{name}] = value # @values[:where] = value - end # end - CODE - end - def bound_attributes - if limit_value && !string_containing_comma?(limit_value) + if limit_value limit_bind = Attribute.with_cast_value( "LIMIT".freeze, connection.sanitize_limit(limit_value), - Type::Value.new, + Type.default_value, ) end if offset_value offset_bind = Attribute.with_cast_value( "OFFSET".freeze, offset_value.to_i, - Type::Value.new, + Type.default_value, ) end connection.combine_bind_parameters( @@ -124,11 +99,6 @@ module ActiveRecord ) end - FROZEN_EMPTY_HASH = {}.freeze - def create_with_value # :nodoc: - @values[:create_with] || FROZEN_EMPTY_HASH - end - alias extensions extending_values # Specify relationships to be included in the result set. For @@ -270,7 +240,14 @@ module ActiveRecord # Model.select(:field).first.other_field # # => ActiveModel::MissingAttributeError: missing attribute: other_field def select(*fields) - return super if block_given? + if block_given? + if fields.any? + raise ArgumentError, "`select' with block doesn't take arguments." + end + + return super() + end + raise ArgumentError, "Call this with at least one field" if fields.empty? spawn._select!(*fields) end @@ -418,7 +395,10 @@ module ActiveRecord args.each do |scope| case scope when Symbol - symbol_unscoping(scope) + 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) when Hash scope.each do |key, target_value| if key != :where @@ -496,7 +476,6 @@ module ActiveRecord self.left_outer_joins_values += args self end - alias :left_joins! :left_outer_joins! # Returns a new relation, which is the result of filtering the current relation # according to the conditions in the arguments. @@ -677,7 +656,7 @@ module ActiveRecord end self.where_clause = self.where_clause.or(other.where_clause) - self.having_clause = self.having_clause.or(other.having_clause) + self.having_clause = having_clause.or(other.having_clause) self end @@ -708,13 +687,6 @@ module ActiveRecord end def limit!(value) # :nodoc: - if string_containing_comma?(value) - # Remove `string_containing_comma?` when removing this deprecation - ActiveSupport::Deprecation.warn(<<-WARNING.squish) - Passing a string to limit in the form "1,2" is deprecated and will be - removed in Rails 5.1. Please call `offset` explicitly instead. - WARNING - end self.limit_value = value self end @@ -781,7 +753,7 @@ module ActiveRecord # end # def none - where("1=0").extending!(NullRelation) + spawn.none! end def none! # :nodoc: @@ -866,16 +838,12 @@ module ActiveRecord def distinct(value = true) spawn.distinct!(value) end - alias uniq distinct - deprecate uniq: :distinct # Like #distinct, but modifies relation in place. def distinct!(value = true) # :nodoc: self.distinct_value = value self end - alias uniq! distinct! - deprecate uniq!: :distinct! # Used to extend a scope with additional methods, either through # a module or through a block provided. @@ -950,6 +918,17 @@ module ActiveRecord @arel ||= build_arel end + # Returns a relation value with a given name + def get_value(name) # :nodoc: + @values[name] || default_value_for(name) + end + + # Sets the relation value with the given name + def set_value(name, value) # :nodoc: + assert_mutability! + @values[name] = value + end + private def assert_mutability! @@ -965,13 +944,7 @@ module ActiveRecord arel.where(where_clause.ast) unless where_clause.empty? arel.having(having_clause.ast) unless having_clause.empty? - if limit_value - if string_containing_comma?(limit_value) - arel.take(connection.sanitize_limit(limit_value)) - else - arel.take(Arel::Nodes::BindParam.new) - end - end + arel.take(Arel::Nodes::BindParam.new) if limit_value arel.skip(Arel::Nodes::BindParam.new) if offset_value arel.group(*arel_columns(group_values.uniq.reject(&:blank?))) unless group_values.empty? @@ -986,29 +959,6 @@ module ActiveRecord arel end - def symbol_unscoping(scope) - 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 - - clause_method = Relation::CLAUSE_METHODS.include?(scope) - multi_val_method = Relation::MULTI_VALUE_METHODS.include?(scope) - if clause_method - unscope_code = "#{scope}_clause=" - else - unscope_code = "#{scope}_value#{'s' if multi_val_method}=" - end - - case scope - when :order - result = [] - else - result = [] if multi_val_method - end - - send(unscope_code, result) - end - def build_from opts = from_clause.value name = from_clause.name @@ -1137,9 +1087,9 @@ module ActiveRecord def does_not_support_reverse?(order) # Uses SQL function with multiple arguments. - /\([^()]*,[^()]*\)/.match?(order) || - # Uses "nulls first" like construction. - /nulls (first|last)\Z/i.match?(order) + (order.include?(",") && order.split(",").find { |section| section.count("(") != section.count(")") }) || + # Uses "nulls first" like construction. + /nulls (first|last)\Z/i.match?(order) end def build_order(arel) @@ -1188,50 +1138,57 @@ module ActiveRecord end.flatten! end - # Checks to make sure that the arguments are not blank. Note that if some - # blank-like object were initially passed into the query method, then this - # method will not raise an error. - # - # Example: - # - # Post.references() # raises an error - # Post.references([]) # does not raise an error - # - # This particular method should be called with a method_name and the args - # passed into that method as an input. For example: - # - # def references(*args) - # check_if_method_has_arguments!("references", args) - # ... - # end + # Checks to make sure that the arguments are not blank. Note that if some + # blank-like object were initially passed into the query method, then this + # method will not raise an error. + # + # Example: + # + # Post.references() # raises an error + # Post.references([]) # does not raise an error + # + # This particular method should be called with a method_name and the args + # passed into that method as an input. For example: + # + # def references(*args) + # check_if_method_has_arguments!("references", args) + # ... + # end def check_if_method_has_arguments!(method_name, args) if args.blank? raise ArgumentError, "The method .#{method_name}() must contain arguments." end end + STRUCTURAL_OR_METHODS = Relation::VALUE_METHODS - [:extending, :where, :having] def structurally_incompatible_values_for_or(other) - Relation::SINGLE_VALUE_METHODS.reject { |m| send("#{m}_value") == other.send("#{m}_value") } + - (Relation::MULTI_VALUE_METHODS - [:extending]).reject { |m| send("#{m}_values") == other.send("#{m}_values") } + - (Relation::CLAUSE_METHODS - [:having, :where]).reject { |m| send("#{m}_clause") == other.send("#{m}_clause") } - end - - def new_where_clause - Relation::WhereClause.empty + STRUCTURAL_OR_METHODS.reject do |method| + get_value(method) == other.get_value(method) + end end - alias new_having_clause new_where_clause def where_clause_factory @where_clause_factory ||= Relation::WhereClauseFactory.new(klass, predicate_builder) end alias having_clause_factory where_clause_factory - def new_from_clause - Relation::FromClause.empty - end - - def string_containing_comma?(value) - ::String === value && value.include?(",") + 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 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 dbd08811fa..31544c730e 100644 --- a/activerecord/lib/active_record/relation/record_fetch_warning.rb +++ b/activerecord/lib/active_record/relation/record_fetch_warning.rb @@ -2,15 +2,15 @@ module ActiveRecord class Relation module RecordFetchWarning # When this module is prepended to ActiveRecord::Relation and - # `config.active_record.warn_on_records_fetched_greater_than` is + # +config.active_record.warn_on_records_fetched_greater_than+ is # set to an integer, if the number of records a query returns is - # greater than the value of `warn_on_records_fetched_greater_than`, + # greater than the value of +warn_on_records_fetched_greater_than+, # a warning is logged. This allows for the detection of queries that # return a large number of records, which could cause memory bloat. # # In most cases, fetching large number of records can be performed # efficiently using the ActiveRecord::Batches methods. - # See active_record/lib/relation/batches.rb for more information. + # See ActiveRecord::Batches for more information. def exec_queries QueryRegistry.reset diff --git a/activerecord/lib/active_record/relation/spawn_methods.rb b/activerecord/lib/active_record/relation/spawn_methods.rb index 190e339ea8..ada89b5ec3 100644 --- a/activerecord/lib/active_record/relation/spawn_methods.rb +++ b/activerecord/lib/active_record/relation/spawn_methods.rb @@ -66,7 +66,7 @@ module ActiveRecord private - def relation_with(values) # :nodoc: + def relation_with(values) result = Relation.create(klass, table, predicate_builder, values) result.extend(*extending_values) if extending_values.any? result diff --git a/activerecord/lib/active_record/relation/where_clause.rb b/activerecord/lib/active_record/relation/where_clause.rb index 402f8acfd1..ef0d059d1c 100644 --- a/activerecord/lib/active_record/relation/where_clause.rb +++ b/activerecord/lib/active_record/relation/where_clause.rb @@ -84,6 +84,8 @@ module ActiveRecord @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 diff --git a/activerecord/lib/active_record/relation/where_clause_factory.rb b/activerecord/lib/active_record/relation/where_clause_factory.rb index dc00149130..737bc278bd 100644 --- a/activerecord/lib/active_record/relation/where_clause_factory.rb +++ b/activerecord/lib/active_record/relation/where_clause_factory.rb @@ -7,8 +7,6 @@ module ActiveRecord end def build(opts, other) - binds = [] - case opts when String, Array parts = [klass.send(:sanitize_sql, other.empty? ? opts : ([opts] + other))] @@ -26,9 +24,11 @@ module ActiveRecord raise ArgumentError, "Unsupported argument type: #{opts} (#{opts.class})" end - WhereClause.new(parts, binds) + WhereClause.new(parts, binds || []) 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 diff --git a/activerecord/lib/active_record/result.rb b/activerecord/lib/active_record/result.rb index f5383f4c14..9ed70a9c2b 100644 --- a/activerecord/lib/active_record/result.rb +++ b/activerecord/lib/active_record/result.rb @@ -32,8 +32,6 @@ module ActiveRecord class Result include Enumerable - IDENTITY_TYPE = Type::Value.new # :nodoc: - attr_reader :columns, :rows, :column_types def initialize(columns, rows, column_types = {}) @@ -105,7 +103,7 @@ module ActiveRecord def column_type(name, type_overrides = {}) type_overrides.fetch(name) do - column_types.fetch(name, IDENTITY_TYPE) + column_types.fetch(name, Type.default_value) end end diff --git a/activerecord/lib/active_record/sanitization.rb b/activerecord/lib/active_record/sanitization.rb index 6617008344..427c0019c6 100644 --- a/activerecord/lib/active_record/sanitization.rb +++ b/activerecord/lib/active_record/sanitization.rb @@ -1,34 +1,26 @@ -require "active_support/core_ext/regexp" module ActiveRecord module Sanitization extend ActiveSupport::Concern module ClassMethods - # Used to sanitize objects before they're used in an SQL SELECT statement. - # Delegates to {connection.quote}[rdoc-ref:ConnectionAdapters::Quoting#quote]. - def sanitize(object) # :nodoc: - connection.quote(object) - end - alias_method :quote_value, :sanitize - - protected - - # 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) + 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) # :doc: return nil if condition.blank? case condition @@ -36,24 +28,25 @@ module ActiveRecord else condition end end - alias_method :sanitize_sql, :sanitize_sql_for_conditions - alias_method :sanitize_conditions, :sanitize_sql - - # 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 = self.table_name) + alias :sanitize_sql :sanitize_sql_for_conditions + alias :sanitize_conditions :sanitize_sql + deprecate sanitize_conditions: :sanitize_sql + + # 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) @@ -61,15 +54,15 @@ module ActiveRecord 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) + # 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 @@ -77,22 +70,22 @@ module ActiveRecord end end - # 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. - # - # Given: - # - # class Person < ActiveRecord::Base - # composed_of :address, class_name: "Address", - # mapping: [%w(address_street street), %w(address_city city)] - # end - # - # Then: - # - # { address: Address.new("813 abc st.", "chicago") } - # # => { address_street: "813 abc st.", address_city: "chicago" } - def expand_hash_conditions_for_aggregates(attrs) + # 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. + # + # Given: + # + # class Person < ActiveRecord::Base + # composed_of :address, class_name: "Address", + # mapping: [%w(address_street street), %w(address_city city)] + # end + # + # Then: + # + # { address: Address.new("813 abc st.", "chicago") } + # # => { address_street: "813 abc st.", address_city: "chicago" } + def expand_hash_conditions_for_aggregates(attrs) # :doc: expanded_attrs = {} attrs.each do |attr, value| if aggregation = reflect_on_aggregation(attr.to_sym) @@ -111,11 +104,11 @@ 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) + # 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) @@ -123,37 +116,37 @@ module ActiveRecord end.join(", ") end - # Sanitizes a +string+ so that it is safe to use within an SQL - # LIKE statement. This method uses +escape_character+ to escape all occurrences of "\", "_" and "%". - # - # 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 = "\\") + # 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) + # 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) @@ -166,7 +159,7 @@ module ActiveRecord end end - def replace_bind_variables(statement, values) # :nodoc: + def replace_bind_variables(statement, values) raise_if_bind_arity_mismatch(statement, statement.count("?"), values.size) bound = values.dup c = connection @@ -175,7 +168,7 @@ module ActiveRecord end end - def replace_bind_variable(value, c = connection) # :nodoc: + def replace_bind_variable(value, c = connection) if ActiveRecord::Relation === value value.to_sql else @@ -183,7 +176,7 @@ module ActiveRecord end end - def replace_named_bind_variables(statement, bind_vars) # :nodoc: + def replace_named_bind_variables(statement, bind_vars) statement.gsub(/(:?):([a-zA-Z]\w*)/) do |match| if $1 == ":" # skip postgresql casts match # return the whole match @@ -195,7 +188,7 @@ module ActiveRecord end end - def quote_bound_value(value, c = connection) # :nodoc: + def quote_bound_value(value, c = connection) if value.respond_to?(:map) && !value.acts_like?(:string) if value.respond_to?(:empty?) && value.empty? c.quote(nil) @@ -207,7 +200,7 @@ module ActiveRecord end end - def raise_if_bind_arity_mismatch(statement, expected, provided) # :nodoc: + def raise_if_bind_arity_mismatch(statement, expected, provided) unless expected == provided raise PreparedStatementInvalid, "wrong number of bind variables (#{provided} for #{expected}) in: #{statement}" end @@ -216,7 +209,7 @@ module ActiveRecord # TODO: Deprecate this def quoted_id # :nodoc: - self.class.quote_value(@attributes[self.class.primary_key].value_for_database) + self.class.connection.quote(@attributes[self.class.primary_key].value_for_database) end end end diff --git a/activerecord/lib/active_record/schema.rb b/activerecord/lib/active_record/schema.rb index 784a02d2c3..7a2bc9c8af 100644 --- a/activerecord/lib/active_record/schema.rb +++ b/activerecord/lib/active_record/schema.rb @@ -40,7 +40,7 @@ module ActiveRecord # ActiveRecord::Schema.define(version: 20380119000001) do # ... # end - def self.define(info={}, &block) + def self.define(info = {}, &block) new.define(info, &block) end @@ -61,7 +61,7 @@ module ActiveRecord # # ActiveRecord::Schema.new.migrations_paths # # => ["db/migrate"] # Rails migration path by default. - def migrations_paths # :nodoc: + def migrations_paths ActiveRecord::Migrator.migrations_paths end end diff --git a/activerecord/lib/active_record/schema_dumper.rb b/activerecord/lib/active_record/schema_dumper.rb index 5105088b2f..12289511b7 100644 --- a/activerecord/lib/active_record/schema_dumper.rb +++ b/activerecord/lib/active_record/schema_dumper.rb @@ -17,7 +17,7 @@ module ActiveRecord @@ignore_tables = [] class << self - def dump(connection=ActiveRecord::Base.connection, stream=STDOUT, config = ActiveRecord::Base) + def dump(connection = ActiveRecord::Base.connection, stream = STDOUT, config = ActiveRecord::Base) new(connection, generate_options(config)).dump(stream) stream end @@ -115,9 +115,7 @@ HEADER pkcol = columns.detect { |c| c.name == pk } pkcolspec = @connection.column_spec_for_primary_key(pkcol) if pkcolspec.present? - pkcolspec.each do |key, value| - tbl.print ", #{key}: #{value}" - end + tbl.print ", #{format_colspec(pkcolspec)}" end when Array tbl.print ", primary_key: #{pk.inspect}" @@ -128,45 +126,18 @@ HEADER table_options = @connection.table_options(table) if table_options.present? - table_options.each do |key, value| - tbl.print ", #{key}: #{value.inspect}" if value.present? - end + tbl.print ", #{format_options(table_options)}" end tbl.puts " do |t|" # then dump all non-primary key columns - column_specs = columns.map do |column| + 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 - @connection.column_spec(column) - end.compact - - # find all migration keys used in this table - keys = @connection.migration_keys - - # figure out the lengths for each column based on above keys - lengths = keys.map { |key| - column_specs.map { |spec| - spec[key] ? spec[key].length + 2 : 0 - }.max - } - - # the string we're going to sprintf our values against, with standardized column widths - format_string = lengths.map{ |len| "%-#{len}s" } - - # find the max length for the 'type' column, which is special - type_length = column_specs.map{ |column| column[:type].length }.max - - # add column type definition to our format string - format_string.unshift " t.%-#{type_length}s " - - format_string *= "" - - column_specs.each do |colspec| - values = keys.zip(lengths).map{ |key, len| colspec.key?(key) ? colspec[key] + ", " : " " * len } - values.unshift colspec[:type] - tbl.print((format_string % values).gsub(/,\s*$/, "")) + type, colspec = @connection.column_spec(column) + tbl.print " t.#{type} #{column.name.inspect}" + tbl.print ", #{format_colspec(colspec)}" if colspec.present? tbl.puts end @@ -191,7 +162,7 @@ HEADER if (indexes = @connection.indexes(table)).any? add_index_statements = indexes.map do |index| table_name = remove_prefix_and_suffix(index.table).inspect - " add_index #{([table_name]+index_parts(index)).join(', ')}" + " add_index #{([table_name] + index_parts(index)).join(', ')}" end stream.puts add_index_statements.sort.join("\n") @@ -214,12 +185,8 @@ HEADER "name: #{index.name.inspect}", ] index_parts << "unique: true" if index.unique - - index_lengths = (index.lengths || []).compact - index_parts << "length: #{Hash[index.columns.zip(index.lengths)].inspect}" if index_lengths.any? - - index_orders = index.orders || {} - index_parts << "order: #{index.orders.inspect}" if index_orders.any? + index_parts << "length: { #{format_options(index.lengths)} }" if index.lengths.present? + index_parts << "order: { #{format_options(index.orders)} }" if index.orders.present? index_parts << "where: #{index.where.inspect}" if index.where index_parts << "using: #{index.using.inspect}" if index.using index_parts << "type: #{index.type.inspect}" if index.type @@ -257,6 +224,14 @@ HEADER end end + def format_colspec(colspec) + colspec.map { |key, value| "#{key}: #{value}" }.join(", ") + end + + def format_options(options) + options.map { |key, value| "#{key}: #{value.inspect}" }.join(", ") + end + def remove_prefix_and_suffix(table) table.gsub(/^(#{@options[:table_name_prefix]})(.+)(#{@options[:table_name_suffix]})$/, "\\2") end diff --git a/activerecord/lib/active_record/schema_migration.rb b/activerecord/lib/active_record/schema_migration.rb index 99b23e5593..5efbcff96a 100644 --- a/activerecord/lib/active_record/schema_migration.rb +++ b/activerecord/lib/active_record/schema_migration.rb @@ -17,7 +17,7 @@ module ActiveRecord end def table_exists? - ActiveSupport::Deprecation.silence { connection.table_exists?(table_name) } + connection.table_exists?(table_name) end def create_table diff --git a/activerecord/lib/active_record/scoping.rb b/activerecord/lib/active_record/scoping.rb index d1bd1cd89a..7c00e7e4ed 100644 --- a/activerecord/lib/active_record/scoping.rb +++ b/activerecord/lib/active_record/scoping.rb @@ -33,7 +33,7 @@ module ActiveRecord def populate_with_current_scope_attributes # :nodoc: return unless self.class.scope_attributes? - self.class.scope_attributes.each do |att,value| + self.class.scope_attributes.each do |att, value| send("#{att}=", value) if respond_to?("#{att}=") end end diff --git a/activerecord/lib/active_record/scoping/default.rb b/activerecord/lib/active_record/scoping/default.rb index 7409706851..2daa48859a 100644 --- a/activerecord/lib/active_record/scoping/default.rb +++ b/activerecord/lib/active_record/scoping/default.rb @@ -44,50 +44,50 @@ module ActiveRecord self.current_scope = nil end - protected - - # Use this macro in your model to set a default scope for all operations on - # the model. - # - # class Article < ActiveRecord::Base - # default_scope { where(published: true) } - # end - # - # Article.all # => SELECT * FROM articles WHERE published = true - # - # The #default_scope is also applied while creating/building a record. - # It is not applied while updating a record. - # - # Article.new.published # => true - # Article.create.published # => true - # - # (You can also pass any object which responds to +call+ to the - # +default_scope+ macro, and it will be called when building the - # default scope.) - # - # If you use multiple #default_scope declarations in your model then - # they will be merged together: - # - # class Article < ActiveRecord::Base - # default_scope { where(published: true) } - # default_scope { where(rating: 'G') } - # end - # - # Article.all # => SELECT * FROM articles WHERE published = true AND rating = 'G' - # - # This is also the case with inheritance and module includes where the - # parent or module defines a #default_scope and the child or including - # class defines a second one. - # - # If you need to do more complex things with a default scope, you can - # alternatively define it as a class method: - # - # class Article < ActiveRecord::Base - # def self.default_scope - # # Should return a scope, you can call 'super' here etc. - # end - # end - def default_scope(scope = nil) + private + + # Use this macro in your model to set a default scope for all operations on + # the model. + # + # class Article < ActiveRecord::Base + # default_scope { where(published: true) } + # end + # + # Article.all # => SELECT * FROM articles WHERE published = true + # + # The #default_scope is also applied while creating/building a record. + # It is not applied while updating a record. + # + # Article.new.published # => true + # Article.create.published # => true + # + # (You can also pass any object which responds to +call+ to the + # +default_scope+ macro, and it will be called when building the + # default scope.) + # + # If you use multiple #default_scope declarations in your model then + # they will be merged together: + # + # class Article < ActiveRecord::Base + # default_scope { where(published: true) } + # default_scope { where(rating: 'G') } + # end + # + # Article.all # => SELECT * FROM articles WHERE published = true AND rating = 'G' + # + # This is also the case with inheritance and module includes where the + # parent or module defines a #default_scope and the child or including + # class defines a second one. + # + # If you need to do more complex things with a default scope, you can + # alternatively define it as a class method: + # + # class Article < ActiveRecord::Base + # def self.default_scope + # # Should return a scope, you can call 'super' here etc. + # end + # end + def default_scope(scope = nil) # :doc: scope = Proc.new if block_given? if scope.is_a?(Relation) || !scope.respond_to?(:call) @@ -101,7 +101,7 @@ module ActiveRecord self.default_scopes += [scope] end - def build_default_scope(base_rel = nil) # :nodoc: + def build_default_scope(base_rel = nil) return if abstract_class? if default_scope_override.nil? @@ -122,18 +122,18 @@ module ActiveRecord end end - def ignore_default_scope? # :nodoc: + def ignore_default_scope? ScopeRegistry.value_for(:ignore_default_scope, base_class) end - def ignore_default_scope=(ignore) # :nodoc: + def ignore_default_scope=(ignore) ScopeRegistry.set_value_for(:ignore_default_scope, base_class, ignore) end - # The ignore_default_scope flag is used to prevent an infinite recursion - # situation where a default scope references a scope which has a default - # scope which references a scope... - def evaluate_default_scope # :nodoc: + # The ignore_default_scope flag is used to prevent an infinite recursion + # situation where a default scope references a scope which has a default + # scope which references a scope... + def evaluate_default_scope return if ignore_default_scope? begin diff --git a/activerecord/lib/active_record/scoping/named.rb b/activerecord/lib/active_record/scoping/named.rb index 46ee3540bd..27cdf8cb7e 100644 --- a/activerecord/lib/active_record/scoping/named.rb +++ b/activerecord/lib/active_record/scoping/named.rb @@ -42,7 +42,7 @@ module ActiveRecord # Adds a class method for retrieving and querying objects. # The method is intended to return an ActiveRecord::Relation # object, which is composable with other scopes. - # If it returns nil or false, an + # If it returns +nil+ or +false+, an # {all}[rdoc-ref:Scoping::Named::ClassMethods#all] scope is returned instead. # # A \scope represents a narrowing of a database query, such as @@ -171,14 +171,14 @@ module ActiveRecord end end - protected + private - def valid_scope_name?(name) - if respond_to?(name, true) - logger.warn "Creating scope :#{name}. " \ - "Overwriting existing method #{self.name}.#{name}." + def valid_scope_name?(name) + if respond_to?(name, true) && logger + logger.warn "Creating scope :#{name}. " \ + "Overwriting existing method #{self.name}.#{name}." + end end - end end end end diff --git a/activerecord/lib/active_record/secure_token.rb b/activerecord/lib/active_record/secure_token.rb index 2c412a45ed..115799cc20 100644 --- a/activerecord/lib/active_record/secure_token.rb +++ b/activerecord/lib/active_record/secure_token.rb @@ -27,7 +27,7 @@ module ActiveRecord # Load securerandom only when has_secure_token is used. require "active_support/core_ext/securerandom" define_method("regenerate_#{attribute}") { update! attribute => self.class.generate_unique_secure_token } - before_create { self.send("#{attribute}=", self.class.generate_unique_secure_token) unless self.send("#{attribute}?")} + before_create { send("#{attribute}=", self.class.generate_unique_secure_token) unless send("#{attribute}?") } end def generate_unique_secure_token diff --git a/activerecord/lib/active_record/statement_cache.rb b/activerecord/lib/active_record/statement_cache.rb index fd67032235..1877489e55 100644 --- a/activerecord/lib/active_record/statement_cache.rb +++ b/activerecord/lib/active_record/statement_cache.rb @@ -7,12 +7,12 @@ module ActiveRecord # end # # The cached statement is executed by using the - # [connection.execute]{rdoc-ref:ConnectionAdapters::DatabaseStatements#execute} method: + # {connection.execute}[rdoc-ref:ConnectionAdapters::DatabaseStatements#execute] method: # # cache.execute([], Book, Book.connection) # # The relation returned by the block is cached, and for each - # [execute]{rdoc-ref:ConnectionAdapters::DatabaseStatements#execute} + # {execute}[rdoc-ref:ConnectionAdapters::DatabaseStatements#execute] # call the cached relation gets duped. Database is queried when +to_a+ is called on the relation. # # If you want to cache the statement without the values you can use the +bind+ method of the @@ -41,7 +41,7 @@ module ActiveRecord class PartialQuery < Query # :nodoc: def initialize(values) @values = values - @indexes = values.each_with_index.find_all { |thing,i| + @indexes = values.each_with_index.find_all { |thing, i| Arel::Nodes::BindParam === thing }.map(&:last) end @@ -68,7 +68,7 @@ module ActiveRecord class BindMap # :nodoc: def initialize(bound_attributes) - @indexes = [] + @indexes = [] @bound_attributes = bound_attributes bound_attributes.each_with_index do |attr, i| @@ -80,7 +80,7 @@ module ActiveRecord def bind(values) bas = @bound_attributes.dup - @indexes.each_with_index { |offset,i| bas[offset] = bas[offset].with_cast_value(values[i]) } + @indexes.each_with_index { |offset, i| bas[offset] = bas[offset].with_cast_value(values[i]) } bas end end @@ -99,12 +99,12 @@ module ActiveRecord @bind_map = bind_map end - def execute(params, klass, connection) + def execute(params, klass, 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) + klass.find_by_sql(sql, bind_values, preparable: true, &block) end alias :call :execute end diff --git a/activerecord/lib/active_record/store.rb b/activerecord/lib/active_record/store.rb index 066573192e..d4be20d999 100644 --- a/activerecord/lib/active_record/store.rb +++ b/activerecord/lib/active_record/store.rb @@ -121,18 +121,17 @@ module ActiveRecord end end - protected - def read_store_attribute(store_attribute, key) + private + def read_store_attribute(store_attribute, key) # :doc: accessor = store_accessor_for(store_attribute) accessor.read(self, store_attribute, key) end - def write_store_attribute(store_attribute, key, value) + def write_store_attribute(store_attribute, key, value) # :doc: accessor = store_accessor_for(store_attribute) accessor.write(self, store_attribute, key, value) end - private def store_accessor_for(store_attribute) type_for_attribute(store_attribute.to_s).accessor end diff --git a/activerecord/lib/active_record/table_metadata.rb b/activerecord/lib/active_record/table_metadata.rb index e8d6a144f9..71efc1829a 100644 --- a/activerecord/lib/active_record/table_metadata.rb +++ b/activerecord/lib/active_record/table_metadata.rb @@ -10,9 +10,7 @@ module ActiveRecord end def resolve_column_aliases(hash) - # This method is a hot spot, so for now, use Hash[] to dup the hash. - # https://bugs.ruby-lang.org/issues/7166 - new_hash = Hash[hash] + new_hash = hash.dup hash.each do |key, _| if (key.is_a?(Symbol)) && klass.attribute_alias?(key) new_hash[klass.attribute_alias(key)] = new_hash.delete(key) @@ -33,16 +31,20 @@ module ActiveRecord if klass klass.type_for_attribute(column_name.to_s) else - Type::Value.new + Type.default_value end end + def has_column?(column_name) + klass && klass.columns_hash.key?(column_name.to_s) + end + def associated_with?(association_name) klass && klass._reflect_on_association(association_name) end def associated_table(table_name) - association = klass._reflect_on_association(table_name) || klass._reflect_on_association(table_name.singularize) + association = klass._reflect_on_association(table_name) || klass._reflect_on_association(table_name.to_s.singularize) if !association && table_name == arel_table.name return self @@ -62,6 +64,8 @@ module ActiveRecord association && association.polymorphic? end + # TODO Change this to private once we've dropped Ruby 2.2 support. + # Workaround for Ruby 2.2 "private attribute?" warning. protected attr_reader :klass, :arel_table, :association diff --git a/activerecord/lib/active_record/tasks/database_tasks.rb b/activerecord/lib/active_record/tasks/database_tasks.rb index a19913f2a8..bdb5184599 100644 --- a/activerecord/lib/active_record/tasks/database_tasks.rb +++ b/activerecord/lib/active_record/tasks/database_tasks.rb @@ -1,5 +1,3 @@ -require "active_support/core_ext/string/filters" - module ActiveRecord module Tasks # :nodoc: class DatabaseAlreadyExists < StandardError; end # :nodoc: @@ -35,12 +33,22 @@ module ActiveRecord # # DatabaseTasks.create_current('production') module DatabaseTasks + ## + # :singleton-method: + # Extra flags passed to database CLI tool (mysqldump/pg_dump) when calling db:structure:dump + mattr_accessor :structure_dump_flags, instance_accessor: false + + ## + # :singleton-method: + # Extra flags passed to database CLI tool when calling db:structure:load + mattr_accessor :structure_load_flags, instance_accessor: false + extend self attr_writer :current_config, :db_dir, :migrations_paths, :fixtures_path, :root, :env, :seed_loader attr_accessor :database_configuration - LOCAL_HOSTS = ["127.0.0.1", "localhost"] + LOCAL_HOSTS = ["127.0.0.1", "localhost"] def check_protected_environments! unless ENV["DISABLE_DATABASE_ENVIRONMENT_CHECK"] @@ -204,13 +212,13 @@ module ActiveRecord def structure_dump(*arguments) configuration = arguments.first filename = arguments.delete_at 1 - class_for_adapter(configuration["adapter"]).new(*arguments).structure_dump(filename) + class_for_adapter(configuration["adapter"]).new(*arguments).structure_dump(filename, structure_dump_flags) end def structure_load(*arguments) configuration = arguments.first filename = arguments.delete_at 1 - class_for_adapter(configuration["adapter"]).new(*arguments).structure_load(filename) + 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: @@ -231,14 +239,6 @@ module ActiveRecord ActiveRecord::InternalMetadata[:environment] = ActiveRecord::Migrator.current_environment end - def load_schema_for(*args) - ActiveSupport::Deprecation.warn(<<-MSG.squish) - This method was renamed to `#load_schema` and will be removed in the future. - Use `#load_schema` instead. - MSG - load_schema(*args) - end - def schema_file(format = ActiveRecord::Base.schema_format) case format when :ruby @@ -273,6 +273,16 @@ module ActiveRecord end end + # Dumps the schema cache in YAML format for the connection into the file + # + # ==== Examples: + # ActiveRecord::Tasks::DatabaseTasks.dump_schema_cache(ActiveRecord::Base.connection, "tmp/schema_dump.yaml") + def dump_schema_cache(conn, filename) + conn.schema_cache.clear! + conn.data_sources.each { |table| conn.schema_cache.add(table) } + open(filename, "wb") { |f| f.write(YAML.dump(conn.schema_cache)) } + end + private def class_for_adapter(adapter) diff --git a/activerecord/lib/active_record/tasks/mysql_database_tasks.rb b/activerecord/lib/active_record/tasks/mysql_database_tasks.rb index c8b89f1fdf..920830b9cf 100644 --- a/activerecord/lib/active_record/tasks/mysql_database_tasks.rb +++ b/activerecord/lib/active_record/tasks/mysql_database_tasks.rb @@ -14,7 +14,7 @@ module ActiveRecord connection.create_database configuration["database"], creation_options establish_connection configuration rescue ActiveRecord::StatementInvalid => error - if /database exists/ === error.message + if error.message.include?("database exists") raise DatabaseAlreadyExists else raise @@ -53,21 +53,23 @@ module ActiveRecord connection.collation end - def structure_dump(filename) + def structure_dump(filename, extra_flags) args = prepare_command_options args.concat(["--result-file", "#{filename}"]) args.concat(["--no-data"]) args.concat(["--routines"]) args.concat(["--skip-comments"]) + args.concat(Array(extra_flags)) if extra_flags args.concat(["#{configuration['database']}"]) run_cmd("mysqldump", args, "dumping") end - def structure_load(filename) + def structure_load(filename, extra_flags) args = prepare_command_options args.concat(["--execute", %{SET FOREIGN_KEY_CHECKS = 0; SOURCE #{filename}; SET FOREIGN_KEY_CHECKS = 1}]) args.concat(["--database", "#{configuration['database']}"]) + args.concat(Array(extra_flags)) if extra_flags run_cmd("mysql", args, "loading") end @@ -105,7 +107,7 @@ module ActiveRecord GRANT ALL PRIVILEGES ON #{configuration['database']}.* TO '#{configuration['username']}'@'localhost' IDENTIFIED BY '#{configuration['password']}' WITH GRANT OPTION; - SQL + SQL end def root_configuration_without_database diff --git a/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb b/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb index 6eac9af236..5155ced0e2 100644 --- a/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb +++ b/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb @@ -17,7 +17,7 @@ module ActiveRecord configuration.merge("encoding" => encoding) establish_connection configuration rescue ActiveRecord::StatementInvalid => error - if /database .* already exists/ === error.message + if /database .* already exists/.match?(error.message) raise DatabaseAlreadyExists else raise @@ -43,19 +43,21 @@ module ActiveRecord create true end - def structure_dump(filename) + def structure_dump(filename, extra_flags) set_psql_env - search_path = case ActiveRecord::Base.dump_schemas - when :schema_search_path - configuration["schema_search_path"] - when :all - nil - when String - ActiveRecord::Base.dump_schemas - end + search_path = \ + case ActiveRecord::Base.dump_schemas + when :schema_search_path + configuration["schema_search_path"] + when :all + nil + when String + ActiveRecord::Base.dump_schemas + end args = ["-s", "-x", "-O", "-f", filename] + args.concat(Array(extra_flags)) if extra_flags unless search_path.blank? args += search_path.split(",").map do |part| "--schema=#{part.strip}" @@ -66,10 +68,12 @@ module ActiveRecord File.open(filename, "a") { |f| f << "SET search_path TO #{connection.schema_search_path};\n\n" } end - def structure_load(filename) + def structure_load(filename, extra_flags) set_psql_env - args = [ "-v", ON_ERROR_STOP_1, "-q", "-f", filename, configuration["database"] ] - run_cmd("psql", args, "loading" ) + args = ["-v", ON_ERROR_STOP_1, "-q", "-f", filename] + args.concat(Array(extra_flags)) if extra_flags + args << configuration["database"] + run_cmd("psql", args, "loading") end private diff --git a/activerecord/lib/active_record/tasks/sqlite_database_tasks.rb b/activerecord/lib/active_record/tasks/sqlite_database_tasks.rb index 31f1b7efd4..1f756c2979 100644 --- a/activerecord/lib/active_record/tasks/sqlite_database_tasks.rb +++ b/activerecord/lib/active_record/tasks/sqlite_database_tasks.rb @@ -21,7 +21,7 @@ module ActiveRecord FileUtils.rm(file) rescue Errno::ENOENT => error - raise NoDatabaseError.new(error.message, error) + raise NoDatabaseError.new(error.message) end def purge @@ -35,14 +35,16 @@ module ActiveRecord connection.encoding end - def structure_dump(filename) + def structure_dump(filename, extra_flags) dbfile = configuration["database"] - `sqlite3 #{dbfile} .schema > #{filename}` + flags = extra_flags.join(" ") if extra_flags + `sqlite3 #{flags} #{dbfile} .schema > #{filename}` end - def structure_load(filename) + def structure_load(filename, extra_flags) dbfile = configuration["database"] - `sqlite3 #{dbfile} < "#{filename}"` + flags = extra_flags.join(" ") if extra_flags + `sqlite3 #{flags} #{dbfile} < "#{filename}"` end private diff --git a/activerecord/lib/active_record/timestamp.rb b/activerecord/lib/active_record/timestamp.rb index 6641ab5df1..09d8d1cdd4 100644 --- a/activerecord/lib/active_record/timestamp.rb +++ b/activerecord/lib/active_record/timestamp.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true module ActiveRecord # = Active Record \Timestamp # @@ -51,15 +52,41 @@ module ActiveRecord clear_timestamp_attributes end + class_methods do + private + def timestamp_attributes_for_create_in_model + timestamp_attributes_for_create.select { |c| column_names.include?(c) } + end + + def timestamp_attributes_for_update_in_model + timestamp_attributes_for_update.select { |c| column_names.include?(c) } + end + + def all_timestamp_attributes_in_model + timestamp_attributes_for_create_in_model + timestamp_attributes_for_update_in_model + end + + def timestamp_attributes_for_create + ["created_at", "created_on"] + end + + def timestamp_attributes_for_update + ["updated_at", "updated_on"] + end + + def current_time_from_proper_timezone + default_timezone == :utc ? Time.now.utc : Time.now + end + end + private def _create_record if record_timestamps current_time = current_time_from_proper_timezone - all_timestamp_attributes.each do |column| - column = column.to_s - if has_attribute?(column) && !attribute_present?(column) + all_timestamp_attributes_in_model.each do |column| + if !attribute_present?(column) write_attribute(column, current_time) end end @@ -73,8 +100,7 @@ module ActiveRecord current_time = current_time_from_proper_timezone timestamp_attributes_for_update_in_model.each do |column| - column = column.to_s - next if attribute_changed?(column) + next if will_save_change_to_attribute?(column) write_attribute(column, current_time) end end @@ -82,34 +108,26 @@ module ActiveRecord end def should_record_timestamps? - record_timestamps && (!partial_writes? || changed?) + record_timestamps && (!partial_writes? || has_changes_to_save?) end def timestamp_attributes_for_create_in_model - timestamp_attributes_for_create.select { |c| self.class.column_names.include?(c.to_s) } + self.class.send(:timestamp_attributes_for_create_in_model) end def timestamp_attributes_for_update_in_model - timestamp_attributes_for_update.select { |c| self.class.column_names.include?(c.to_s) } + self.class.send(:timestamp_attributes_for_update_in_model) end def all_timestamp_attributes_in_model - timestamp_attributes_for_create_in_model + timestamp_attributes_for_update_in_model - end - - def timestamp_attributes_for_update - [:updated_at, :updated_on] - end - - def timestamp_attributes_for_create - [:created_at, :created_on] + self.class.send(:all_timestamp_attributes_in_model) end - def all_timestamp_attributes - timestamp_attributes_for_create + timestamp_attributes_for_update + def current_time_from_proper_timezone + self.class.send(:current_time_from_proper_timezone) end - def max_updated_column_timestamp(timestamp_names = timestamp_attributes_for_update) + def max_updated_column_timestamp(timestamp_names = self.class.send(:timestamp_attributes_for_update)) timestamp_names .map { |attr| self[attr] } .compact @@ -117,10 +135,6 @@ module ActiveRecord .max end - def current_time_from_proper_timezone - self.class.default_timezone == :utc ? Time.now.utc : Time.now - end - # Clear attributes and changed_attributes def clear_timestamp_attributes all_timestamp_attributes_in_model.each do |attribute_name| diff --git a/activerecord/lib/active_record/touch_later.rb b/activerecord/lib/active_record/touch_later.rb index c337a7532f..cacde9c881 100644 --- a/activerecord/lib/active_record/touch_later.rb +++ b/activerecord/lib/active_record/touch_later.rb @@ -25,7 +25,7 @@ module ActiveRecord # touch the parents as we are not calling the after_save callbacks self.class.reflect_on_all_associations(:belongs_to).each do |r| if touch = r.options[:touch] - ActiveRecord::Associations::Builder::BelongsTo.touch_record(self, r.foreign_key, r.name, touch, :touch_later) + ActiveRecord::Associations::Builder::BelongsTo.touch_record(self, changes_to_save, r.foreign_key, r.name, touch, :touch_later) end end end diff --git a/activerecord/lib/active_record/transactions.rb b/activerecord/lib/active_record/transactions.rb index b19ae5c46e..56b75540e3 100644 --- a/activerecord/lib/active_record/transactions.rb +++ b/activerecord/lib/active_record/transactions.rb @@ -274,16 +274,6 @@ module ActiveRecord set_callback(:rollback_without_transaction_enrollment, :after, *args, &block) end - def raise_in_transactional_callbacks - ActiveSupport::Deprecation.warn("ActiveRecord::Base.raise_in_transactional_callbacks is deprecated and will be removed without replacement.") - true - end - - def raise_in_transactional_callbacks=(value) - ActiveSupport::Deprecation.warn("ActiveRecord::Base.raise_in_transactional_callbacks= is deprecated, has no effect and will be removed without replacement.") - value - end - private def set_options_for_callbacks!(args, enforced_options = {}) @@ -407,10 +397,10 @@ module ActiveRecord end end - protected + private - # Save the new record state and id of a record so it can be restored later if a transaction fails. - def remember_transaction_record_state #:nodoc: + # Save the new record state and id of a record so it can be restored later if a transaction fails. + def remember_transaction_record_state @_start_transaction_state[:id] = id @_start_transaction_state.reverse_merge!( new_record: @new_record, @@ -420,19 +410,19 @@ module ActiveRecord @_start_transaction_state[:level] = (@_start_transaction_state[:level] || 0) + 1 end - # Clear the new record state and id of a record. - def clear_transaction_record_state #:nodoc: + # Clear the new record state and id of a record. + def clear_transaction_record_state @_start_transaction_state[:level] = (@_start_transaction_state[:level] || 0) - 1 force_clear_transaction_record_state if @_start_transaction_state[:level] < 1 end - # Force to clear the transaction record state. - def force_clear_transaction_record_state #:nodoc: + # Force to clear the transaction record state. + def force_clear_transaction_record_state @_start_transaction_state.clear end - # Restore the new record state and id of a record that was previously saved by a call to save_record_state. - def restore_transaction_record_state(force = false) #:nodoc: + # Restore the new record state and id of a record that was previously saved by a call to save_record_state. + def restore_transaction_record_state(force = false) unless @_start_transaction_state.empty? transaction_level = (@_start_transaction_state[:level] || 0) - 1 if transaction_level < 1 || force @@ -449,52 +439,51 @@ module ActiveRecord end end - # Determine if a record was created or destroyed in a transaction. State should be one of :new_record or :destroyed. - def transaction_record_state(state) #:nodoc: + # Determine if a record was created or destroyed in a transaction. State should be one of :new_record or :destroyed. + def transaction_record_state(state) @_start_transaction_state[state] end - # Determine if a transaction included an action for :create, :update, or :destroy. Used in filtering callbacks. - def transaction_include_any_action?(actions) #:nodoc: + # Determine if a transaction included an action for :create, :update, or :destroy. Used in filtering callbacks. + def transaction_include_any_action?(actions) actions.any? do |action| case action when :create transaction_record_state(:new_record) when :destroy - destroyed? + defined?(@_trigger_destroy_callback) && @_trigger_destroy_callback when :update - !(transaction_record_state(:new_record) || destroyed?) + !(transaction_record_state(:new_record) || destroyed?) && + (defined?(@_trigger_update_callback) && @_trigger_update_callback) end end end - private - - def set_transaction_state(state) # :nodoc: + def set_transaction_state(state) @transaction_state = state end - def has_transactional_callbacks? # :nodoc: + def has_transactional_callbacks? !_rollback_callbacks.empty? || !_commit_callbacks.empty? || !_before_commit_callbacks.empty? end - # Updates the attributes on this particular Active Record object so that - # 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 - # 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 - # TransactionState. - # - # This method checks to see if the ActiveRecord object's state reflects - # the TransactionState, and rolls back or commits the Active Record object - # as appropriate. - # - # Since Active Record objects can be inside multiple transactions, this - # method recursively goes through the parent of the TransactionState and - # checks if the Active Record object reflects the state of the object. + # Updates the attributes on this particular Active Record object so that + # 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 + # 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 + # TransactionState. + # + # This method checks to see if the ActiveRecord object's state reflects + # the TransactionState, and rolls back or commits the Active Record object + # as appropriate. + # + # Since Active Record objects can be inside multiple transactions, this + # method recursively goes through the parent of the TransactionState and + # checks if the Active Record object reflects the state of the object. def sync_with_transaction_state update_attributes_from_transaction_state(@transaction_state) end diff --git a/activerecord/lib/active_record/type.rb b/activerecord/lib/active_record/type.rb index 1b2fc1b034..4f632660a8 100644 --- a/activerecord/lib/active_record/type.rb +++ b/activerecord/lib/active_record/type.rb @@ -5,7 +5,10 @@ require "active_record/type/internal/timezone" require "active_record/type/date" require "active_record/type/date_time" +require "active_record/type/decimal_without_scale" require "active_record/type/time" +require "active_record/type/text" +require "active_record/type/unsigned_integer" require "active_record/type/serialized" require "active_record/type/adapter_specific_registry" @@ -37,6 +40,10 @@ module ActiveRecord registry.lookup(*args, adapter: adapter, **kwargs) end + def default_value # :nodoc: + @default_value ||= Value.new + end + private def current_adapter_name @@ -49,12 +56,9 @@ module ActiveRecord Binary = ActiveModel::Type::Binary Boolean = ActiveModel::Type::Boolean Decimal = ActiveModel::Type::Decimal - DecimalWithoutScale = ActiveModel::Type::DecimalWithoutScale Float = ActiveModel::Type::Float Integer = ActiveModel::Type::Integer String = ActiveModel::Type::String - Text = ActiveModel::Type::Text - UnsignedInteger = ActiveModel::Type::UnsignedInteger Value = ActiveModel::Type::Value register(:big_integer, Type::BigInteger, override: false) diff --git a/activerecord/lib/active_record/type/adapter_specific_registry.rb b/activerecord/lib/active_record/type/adapter_specific_registry.rb index d0f9581576..7cc866f7a7 100644 --- a/activerecord/lib/active_record/type/adapter_specific_registry.rb +++ b/activerecord/lib/active_record/type/adapter_specific_registry.rb @@ -50,6 +50,8 @@ module ActiveRecord priority <=> other.priority end + # TODO Change this to private once we've dropped Ruby 2.2 support. + # Workaround for Ruby 2.2 "private attribute?" warning. protected attr_reader :name, :block, :adapter, :override @@ -110,6 +112,8 @@ module ActiveRecord super | 4 end + # TODO Change this to private once we've dropped Ruby 2.2 support. + # Workaround for Ruby 2.2 "private attribute?" warning. protected attr_reader :options, :klass diff --git a/activerecord/lib/active_record/type/decimal_without_scale.rb b/activerecord/lib/active_record/type/decimal_without_scale.rb new file mode 100644 index 0000000000..7ce33e9cd3 --- /dev/null +++ b/activerecord/lib/active_record/type/decimal_without_scale.rb @@ -0,0 +1,9 @@ +module ActiveRecord + module Type + class DecimalWithoutScale < ActiveModel::Type::BigInteger # :nodoc: + def type + :decimal + end + end + end +end diff --git a/activerecord/lib/active_record/type/internal/abstract_json.rb b/activerecord/lib/active_record/type/internal/abstract_json.rb index 513c938088..e19c5a14da 100644 --- a/activerecord/lib/active_record/type/internal/abstract_json.rb +++ b/activerecord/lib/active_record/type/internal/abstract_json.rb @@ -17,7 +17,11 @@ module ActiveRecord end def serialize(value) - ::ActiveSupport::JSON.encode(value) + if value.nil? + nil + else + ::ActiveSupport::JSON.encode(value) + end end def accessor diff --git a/activerecord/lib/active_record/type/text.rb b/activerecord/lib/active_record/type/text.rb new file mode 100644 index 0000000000..cb1949700a --- /dev/null +++ b/activerecord/lib/active_record/type/text.rb @@ -0,0 +1,9 @@ +module ActiveRecord + module Type + class Text < ActiveModel::Type::String # :nodoc: + def type + :text + end + end + end +end diff --git a/activerecord/lib/active_record/type/type_map.rb b/activerecord/lib/active_record/type/type_map.rb index 9618ff8787..7bce82a1ff 100644 --- a/activerecord/lib/active_record/type/type_map.rb +++ b/activerecord/lib/active_record/type/type_map.rb @@ -11,7 +11,7 @@ module ActiveRecord end def lookup(lookup_key, *args) - fetch(lookup_key, *args) { default_value } + fetch(lookup_key, *args) { Type.default_value } end def fetch(lookup_key, *args, &block) @@ -55,10 +55,6 @@ module ActiveRecord yield lookup_key, *args end end - - def default_value - @default_value ||= ActiveModel::Type::Value.new - end end end end diff --git a/activerecord/lib/active_record/type/unsigned_integer.rb b/activerecord/lib/active_record/type/unsigned_integer.rb new file mode 100644 index 0000000000..9ae0109f9f --- /dev/null +++ b/activerecord/lib/active_record/type/unsigned_integer.rb @@ -0,0 +1,15 @@ +module ActiveRecord + module Type + class UnsignedInteger < ActiveModel::Type::Integer # :nodoc: + private + + def max_value + super * 2 + end + + def min_value + 0 + end + end + end +end diff --git a/activerecord/lib/active_record/type_caster/connection.rb b/activerecord/lib/active_record/type_caster/connection.rb index 6c54792e26..9f7bbe8843 100644 --- a/activerecord/lib/active_record/type_caster/connection.rb +++ b/activerecord/lib/active_record/type_caster/connection.rb @@ -12,6 +12,8 @@ module ActiveRecord connection.type_cast_from_column(column, value) end + # TODO Change this to private once we've dropped Ruby 2.2 support. + # Workaround for Ruby 2.2 "private attribute?" warning. protected attr_reader :table_name diff --git a/activerecord/lib/active_record/type_caster/map.rb b/activerecord/lib/active_record/type_caster/map.rb index 52529a6b42..9f79723125 100644 --- a/activerecord/lib/active_record/type_caster/map.rb +++ b/activerecord/lib/active_record/type_caster/map.rb @@ -11,6 +11,8 @@ module ActiveRecord type.serialize(value) end + # TODO Change this to private once we've dropped Ruby 2.2 support. + # Workaround for Ruby 2.2 "private attribute?" warning. protected attr_reader :types diff --git a/activerecord/lib/active_record/validations.rb b/activerecord/lib/active_record/validations.rb index ecaf04e39e..9633f226f0 100644 --- a/activerecord/lib/active_record/validations.rb +++ b/activerecord/lib/active_record/validations.rb @@ -40,13 +40,13 @@ module ActiveRecord # The validation process on save can be skipped by passing <tt>validate: false</tt>. # The regular {ActiveRecord::Base#save}[rdoc-ref:Persistence#save] method is replaced # with this when the validations module is mixed in, which it is by default. - def save(options={}) + def save(options = {}) perform_validations(options) ? super : false end # Attempts to save the record just like {ActiveRecord::Base#save}[rdoc-ref:Base#save] but # will raise an ActiveRecord::RecordInvalid exception instead of returning +false+ if the record is not valid. - def save!(options={}) + def save!(options = {}) perform_validations(options) ? super : raise_validation_error end @@ -68,7 +68,7 @@ module ActiveRecord alias_method :validate, :valid? - protected + private def default_validation_context new_record? ? :create : :update @@ -78,7 +78,7 @@ module ActiveRecord raise(RecordInvalid.new(self)) end - def perform_validations(options={}) # :nodoc: + def perform_validations(options = {}) options[:validate] == false || valid?(options[:context]) end end diff --git a/activerecord/lib/active_record/validations/associated.rb b/activerecord/lib/active_record/validations/associated.rb index b14db85167..c695965d7b 100644 --- a/activerecord/lib/active_record/validations/associated.rb +++ b/activerecord/lib/active_record/validations/associated.rb @@ -37,7 +37,7 @@ module ActiveRecord # # * <tt>:message</tt> - A custom error message (default is: "is invalid"). # * <tt>:on</tt> - Specifies the contexts where this validation is active. - # Runs in all validation contexts by default (nil). You can pass a symbol + # Runs in all validation contexts by default +nil+. You can pass a symbol # or an array of symbols. (e.g. <tt>on: :create</tt> or # <tt>on: :custom_validation_context</tt> or # <tt>on: [:create, :custom_validation_context]</tt>) diff --git a/activerecord/lib/active_record/validations/presence.rb b/activerecord/lib/active_record/validations/presence.rb index ad82ea66c4..ca5eda2f84 100644 --- a/activerecord/lib/active_record/validations/presence.rb +++ b/activerecord/lib/active_record/validations/presence.rb @@ -44,7 +44,7 @@ module ActiveRecord # Configuration options: # * <tt>:message</tt> - A custom error message (default is: "can't be blank"). # * <tt>:on</tt> - Specifies the contexts where this validation is active. - # Runs in all validation contexts by default (nil). You can pass a symbol + # Runs in all validation contexts by default +nil+. You can pass a symbol # or an array of symbols. (e.g. <tt>on: :create</tt> or # <tt>on: :custom_validation_context</tt> or # <tt>on: [:create, :custom_validation_context]</tt>) diff --git a/activerecord/lib/active_record/validations/uniqueness.rb b/activerecord/lib/active_record/validations/uniqueness.rb index 8c4930a81d..9e8edfbfaf 100644 --- a/activerecord/lib/active_record/validations/uniqueness.rb +++ b/activerecord/lib/active_record/validations/uniqueness.rb @@ -17,7 +17,7 @@ module ActiveRecord relation = build_relation(finder_class, attribute, value) if record.persisted? if finder_class.primary_key - relation = relation.where.not(finder_class.primary_key => record.id_was || record.id) + relation = relation.where.not(finder_class.primary_key => record.id_in_database || record.id) else raise UnknownPrimaryKey.new(finder_class, "Can not validate uniqueness for persisted record without primary key.") end @@ -33,13 +33,13 @@ module ActiveRecord end end - protected + private # The check for an existing value should be run from a class that # isn't abstract. This means working down from the current class # (self), to the first non-abstract class. Since classes don't know # their subclasses, we have to build the hierarchy between self and # the record's class. - def find_finder_class_for(record) #:nodoc: + def find_finder_class_for(record) class_hierarchy = [record.class] while class_hierarchy.first != @klass @@ -49,7 +49,7 @@ module ActiveRecord class_hierarchy.detect { |klass| !klass.abstract_class? } end - def build_relation(klass, attribute, value) # :nodoc: + def build_relation(klass, attribute, value) if reflection = klass._reflect_on_association(attribute) attribute = reflection.foreign_key value = value.attributes[reflection.klass.primary_key] unless value.nil? @@ -85,11 +85,10 @@ module ActiveRecord def scope_relation(record, relation) Array(options[:scope]).each do |scope_item| - if reflection = record.class._reflect_on_association(scope_item) - scope_value = record.send(reflection.foreign_key) - scope_item = reflection.foreign_key + scope_value = if record.class._reflect_on_association(scope_item) + record.association(scope_item).reader else - scope_value = record._read_attribute(scope_item) + record._read_attribute(scope_item) end relation = relation.where(scope_item => scope_value) end diff --git a/activerecord/lib/rails/generators/active_record/migration/migration_generator.rb b/activerecord/lib/rails/generators/active_record/migration/migration_generator.rb index 76182a3c24..8511531af7 100644 --- a/activerecord/lib/rails/generators/active_record/migration/migration_generator.rb +++ b/activerecord/lib/rails/generators/active_record/migration/migration_generator.rb @@ -1,5 +1,4 @@ require "rails/generators/active_record" -require "active_support/core_ext/regexp" module ActiveRecord module Generators # :nodoc: @@ -14,12 +13,16 @@ module ActiveRecord migration_template @migration_template, "db/migrate/#{file_name}.rb" end + # TODO Change this to private once we've dropped Ruby 2.2 support. + # Workaround for Ruby 2.2 "private attribute?" warning. protected attr_reader :migration_action, :join_tables - # Sets the default migration template that is being used for the generation of the migration. - # Depending on command line arguments, the migration template and the table name instance - # variables are set up. + private + + # Sets the default migration template that is being used for the generation of the migration. + # Depending on command line arguments, the migration template and the table name instance + # variables are set up. def set_local_assigns! @migration_template = "migration.rb" case file_name @@ -41,7 +44,7 @@ module ActiveRecord def set_index_names attributes.each_with_index do |attr, i| - attr.index_name = [attr, attributes[i - 1]].map{ |a| index_name_for(a) } + attr.index_name = [attr, attributes[i - 1]].map { |a| index_name_for(a) } end end @@ -53,7 +56,6 @@ module ActiveRecord end.to_sym end - private def attributes_with_index attributes.select { |a| !a.reference? && a.has_index? } end diff --git a/activerecord/lib/rails/generators/active_record/model/model_generator.rb b/activerecord/lib/rails/generators/active_record/model/model_generator.rb index f1ddc61688..a9df5212e3 100644 --- a/activerecord/lib/rails/generators/active_record/model/model_generator.rb +++ b/activerecord/lib/rails/generators/active_record/model/model_generator.rb @@ -33,7 +33,7 @@ module ActiveRecord hook_for :test_framework - protected + private def attributes_with_index attributes.select { |a| !a.reference? && a.has_index? } @@ -41,7 +41,7 @@ module ActiveRecord # FIXME: Change this file to a symlink once RubyGems 2.5.0 is required. def generate_application_record - if self.behavior == :invoke && !application_record_exist? + if behavior == :invoke && !application_record_exist? template "application_record.rb", application_record_file_name end end diff --git a/activerecord/test/active_record/connection_adapters/fake_adapter.rb b/activerecord/test/active_record/connection_adapters/fake_adapter.rb index 43c817e057..b0d8050721 100644 --- a/activerecord/test/active_record/connection_adapters/fake_adapter.rb +++ b/activerecord/test/active_record/connection_adapters/fake_adapter.rb @@ -9,7 +9,7 @@ module ActiveRecord class FakeAdapter < AbstractAdapter attr_accessor :data_sources, :primary_keys - @columns = Hash.new { |h,k| h[k] = [] } + @columns = Hash.new { |h, k| h[k] = [] } class << self attr_reader :columns end diff --git a/activerecord/test/assets/schema_dump_5_1.yml b/activerecord/test/assets/schema_dump_5_1.yml new file mode 100644 index 0000000000..f37977daf2 --- /dev/null +++ b/activerecord/test/assets/schema_dump_5_1.yml @@ -0,0 +1,345 @@ +--- !ruby/object:ActiveRecord::ConnectionAdapters::SchemaCache +columns: + posts: + - &1 !ruby/object:ActiveRecord::ConnectionAdapters::Column + name: id + table_name: posts + sql_type_metadata: !ruby/object:ActiveRecord::ConnectionAdapters::SqlTypeMetadata + sql_type: INTEGER + type: :integer + limit: + precision: + scale: + 'null': false + default: + default_function: + collation: + comment: + - &2 !ruby/object:ActiveRecord::ConnectionAdapters::Column + name: author_id + table_name: posts + sql_type_metadata: !ruby/object:ActiveRecord::ConnectionAdapters::SqlTypeMetadata + sql_type: integer + type: :integer + limit: + precision: + scale: + 'null': true + default: + default_function: + collation: + comment: + - &3 !ruby/object:ActiveRecord::ConnectionAdapters::Column + name: title + table_name: posts + sql_type_metadata: !ruby/object:ActiveRecord::ConnectionAdapters::SqlTypeMetadata + sql_type: varchar + type: :string + limit: + precision: + scale: + 'null': false + default: + default_function: + collation: + comment: + - &4 !ruby/object:ActiveRecord::ConnectionAdapters::Column + name: body + table_name: posts + sql_type_metadata: !ruby/object:ActiveRecord::ConnectionAdapters::SqlTypeMetadata + sql_type: text + type: :text + limit: + precision: + scale: + 'null': false + default: + default_function: + collation: + comment: + - &5 !ruby/object:ActiveRecord::ConnectionAdapters::Column + name: type + table_name: posts + sql_type_metadata: !ruby/object:ActiveRecord::ConnectionAdapters::SqlTypeMetadata + sql_type: varchar + type: :string + limit: + precision: + scale: + 'null': true + default: + default_function: + collation: + comment: + - &6 !ruby/object:ActiveRecord::ConnectionAdapters::Column + name: comments_count + table_name: posts + sql_type_metadata: !ruby/object:ActiveRecord::ConnectionAdapters::SqlTypeMetadata + sql_type: integer + type: :integer + limit: + precision: + scale: + 'null': true + default: '0' + default_function: + collation: + comment: + - &7 !ruby/object:ActiveRecord::ConnectionAdapters::Column + name: taggings_with_delete_all_count + table_name: posts + sql_type_metadata: !ruby/object:ActiveRecord::ConnectionAdapters::SqlTypeMetadata + sql_type: integer + type: :integer + limit: + precision: + scale: + 'null': true + default: '0' + default_function: + collation: + comment: + - &8 !ruby/object:ActiveRecord::ConnectionAdapters::Column + name: taggings_with_destroy_count + table_name: posts + sql_type_metadata: !ruby/object:ActiveRecord::ConnectionAdapters::SqlTypeMetadata + sql_type: integer + type: :integer + limit: + precision: + scale: + 'null': true + default: '0' + default_function: + collation: + comment: + - &9 !ruby/object:ActiveRecord::ConnectionAdapters::Column + name: tags_count + table_name: posts + sql_type_metadata: !ruby/object:ActiveRecord::ConnectionAdapters::SqlTypeMetadata + sql_type: integer + type: :integer + limit: + precision: + scale: + 'null': true + default: '0' + default_function: + collation: + comment: + - &10 !ruby/object:ActiveRecord::ConnectionAdapters::Column + name: tags_with_destroy_count + table_name: posts + sql_type_metadata: !ruby/object:ActiveRecord::ConnectionAdapters::SqlTypeMetadata + sql_type: integer + type: :integer + limit: + precision: + scale: + 'null': true + default: '0' + default_function: + collation: + comment: + - &11 !ruby/object:ActiveRecord::ConnectionAdapters::Column + name: tags_with_nullify_count + table_name: posts + sql_type_metadata: !ruby/object:ActiveRecord::ConnectionAdapters::SqlTypeMetadata + sql_type: integer + type: :integer + limit: + precision: + scale: + 'null': true + default: '0' + default_function: + collation: + comment: +columns_hash: + posts: + id: *1 + author_id: *2 + title: *3 + body: *4 + type: *5 + comments_count: *6 + taggings_with_delete_all_count: *7 + taggings_with_destroy_count: *8 + tags_count: *9 + tags_with_destroy_count: *10 + tags_with_nullify_count: *11 +primary_keys: + posts: id +data_sources: + ar_internal_metadata: true + table_with_autoincrement: true + accounts: true + admin_accounts: true + admin_users: true + aircraft: true + articles: true + articles_magazines: true + articles_tags: true + audit_logs: true + authors: true + author_addresses: true + author_favorites: true + auto_id_tests: true + binaries: true + birds: true + books: true + booleans: true + bulbs: true + CamelCase: true + cars: true + carriers: true + categories: true + categories_posts: true + categorizations: true + citations: true + clubs: true + collections: true + colnametests: true + columns: true + comments: true + companies: true + content: true + content_positions: true + vegetables: true + computers: true + computers_developers: true + contracts: true + customers: true + customer_carriers: true + dashboards: true + developers: true + developers_projects: true + dog_lovers: true + dogs: true + doubloons: true + edges: true + engines: true + entrants: true + essays: true + events: true + eyes: true + funny_jokes: true + cold_jokes: true + friendships: true + goofy_string_id: true + having: true + guids: true + guitars: true + inept_wizards: true + integer_limits: true + invoices: true + iris: true + items: true + jobs: true + jobs_pool: true + keyboards: true + legacy_things: true + lessons: true + lessons_students: true + students: true + lint_models: true + line_items: true + lions: true + lock_without_defaults: true + lock_without_defaults_cust: true + magazines: true + mateys: true + members: true + member_details: true + member_friends: true + memberships: true + member_types: true + mentors: true + minivans: true + minimalistics: true + mixed_case_monkeys: true + mixins: true + movies: true + notifications: true + numeric_data: true + orders: true + organizations: true + owners: true + paint_colors: true + paint_textures: true + parrots: true + parrots_pirates: true + parrots_treasures: true + people: true + peoples_treasures: true + personal_legacy_things: true + pets: true + pets_treasures: true + pirates: true + posts: true + serialized_posts: true + images: true + price_estimates: true + products: true + product_types: true + projects: true + randomly_named_table1: true + randomly_named_table2: true + randomly_named_table3: true + ratings: true + readers: true + references: true + shape_expressions: true + ships: true + ship_parts: true + prisoners: true + shop_accounts: true + speedometers: true + sponsors: true + string_key_objects: true + subscribers: true + subscriptions: true + tags: true + taggings: true + tasks: true + topics: true + toys: true + traffic_lights: true + treasures: true + tuning_pegs: true + tyres: true + variants: true + vertices: true + warehouse-things: true + circles: true + squares: true + triangles: true + non_poly_ones: true + non_poly_twos: true + men: true + faces: true + interests: true + zines: true + wheels: true + countries: true + treaties: true + countries_treaties: true + liquid: true + molecules: true + electrons: true + weirds: true + nodes: true + trees: true + hotels: true + departments: true + cake_designers: true + drink_designers: true + chefs: true + recipes: true + records: true + overloaded_types: true + users: true + test_with_keyword_column_name: true + fk_test_has_pk: true + fk_test_has_fk: true +version: 0 diff --git a/activerecord/test/cases/adapter_test.rb b/activerecord/test/cases/adapter_test.rb index b164c12c89..9828e682ef 100644 --- a/activerecord/test/cases/adapter_test.rb +++ b/activerecord/test/cases/adapter_test.rb @@ -32,57 +32,49 @@ module ActiveRecord def test_tables tables = nil - ActiveSupport::Deprecation.silence { tables = @connection.tables } - assert tables.include?("accounts") - assert tables.include?("authors") - assert tables.include?("tasks") - assert tables.include?("topics") + tables = @connection.tables + assert_includes tables, "accounts" + assert_includes tables, "authors" + assert_includes tables, "tasks" + assert_includes tables, "topics" end def test_table_exists? - ActiveSupport::Deprecation.silence do - assert @connection.table_exists?("accounts") - assert !@connection.table_exists?("nonexistingtable") - assert !@connection.table_exists?(nil) - end - end - - def test_table_exists_checking_both_tables_and_views_is_deprecated - assert_deprecated { @connection.table_exists?("accounts") } + assert @connection.table_exists?("accounts") + assert @connection.table_exists?(:accounts) + assert_not @connection.table_exists?("nonexistingtable") + assert_not @connection.table_exists?("'") + assert_not @connection.table_exists?(nil) end def test_data_sources data_sources = @connection.data_sources - assert data_sources.include?("accounts") - assert data_sources.include?("authors") - assert data_sources.include?("tasks") - assert data_sources.include?("topics") + assert_includes data_sources, "accounts" + assert_includes data_sources, "authors" + assert_includes data_sources, "tasks" + assert_includes data_sources, "topics" end def test_data_source_exists? assert @connection.data_source_exists?("accounts") assert @connection.data_source_exists?(:accounts) assert_not @connection.data_source_exists?("nonexistingtable") + assert_not @connection.data_source_exists?("'") assert_not @connection.data_source_exists?(nil) end def test_indexes idx_name = "accounts_idx" - if @connection.respond_to?(:indexes) - indexes = @connection.indexes("accounts") - assert indexes.empty? - - @connection.add_index :accounts, :firm_id, name: idx_name - indexes = @connection.indexes("accounts") - assert_equal "accounts", indexes.first.table - assert_equal idx_name, indexes.first.name - assert !indexes.first.unique - assert_equal ["firm_id"], indexes.first.columns - else - warn "#{@connection.class} does not respond to #indexes" - end + indexes = @connection.indexes("accounts") + assert indexes.empty? + @connection.add_index :accounts, :firm_id, name: idx_name + indexes = @connection.indexes("accounts") + assert_equal "accounts", indexes.first.table + assert_equal idx_name, indexes.first.name + assert !indexes.first.unique + assert_equal ["firm_id"], indexes.first.columns ensure @connection.remove_index(:accounts, name: idx_name) rescue nil end @@ -184,6 +176,14 @@ module ActiveRecord assert_not_nil error.cause end + def test_not_null_violations_are_translated_to_specific_exception + error = assert_raises(ActiveRecord::NotNullViolation) do + Post.create + end + + assert_not_nil error.cause + end + unless current_adapter?(:SQLite3Adapter) def test_foreign_key_violations_are_translated_to_specific_exception error = assert_raises(ActiveRecord::InvalidForeignKey) do @@ -220,6 +220,14 @@ module ActiveRecord assert_not_nil error.cause end + + def test_numeric_value_out_of_ranges_are_translated_to_specific_exception + error = assert_raises(ActiveRecord::RangeError) do + Book.connection.create("INSERT INTO books(author_id) VALUES (2147483648)") + end + + assert_not_nil error.cause + end end def test_disable_referential_integrity @@ -248,8 +256,8 @@ module ActiveRecord 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_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) assert_equal ["foo"], @connection.select_values(query) @@ -258,8 +266,8 @@ 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_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) assert_equal ["foo"], @connection.select_values(query) @@ -271,25 +279,15 @@ module ActiveRecord unless current_adapter?(:PostgreSQLAdapter) def test_log_invalid_encoding - error = assert_raise ActiveRecord::StatementInvalid do + error = assert_raises RuntimeError do @connection.send :log, "SELECT 'ы' FROM DUAL" do raise "ы".force_encoding(Encoding::ASCII_8BIT) end end - assert_not_nil error.cause - end - end - - if current_adapter?(:Mysql2Adapter, :SQLite3Adapter) - def test_tables_returning_both_tables_and_views_is_deprecated - assert_deprecated { @connection.tables } + assert_not_nil error.message end end - - def test_passing_arguments_to_tables_is_deprecated - assert_deprecated { @connection.tables(:books) } - end end class AdapterTestWithoutTransaction < ActiveRecord::TestCase diff --git a/activerecord/test/cases/adapters/mysql2/active_schema_test.rb b/activerecord/test/cases/adapters/mysql2/active_schema_test.rb index e7e37cac36..2a528b2cb1 100644 --- a/activerecord/test/cases/adapters/mysql2/active_schema_test.rb +++ b/activerecord/test/cases/adapters/mysql2/active_schema_test.rb @@ -28,12 +28,15 @@ class Mysql2ActiveSchemaTest < ActiveRecord::Mysql2TestCase expected = "CREATE INDEX `index_people_on_last_name_and_first_name` ON `people` (`last_name`(15), `first_name`(15)) " assert_equal expected, add_index(:people, [:last_name, :first_name], length: 15) + assert_equal expected, add_index(:people, ["last_name", "first_name"], length: 15) expected = "CREATE INDEX `index_people_on_last_name_and_first_name` ON `people` (`last_name`(15), `first_name`) " - assert_equal expected, add_index(:people, [:last_name, :first_name], length: {last_name: 15}) + assert_equal expected, add_index(:people, [:last_name, :first_name], length: { last_name: 15 }) + assert_equal expected, add_index(:people, ["last_name", "first_name"], length: { last_name: 15 }) expected = "CREATE INDEX `index_people_on_last_name_and_first_name` ON `people` (`last_name`(15), `first_name`(10)) " - assert_equal expected, add_index(:people, [:last_name, :first_name], length: {last_name: 15, first_name: 10}) + assert_equal expected, add_index(:people, [:last_name, :first_name], length: { last_name: 15, first_name: 10 }) + assert_equal expected, add_index(:people, ["last_name", :first_name], length: { last_name: 15, "first_name" => 10 }) %w(SPATIAL FULLTEXT UNIQUE).each do |type| expected = "CREATE #{type} INDEX `index_people_on_last_name` ON `people` (`last_name`) " diff --git a/activerecord/test/cases/adapters/mysql2/case_sensitivity_test.rb b/activerecord/test/cases/adapters/mysql2/case_sensitivity_test.rb index 4d81877a9a..50ba9ab831 100644 --- a/activerecord/test/cases/adapters/mysql2/case_sensitivity_test.rb +++ b/activerecord/test/cases/adapters/mysql2/case_sensitivity_test.rb @@ -30,7 +30,7 @@ class Mysql2CaseSensitivityTest < ActiveRecord::Mysql2TestCase CollationTest.create!(string_cs_column: "A") invalid = CollationTest.new(string_cs_column: "a") queries = assert_sql { invalid.save } - cs_uniqueness_query = queries.detect { |q| q.match(/string_cs_column/)} + cs_uniqueness_query = queries.detect { |q| q.match(/string_cs_column/) } assert_match(/lower/i, cs_uniqueness_query) end diff --git a/activerecord/test/cases/adapters/mysql2/connection_test.rb b/activerecord/test/cases/adapters/mysql2/connection_test.rb index a3f83e73a6..c1de2218e2 100644 --- a/activerecord/test/cases/adapters/mysql2/connection_test.rb +++ b/activerecord/test/cases/adapters/mysql2/connection_test.rb @@ -63,6 +63,27 @@ class Mysql2ConnectionTest < ActiveRecord::Mysql2TestCase assert @connection.active? end + def test_execute_after_disconnect + @connection.disconnect! + + assert_raise(ActiveRecord::StatementInvalid) do + @connection.execute("SELECT 1") + end + end + + def test_quote_after_disconnect + @connection.disconnect! + + assert_raise(Mysql2::Error) do + @connection.quote("string") + end + end + + def test_active_after_disconnect + @connection.disconnect! + assert_equal false, @connection.active? + end + def test_mysql_connection_collation_is_configured assert_equal "utf8_unicode_ci", @connection.show_variable("collation_connection") assert_equal "utf8_general_ci", ARUnit2Model.connection.show_variable("collation_connection") @@ -101,7 +122,7 @@ class Mysql2ConnectionTest < ActiveRecord::Mysql2TestCase def test_passing_arbitary_flags_to_adapter run_without_connection do |orig_connection| ActiveRecord::Base.establish_connection(orig_connection.merge(flags: Mysql2::Client::COMPRESS)) - assert_equal (Mysql2::Client::COMPRESS | Mysql2::Client::FOUND_ROWS), ActiveRecord::Base.connection.raw_connection.query_options[:flags] + assert_equal (Mysql2::Client::COMPRESS | Mysql2::Client::FOUND_ROWS), ActiveRecord::Base.connection.raw_connection.query_options[:flags] end end @@ -114,7 +135,7 @@ class Mysql2ConnectionTest < ActiveRecord::Mysql2TestCase def test_mysql_set_session_variable run_without_connection do |orig_connection| - ActiveRecord::Base.establish_connection(orig_connection.deep_merge(variables: {default_week_format: 3})) + ActiveRecord::Base.establish_connection(orig_connection.deep_merge(variables: { default_week_format: 3 })) session_mode = ActiveRecord::Base.connection.exec_query "SELECT @@SESSION.DEFAULT_WEEK_FORMAT" assert_equal 3, session_mode.rows.first.first.to_i end @@ -122,7 +143,7 @@ class Mysql2ConnectionTest < ActiveRecord::Mysql2TestCase def test_mysql_set_session_variable_to_default run_without_connection do |orig_connection| - ActiveRecord::Base.establish_connection(orig_connection.deep_merge(variables: {default_week_format: :default})) + ActiveRecord::Base.establish_connection(orig_connection.deep_merge(variables: { default_week_format: :default })) global_mode = ActiveRecord::Base.connection.exec_query "SELECT @@GLOBAL.DEFAULT_WEEK_FORMAT" session_mode = ActiveRecord::Base.connection.exec_query "SELECT @@SESSION.DEFAULT_WEEK_FORMAT" assert_equal global_mode.rows, session_mode.rows @@ -165,7 +186,7 @@ class Mysql2ConnectionTest < ActiveRecord::Mysql2TestCase "expected release_advisory_lock to return false when there was no lock to release" end - protected + private def test_lock_free(lock_name) @connection.select_value("SELECT IS_FREE_LOCK(#{@connection.quote(lock_name)})") == 1 diff --git a/activerecord/test/cases/adapters/mysql2/enum_test.rb b/activerecord/test/cases/adapters/mysql2/enum_test.rb index 9f51223d58..7ad3e3ca2d 100644 --- a/activerecord/test/cases/adapters/mysql2/enum_test.rb +++ b/activerecord/test/cases/adapters/mysql2/enum_test.rb @@ -9,11 +9,6 @@ class Mysql2EnumTest < ActiveRecord::Mysql2TestCase assert_equal 8, column.limit end - def test_should_not_be_blob_or_text_column - column = EnumTest.columns_hash["enum_column"] - assert_not column.blob_or_text_column? - end - def test_should_not_be_unsigned column = EnumTest.columns_hash["enum_column"] assert_not column.unsigned? diff --git a/activerecord/test/cases/adapters/mysql2/json_test.rb b/activerecord/test/cases/adapters/mysql2/json_test.rb index 2f7dc3f7ef..6954006003 100644 --- a/activerecord/test/cases/adapters/mysql2/json_test.rb +++ b/activerecord/test/cases/adapters/mysql2/json_test.rb @@ -51,11 +51,11 @@ if ActiveRecord::Base.connection.supports_json? end def test_cast_value_on_write - x = JsonDataType.new payload: {"string" => "foo", :symbol => :bar} - assert_equal({"string" => "foo", :symbol => :bar}, x.payload_before_type_cast) - assert_equal({"string" => "foo", "symbol" => "bar"}, x.payload) + x = JsonDataType.new payload: { "string" => "foo", :symbol => :bar } + assert_equal({ "string" => "foo", :symbol => :bar }, x.payload_before_type_cast) + assert_equal({ "string" => "foo", "symbol" => "bar" }, x.payload) x.save - assert_equal({"string" => "foo", "symbol" => "bar"}, x.reload.payload) + assert_equal({ "string" => "foo", "symbol" => "bar" }, x.reload.payload) end def test_type_cast_json @@ -63,12 +63,12 @@ if ActiveRecord::Base.connection.supports_json? data = "{\"a_key\":\"a_value\"}" hash = type.deserialize(data) - assert_equal({"a_key" => "a_value"}, hash) - assert_equal({"a_key" => "a_value"}, type.deserialize(data)) + assert_equal({ "a_key" => "a_value" }, hash) + assert_equal({ "a_key" => "a_value" }, type.deserialize(data)) 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"}))) + 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_rewrite @@ -81,31 +81,47 @@ if ActiveRecord::Base.connection.supports_json? def test_select @connection.execute "insert into json_data_type (payload) VALUES ('{\"k\":\"v\"}')" x = JsonDataType.first - assert_equal({"k" => "v"}, x.payload) + 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]}')| x = JsonDataType.first - assert_equal({"k1" => "v1", "k2" => "v2", "k3" => [1,2,3]}, x.payload) + 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)" x = JsonDataType.first - assert_equal(nil, x.payload) + assert_nil(x.payload) end def test_select_array_json_value @connection.execute %q|insert into json_data_type (payload) VALUES ('["v0",{"k1":"v1"}]')| x = JsonDataType.first - assert_equal(["v0", {"k1" => "v1"}], x.payload) + assert_equal(["v0", { "k1" => "v1" }], x.payload) + end + + def test_select_nil_json_after_create + json = JsonDataType.create(payload: nil) + x = JsonDataType.where(payload: nil).first + assert_equal(json, x) + end + + def test_select_nil_json_after_update + json = JsonDataType.create(payload: "foo") + x = JsonDataType.where(payload: nil).first + assert_nil(x) + + json.update_attributes payload: nil + x = JsonDataType.where(payload: nil).first + assert_equal(json.reload, x) end def test_rewrite_array_json_value @connection.execute %q|insert into json_data_type (payload) VALUES ('["v0",{"k1":"v1"}]')| x = JsonDataType.first - x.payload = ["v1", {"k2" => "v2"}, "v3"] + x.payload = ["v1", { "k2" => "v2" }, "v3"] assert x.save! end diff --git a/activerecord/test/cases/adapters/mysql2/legacy_migration_test.rb b/activerecord/test/cases/adapters/mysql2/legacy_migration_test.rb new file mode 100644 index 0000000000..5d3125c2be --- /dev/null +++ b/activerecord/test/cases/adapters/mysql2/legacy_migration_test.rb @@ -0,0 +1,60 @@ +require "cases/helper" + +class MysqlLegacyMigrationTest < ActiveRecord::Mysql2TestCase + self.use_transactional_tests = false + + class GenerateTableWithoutBigint < ActiveRecord::Migration[5.0] + def change + create_table :legacy_integer_pk do |table| + table.string :foo + end + + create_table :override_pk, id: :bigint do |table| + table.string :bar + end + end + end + + def setup + super + @connection = ActiveRecord::Base.connection + + @migration_verbose_old = ActiveRecord::Migration.verbose + ActiveRecord::Migration.verbose = false + + migrations = [GenerateTableWithoutBigint.new(nil, 1)] + + ActiveRecord::Migrator.new(:up, migrations).migrate + end + + def teardown + ActiveRecord::Migration.verbose = @migration_verbose_old + @connection.drop_table("legacy_integer_pk") + @connection.drop_table("override_pk") + ActiveRecord::SchemaMigration.delete_all rescue nil + super + end + + def test_create_table_uses_integer_as_pkey_by_default + col = column(:legacy_integer_pk, :id) + assert_equal "int(11)", sql_type_for(col) + assert col.auto_increment? + end + + def test_create_tables_respects_pk_column_type_override + col = column(:override_pk, :id) + assert_equal "bigint(20)", sql_type_for(col) + end + + private + + def column(table_name, column_name) + ActiveRecord::Base.connection + .columns(table_name.to_s) + .detect { |c| c.name == column_name.to_s } + end + + def sql_type_for(col) + col && col.sql_type + end +end diff --git a/activerecord/test/cases/adapters/mysql2/mysql2_adapter_test.rb b/activerecord/test/cases/adapters/mysql2/mysql2_adapter_test.rb index 69336eb906..aab3dcb724 100644 --- a/activerecord/test/cases/adapters/mysql2/mysql2_adapter_test.rb +++ b/activerecord/test/cases/adapters/mysql2/mysql2_adapter_test.rb @@ -65,6 +65,19 @@ class Mysql2AdapterTest < ActiveRecord::Mysql2TestCase @conn.columns_for_distinct("posts.id", [order]) end + def test_errors_for_bigint_fks_on_integer_pk_table + # table old_cars has primary key of integer + + error = assert_raises(ActiveRecord::MismatchedForeignKey) do + @conn.add_reference :engines, :old_car + @conn.add_foreign_key :engines, :old_cars + end + + assert_match "Column `old_car_id` on table `engines` has a type of `bigint(20)`", error.message + assert_not_nil error.cause + @conn.exec_query("ALTER TABLE engines DROP COLUMN old_car_id") + end + private def with_example_table(definition = "id int auto_increment primary key, number int, data varchar(255)", &block) diff --git a/activerecord/test/cases/adapters/mysql2/reserved_word_test.rb b/activerecord/test/cases/adapters/mysql2/reserved_word_test.rb index 7736113a19..2c778b1150 100644 --- a/activerecord/test/cases/adapters/mysql2/reserved_word_test.rb +++ b/activerecord/test/cases/adapters/mysql2/reserved_word_test.rb @@ -30,11 +30,11 @@ class Mysql2ReservedWordTest < ActiveRecord::Mysql2TestCase # we call execute directly here (and do similar below) because ActiveRecord::Base#create_table() # will fail with these table names if these test cases fail - create_tables_directly "group"=>"id int auto_increment primary key, `order` varchar(255), select_id int", - "select"=>"id int auto_increment primary key", - "values"=>"id int auto_increment primary key, group_id int", - "distinct"=>"id int auto_increment primary key", - "distinct_select"=>"distinct_id int, select_id int" + create_tables_directly "group" => "id int auto_increment primary key, `order` varchar(255), select_id int", + "select" => "id int auto_increment primary key", + "values" => "id int auto_increment primary key, group_id int", + "distinct" => "id int auto_increment primary key", + "distinct_select" => "distinct_id int, select_id int" end teardown do @@ -130,20 +130,20 @@ class Mysql2ReservedWordTest < ActiveRecord::Mysql2TestCase #the following functions were added to DRY test cases private - # custom fixture loader, uses FixtureSet#create_fixtures and appends base_path to the current file's path + # custom fixture loader, uses FixtureSet#create_fixtures and appends base_path to the current file's path def create_test_fixtures(*fixture_names) ActiveRecord::FixtureSet.create_fixtures(FIXTURES_ROOT + "/reserved_words", fixture_names) end - # custom drop table, uses execute on connection to drop a table if it exists. note: escapes table_name + # custom drop table, uses execute on connection to drop a table if it exists. note: escapes table_name def drop_tables_directly(table_names, connection = @connection) table_names.each do |name| connection.drop_table name, if_exists: true end end - # custom create table, uses execute on connection to create a table, note: escapes table_name, does NOT escape columns - def create_tables_directly (tables, connection = @connection) + # custom create table, uses execute on connection to create a table, note: escapes table_name, does NOT escape columns + def create_tables_directly(tables, connection = @connection) tables.each do |table_name, column_properties| connection.execute("CREATE TABLE `#{table_name}` ( #{column_properties} )") end diff --git a/activerecord/test/cases/adapters/mysql2/schema_test.rb b/activerecord/test/cases/adapters/mysql2/schema_test.rb index 3c08a4ed1f..1fad5585de 100644 --- a/activerecord/test/cases/adapters/mysql2/schema_test.rb +++ b/activerecord/test/cases/adapters/mysql2/schema_test.rb @@ -76,11 +76,11 @@ module ActiveRecord table = "key_tests" indexes = @connection.indexes(table).sort_by(&:name) - assert_equal 3,indexes.size + assert_equal 3, indexes.size - index_a = indexes.select{|i| i.name == index_a_name}[0] - index_b = indexes.select{|i| i.name == index_b_name}[0] - index_c = indexes.select{|i| i.name == index_c_name}[0] + index_a = indexes.select { |i| i.name == index_a_name }[0] + index_b = indexes.select { |i| i.name == index_b_name }[0] + index_c = indexes.select { |i| i.name == index_c_name }[0] assert_equal :btree, index_a.using assert_nil index_a.type assert_equal :btree, index_b.using @@ -121,6 +121,6 @@ class Mysql2AnsiQuotesTest < ActiveRecord::Mysql2TestCase def test_foreign_keys_method_with_ansi_quotes fks = @connection.foreign_keys("lessons_students") assert_equal([["lessons_students", "students", :cascade]], - fks.map {|fk| [fk.from_table, fk.to_table, fk.on_delete] }) + fks.map { |fk| [fk.from_table, fk.to_table, fk.on_delete] }) end end diff --git a/activerecord/test/cases/adapters/mysql2/transaction_test.rb b/activerecord/test/cases/adapters/mysql2/transaction_test.rb index edd5353ee3..16101e38cb 100644 --- a/activerecord/test/cases/adapters/mysql2/transaction_test.rb +++ b/activerecord/test/cases/adapters/mysql2/transaction_test.rb @@ -10,6 +10,8 @@ module ActiveRecord end setup do + @abort, Thread.abort_on_exception = Thread.abort_on_exception, false + @connection = ActiveRecord::Base.connection @connection.clear_cache! @@ -25,30 +27,34 @@ module ActiveRecord teardown do @connection.drop_table "samples", if_exists: true + + Thread.abort_on_exception = @abort end test "raises Deadlocked when a deadlock is encountered" do assert_raises(ActiveRecord::Deadlocked) do + barrier = Concurrent::CyclicBarrier.new(2) + s1 = Sample.create value: 1 s2 = Sample.create value: 2 thread = Thread.new do Sample.transaction do s1.lock! - sleep 1 + barrier.wait s2.update_attributes value: 1 end end - sleep 0.5 - - Sample.transaction do - s2.lock! - sleep 1 - s1.update_attributes value: 2 + begin + Sample.transaction do + s2.lock! + barrier.wait + s1.update_attributes value: 2 + end + ensure + thread.join end - - thread.join 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 3df11ce11b..a0823be143 100644 --- a/activerecord/test/cases/adapters/mysql2/unsigned_type_test.rb +++ b/activerecord/test/cases/adapters/mysql2/unsigned_type_test.rb @@ -15,6 +15,7 @@ class Mysql2UnsignedTypeTest < ActiveRecord::Mysql2TestCase t.bigint :unsigned_bigint, unsigned: true t.float :unsigned_float, unsigned: true t.decimal :unsigned_decimal, unsigned: true, precision: 10, scale: 2 + t.column :unsigned_zerofill, "int unsigned zerofill" end end @@ -34,10 +35,10 @@ class Mysql2UnsignedTypeTest < ActiveRecord::Mysql2TestCase assert_raise(ActiveModel::RangeError) do UnsignedType.create(unsigned_bigint: -10) end - assert_raise(ActiveRecord::StatementInvalid) do + assert_raise(ActiveRecord::RangeError) do UnsignedType.create(unsigned_float: -10.0) end - assert_raise(ActiveRecord::StatementInvalid) do + assert_raise(ActiveRecord::RangeError) do UnsignedType.create(unsigned_decimal: -10.0) end end @@ -50,7 +51,7 @@ class Mysql2UnsignedTypeTest < ActiveRecord::Mysql2TestCase t.unsigned_decimal :unsigned_decimal_t, precision: 10, scale: 2 end - @connection.columns("unsigned_types").select { |c| /^unsigned_/ === c.name }.each do |column| + @connection.columns("unsigned_types").select { |c| /^unsigned_/.match?(c.name) }.each do |column| assert column.unsigned? 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 d3c65f3d94..b787de8453 100644 --- a/activerecord/test/cases/adapters/postgresql/active_schema_test.rb +++ b/activerecord/test/cases/adapters/postgresql/active_schema_test.rb @@ -39,6 +39,10 @@ class PostgresqlActiveSchemaTest < ActiveRecord::PostgreSQLTestCase expected = %(CREATE INDEX CONCURRENTLY "index_people_on_last_name" ON "people" ("last_name")) assert_equal expected, add_index(:people, :last_name, algorithm: :concurrently) + expected = %(CREATE INDEX "index_people_on_last_name_and_first_name" ON "people" ("last_name" DESC, "first_name" ASC)) + assert_equal expected, add_index(:people, [:last_name, :first_name], order: { last_name: :desc, first_name: :asc }) + assert_equal expected, add_index(:people, ["last_name", :first_name], order: { last_name: :desc, "first_name" => :asc }) + %w(gin gist hash btree).each do |type| expected = %(CREATE INDEX "index_people_on_last_name" ON "people" USING #{type} ("last_name")) assert_equal expected, add_index(:people, :last_name, using: type) diff --git a/activerecord/test/cases/adapters/postgresql/array_test.rb b/activerecord/test/cases/adapters/postgresql/array_test.rb index e496960f21..c78c6178ff 100644 --- a/activerecord/test/cases/adapters/postgresql/array_test.rb +++ b/activerecord/test/cases/adapters/postgresql/array_test.rb @@ -16,10 +16,12 @@ class PostgresqlArrayTest < ActiveRecord::PostgreSQLTestCase @connection.transaction do @connection.create_table("pg_arrays") do |t| - t.string "tags", array: true + t.string "tags", array: true, limit: 255 t.integer "ratings", array: true t.datetime :datetimes, array: true t.hstore :hstores, array: true + t.decimal :decimals, array: true, default: [], precision: 10, scale: 2 + t.timestamp :timestamps, array: true, default: [], precision: 6 end end PgArray.reset_column_information @@ -34,7 +36,7 @@ class PostgresqlArrayTest < ActiveRecord::PostgreSQLTestCase def test_column assert_equal :string, @column.type - assert_equal "character varying", @column.sql_type + assert_equal "character varying(255)", @column.sql_type assert @column.array? assert_not @type.binary? @@ -110,22 +112,23 @@ class PostgresqlArrayTest < ActiveRecord::PostgreSQLTestCase def test_schema_dump_with_shorthand output = dump_table_schema "pg_arrays" - assert_match %r[t\.string\s+"tags",\s+array: true], output + assert_match %r[t\.string\s+"tags",\s+limit: 255,\s+array: true], output assert_match %r[t\.integer\s+"ratings",\s+array: true], output + assert_match %r[t\.decimal\s+"decimals",\s+precision: 10,\s+scale: 2,\s+default: \[\],\s+array: true], output end def test_select_with_strings @connection.execute "insert into pg_arrays (tags) VALUES ('{1,2,3}')" x = PgArray.first - assert_equal(["1","2","3"], x.tags) + assert_equal(["1", "2", "3"], x.tags) end def test_rewrite_with_strings @connection.execute "insert into pg_arrays (tags) VALUES ('{1,2,3}')" x = PgArray.first - x.tags = ["1","2","3","4"] + x.tags = ["1", "2", "3", "4"] x.save! - assert_equal ["1","2","3","4"], x.reload.tags + assert_equal ["1", "2", "3", "4"], x.reload.tags end def test_select_with_integers @@ -163,28 +166,28 @@ class PostgresqlArrayTest < ActiveRecord::PostgreSQLTestCase end def test_strings_with_quotes - assert_cycle(:tags, ["this has",'some "s that need to be escaped"']) + assert_cycle(:tags, ["this has", 'some "s that need to be escaped"']) end def test_strings_with_commas - assert_cycle(:tags, ["this,has","many,values"]) + assert_cycle(:tags, ["this,has", "many,values"]) end def test_strings_with_array_delimiters - assert_cycle(:tags, ["{","}"]) + assert_cycle(:tags, ["{", "}"]) end def test_strings_with_null_strings - assert_cycle(:tags, ["NULL","NULL"]) + assert_cycle(:tags, ["NULL", "NULL"]) end def test_contains_nils - assert_cycle(:tags, ["1",nil,nil]) + assert_cycle(:tags, ["1", nil, nil]) end def test_insert_fixture tag_values = ["val1", "val2", "val3_with_'_multiple_quote_'_chars"] - @connection.insert_fixture({"tags" => tag_values}, "pg_arrays" ) + @connection.insert_fixture({ "tags" => tag_values }, "pg_arrays") assert_equal(PgArray.last.tags, tag_values) end @@ -211,7 +214,7 @@ class PostgresqlArrayTest < ActiveRecord::PostgreSQLTestCase x = PgArray.create!(tags: tags) x.reload - assert_equal x.tags_before_type_cast, PgArray.type_for_attribute("tags").serialize(tags) + refute x.changed? end def test_quoting_non_standard_delimiters @@ -219,9 +222,10 @@ class PostgresqlArrayTest < ActiveRecord::PostgreSQLTestCase oid = ActiveRecord::ConnectionAdapters::PostgreSQL::OID comma_delim = oid::Array.new(ActiveRecord::Type::String.new, ",") semicolon_delim = oid::Array.new(ActiveRecord::Type::String.new, ";") + conn = PgArray.connection - assert_equal %({"hello,",world;}), comma_delim.serialize(strings) - assert_equal %({hello,;"world;"}), semicolon_delim.serialize(strings) + assert_equal %({"hello,",world;}), conn.type_cast(comma_delim.serialize(strings)) + assert_equal %({hello,;"world;"}), conn.type_cast(semicolon_delim.serialize(strings)) end def test_mutate_array @@ -290,6 +294,12 @@ class PostgresqlArrayTest < ActiveRecord::PostgreSQLTestCase assert_equal record.tags, record.reload.tags end + def test_where_by_attribute_with_array + tags = ["black", "blue"] + record = PgArray.create!(tags: tags) + assert_equal record, PgArray.where(tags: tags).take + end + def test_uniqueness_validation klass = Class.new(PgArray) do validates_uniqueness_of :tags @@ -305,6 +315,21 @@ class PostgresqlArrayTest < ActiveRecord::PostgreSQLTestCase assert_equal ["has already been taken"], e2.errors[:tags], "Should have uniqueness message for tags" end + def test_encoding_arrays_of_utf8_strings + arrays_of_utf8_strings = %w(nový ファイル) + assert_equal arrays_of_utf8_strings, @type.deserialize(@type.serialize(arrays_of_utf8_strings)) + assert_equal [arrays_of_utf8_strings], @type.deserialize(@type.serialize([arrays_of_utf8_strings])) + end + + def test_precision_is_respected_on_timestamp_columns + time = Time.now.change(usec: 123) + record = PgArray.create!(timestamps: [time]) + + assert_equal 123, record.timestamps.first.usec + record.reload + assert_equal 123, record.timestamps.first.usec + end + private def assert_cycle(field, array) # test creation diff --git a/activerecord/test/cases/adapters/postgresql/bit_string_test.rb b/activerecord/test/cases/adapters/postgresql/bit_string_test.rb index f646e59848..7712e809a2 100644 --- a/activerecord/test/cases/adapters/postgresql/bit_string_test.rb +++ b/activerecord/test/cases/adapters/postgresql/bit_string_test.rb @@ -65,10 +65,11 @@ class PostgresqlBitStringTest < ActiveRecord::PostgreSQLTestCase end def test_roundtrip - PostgresqlBitString.create! a_bit: "00001010", a_bit_varying: "0101" - record = PostgresqlBitString.first + record = PostgresqlBitString.create!(a_bit: "00001010", a_bit_varying: "0101") assert_equal "00001010", record.a_bit assert_equal "0101", record.a_bit_varying + assert_nil record.another_bit + assert_nil record.another_bit_varying record.a_bit = "11111111" record.a_bit_varying = "0xF" diff --git a/activerecord/test/cases/adapters/postgresql/bytea_test.rb b/activerecord/test/cases/adapters/postgresql/bytea_test.rb index dc0df8715a..5c207116c4 100644 --- a/activerecord/test/cases/adapters/postgresql/bytea_test.rb +++ b/activerecord/test/cases/adapters/postgresql/bytea_test.rb @@ -52,7 +52,7 @@ class PostgresqlByteaTest < ActiveRecord::PostgreSQLTestCase end def test_type_case_nil - assert_equal(nil, @type.deserialize(nil)) + assert_nil(@type.deserialize(nil)) end def test_read_value @@ -66,7 +66,7 @@ class PostgresqlByteaTest < ActiveRecord::PostgreSQLTestCase def test_read_nil_value @connection.execute "insert into bytea_data_type (payload) VALUES (null)" record = ByteaDataType.first - assert_equal(nil, record.payload) + assert_nil(record.payload) record.delete end @@ -106,8 +106,8 @@ class PostgresqlByteaTest < ActiveRecord::PostgreSQLTestCase def test_write_nil record = ByteaDataType.create(payload: nil) assert_not record.new_record? - assert_equal(nil, record.payload) - assert_equal(nil, ByteaDataType.where(id: record.id).first.payload) + assert_nil(record.payload) + assert_nil(ByteaDataType.where(id: record.id).first.payload) end class Serializer diff --git a/activerecord/test/cases/adapters/postgresql/case_insensitive_test.rb b/activerecord/test/cases/adapters/postgresql/case_insensitive_test.rb new file mode 100644 index 0000000000..03b44feab6 --- /dev/null +++ b/activerecord/test/cases/adapters/postgresql/case_insensitive_test.rb @@ -0,0 +1,26 @@ +require "cases/helper" + +class PostgresqlCaseInsensitiveTest < ActiveRecord::PostgreSQLTestCase + class Default < ActiveRecord::Base; end + + def test_case_insensitiveness + connection = ActiveRecord::Base.connection + table = Default.arel_table + + column = Default.columns_hash["char1"] + comparison = connection.case_insensitive_comparison table, :char1, column, nil + assert_match(/lower/i, comparison.to_sql) + + column = Default.columns_hash["char2"] + comparison = connection.case_insensitive_comparison table, :char2, column, nil + assert_match(/lower/i, comparison.to_sql) + + column = Default.columns_hash["char3"] + comparison = connection.case_insensitive_comparison table, :char3, column, nil + assert_match(/lower/i, comparison.to_sql) + + column = Default.columns_hash["multiline_default"] + comparison = connection.case_insensitive_comparison table, :multiline_default, column, nil + assert_match(/lower/i, comparison.to_sql) + end +end diff --git a/activerecord/test/cases/adapters/postgresql/connection_test.rb b/activerecord/test/cases/adapters/postgresql/connection_test.rb index d01f601031..075301d6d5 100644 --- a/activerecord/test/cases/adapters/postgresql/connection_test.rb +++ b/activerecord/test/cases/adapters/postgresql/connection_test.rb @@ -90,17 +90,17 @@ module ActiveRecord end def test_tables_logs_name - ActiveSupport::Deprecation.silence { @connection.tables("hello") } + @connection.tables assert_equal "SCHEMA", @subscriber.logged[0][1] end def test_indexes_logs_name - @connection.indexes("items", "hello") + assert_deprecated { @connection.indexes("items", "hello") } assert_equal "SCHEMA", @subscriber.logged[0][1] end def test_table_exists_logs_name - ActiveSupport::Deprecation.silence { @connection.table_exists?("items") } + @connection.table_exists?("items") assert_equal "SCHEMA", @subscriber.logged[0][1] end @@ -184,7 +184,7 @@ module ActiveRecord def test_set_session_variable_true run_without_connection do |orig_connection| - ActiveRecord::Base.establish_connection(orig_connection.deep_merge(variables: {debug_print_plan: true})) + ActiveRecord::Base.establish_connection(orig_connection.deep_merge(variables: { debug_print_plan: true })) set_true = ActiveRecord::Base.connection.exec_query "SHOW DEBUG_PRINT_PLAN" assert_equal set_true.rows, [["on"]] end @@ -192,7 +192,7 @@ module ActiveRecord def test_set_session_variable_false run_without_connection do |orig_connection| - ActiveRecord::Base.establish_connection(orig_connection.deep_merge(variables: {debug_print_plan: false})) + ActiveRecord::Base.establish_connection(orig_connection.deep_merge(variables: { debug_print_plan: false })) set_false = ActiveRecord::Base.connection.exec_query "SHOW DEBUG_PRINT_PLAN" assert_equal set_false.rows, [["off"]] end @@ -201,14 +201,14 @@ module ActiveRecord def test_set_session_variable_nil run_without_connection do |orig_connection| # This should be a no-op that does not raise an error - ActiveRecord::Base.establish_connection(orig_connection.deep_merge(variables: {debug_print_plan: nil})) + ActiveRecord::Base.establish_connection(orig_connection.deep_merge(variables: { debug_print_plan: nil })) end end def test_set_session_variable_default run_without_connection do |orig_connection| # This should execute a query that does not raise an error - ActiveRecord::Base.establish_connection(orig_connection.deep_merge(variables: {debug_print_plan: :default})) + ActiveRecord::Base.establish_connection(orig_connection.deep_merge(variables: { debug_print_plan: :default })) end end @@ -224,14 +224,14 @@ module ActiveRecord got_lock = @connection.get_advisory_lock(lock_id) assert got_lock, "get_advisory_lock should have returned true but it didn't" - advisory_lock = @connection.query(list_advisory_locks).find {|l| l[1] == lock_id} + advisory_lock = @connection.query(list_advisory_locks).find { |l| l[1] == lock_id } assert advisory_lock, "expected to find an advisory lock with lock_id #{lock_id} but there wasn't one" released_lock = @connection.release_advisory_lock(lock_id) assert released_lock, "expected release_advisory_lock to return true but it didn't" - advisory_locks = @connection.query(list_advisory_locks).select {|l| l[1] == lock_id} + advisory_locks = @connection.query(list_advisory_locks).select { |l| l[1] == lock_id } assert_empty advisory_locks, "expected to have released advisory lock with lock_id #{lock_id} but it was still held" end @@ -245,7 +245,7 @@ module ActiveRecord end end - protected + private def with_warning_suppression log_level = @connection.client_min_messages diff --git a/activerecord/test/cases/adapters/postgresql/geometric_test.rb b/activerecord/test/cases/adapters/postgresql/geometric_test.rb index 27a7467326..c1f3a4ae2c 100644 --- a/activerecord/test/cases/adapters/postgresql/geometric_test.rb +++ b/activerecord/test/cases/adapters/postgresql/geometric_test.rb @@ -27,22 +27,10 @@ class PostgresqlPointTest < ActiveRecord::PostgreSQLTestCase t.point :legacy_y, default: [12.2, 13.3] t.point :legacy_z, default: "(14.4,15.5)" end - @connection.create_table("deprecated_points") do |t| - t.point :x - end end teardown do @connection.drop_table "postgresql_points", if_exists: true - @connection.drop_table "deprecated_points", if_exists: true - end - - class DeprecatedPoint < ActiveRecord::Base; end - - def test_deprecated_legacy_type - assert_deprecated do - DeprecatedPoint.new - end end def test_column @@ -108,7 +96,7 @@ class PostgresqlPointTest < ActiveRecord::PostgreSQLTestCase assert_nothing_raised { PostgresqlPoint.new(x: "") } p = PostgresqlPoint.new(x: "") - assert_equal nil, p.x + assert_nil p.x end def test_array_of_points_round_trip diff --git a/activerecord/test/cases/adapters/postgresql/hstore_test.rb b/activerecord/test/cases/adapters/postgresql/hstore_test.rb index 817dfbf705..f9cce10fb8 100644 --- a/activerecord/test/cases/adapters/postgresql/hstore_test.rb +++ b/activerecord/test/cases/adapters/postgresql/hstore_test.rb @@ -10,6 +10,12 @@ if ActiveRecord::Base.connection.supports_extensions? store_accessor :settings, :language, :timezone end + class FakeParameters + def to_unsafe_h + { "hi" => "hi" } + end + end + def setup @connection = ActiveRecord::Base.connection @@ -38,7 +44,7 @@ if ActiveRecord::Base.connection.supports_extensions? def test_hstore_included_in_extensions assert @connection.respond_to?(:extensions), "connection should have a list of extensions" - assert @connection.extensions.include?("hstore"), "extension list should include hstore" + assert_includes @connection.extensions, "hstore", "extension list should include hstore" end def test_disable_enable_hstore @@ -64,8 +70,8 @@ if ActiveRecord::Base.connection.supports_extensions? @connection.add_column "hstores", "permissions", :hstore, default: '"users"=>"read", "articles"=>"write"' Hstore.reset_column_information - assert_equal({"users"=>"read", "articles"=>"write"}, Hstore.column_defaults["permissions"]) - assert_equal({"users"=>"read", "articles"=>"write"}, Hstore.new.permissions) + 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 @@ -103,18 +109,18 @@ if ActiveRecord::Base.connection.supports_extensions? 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 = 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) + assert_equal({ "bool" => "true", "number" => "5" }, x.reload.tags) end def test_type_cast_hstore - assert_equal({"1" => "2"}, @type.deserialize("\"1\"=>\"2\"")) + 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"))) + 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_with_store_accessors @@ -165,48 +171,67 @@ if ActiveRecord::Base.connection.supports_extensions? assert_not hstore.changed? end + def test_dirty_from_user_equal + settings = { "alongkey" => "anything", "key" => "value" } + hstore = Hstore.create!(settings: settings) + + hstore.settings = { "key" => "value", "alongkey" => "anything" } + assert_equal settings, hstore.settings + refute hstore.changed? + end + + def test_hstore_dirty_from_database_equal + settings = { "alongkey" => "anything", "key" => "value" } + hstore = Hstore.create!(settings: settings) + hstore.reload + + assert_equal settings, hstore.settings + hstore.settings = settings + refute hstore.changed? + end + def test_gen1 - assert_equal('" "=>""', @type.serialize(" "=>"")) + assert_equal('" "=>""', @type.serialize(" " => "")) end def test_gen2 - assert_equal('","=>""', @type.serialize(","=>"")) + assert_equal('","=>""', @type.serialize("," => "")) end def test_gen3 - assert_equal('"="=>""', @type.serialize("="=>"")) + assert_equal('"="=>""', @type.serialize("=" => "")) end def test_gen4 - assert_equal('">"=>""', @type.serialize(">"=>"")) + 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')) + assert_equal({ "a" => nil, "b" => nil, "c" => "NuLl", "null" => "c" }, @type.deserialize('a=>null,b=>NuLl,c=>"NuLl",null=>c')) end def test_parse2 - assert_equal({" " => " "}, @type.deserialize("\\ =>\\ ")) + assert_equal({ " " => " " }, @type.deserialize("\\ =>\\ ")) end def test_parse3 - assert_equal({"=" => ">"}, @type.deserialize("==>>")) + assert_equal({ "=" => ">" }, @type.deserialize("==>>")) end def test_parse4 - assert_equal({"=a"=>"q=w"}, @type.deserialize('\=a=>q=w')) + assert_equal({ "=a" => "q=w" }, @type.deserialize('\=a=>q=w')) end def test_parse5 - assert_equal({"=a"=>"q=w"}, @type.deserialize('"=a"=>q\=w')) + assert_equal({ "=a" => "q=w" }, @type.deserialize('"=a"=>q\=w')) end def test_parse6 - assert_equal({"\"a"=>"q>w"}, @type.deserialize('"\"a"=>q>w')) + assert_equal({ "\"a" => "q>w" }, @type.deserialize('"\"a"=>q>w')) end def test_parse7 - assert_equal({"\"a"=>"q\"w"}, @type.deserialize('\"a=>q"w')) + assert_equal({ "\"a" => "q\"w" }, @type.deserialize('\"a=>q"w')) end def test_rewrite @@ -219,19 +244,19 @@ if ActiveRecord::Base.connection.supports_extensions? def test_select @connection.execute "insert into hstores (tags) VALUES ('1=>2')" x = Hstore.first - assert_equal({"1" => "2"}, x.tags) + assert_equal({ "1" => "2" }, x.tags) end def test_array_cycle - assert_array_cycle([{"AA" => "BB", "CC" => "DD"}, {"AA" => nil}]) + assert_array_cycle([{ "AA" => "BB", "CC" => "DD" }, { "AA" => nil }]) end def test_array_strings_with_quotes - assert_array_cycle([{"this has" => 'some "s that need to be escaped"'}]) + assert_array_cycle([{ "this has" => 'some "s that need to be escaped"' }]) end def test_array_strings_with_commas - assert_array_cycle([{"this,has" => "many,values"}]) + assert_array_cycle([{ "this,has" => "many,values" }]) end def test_array_strings_with_array_delimiters @@ -239,17 +264,17 @@ if ActiveRecord::Base.connection.supports_extensions? end def test_array_strings_with_null_strings - assert_array_cycle([{"NULL" => "NULL"}]) + assert_array_cycle([{ "NULL" => "NULL" }]) end def test_contains_nils - assert_array_cycle([{"NULL" => nil}]) + assert_array_cycle([{ "NULL" => nil }]) 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) + assert_equal({ "1" => "2", "2" => "3" }, x.tags) end def test_create @@ -303,17 +328,17 @@ if ActiveRecord::Base.connection.supports_extensions? HstoreWithSerialize.create! tags: TagCollection.new("one" => "two") record = HstoreWithSerialize.first assert_instance_of TagCollection, record.tags - assert_equal({"one" => "two"}, record.tags.to_hash) + 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) + assert_equal({ "three" => "four" }, HstoreWithSerialize.first.tags.to_hash) 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) + assert_equal({ "one" => "two" }, dupe.tags.to_hash) end def test_schema_dump_with_shorthand @@ -321,6 +346,10 @@ if ActiveRecord::Base.connection.supports_extensions? 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 + private def assert_array_cycle(array) # test creation diff --git a/activerecord/test/cases/adapters/postgresql/json_test.rb b/activerecord/test/cases/adapters/postgresql/json_test.rb index d0e206ae80..93558ac4d2 100644 --- a/activerecord/test/cases/adapters/postgresql/json_test.rb +++ b/activerecord/test/cases/adapters/postgresql/json_test.rb @@ -38,11 +38,11 @@ module PostgresqlJSONSharedTestCases end def test_default - @connection.add_column "json_data_type", "permissions", column_type, default: {"users": "read", "posts": ["read", "write"]} + @connection.add_column "json_data_type", "permissions", column_type, default: { "users": "read", "posts": ["read", "write"] } JsonDataType.reset_column_information - assert_equal({"users"=>"read", "posts"=>["read", "write"]}, JsonDataType.column_defaults["permissions"]) - assert_equal({"users"=>"read", "posts"=>["read", "write"]}, JsonDataType.new.permissions) + assert_equal({ "users" => "read", "posts" => ["read", "write"] }, JsonDataType.column_defaults["permissions"]) + assert_equal({ "users" => "read", "posts" => ["read", "write"] }, JsonDataType.new.permissions) ensure JsonDataType.reset_column_information end @@ -68,11 +68,11 @@ module PostgresqlJSONSharedTestCases end def test_cast_value_on_write - x = JsonDataType.new payload: {"string" => "foo", :symbol => :bar} - assert_equal({"string" => "foo", :symbol => :bar}, x.payload_before_type_cast) - assert_equal({"string" => "foo", "symbol" => "bar"}, x.payload) + x = JsonDataType.new payload: { "string" => "foo", :symbol => :bar } + assert_equal({ "string" => "foo", :symbol => :bar }, x.payload_before_type_cast) + assert_equal({ "string" => "foo", "symbol" => "bar" }, x.payload) x.save - assert_equal({"string" => "foo", "symbol" => "bar"}, x.reload.payload) + assert_equal({ "string" => "foo", "symbol" => "bar" }, x.reload.payload) end def test_type_cast_json @@ -80,12 +80,12 @@ module PostgresqlJSONSharedTestCases data = "{\"a_key\":\"a_value\"}" hash = type.deserialize(data) - assert_equal({"a_key" => "a_value"}, hash) - assert_equal({"a_key" => "a_value"}, type.deserialize(data)) + assert_equal({ "a_key" => "a_value" }, hash) + assert_equal({ "a_key" => "a_value" }, type.deserialize(data)) 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"}))) + 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_rewrite @@ -98,31 +98,47 @@ module PostgresqlJSONSharedTestCases def test_select @connection.execute "insert into json_data_type (payload) VALUES ('{\"k\":\"v\"}')" x = JsonDataType.first - assert_equal({"k" => "v"}, x.payload) + 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]}')| x = JsonDataType.first - assert_equal({"k1" => "v1", "k2" => "v2", "k3" => [1,2,3]}, x.payload) + 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)" x = JsonDataType.first - assert_equal(nil, x.payload) + assert_nil(x.payload) + end + + def test_select_nil_json_after_create + json = JsonDataType.create(payload: nil) + x = JsonDataType.where(payload: nil).first + assert_equal(json, x) + end + + def test_select_nil_json_after_update + json = JsonDataType.create(payload: "foo") + x = JsonDataType.where(payload: nil).first + assert_nil(x) + + json.update_attributes payload: nil + x = JsonDataType.where(payload: nil).first + assert_equal(json.reload, x) end def test_select_array_json_value @connection.execute %q|insert into json_data_type (payload) VALUES ('["v0",{"k1":"v1"}]')| x = JsonDataType.first - assert_equal(["v0", {"k1" => "v1"}], x.payload) + 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"}]')| x = JsonDataType.first - x.payload = ["v1", {"k2" => "v2"}, "v3"] + x.payload = ["v1", { "k2" => "v2" }, "v3"] assert x.save! end diff --git a/activerecord/test/cases/adapters/postgresql/legacy_migration_test.rb b/activerecord/test/cases/adapters/postgresql/legacy_migration_test.rb new file mode 100644 index 0000000000..082fe95053 --- /dev/null +++ b/activerecord/test/cases/adapters/postgresql/legacy_migration_test.rb @@ -0,0 +1,54 @@ +require "cases/helper" + +class PostgresqlLegacyMigrationTest < ActiveRecord::PostgreSQLTestCase + class GenerateTableWithoutBigserial < ActiveRecord::Migration[5.0] + def change + create_table :legacy_integer_pk do |table| + table.string :foo + end + + create_table :override_pk, id: :bigint do |table| + table.string :bar + end + end + end + + def setup + super + + @migration_verbose_old = ActiveRecord::Migration.verbose + ActiveRecord::Migration.verbose = false + + migrations = [GenerateTableWithoutBigserial.new(nil, 1)] + ActiveRecord::Migrator.new(:up, migrations).migrate + end + + def teardown + ActiveRecord::Migration.verbose = @migration_verbose_old + + super + end + + def test_create_table_uses_serial_as_pkey_by_default + col = column(:legacy_integer_pk, :id) + assert_equal "integer", sql_type_for(col) + assert col.serial? + end + + def test_create_tables_respects_pk_column_type_override + col = column(:override_pk, :id) + assert_equal "bigint", sql_type_for(col) + end + + private + + def column(table_name, column_name) + ActiveRecord::Base.connection. + columns(table_name.to_s). + detect { |c| c.name == column_name.to_s } + end + + def sql_type_for(col) + col && col.sql_type + end +end diff --git a/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb b/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb index 4b8d06be4b..e6af93a53e 100644 --- a/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb +++ b/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb @@ -88,12 +88,6 @@ module ActiveRecord assert_equal expect.to_i, result.rows.first.first end - def test_sql_for_insert_with_returning_disabled - connection = connection_without_insert_returning - sql, binds = connection.sql_for_insert("sql", nil, nil, nil, "binds") - assert_equal ["sql", "binds"], [sql, binds] - end - def test_serial_sequence assert_equal "public.accounts_id_seq", @connection.serial_sequence("accounts", "id") diff --git a/activerecord/test/cases/adapters/postgresql/prepared_statements_disabled_test.rb b/activerecord/test/cases/adapters/postgresql/prepared_statements_disabled_test.rb new file mode 100644 index 0000000000..8c62690866 --- /dev/null +++ b/activerecord/test/cases/adapters/postgresql/prepared_statements_disabled_test.rb @@ -0,0 +1,25 @@ +require "cases/helper" +require "models/computer" +require "models/developer" + +class PreparedStatementsDisabledTest < ActiveRecord::PostgreSQLTestCase + fixtures :developers + + def setup + @conn = ActiveRecord::Base.establish_connection :arunit_without_prepared_statements + end + + def teardown + @conn.release_connection + ActiveRecord::Base.establish_connection :arunit + end + + def test_select_query_works_even_when_prepared_statements_are_disabled + assert_not Developer.connection.prepared_statements + + david = developers(:david) + + assert_equal david, Developer.where(name: "David").last # With Binds + assert_operator Developer.count, :>, 0 # Without Binds + end +end diff --git a/activerecord/test/cases/adapters/postgresql/prepared_statements_test.rb b/activerecord/test/cases/adapters/postgresql/prepared_statements_test.rb deleted file mode 100644 index b898929f8a..0000000000 --- a/activerecord/test/cases/adapters/postgresql/prepared_statements_test.rb +++ /dev/null @@ -1,21 +0,0 @@ -require "cases/helper" -require "models/developer" - -class PreparedStatementsTest < ActiveRecord::PostgreSQLTestCase - fixtures :developers - - def setup - @default_prepared_statements = Developer.connection_config[:prepared_statements] - Developer.connection_config[:prepared_statements] = false - end - - def teardown - Developer.connection_config[:prepared_statements] = @default_prepared_statements - end - - def nothing_raised_with_falsy_prepared_statements - assert_nothing_raised do - Developer.where(id: 1) - end - end -end diff --git a/activerecord/test/cases/adapters/postgresql/quoting_test.rb b/activerecord/test/cases/adapters/postgresql/quoting_test.rb index 865a3a5098..141baffa5b 100644 --- a/activerecord/test/cases/adapters/postgresql/quoting_test.rb +++ b/activerecord/test/cases/adapters/postgresql/quoting_test.rb @@ -18,12 +18,12 @@ module ActiveRecord end def test_quote_float_nan - nan = 0.0/0 + nan = 0.0 / 0 assert_equal "'NaN'", @conn.quote(nan) end def test_quote_float_infinity - infinity = 1.0/0 + infinity = 1.0 / 0 assert_equal "'Infinity'", @conn.quote(infinity) end @@ -36,7 +36,7 @@ module ActiveRecord def test_quote_bit_string value = "'); SELECT * FROM users; /*\n01\n*/--" type = OID::Bit.new - assert_equal nil, @conn.quote(type.serialize(value)) + assert_nil @conn.quote(type.serialize(value)) end end end diff --git a/activerecord/test/cases/adapters/postgresql/range_test.rb b/activerecord/test/cases/adapters/postgresql/range_test.rb index 3f819f7bd5..f411884dfd 100644 --- a/activerecord/test/cases/adapters/postgresql/range_test.rb +++ b/activerecord/test/cases/adapters/postgresql/range_test.rb @@ -282,6 +282,12 @@ _SQL assert_raises(ArgumentError) { PostgresqlRange.create!(tstz_range: "(''2010-01-01 14:30:00+05'', ''2011-01-01 14:30:00-03'']") } end + def test_where_by_attribute_with_range + range = 1..100 + record = PostgresqlRange.create!(int4_range: range) + assert_equal record, PostgresqlRange.where(int4_range: range).take + end + def test_update_all_with_ranges PostgresqlRange.create! diff --git a/activerecord/test/cases/adapters/postgresql/rename_table_test.rb b/activerecord/test/cases/adapters/postgresql/rename_table_test.rb index 461353d239..e9e7f717ac 100644 --- a/activerecord/test/cases/adapters/postgresql/rename_table_test.rb +++ b/activerecord/test/cases/adapters/postgresql/rename_table_test.rb @@ -26,9 +26,9 @@ class PostgresqlRenameTableTest < ActiveRecord::PostgreSQLTestCase def num_indices_named(name) @connection.execute(<<-SQL).values.length - SELECT 1 FROM "pg_index" - JOIN "pg_class" ON "pg_index"."indexrelid" = "pg_class"."oid" - WHERE "pg_class"."relname" = '#{name}' - SQL + SELECT 1 FROM "pg_index" + JOIN "pg_class" ON "pg_index"."indexrelid" = "pg_class"."oid" + WHERE "pg_class"."relname" = '#{name}' + SQL end end diff --git a/activerecord/test/cases/adapters/postgresql/schema_authorization_test.rb b/activerecord/test/cases/adapters/postgresql/schema_authorization_test.rb index 22f935f24e..f6a07da85f 100644 --- a/activerecord/test/cases/adapters/postgresql/schema_authorization_test.rb +++ b/activerecord/test/cases/adapters/postgresql/schema_authorization_test.rb @@ -100,17 +100,17 @@ class SchemaAuthorizationTest < ActiveRecord::PostgreSQLTestCase end def test_tables_in_current_schemas - assert !@connection.tables.include?(TABLE_NAME) + assert_not_includes @connection.tables, TABLE_NAME USERS.each do |u| set_session_auth u - assert @connection.tables.include?(TABLE_NAME) + assert_includes @connection.tables, TABLE_NAME set_session_auth end end private def set_session_auth(auth = nil) - @connection.session_auth = auth || "default" + @connection.session_auth = auth || "default" end def bind_param(value) diff --git a/activerecord/test/cases/adapters/postgresql/schema_test.rb b/activerecord/test/cases/adapters/postgresql/schema_test.rb index d821cbafdf..237e9ff6a5 100644 --- a/activerecord/test/cases/adapters/postgresql/schema_test.rb +++ b/activerecord/test/cases/adapters/postgresql/schema_test.rb @@ -130,7 +130,7 @@ class SchemaTest < ActiveRecord::PostgreSQLTestCase ensure @connection.drop_schema "test_schema3" end - assert !@connection.schema_names.include?("test_schema3") + assert_not_includes @connection.schema_names, "test_schema3" end def test_drop_schema_if_exists @@ -332,7 +332,7 @@ class SchemaTest < ActiveRecord::PostgreSQLTestCase @connection.execute "CREATE INDEX \"things_Index\" ON #{SCHEMA_NAME}.things (name)" with_schema_search_path SCHEMA_NAME do - assert_nothing_raised { @connection.remove_index "things", name: "things_Index"} + assert_nothing_raised { @connection.remove_index "things", name: "things_Index" } end end @@ -383,7 +383,7 @@ class SchemaTest < ActiveRecord::PostgreSQLTestCase pk, seq = @connection.pk_and_sequence_for(given) assert_equal "id", pk, "primary key should be found when table referenced as #{given}" assert_equal pg_name.new(SCHEMA_NAME, "#{PK_TABLE_NAME}_id_seq"), seq, "sequence name should be found when table referenced as #{given}" if given == %("#{SCHEMA_NAME}"."#{PK_TABLE_NAME}") - assert_equal pg_name.new(SCHEMA_NAME, UNMATCHED_SEQUENCE_NAME), seq, "sequence name should be found when table referenced as #{given}" if given == %("#{SCHEMA_NAME}"."#{UNMATCHED_PK_TABLE_NAME}") + assert_equal pg_name.new(SCHEMA_NAME, UNMATCHED_SEQUENCE_NAME), seq, "sequence name should be found when table referenced as #{given}" if given == %("#{SCHEMA_NAME}"."#{UNMATCHED_PK_TABLE_NAME}") end end @@ -393,7 +393,7 @@ class SchemaTest < ActiveRecord::PostgreSQLTestCase SCHEMA_NAME => SCHEMA_NAME, %(#{SCHEMA2_NAME},#{SCHEMA_NAME},public) => SCHEMA2_NAME, %(public,#{SCHEMA2_NAME},#{SCHEMA_NAME}) => "public" - }.each do |given,expect| + }.each do |given, expect| with_schema_search_path(given) { assert_equal expect, @connection.current_schema } end end @@ -418,7 +418,7 @@ class SchemaTest < ActiveRecord::PostgreSQLTestCase SCHEMA_NAME => true, SCHEMA2_NAME => true, "darkside" => false - }.each do |given,expect| + }.each do |given, expect| assert_equal expect, @connection.schema_exists?(given) end end diff --git a/activerecord/test/cases/adapters/postgresql/timestamp_test.rb b/activerecord/test/cases/adapters/postgresql/timestamp_test.rb index e7c1d97d16..962450aada 100644 --- a/activerecord/test/cases/adapters/postgresql/timestamp_test.rb +++ b/activerecord/test/cases/adapters/postgresql/timestamp_test.rb @@ -21,7 +21,7 @@ class PostgresqlTimestampTest < ActiveRecord::PostgreSQLTestCase @connection.reconnect! timestamp = PostgresqlTimestampWithZone.find(1) - assert_equal Time.utc(2010,1,1, 11,0,0), timestamp.time + assert_equal Time.utc(2010, 1, 1, 11, 0, 0), timestamp.time assert_instance_of Time, timestamp.time end ensure @@ -35,7 +35,7 @@ class PostgresqlTimestampTest < ActiveRecord::PostgreSQLTestCase @connection.execute("SET time zone 'America/Jamaica'", "SCHEMA") timestamp = PostgresqlTimestampWithZone.find(1) - assert_equal Time.utc(2010,1,1, 11,0,0), timestamp.time + assert_equal Time.utc(2010, 1, 1, 11, 0, 0), timestamp.time assert_instance_of Time, timestamp.time end ensure diff --git a/activerecord/test/cases/adapters/postgresql/transaction_test.rb b/activerecord/test/cases/adapters/postgresql/transaction_test.rb index d992e22305..9b42d0383d 100644 --- a/activerecord/test/cases/adapters/postgresql/transaction_test.rb +++ b/activerecord/test/cases/adapters/postgresql/transaction_test.rb @@ -1,5 +1,6 @@ require "cases/helper" require "support/connection_helper" +require "concurrent/atomic/cyclic_barrier" module ActiveRecord class PostgresqlTransactionTest < ActiveRecord::PostgreSQLTestCase @@ -10,6 +11,8 @@ module ActiveRecord end setup do + @abort, Thread.abort_on_exception = Thread.abort_on_exception, false + @connection = ActiveRecord::Base.connection @connection.transaction do @@ -24,35 +27,34 @@ module ActiveRecord teardown do @connection.drop_table "samples", if_exists: true + + Thread.abort_on_exception = @abort end test "raises SerializationFailure when a serialization failure occurs" do - with_warning_suppression do - assert_raises(ActiveRecord::SerializationFailure) do - thread = Thread.new do - Sample.transaction isolation: :serializable do - Sample.delete_all + assert_raises(ActiveRecord::SerializationFailure) do + before = Concurrent::CyclicBarrier.new(2) + after = Concurrent::CyclicBarrier.new(2) - 10.times do |i| - sleep 0.1 - - Sample.create value: i - end + thread = Thread.new do + with_warning_suppression do + Sample.transaction isolation: :serializable do + before.wait + Sample.create value: Sample.sum(:value) + after.wait end end + end - sleep 0.1 - - Sample.transaction isolation: :serializable do - Sample.delete_all - - 10.times do |i| - sleep 0.1 - - Sample.create value: i + begin + with_warning_suppression do + Sample.transaction isolation: :serializable do + before.wait + Sample.create value: Sample.sum(:value) + after.wait end end - + ensure thread.join end end @@ -61,37 +63,40 @@ module ActiveRecord test "raises Deadlocked when a deadlock is encountered" do with_warning_suppression do assert_raises(ActiveRecord::Deadlocked) do + barrier = Concurrent::CyclicBarrier.new(2) + s1 = Sample.create value: 1 s2 = Sample.create value: 2 thread = Thread.new do Sample.transaction do s1.lock! - sleep 1 + barrier.wait s2.update_attributes value: 1 end end - sleep 0.5 - - Sample.transaction do - s2.lock! - sleep 1 - s1.update_attributes value: 2 + begin + Sample.transaction do + s2.lock! + barrier.wait + s1.update_attributes value: 2 + end + ensure + thread.join end - - thread.join end end end - protected + private def with_warning_suppression - log_level = @connection.client_min_messages - @connection.client_min_messages = "error" + log_level = ActiveRecord::Base.connection.client_min_messages + ActiveRecord::Base.connection.client_min_messages = "error" yield - @connection.client_min_messages = log_level + ensure + ActiveRecord::Base.connection.client_min_messages = log_level end end end diff --git a/activerecord/test/cases/adapters/postgresql/type_lookup_test.rb b/activerecord/test/cases/adapters/postgresql/type_lookup_test.rb index bd45a9daa0..784d77a8d1 100644 --- a/activerecord/test/cases/adapters/postgresql/type_lookup_test.rb +++ b/activerecord/test/cases/adapters/postgresql/type_lookup_test.rb @@ -19,7 +19,7 @@ class PostgresqlTypeLookupTest < ActiveRecord::PostgreSQLTestCase big_array = [123456789123456789] assert_raises(ActiveModel::RangeError) { int_array.serialize(big_array) } - assert_equal "{123456789123456789}", bigint_array.serialize(big_array) + assert_equal "{123456789123456789}", @connection.type_cast(bigint_array.serialize(big_array)) end test "range types correctly respect registration of subtypes" do diff --git a/activerecord/test/cases/adapters/postgresql/utils_test.rb b/activerecord/test/cases/adapters/postgresql/utils_test.rb index 70f72dd929..9f9e3bda2f 100644 --- a/activerecord/test/cases/adapters/postgresql/utils_test.rb +++ b/activerecord/test/cases/adapters/postgresql/utils_test.rb @@ -7,13 +7,13 @@ class PostgreSQLUtilsTest < ActiveRecord::PostgreSQLTestCase def test_extract_schema_qualified_name { - %(table_name) => [nil,"table_name"], - %("table.name") => [nil,"table.name"], + %(table_name) => [nil, "table_name"], + %("table.name") => [nil, "table.name"], %(schema.table_name) => %w{schema table_name}, %("schema".table_name) => %w{schema table_name}, %(schema."table_name") => %w{schema table_name}, %("schema"."table_name") => %w{schema table_name}, - %("even spaces".table) => ["even spaces","table"], + %("even spaces".table) => ["even spaces", "table"], %(schema."table.name") => ["schema", "table.name"] }.each do |given, expect| assert_equal Name.new(*expect), extract_schema_qualified_name(given) @@ -54,9 +54,9 @@ class PostgreSQLNameTest < ActiveRecord::PostgreSQLTestCase end test "can be used as hash key" do - hash = {Name.new("schema", "article_seq") => "success"} + hash = { Name.new("schema", "article_seq") => "success" } assert_equal "success", hash[Name.new("schema", "article_seq")] - assert_equal nil, hash[Name.new("schema", "articles")] - assert_equal nil, hash[Name.new("public", "article_seq")] + assert_nil hash[Name.new("schema", "articles")] + assert_nil hash[Name.new("public", "article_seq")] end end diff --git a/activerecord/test/cases/adapters/postgresql/uuid_test.rb b/activerecord/test/cases/adapters/postgresql/uuid_test.rb index 321b74c1f5..f34d50e25c 100644 --- a/activerecord/test/cases/adapters/postgresql/uuid_test.rb +++ b/activerecord/test/cases/adapters/postgresql/uuid_test.rb @@ -9,6 +9,10 @@ module PostgresqlUUIDHelper def drop_table(name) connection.drop_table name, if_exists: true end + + def uuid_function + connection.supports_pgcrypto_uuid? ? "gen_random_uuid()" : "uuid_generate_v4()" + end end class PostgresqlUUIDTest < ActiveRecord::PostgreSQLTestCase @@ -21,6 +25,7 @@ class PostgresqlUUIDTest < ActiveRecord::PostgreSQLTestCase setup do enable_extension!("uuid-ossp", connection) + enable_extension!("pgcrypto", connection) if connection.supports_pgcrypto_uuid? connection.create_table "uuid_data_type" do |t| t.uuid "guid" @@ -31,14 +36,22 @@ class PostgresqlUUIDTest < ActiveRecord::PostgreSQLTestCase drop_table "uuid_data_type" end + if ActiveRecord::Base.connection.supports_pgcrypto_uuid? + def test_uuid_column_default + connection.add_column :uuid_data_type, :thingy, :uuid, null: false, default: "gen_random_uuid()" + UUIDType.reset_column_information + column = UUIDType.columns_hash["thingy"] + assert_equal "gen_random_uuid()", column.default_function + end + end + def test_change_column_default - @connection.add_column :uuid_data_type, :thingy, :uuid, null: false, default: "uuid_generate_v1()" + connection.add_column :uuid_data_type, :thingy, :uuid, null: false, default: "uuid_generate_v1()" UUIDType.reset_column_information column = UUIDType.columns_hash["thingy"] assert_equal "uuid_generate_v1()", column.default_function - @connection.change_column :uuid_data_type, :thingy, :uuid, null: false, default: "uuid_generate_v4()" - + connection.change_column :uuid_data_type, :thingy, :uuid, null: false, default: "uuid_generate_v4()" UUIDType.reset_column_information column = UUIDType.columns_hash["thingy"] assert_equal "uuid_generate_v4()", column.default_function @@ -58,12 +71,12 @@ class PostgresqlUUIDTest < ActiveRecord::PostgreSQLTestCase def test_treat_blank_uuid_as_nil UUIDType.create! guid: "" - assert_equal(nil, UUIDType.last.guid) + assert_nil(UUIDType.last.guid) end def test_treat_invalid_uuid_as_nil uuid = UUIDType.create! guid: "foobar" - assert_equal(nil, uuid.guid) + assert_nil(uuid.guid) end def test_invalid_uuid_dont_modify_before_type_cast @@ -155,7 +168,7 @@ class PostgresqlUUIDGenerationTest < ActiveRecord::PostgreSQLTestCase # to test dumping tables which columns have defaults with custom functions connection.execute <<-SQL CREATE OR REPLACE FUNCTION my_uuid_generator() RETURNS uuid - AS $$ SELECT * FROM uuid_generate_v4() $$ + AS $$ SELECT * FROM #{uuid_function} $$ LANGUAGE SQL VOLATILE; SQL @@ -164,11 +177,16 @@ class PostgresqlUUIDGenerationTest < ActiveRecord::PostgreSQLTestCase t.string "name" t.uuid "other_uuid_2", default: "my_uuid_generator()" end + + connection.create_table("pg_uuids_3", id: :uuid) do |t| + t.string "name" + end end teardown do drop_table "pg_uuids" drop_table "pg_uuids_2" + drop_table "pg_uuids_3" connection.execute "DROP FUNCTION IF EXISTS my_uuid_generator();" end @@ -192,19 +210,49 @@ class PostgresqlUUIDGenerationTest < ActiveRecord::PostgreSQLTestCase def test_pk_and_sequence_for_uuid_primary_key pk, seq = connection.pk_and_sequence_for("pg_uuids") assert_equal "id", pk - assert_equal nil, seq + 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) + 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) + assert_match(/t\.uuid "other_uuid_2", default: -> { "my_uuid_generator\(\)" }/, schema) + end + + 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 + + if ActiveRecord::Base.connection.supports_pgcrypto_uuid? + 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[4.2]) 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 end end end diff --git a/activerecord/test/cases/adapters/sqlite3/copy_table_test.rb b/activerecord/test/cases/adapters/sqlite3/copy_table_test.rb index 70e2f0fec1..91967c1e33 100644 --- a/activerecord/test/cases/adapters/sqlite3/copy_table_test.rb +++ b/activerecord/test/cases/adapters/sqlite3/copy_table_test.rb @@ -11,7 +11,7 @@ class CopyTableTest < ActiveRecord::SQLite3TestCase end def test_copy_table(from = "customers", to = "customers2", options = {}) - assert_nothing_raised {copy_table(from, to, options)} + assert_nothing_raised { copy_table(from, to, options) } assert_equal row_count(from), row_count(to) if block_given? @@ -25,7 +25,7 @@ class CopyTableTest < ActiveRecord::SQLite3TestCase def test_copy_table_renaming_column test_copy_table("customers", "customers2", - rename: {"name" => "person_name"}) do |from, to, options| + rename: { "name" => "person_name" }) do |from, to, options| expected = column_values(from, "name") assert_equal expected, column_values(to, "person_name") assert expected.any?, "No values in table: #{expected.inspect}" @@ -55,8 +55,8 @@ class CopyTableTest < ActiveRecord::SQLite3TestCase def test_copy_table_with_id_col_that_is_not_primary_key test_copy_table("goofy_string_id", "goofy_string_id2") do - original_id = @connection.columns("goofy_string_id").detect{|col| col.name == "id" } - copied_id = @connection.columns("goofy_string_id2").detect{|col| col.name == "id" } + original_id = @connection.columns("goofy_string_id").detect { |col| col.name == "id" } + copied_id = @connection.columns("goofy_string_id2").detect { |col| col.name == "id" } assert_equal original_id.type, copied_id.type assert_equal original_id.sql_type, copied_id.sql_type assert_equal original_id.limit, copied_id.limit @@ -75,17 +75,17 @@ class CopyTableTest < ActiveRecord::SQLite3TestCase test_copy_table "binaries", "binaries2" end -protected +private def copy_table(from, to, options = {}) - @connection.copy_table(from, to, {temporary: true}.merge(options)) + @connection.copy_table(from, to, { temporary: true }.merge(options)) end def column_names(table) - @connection.table_structure(table).map {|column| column["name"]} + @connection.table_structure(table).map { |column| column["name"] } end def column_values(table, column) - @connection.select_all("SELECT #{column} FROM #{table} ORDER BY id").map {|row| row[column]} + @connection.select_all("SELECT #{column} FROM #{table} ORDER BY id").map { |row| row[column] } end def table_indexes_without_name(table) diff --git a/activerecord/test/cases/adapters/sqlite3/legacy_migration_test.rb b/activerecord/test/cases/adapters/sqlite3/legacy_migration_test.rb new file mode 100644 index 0000000000..fcca8d66b5 --- /dev/null +++ b/activerecord/test/cases/adapters/sqlite3/legacy_migration_test.rb @@ -0,0 +1,59 @@ +require "cases/helper" + +class SqliteLegacyMigrationTest < ActiveRecord::SQLite3TestCase + self.use_transactional_tests = false + + class GenerateTableWithoutBigint < ActiveRecord::Migration[5.0] + def change + create_table :legacy_integer_pk do |table| + table.string :foo + end + + create_table :override_pk, id: :bigint do |table| + table.string :bar + end + end + end + + def setup + super + @connection = ActiveRecord::Base.connection + + @migration_verbose_old = ActiveRecord::Migration.verbose + ActiveRecord::Migration.verbose = false + + migrations = [GenerateTableWithoutBigint.new(nil, 1)] + + ActiveRecord::Migrator.new(:up, migrations).migrate + end + + def teardown + ActiveRecord::Migration.verbose = @migration_verbose_old + @connection.drop_table("legacy_integer_pk") + @connection.drop_table("override_pk") + ActiveRecord::SchemaMigration.delete_all rescue nil + super + end + + def test_create_table_uses_integer_as_pkey_by_default + col = column(:legacy_integer_pk, :id) + assert_equal "INTEGER", sql_type_for(col) + assert primary_key?(:legacy_integer_pk, "id"), "id is not primary key" + end + + private + + def column(table_name, column_name) + ActiveRecord::Base.connection + .columns(table_name.to_s) + .detect { |c| c.name == column_name.to_s } + end + + def sql_type_for(col) + col && col.sql_type + end + + def primary_key?(table_name, column) + ActiveRecord::Base.connection.execute("PRAGMA table_info(#{table_name})").find { |col| col["name"] == column }["pk"] == 1 + end +end diff --git a/activerecord/test/cases/adapters/sqlite3/quoting_test.rb b/activerecord/test/cases/adapters/sqlite3/quoting_test.rb index 80a37e83ff..9750840051 100644 --- a/activerecord/test/cases/adapters/sqlite3/quoting_test.rb +++ b/activerecord/test/cases/adapters/sqlite3/quoting_test.rb @@ -37,7 +37,7 @@ class SQLite3QuotingTest < ActiveRecord::SQLite3TestCase end def test_type_cast_nil - assert_equal nil, @conn.type_cast(nil) + assert_nil @conn.type_cast(nil) end def test_type_cast_true diff --git a/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb b/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb index 4800d3d7d5..a6afb7816b 100644 --- a/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb +++ b/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb @@ -190,7 +190,7 @@ module ActiveRecord end def test_type_cast_should_not_mutate_encoding - name = "hello".force_encoding(Encoding::ASCII_8BIT) + name = "hello".force_encoding(Encoding::ASCII_8BIT) Owner.create(name: name) assert_equal Encoding::ASCII_8BIT, name.encoding ensure @@ -267,9 +267,9 @@ module ActiveRecord def test_tables with_example_table do - ActiveSupport::Deprecation.silence { assert_equal %w{ ex }, @conn.tables } + assert_equal %w{ ex }, @conn.tables with_example_table "id integer PRIMARY KEY AUTOINCREMENT, number integer", "people" do - ActiveSupport::Deprecation.silence { assert_equal %w{ ex people }.sort, @conn.tables.sort } + assert_equal %w{ ex people }.sort, @conn.tables.sort end end end @@ -277,19 +277,17 @@ module ActiveRecord def test_tables_logs_name sql = <<-SQL SELECT name FROM sqlite_master - WHERE type IN ('table','view') AND name <> 'sqlite_sequence' + WHERE type = 'table' AND name <> 'sqlite_sequence' SQL assert_logged [[sql.squish, "SCHEMA", []]] do - ActiveSupport::Deprecation.silence do - @conn.tables("hello") - end + @conn.tables end end def test_indexes_logs_name with_example_table do assert_logged [["PRAGMA index_list(\"ex\")", "SCHEMA", []]] do - @conn.indexes("ex", "hello") + assert_deprecated { @conn.indexes("ex", "hello") } end end end @@ -298,12 +296,10 @@ module ActiveRecord with_example_table do sql = <<-SQL SELECT name FROM sqlite_master - WHERE type IN ('table','view') AND name <> 'sqlite_sequence' AND name = 'ex' + WHERE type = 'table' AND name <> 'sqlite_sequence' AND name = 'ex' SQL assert_logged [[sql.squish, "SCHEMA", []]] do - ActiveSupport::Deprecation.silence do - assert @conn.table_exists?("ex") - end + assert @conn.table_exists?("ex") end end end @@ -405,7 +401,7 @@ module ActiveRecord configurations["arunit"]["database"]) statement = ::SQLite3::Statement.new(db, "CREATE TABLE statement_test (number integer not null)") - statement.stub(:step, ->{ raise ::SQLite3::BusyException.new("busy") }) do + statement.stub(:step, -> { raise ::SQLite3::BusyException.new("busy") }) do assert_called(statement, :columns, returns: []) do assert_called(statement, :close) do ::SQLite3::Statement.stub(:new, statement) do @@ -431,9 +427,9 @@ module ActiveRecord def with_example_table(definition = nil, table_name = "ex", &block) definition ||= <<-SQL - id integer PRIMARY KEY AUTOINCREMENT, - number integer - SQL + id integer PRIMARY KEY AUTOINCREMENT, + number integer + SQL super(@conn, table_name, definition, &block) end end diff --git a/activerecord/test/cases/ar_schema_test.rb b/activerecord/test/cases/ar_schema_test.rb index a4919c913d..397ac599b9 100644 --- a/activerecord/test/cases/ar_schema_test.rb +++ b/activerecord/test/cases/ar_schema_test.rb @@ -17,6 +17,7 @@ if ActiveRecord::Base.connection.supports_migrations? @connection.drop_table :nep_fruits rescue nil @connection.drop_table :nep_schema_migrations rescue nil @connection.drop_table :has_timestamps rescue nil + @connection.drop_table :multiple_indexes rescue nil ActiveRecord::SchemaMigration.delete_all rescue nil ActiveRecord::Migration.verbose = @original_verbose end @@ -53,7 +54,7 @@ if ActiveRecord::Base.connection.supports_migrations? def test_schema_define_w_table_name_prefix table_name = ActiveRecord::SchemaMigration.table_name old_table_name_prefix = ActiveRecord::Base.table_name_prefix - ActiveRecord::Base.table_name_prefix = "nep_" + ActiveRecord::Base.table_name_prefix = "nep_" ActiveRecord::SchemaMigration.table_name = "nep_#{table_name}" ActiveRecord::Schema.define(version: 7) do create_table :fruits do |t| @@ -65,7 +66,7 @@ if ActiveRecord::Base.connection.supports_migrations? end assert_equal 7, ActiveRecord::Migrator::current_version ensure - ActiveRecord::Base.table_name_prefix = old_table_name_prefix + ActiveRecord::Base.table_name_prefix = old_table_name_prefix ActiveRecord::SchemaMigration.table_name = table_name end @@ -93,6 +94,21 @@ if ActiveRecord::Base.connection.supports_migrations? assert_equal "20131219224947", ActiveRecord::SchemaMigration.normalize_migration_number("20131219224947") end + def test_schema_load_with_multiple_indexes_for_column_of_different_names + ActiveRecord::Schema.define do + create_table :multiple_indexes do |t| + t.string "foo" + t.index ["foo"], name: "multiple_indexes_foo_1" + t.index ["foo"], name: "multiple_indexes_foo_2" + end + end + + indexes = @connection.indexes("multiple_indexes") + + assert_equal 2, indexes.length + assert_equal ["multiple_indexes_foo_1", "multiple_indexes_foo_2"], indexes.collect(&:name).sort + end + def test_timestamps_without_null_set_null_to_false_on_create_table ActiveRecord::Schema.define do create_table :has_timestamps do |t| diff --git a/activerecord/test/cases/associations/belongs_to_associations_test.rb b/activerecord/test/cases/associations/belongs_to_associations_test.rb index 2413bf0c3a..5875a1871f 100644 --- a/activerecord/test/cases/associations/belongs_to_associations_test.rb +++ b/activerecord/test/cases/associations/belongs_to_associations_test.rb @@ -94,7 +94,7 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase account = model.new assert_not account.valid? - assert_equal [{error: :blank}], account.errors.details[:company] + assert_equal [{ error: :blank }], account.errors.details[:company] ensure ActiveRecord::Base.belongs_to_required_by_default = original_value end @@ -111,7 +111,7 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase account = model.new assert_not account.valid? - assert_equal [{error: :blank}], account.errors.details[:company] + assert_equal [{ error: :blank }], account.errors.details[:company] ensure ActiveRecord::Base.belongs_to_required_by_default = original_value end @@ -203,14 +203,14 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase def test_eager_loading_with_primary_key Firm.create("name" => "Apple") Client.create("name" => "Citibank", :firm_name => "Apple") - citibank_result = Client.all.merge!(where: {name: "Citibank"}, includes: :firm_with_primary_key).first + citibank_result = Client.all.merge!(where: { name: "Citibank" }, includes: :firm_with_primary_key).first assert citibank_result.association(:firm_with_primary_key).loaded? end def test_eager_loading_with_primary_key_as_symbol Firm.create("name" => "Apple") Client.create("name" => "Citibank", :firm_name => "Apple") - citibank_result = Client.all.merge!(where: {name: "Citibank"}, includes: :firm_with_primary_key_symbols).first + citibank_result = Client.all.merge!(where: { name: "Citibank" }, includes: :firm_with_primary_key_symbols).first assert citibank_result.association(:firm_with_primary_key_symbols).loaded? end @@ -285,12 +285,22 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase end def test_failing_create! - client = Client.create!(name: "Jimmy") + client = Client.create!(name: "Jimmy") assert_raise(ActiveRecord::RecordInvalid) { client.create_account! } assert_not_nil client.account assert client.account.new_record? end + def test_reloading_the_belonging_object + odegy_account = accounts(:odegy_account) + + assert_equal "Odegy", odegy_account.firm.name + Company.where(id: odegy_account.firm_id).update_all(name: "ODEGY") + assert_equal "Odegy", odegy_account.firm.name + + assert_equal "ODEGY", odegy_account.reload_firm.name + end + def test_natural_assignment_to_nil client = Client.find(3) client.firm = nil @@ -346,7 +356,7 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase def test_with_select assert_equal 1, Company.find(2).firm_with_select.attributes.size - assert_equal 1, Company.all.merge!(includes: :firm_with_select ).find(2).firm_with_select.attributes.size + assert_equal 1, Company.all.merge!(includes: :firm_with_select).find(2).firm_with_select.attributes.size end def test_belongs_to_without_counter_cache_option @@ -626,6 +636,12 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase assert_queries(0) { tagging.super_tag } end + def test_dont_find_target_when_saving_foreign_key_after_stale_association_loaded + client = Client.create!(name: "Test client", firm_with_basic_id: Firm.find(1)) + client.firm_id = Firm.create!(name: "Test firm").id + assert_queries(1) { client.save! } + end + def test_field_name_same_as_foreign_key computer = Computer.find(1) assert_not_nil computer.developer, ":foreign key == attribute didn't lock up" # ' @@ -1004,21 +1020,21 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase def test_build_with_block client = Client.create(name: "Client Company") - firm = client.build_firm{ |f| f.name = "Agency Company" } + firm = client.build_firm { |f| f.name = "Agency Company" } assert_equal "Agency Company", firm.name end def test_create_with_block client = Client.create(name: "Client Company") - firm = client.create_firm{ |f| f.name = "Agency Company" } + firm = client.create_firm { |f| f.name = "Agency Company" } assert_equal "Agency Company", firm.name end def test_create_bang_with_block client = Client.create(name: "Client Company") - firm = client.create_firm!{ |f| f.name = "Agency Company" } + firm = client.create_firm! { |f| f.name = "Agency Company" } assert_equal "Agency Company", firm.name end @@ -1041,7 +1057,7 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase comment.parent = nil comment.save! - assert_equal nil, comment.reload.parent + assert_nil comment.reload.parent assert_equal 0, comments(:greetings).reload.children_count end @@ -1056,6 +1072,20 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase assert_equal 1, parent.reload.children_count end + def test_belongs_to_with_out_of_range_value_assigning + model = Class.new(Comment) do + def self.name; "Temp"; end + validates :post, presence: true + end + + comment = model.new + comment.post_id = 9223372036854775808 # out of range in the bigint + + assert_nil comment.post + assert_not comment.valid? + assert_equal [{ error: :blank }], comment.errors.details[:post] + end + def test_polymorphic_with_custom_primary_key toy = Toy.create! sponsor = Sponsor.create!(sponsorable: toy) @@ -1101,12 +1131,6 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase Column.create! record: record assert_equal 1, Column.count end - - def test_association_force_reload_with_only_true_is_deprecated - client = Client.find(3) - - assert_deprecated { client.firm(true) } - end end class BelongsToWithForeignKeyTest < ActiveRecord::TestCase diff --git a/activerecord/test/cases/associations/callbacks_test.rb b/activerecord/test/cases/associations/callbacks_test.rb index 74876c8cf0..5fd2411f6f 100644 --- a/activerecord/test/cases/associations/callbacks_test.rb +++ b/activerecord/test/cases/associations/callbacks_test.rb @@ -109,7 +109,7 @@ class AssociationCallbacksTest < ActiveRecord::TestCase def self.name; Project.name; end has_and_belongs_to_many :developers_with_callbacks, class_name: "Developer", - before_add: lambda { |o,r| + before_add: lambda { |o, r| dev = r new_dev = r.new_record? } @@ -159,7 +159,7 @@ class AssociationCallbacksTest < ActiveRecord::TestCase activerecord.reload assert activerecord.developers_with_callbacks.size == 2 end - activerecord.developers_with_callbacks.flat_map {|d| ["before_removing#{d.id}","after_removing#{d.id}"]}.sort + activerecord.developers_with_callbacks.flat_map { |d| ["before_removing#{d.id}", "after_removing#{d.id}"] }.sort assert activerecord.developers_with_callbacks.clear assert_predicate activerecord.developers_log, :empty? end @@ -176,14 +176,14 @@ class AssociationCallbacksTest < ActiveRecord::TestCase end def test_dont_add_if_before_callback_raises_exception - assert !@david.unchangeable_posts.include?(@authorless) + assert_not_includes @david.unchangeable_posts, @authorless begin @david.unchangeable_posts << @authorless rescue Exception end assert @david.post_log.empty? - assert !@david.unchangeable_posts.include?(@authorless) + assert_not_includes @david.unchangeable_posts, @authorless @david.reload - assert !@david.unchangeable_posts.include?(@authorless) + assert_not_includes @david.unchangeable_posts, @authorless end end diff --git a/activerecord/test/cases/associations/cascaded_eager_loading_test.rb b/activerecord/test/cases/associations/cascaded_eager_loading_test.rb index d79246a6c7..ddb5c7a4aa 100644 --- a/activerecord/test/cases/associations/cascaded_eager_loading_test.rb +++ b/activerecord/test/cases/associations/cascaded_eager_loading_test.rb @@ -16,28 +16,28 @@ class CascadedEagerLoadingTest < ActiveRecord::TestCase :categorizations, :people, :categories, :edges, :vertices def test_eager_association_loading_with_cascaded_two_levels - authors = Author.all.merge!(includes: {posts: :comments}, order: "authors.id").to_a + authors = Author.all.merge!(includes: { posts: :comments }, order: "authors.id").to_a assert_equal 3, authors.size assert_equal 5, authors[0].posts.size assert_equal 3, authors[1].posts.size - assert_equal 10, authors[0].posts.collect{|post| post.comments.size }.inject(0){|sum,i| sum+i} + assert_equal 10, authors[0].posts.collect { |post| post.comments.size }.inject(0) { |sum, i| sum + i } end def test_eager_association_loading_with_cascaded_two_levels_and_one_level - authors = Author.all.merge!(includes: [{posts: :comments}, :categorizations], order: "authors.id").to_a + authors = Author.all.merge!(includes: [{ posts: :comments }, :categorizations], order: "authors.id").to_a assert_equal 3, authors.size assert_equal 5, authors[0].posts.size assert_equal 3, authors[1].posts.size - assert_equal 10, authors[0].posts.collect{|post| post.comments.size }.inject(0){|sum,i| sum+i} + assert_equal 10, authors[0].posts.collect { |post| post.comments.size }.inject(0) { |sum, i| sum + i } assert_equal 1, authors[0].categorizations.size assert_equal 2, authors[1].categorizations.size end def test_eager_association_loading_with_hmt_does_not_table_name_collide_when_joining_associations assert_nothing_raised do - Author.joins(:posts).eager_load(:comments).where(posts: {tags_count: 1}).to_a + Author.joins(:posts).eager_load(:comments).where(posts: { tags_count: 1 }).to_a end - authors = Author.joins(:posts).eager_load(:comments).where(posts: {tags_count: 1}).to_a + authors = Author.joins(:posts).eager_load(:comments).where(posts: { tags_count: 1 }).to_a assert_equal 1, assert_no_queries { authors.size } assert_equal 10, assert_no_queries { authors[0].comments.size } end @@ -50,7 +50,7 @@ class CascadedEagerLoadingTest < ActiveRecord::TestCase end def test_cascaded_eager_association_loading_with_join_for_count - categories = Category.joins(:categorizations).includes([{posts: :comments}, :authors]) + categories = Category.joins(:categorizations).includes([{ posts: :comments }, :authors]) assert_equal 4, categories.count assert_equal 4, categories.to_a.count @@ -82,29 +82,29 @@ class CascadedEagerLoadingTest < ActiveRecord::TestCase end def test_eager_association_loading_with_cascaded_two_levels_with_two_has_many_associations - authors = Author.all.merge!(includes: {posts: [:comments, :categorizations]}, order: "authors.id").to_a + authors = Author.all.merge!(includes: { posts: [:comments, :categorizations] }, order: "authors.id").to_a assert_equal 3, authors.size assert_equal 5, authors[0].posts.size assert_equal 3, authors[1].posts.size - assert_equal 10, authors[0].posts.collect{|post| post.comments.size }.inject(0){|sum,i| sum+i} + assert_equal 10, authors[0].posts.collect { |post| post.comments.size }.inject(0) { |sum, i| sum + i } end def test_eager_association_loading_with_cascaded_two_levels_and_self_table_reference - authors = Author.all.merge!(includes: {posts: [:comments, :author]}, order: "authors.id").to_a + authors = Author.all.merge!(includes: { posts: [:comments, :author] }, order: "authors.id").to_a assert_equal 3, authors.size assert_equal 5, authors[0].posts.size assert_equal authors(:david).name, authors[0].name - assert_equal [authors(:david).name], authors[0].posts.collect{|post| post.author.name}.uniq + assert_equal [authors(:david).name], authors[0].posts.collect { |post| post.author.name }.uniq end def test_eager_association_loading_with_cascaded_two_levels_with_condition - authors = Author.all.merge!(includes: {posts: :comments}, where: "authors.id=1", order: "authors.id").to_a + authors = Author.all.merge!(includes: { posts: :comments }, where: "authors.id=1", order: "authors.id").to_a assert_equal 1, authors.size assert_equal 5, authors[0].posts.size end def test_eager_association_loading_with_cascaded_three_levels_by_ping_pong - firms = Firm.all.merge!(includes: {account: {firm: :account}}, order: "companies.id").to_a + firms = Firm.all.merge!(includes: { account: { firm: :account } }, order: "companies.id").to_a assert_equal 2, firms.size assert_equal firms.first.account, firms.first.account.firm.account assert_equal companies(:first_firm).account, assert_no_queries { firms.first.account.firm.account } @@ -134,8 +134,8 @@ class CascadedEagerLoadingTest < ActiveRecord::TestCase def test_eager_association_loading_with_belongs_to_sti replies = Reply.all.merge!(includes: :topic, order: "topics.id").to_a - assert replies.include?(topics(:second)) - assert !replies.include?(topics(:first)) + assert_includes replies, topics(:second) + assert_not_includes replies, topics(:first) assert_equal topics(:first), assert_no_queries { replies.first.topic } end @@ -158,7 +158,7 @@ class CascadedEagerLoadingTest < ActiveRecord::TestCase end def test_eager_association_loading_where_first_level_returns_nil - authors = Author.all.merge!(includes: {post_about_thinking: :comments}, order: "authors.id DESC").to_a + authors = Author.all.merge!(includes: { post_about_thinking: :comments }, order: "authors.id DESC").to_a assert_equal [authors(:bob), authors(:mary), authors(:david)], authors assert_no_queries do authors[2].post_about_thinking.comments.first @@ -166,12 +166,12 @@ class CascadedEagerLoadingTest < ActiveRecord::TestCase end def test_eager_association_loading_with_recursive_cascading_four_levels_has_many_through - source = Vertex.all.merge!(includes: {sinks: {sinks: {sinks: :sinks}}}, order: "vertices.id").first + source = Vertex.all.merge!(includes: { sinks: { sinks: { sinks: :sinks } } }, order: "vertices.id").first assert_equal vertices(:vertex_4), assert_no_queries { source.sinks.first.sinks.first.sinks.first } end def test_eager_association_loading_with_recursive_cascading_four_levels_has_and_belongs_to_many - sink = Vertex.all.merge!(includes: {sources: {sources: {sources: :sources}}}, order: "vertices.id DESC").first + sink = Vertex.all.merge!(includes: { sources: { sources: { sources: :sources } } }, order: "vertices.id DESC").first assert_equal vertices(:vertex_1), assert_no_queries { sink.sources.first.sources.first.sources.first.sources.first } end @@ -183,6 +183,6 @@ class CascadedEagerLoadingTest < ActiveRecord::TestCase assert_equal 1, authors[1].comments.size assert_equal 5, authors[0].posts.size assert_equal 3, authors[1].posts.size - assert_equal 3, authors[0].posts.collect { |post| post.categorizations.size }.inject(0) { |sum, i| sum+i } + assert_equal 3, authors[0].posts.collect { |post| post.categorizations.size }.inject(0) { |sum, i| sum + i } end end 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 aa82b9dd2a..4f0fe3236e 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 @@ -15,8 +15,8 @@ class EagerLoadIncludeFullStiClassNamesTest < ActiveRecord::TestCase end def generate_test_objects - post = Namespaced::Post.create( title: "Great stuff", body: "This is not", author_id: 1 ) - Tagging.create( taggable: post ) + post = Namespaced::Post.create(title: "Great stuff", body: "This is not", author_id: 1) + Tagging.create(taggable: post) end def test_class_names 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 aebece1b91..e9f551b6b2 100644 --- a/activerecord/test/cases/associations/eager_load_nested_include_test.rb +++ b/activerecord/test/cases/associations/eager_load_nested_include_test.rb @@ -12,7 +12,7 @@ module Remembered included do after_create :remember - protected + private def remember; self.class.remembered << self; end end @@ -39,7 +39,7 @@ class Triangle < ActiveRecord::Base has_many :shape_expressions, as: :shape include Remembered end -class PaintColor < ActiveRecord::Base +class PaintColor < ActiveRecord::Base has_many :shape_expressions, as: :paint belongs_to :non_poly, foreign_key: "non_poly_one_id", class_name: "NonPolyOne" include Remembered @@ -117,8 +117,8 @@ class EagerLoadNestedIncludeWithMissingDataTest < ActiveRecord::TestCase def test_missing_data_in_a_nested_include_should_not_cause_errors_when_constructing_objects assert_nothing_raised do # @davey_mcdave doesn't have any author_favorites - includes = {posts: :comments, categorizations: :category, author_favorites: :favorite_author } - Author.all.merge!(includes: includes, where: {authors: {name: @davey_mcdave.name}}, order: "categories.name").to_a + includes = { posts: :comments, categorizations: :category, author_favorites: :favorite_author } + Author.all.merge!(includes: includes, where: { authors: { name: @davey_mcdave.name } }, order: "categories.name").to_a end end end diff --git a/activerecord/test/cases/associations/eager_test.rb b/activerecord/test/cases/associations/eager_test.rb index c7c945e33f..ff1bf8acd4 100644 --- a/activerecord/test/cases/associations/eager_test.rb +++ b/activerecord/test/cases/associations/eager_test.rb @@ -42,11 +42,11 @@ class EagerAssociationTest < ActiveRecord::TestCase posts = Post.all.merge!(includes: :comments).to_a post = posts.find { |p| p.id == 1 } assert_equal 2, post.comments.size - assert post.comments.include?(comments(:greetings)) + assert_includes post.comments, comments(:greetings) post = Post.all.merge!(includes: :comments, where: "posts.title = 'Welcome to the weblog'").first assert_equal 2, post.comments.size - assert post.comments.include?(comments(:greetings)) + assert_includes post.comments, comments(:greetings) posts = Post.all.merge!(includes: :last_comment).to_a post = posts.find { |p| p.id == 1 } @@ -103,13 +103,13 @@ class EagerAssociationTest < ActiveRecord::TestCase posts = Post.all.merge!(includes: [ :comments, :author, :categories ], order: "posts.id").to_a assert_equal 2, posts.first.comments.size assert_equal 2, posts.first.categories.size - assert posts.first.comments.include?(comments(:greetings)) + assert_includes posts.first.comments, comments(:greetings) end def test_duplicate_middle_objects comments = Comment.all.merge!(where: "post_id = 1", includes: [post: :author]).to_a assert_no_queries do - comments.each {|comment| comment.post.author.name} + comments.each { |comment| comment.post.author.name } end end @@ -175,7 +175,7 @@ class EagerAssociationTest < ActiveRecord::TestCase popular_post.readers.create!(person: people(:david)) readers = Reader.all.merge!(where: ["post_id = ?", popular_post.id], - includes: {post: :comments}).to_a + includes: { post: :comments }).to_a readers.each do |reader| assert_equal [comment], reader.post.comments end @@ -188,7 +188,7 @@ class EagerAssociationTest < ActiveRecord::TestCase comment = car_post.comments.create!(body: "hmm") categories = Category.all.merge!(where: { "posts.id" => car_post.id }, - includes: {posts: :comments}).to_a + includes: { posts: :comments }).to_a categories.each do |category| assert_equal [comment], category.posts[0].comments end @@ -206,7 +206,7 @@ class EagerAssociationTest < ActiveRecord::TestCase def test_finding_with_includes_on_has_many_association_with_same_include_includes_only_once author_id = authors(:david).id - author = assert_queries(3) { Author.all.merge!(includes: {posts_with_comments: :comments}).find(author_id) } # find the author, then find the posts, then find the comments + author = assert_queries(3) { Author.all.merge!(includes: { posts_with_comments: :comments }).find(author_id) } # find the author, then find the posts, then find the comments author.posts_with_comments.each do |post_with_comments| assert_equal post_with_comments.comments.length, post_with_comments.comments.count assert_nil post_with_comments.comments.to_a.uniq! @@ -217,7 +217,7 @@ class EagerAssociationTest < ActiveRecord::TestCase author = authors(:david) post = author.post_about_thinking_with_last_comment last_comment = post.last_comment - author = assert_queries(3) { Author.all.merge!(includes: {post_about_thinking_with_last_comment: :last_comment}).find(author.id)} # find the author, then find the posts, then find the comments + author = assert_queries(3) { Author.all.merge!(includes: { post_about_thinking_with_last_comment: :last_comment }).find(author.id) } # find the author, then find the posts, then find the comments assert_no_queries do assert_equal post, author.post_about_thinking_with_last_comment assert_equal last_comment, author.post_about_thinking_with_last_comment.last_comment @@ -228,7 +228,7 @@ class EagerAssociationTest < ActiveRecord::TestCase post = posts(:welcome) author = post.author author_address = author.author_address - post = assert_queries(3) { Post.all.merge!(includes: {author_with_address: :author_address}).find(post.id) } # find the post, then find the author, then find the address + post = assert_queries(3) { Post.all.merge!(includes: { author_with_address: :author_address }).find(post.id) } # find the post, then find the author, then find the address assert_no_queries do assert_equal author, post.author_with_address assert_equal author_address, post.author_with_address.author_address @@ -238,10 +238,10 @@ class EagerAssociationTest < ActiveRecord::TestCase def test_finding_with_includes_on_null_belongs_to_association_with_same_include_includes_only_once post = posts(:welcome) post.update!(author: nil) - post = assert_queries(1) { Post.all.merge!(includes: {author_with_address: :author_address}).find(post.id) } + post = assert_queries(1) { Post.all.merge!(includes: { author_with_address: :author_address }).find(post.id) } # find the post, then find the author which is null so no query for the author or address assert_no_queries do - assert_equal nil, post.author_with_address + assert_nil post.author_with_address end end @@ -250,7 +250,7 @@ class EagerAssociationTest < ActiveRecord::TestCase sponsor.update!(sponsorable: nil) sponsor = assert_queries(1) { Sponsor.all.merge!(includes: :sponsorable).find(sponsor.id) } assert_no_queries do - assert_equal nil, sponsor.sponsorable + assert_nil sponsor.sponsorable end end @@ -261,7 +261,7 @@ class EagerAssociationTest < ActiveRecord::TestCase assert_nothing_raised { Sponsor.all.merge!(includes: :sponsorable).find(sponsor.id) } end assert_no_queries do - assert_equal nil, sponsor.sponsorable + assert_nil sponsor.sponsorable end end @@ -284,7 +284,7 @@ class EagerAssociationTest < ActiveRecord::TestCase # Regression test for 21c75e5 def test_nested_loading_does_not_raise_exception_when_association_does_not_exist assert_nothing_raised do - Post.all.merge!(includes: {author: :author_addresss}).find(posts(:authorless).id) + Post.all.merge!(includes: { author: :author_addresss }).find(posts(:authorless).id) end end @@ -292,33 +292,33 @@ class EagerAssociationTest < ActiveRecord::TestCase post_id = Comment.where(author_id: nil).where.not(post_id: nil).first.post_id assert_nothing_raised do - Post.preload(comments: [{author: :essays}]).find(post_id) + Post.preload(comments: [{ author: :essays }]).find(post_id) end end def test_nested_loading_through_has_one_association - aa = AuthorAddress.all.merge!(includes: {author: :posts}).find(author_addresses(:david_address).id) + aa = AuthorAddress.all.merge!(includes: { author: :posts }).find(author_addresses(:david_address).id) assert_equal aa.author.posts.count, aa.author.posts.length end def test_nested_loading_through_has_one_association_with_order - aa = AuthorAddress.all.merge!(includes: {author: :posts}, order: "author_addresses.id").find(author_addresses(:david_address).id) + aa = AuthorAddress.all.merge!(includes: { author: :posts }, order: "author_addresses.id").find(author_addresses(:david_address).id) assert_equal aa.author.posts.count, aa.author.posts.length end def test_nested_loading_through_has_one_association_with_order_on_association - aa = AuthorAddress.all.merge!(includes: {author: :posts}, order: "authors.id").find(author_addresses(:david_address).id) + aa = AuthorAddress.all.merge!(includes: { author: :posts }, order: "authors.id").find(author_addresses(:david_address).id) assert_equal aa.author.posts.count, aa.author.posts.length end def test_nested_loading_through_has_one_association_with_order_on_nested_association - aa = AuthorAddress.all.merge!(includes: {author: :posts}, order: "posts.id").find(author_addresses(:david_address).id) + aa = AuthorAddress.all.merge!(includes: { author: :posts }, order: "posts.id").find(author_addresses(:david_address).id) assert_equal aa.author.posts.count, aa.author.posts.length end def test_nested_loading_through_has_one_association_with_conditions aa = AuthorAddress.references(:author_addresses).merge( - includes: {author: :posts}, + includes: { author: :posts }, where: "author_addresses.id > 0" ).find author_addresses(:david_address).id assert_equal aa.author.posts.count, aa.author.posts.length @@ -326,7 +326,7 @@ class EagerAssociationTest < ActiveRecord::TestCase def test_nested_loading_through_has_one_association_with_conditions_on_association aa = AuthorAddress.references(:authors).merge( - includes: {author: :posts}, + includes: { author: :posts }, where: "authors.id > 0" ).find author_addresses(:david_address).id assert_equal aa.author.posts.count, aa.author.posts.length @@ -334,7 +334,7 @@ class EagerAssociationTest < ActiveRecord::TestCase def test_nested_loading_through_has_one_association_with_conditions_on_nested_association aa = AuthorAddress.references(:posts).merge( - includes: {author: :posts}, + includes: { author: :posts }, where: "posts.id > 0" ).find author_addresses(:david_address).id assert_equal aa.author.posts.count, aa.author.posts.length @@ -349,38 +349,38 @@ class EagerAssociationTest < ActiveRecord::TestCase comments = Comment.all.merge!(includes: :post).to_a assert_equal 11, comments.length titles = comments.map { |c| c.post.title } - assert titles.include?(posts(:welcome).title) - assert titles.include?(posts(:sti_post_and_comments).title) + assert_includes titles, posts(:welcome).title + assert_includes titles, posts(:sti_post_and_comments).title end def test_eager_association_loading_with_belongs_to_and_limit comments = Comment.all.merge!(includes: :post, limit: 5, order: "comments.id").to_a assert_equal 5, comments.length - assert_equal [1,2,3,5,6], comments.collect(&:id) + assert_equal [1, 2, 3, 5, 6], comments.collect(&:id) end def test_eager_association_loading_with_belongs_to_and_limit_and_conditions comments = Comment.all.merge!(includes: :post, where: "post_id = 4", limit: 3, order: "comments.id").to_a assert_equal 3, comments.length - assert_equal [5,6,7], comments.collect(&:id) + assert_equal [5, 6, 7], comments.collect(&:id) end def test_eager_association_loading_with_belongs_to_and_limit_and_offset comments = Comment.all.merge!(includes: :post, limit: 3, offset: 2, order: "comments.id").to_a assert_equal 3, comments.length - assert_equal [3,5,6], comments.collect(&:id) + assert_equal [3, 5, 6], comments.collect(&:id) end def test_eager_association_loading_with_belongs_to_and_limit_and_offset_and_conditions comments = Comment.all.merge!(includes: :post, where: "post_id = 4", limit: 3, offset: 1, order: "comments.id").to_a assert_equal 3, comments.length - assert_equal [6,7,8], comments.collect(&:id) + assert_equal [6, 7, 8], comments.collect(&:id) end def test_eager_association_loading_with_belongs_to_and_limit_and_offset_and_conditions_array - comments = Comment.all.merge!(includes: :post, where: ["post_id = ?",4], limit: 3, offset: 1, order: "comments.id").to_a + comments = Comment.all.merge!(includes: :post, where: ["post_id = ?", 4], limit: 3, offset: 1, order: "comments.id").to_a assert_equal 3, comments.length - assert_equal [6,7,8], comments.collect(&:id) + assert_equal [6, 7, 8], comments.collect(&:id) end def test_eager_association_loading_with_belongs_to_and_conditions_string_with_unquoted_table_name @@ -392,17 +392,17 @@ class EagerAssociationTest < ActiveRecord::TestCase def test_eager_association_loading_with_belongs_to_and_conditions_hash comments = [] assert_nothing_raised do - comments = Comment.all.merge!(includes: :post, where: {posts: {id: 4}}, limit: 3, order: "comments.id").to_a + comments = Comment.all.merge!(includes: :post, where: { posts: { id: 4 } }, limit: 3, order: "comments.id").to_a end assert_equal 3, comments.length - assert_equal [5,6,7], comments.collect(&:id) + assert_equal [5, 6, 7], comments.collect(&:id) assert_no_queries do comments.first.post end end def test_eager_association_loading_with_belongs_to_and_conditions_string_with_quoted_table_name - quoted_posts_id= Comment.connection.quote_table_name("posts") + "." + Comment.connection.quote_column_name("id") + quoted_posts_id = Comment.connection.quote_table_name("posts") + "." + Comment.connection.quote_column_name("id") assert_nothing_raised do Comment.includes(:post).references(:posts).where("#{quoted_posts_id} = ?", 4) end @@ -415,7 +415,7 @@ class EagerAssociationTest < ActiveRecord::TestCase end 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") + 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) end @@ -441,30 +441,30 @@ class EagerAssociationTest < ActiveRecord::TestCase def test_eager_load_belongs_to_quotes_table_and_column_names job = Job.includes(:ideal_reference).find jobs(:unicyclist).id references(:michael_unicyclist) - assert_no_queries{ assert_equal references(:michael_unicyclist), job.ideal_reference} + assert_no_queries { assert_equal references(:michael_unicyclist), job.ideal_reference } end def test_eager_load_has_one_quotes_table_and_column_names michael = Person.all.merge!(includes: :favourite_reference).find(people(:michael).id) references(:michael_unicyclist) - assert_no_queries{ assert_equal references(:michael_unicyclist), michael.favourite_reference} + assert_no_queries { assert_equal references(:michael_unicyclist), michael.favourite_reference } end def test_eager_load_has_many_quotes_table_and_column_names michael = Person.all.merge!(includes: :references).find(people(:michael).id) - references(:michael_magician,:michael_unicyclist) - assert_no_queries{ assert_equal references(:michael_magician,:michael_unicyclist), michael.references.sort_by(&:id) } + references(:michael_magician, :michael_unicyclist) + assert_no_queries { assert_equal references(:michael_magician, :michael_unicyclist), michael.references.sort_by(&:id) } end def test_eager_load_has_many_through_quotes_table_and_column_names michael = Person.all.merge!(includes: :jobs).find(people(:michael).id) jobs(:magician, :unicyclist) - assert_no_queries{ assert_equal jobs(:unicyclist, :magician), michael.jobs.sort_by(&:id) } + assert_no_queries { assert_equal jobs(:unicyclist, :magician), michael.jobs.sort_by(&:id) } end def test_eager_load_has_many_with_string_keys subscriptions = subscriptions(:webster_awdr, :webster_rfr) - subscriber =Subscriber.all.merge!(includes: :subscriptions).find(subscribers(:second).id) + subscriber = Subscriber.all.merge!(includes: :subscriptions).find(subscribers(:second).id) assert_equal subscriptions, subscriber.subscriptions.sort_by(&:id) end @@ -563,13 +563,13 @@ class EagerAssociationTest < ActiveRecord::TestCase def test_eager_with_has_many_and_limit_and_conditions posts = Post.all.merge!(includes: [ :author, :comments ], limit: 2, where: "posts.body = 'hello'", order: "posts.id").to_a assert_equal 2, posts.size - assert_equal [4,5], posts.collect(&:id) + assert_equal [4, 5], posts.collect(&:id) end def test_eager_with_has_many_and_limit_and_conditions_array posts = Post.all.merge!(includes: [ :author, :comments ], limit: 2, where: [ "posts.body = ?", "hello" ], order: "posts.id").to_a assert_equal 2, posts.size - assert_equal [4,5], posts.collect(&:id) + assert_equal [4, 5], posts.collect(&:id) end def test_eager_with_has_many_and_limit_and_conditions_array_on_the_eagers @@ -630,8 +630,8 @@ class EagerAssociationTest < ActiveRecord::TestCase assert_equal 2, posts[0].categories.size assert_equal 1, posts[1].categories.size assert_equal 0, posts[2].categories.size - assert posts[0].categories.include?(categories(:technology)) - assert posts[1].categories.include?(categories(:general)) + assert_includes posts[0].categories, categories(:technology) + assert_includes posts[1].categories, categories(:general) end # Since the preloader for habtm gets raw row hashes from the database and then @@ -695,8 +695,8 @@ class EagerAssociationTest < ActiveRecord::TestCase assert_equal 2, posts[0].categories.size assert_equal 1, posts[1].categories.size assert_equal 0, posts[2].categories.size - assert posts[0].categories.include?(categories(:technology)) - assert posts[1].categories.include?(categories(:general)) + assert_includes posts[0].categories, categories(:technology) + assert_includes posts[1].categories, categories(:general) end def test_eager_with_inheritance @@ -740,7 +740,7 @@ class EagerAssociationTest < ActiveRecord::TestCase def test_eager_with_invalid_association_reference assert_raise(ActiveRecord::AssociationNotFoundError, "Association was not found; perhaps you misspelled it? You specified :include => :monkeys") { - Post.all.merge!(includes: :monkeys ).find(6) + Post.all.merge!(includes: :monkeys).find(6) } assert_raise(ActiveRecord::AssociationNotFoundError, "Association was not found; perhaps you misspelled it? You specified :include => :monkeys") { Post.all.merge!(includes: [ :monkeys ]).find(6) @@ -844,7 +844,7 @@ class EagerAssociationTest < ActiveRecord::TestCase end end - def find_all_ordered(className, include=nil) + def find_all_ordered(className, include = nil) className.all.merge!(order: "#{className.table_name}.#{className.primary_key}", includes: include).to_a end @@ -895,16 +895,16 @@ class EagerAssociationTest < ActiveRecord::TestCase def test_polymorphic_type_condition post = Post.all.merge!(includes: :taggings).find(posts(:thinking).id) - assert post.taggings.include?(taggings(:thinking_general)) + assert_includes post.taggings, taggings(:thinking_general) post = SpecialPost.all.merge!(includes: :taggings).find(posts(:thinking).id) - assert post.taggings.include?(taggings(:thinking_general)) + assert_includes post.taggings, taggings(:thinking_general) end def test_eager_with_multiple_associations_with_same_table_has_many_and_habtm # Eager includes of has many and habtm associations aren't necessarily sorted in the same way def assert_equal_after_sort(item1, item2, item3 = nil) - assert_equal(item1.sort{|a,b| a.id <=> b.id}, item2.sort{|a,b| a.id <=> b.id}) - assert_equal(item3.sort{|a,b| a.id <=> b.id}, item2.sort{|a,b| a.id <=> b.id}) if item3 + assert_equal(item1.sort { |a, b| a.id <=> b.id }, item2.sort { |a, b| a.id <=> b.id }) + assert_equal(item3.sort { |a, b| a.id <=> b.id }, item2.sort { |a, b| a.id <=> b.id }) if item3 end # Test regular association, association with conditions, association with # STI, and association with conditions assured not to be true @@ -959,12 +959,12 @@ class EagerAssociationTest < ActiveRecord::TestCase def test_preconfigured_includes_with_belongs_to author = posts(:welcome).author_with_posts - assert_no_queries {assert_equal 5, author.posts.size} + assert_no_queries { assert_equal 5, author.posts.size } end def test_preconfigured_includes_with_has_one comment = posts(:sti_comments).very_special_comment_with_post - assert_no_queries {assert_equal posts(:sti_comments), comment.post} + assert_no_queries { assert_equal posts(:sti_comments), comment.post } end def test_eager_association_with_scope_with_joins @@ -1066,7 +1066,7 @@ class EagerAssociationTest < ActiveRecord::TestCase Post.all.merge!(joins: :comments, includes: :author, order: "comments.id DESC").to_a end assert_equal posts(:eager_other), posts[1] - assert_equal authors(:mary), assert_no_queries { posts[1].author} + assert_equal authors(:mary), assert_no_queries { posts[1].author } end def test_eager_loading_with_conditions_on_joined_table_preloads @@ -1074,21 +1074,21 @@ class EagerAssociationTest < ActiveRecord::TestCase Post.all.merge!(select: "distinct posts.*", includes: :author, joins: [:comments], where: "comments.body like 'Thank you%'", order: "posts.id").to_a end assert_equal [posts(:welcome)], posts - assert_equal authors(:david), assert_no_queries { posts[0].author} + assert_equal authors(:david), assert_no_queries { posts[0].author } posts = assert_queries(2) do Post.all.merge!(select: "distinct posts.*", includes: :author, joins: [:comments], where: "comments.body like 'Thank you%'", order: "posts.id").to_a end assert_equal [posts(:welcome)], posts - assert_equal authors(:david), assert_no_queries { posts[0].author} + assert_equal authors(:david), assert_no_queries { posts[0].author } posts = assert_queries(2) do - Post.all.merge!(includes: :author, joins: {taggings: :tag}, where: "tags.name = 'General'", order: "posts.id").to_a + Post.all.merge!(includes: :author, joins: { taggings: :tag }, where: "tags.name = 'General'", order: "posts.id").to_a end assert_equal posts(:welcome, :thinking), posts posts = assert_queries(2) do - Post.all.merge!(includes: :author, joins: {taggings: {tag: :taggings}}, where: "taggings_tags.super_tag_id=2", order: "posts.id").to_a + Post.all.merge!(includes: :author, joins: { taggings: { tag: :taggings } }, where: "taggings_tags.super_tag_id=2", order: "posts.id").to_a end assert_equal posts(:welcome, :thinking), posts end @@ -1110,13 +1110,13 @@ class EagerAssociationTest < ActiveRecord::TestCase Post.all.merge!(select: "distinct posts.*", includes: :author, joins: "INNER JOIN comments on comments.post_id = posts.id", where: "comments.body like 'Thank you%'", order: "posts.id").to_a end assert_equal [posts(:welcome)], posts - assert_equal authors(:david), assert_no_queries { posts[0].author} + assert_equal authors(:david), assert_no_queries { posts[0].author } posts = assert_queries(2) do Post.all.merge!(select: "distinct posts.*", includes: :author, joins: ["INNER JOIN comments on comments.post_id = posts.id"], where: "comments.body like 'Thank you%'", order: "posts.id").to_a end assert_equal [posts(:welcome)], posts - assert_equal authors(:david), assert_no_queries { posts[0].author} + assert_equal authors(:david), assert_no_queries { posts[0].author } end def test_eager_loading_with_select_on_joined_table_preloads @@ -1124,7 +1124,7 @@ class EagerAssociationTest < ActiveRecord::TestCase Post.all.merge!(select: "posts.*, authors.name as author_name", includes: :comments, joins: :author, order: "posts.id").to_a end assert_equal "David", posts[0].author_name - assert_equal posts(:welcome).comments, assert_no_queries { posts[0].comments} + assert_equal posts(:welcome).comments, assert_no_queries { posts[0].comments } end def test_eager_loading_with_conditions_on_join_model_preloads @@ -1139,7 +1139,7 @@ class EagerAssociationTest < ActiveRecord::TestCase people = Person.males.merge(includes: :primary_contact).to_a assert_not_equal people.length, 0 people.each do |person| - assert_no_queries {assert_not_nil person.primary_contact} + assert_no_queries { assert_not_nil person.primary_contact } assert_equal Person.find(person.id).primary_contact, person.primary_contact end end @@ -1163,7 +1163,7 @@ class EagerAssociationTest < ActiveRecord::TestCase expected = Firm.find(1).clients_using_primary_key.sort_by(&:name) # Oracle adapter truncates alias to 30 characters if current_adapter?(:OracleAdapter) - firm = Firm.all.merge!(includes: :clients_using_primary_key, order: "clients_using_primary_keys_companies"[0,30]+".name").find(1) + firm = Firm.all.merge!(includes: :clients_using_primary_key, order: "clients_using_primary_keys_companies"[0, 30] + ".name").find(1) else firm = Firm.all.merge!(includes: :clients_using_primary_key, order: "clients_using_primary_keys_companies.name").find(1) end @@ -1182,7 +1182,7 @@ class EagerAssociationTest < ActiveRecord::TestCase def test_include_has_one_using_primary_key expected = accounts(:signals37) - firm = Firm.all.merge!(includes: :account_using_primary_key, order: "accounts.id").to_a.detect {|f| f.id == 1} + firm = Firm.all.merge!(includes: :account_using_primary_key, order: "accounts.id").to_a.detect { |f| f.id == 1 } assert_no_queries do assert_equal expected, firm.account_using_primary_key end @@ -1253,16 +1253,16 @@ class EagerAssociationTest < ActiveRecord::TestCase def test_join_eager_with_empty_order_should_generate_valid_sql assert_nothing_raised do - Post.includes(:comments).order("").where(comments: {body: "Thank you for the welcome"}).first + Post.includes(:comments).order("").where(comments: { body: "Thank you for the welcome" }).first end end def test_deep_including_through_habtm # warm up habtm cache - posts = Post.all.merge!(includes: {categories: :categorizations}, order: "posts.id").to_a + posts = Post.all.merge!(includes: { categories: :categorizations }, order: "posts.id").to_a posts[0].categories[0].categorizations.length - posts = Post.all.merge!(includes: {categories: :categorizations}, order: "posts.id").to_a + posts = Post.all.merge!(includes: { categories: :categorizations }, order: "posts.id").to_a assert_no_queries { assert_equal 2, posts[0].categories[0].categorizations.length } assert_no_queries { assert_equal 1, posts[0].categories[1].categorizations.length } assert_no_queries { assert_equal 2, posts[1].categories[0].categorizations.length } @@ -1360,7 +1360,7 @@ class EagerAssociationTest < ActiveRecord::TestCase test "including associations with where.not adds implicit references" do author = assert_queries(2) { - Author.includes(:posts).where.not(posts: { title: "Welcome to the weblog"} ).last + Author.includes(:posts).where.not(posts: { title: "Welcome to the weblog" }).last } assert_no_queries { @@ -1424,6 +1424,24 @@ class EagerAssociationTest < ActiveRecord::TestCase assert david.readonly_comments.first.readonly? end + test "eager-loading non-readonly association" do + # has_one + firm = Firm.where(id: "1").eager_load(:account).first! + assert_not firm.account.readonly? + + # has_and_belongs_to_many + project = Project.where(id: "2").eager_load(:developers).first! + assert_not project.developers.first.readonly? + + # has_many :through + david = Author.where(id: "1").eager_load(:comments).first! + assert_not david.comments.first.readonly? + + # belongs_to + post = Post.where(id: "1").eager_load(:author).first! + assert_not post.author.readonly? + end + test "eager-loading readonly association" do # has-one firm = Firm.where(id: "1").eager_load(:readonly_account).first! @@ -1438,8 +1456,8 @@ class EagerAssociationTest < ActiveRecord::TestCase assert david.readonly_comments.first.readonly? # belongs_to - post = Post.where(id: "1").eager_load(:author).first! - assert post.author.readonly? + post = Post.where(id: "1").eager_load(:readonly_author).first! + assert post.readonly_author.readonly? end test "preloading a polymorphic association with references to the associated table" do diff --git a/activerecord/test/cases/associations/extension_test.rb b/activerecord/test/cases/associations/extension_test.rb index 8f9fc10ba5..974a3080d4 100644 --- a/activerecord/test/cases/associations/extension_test.rb +++ b/activerecord/test/cases/associations/extension_test.rb @@ -45,7 +45,7 @@ class AssociationsExtensionsTest < ActiveRecord::TestCase # Marshaling an association shouldn't make it unusable by wiping its reflection. assert_not_nil david.association(:projects).reflection - david_too = Marshal.load(marshalled) + david_too = Marshal.load(marshalled) assert_equal projects(:action_controller), david_too.projects.find_most_recent end @@ -76,6 +76,6 @@ class AssociationsExtensionsTest < ActiveRecord::TestCase private def extend!(model) - ActiveRecord::Associations::Builder::HasMany.define_extensions(model, :association_name) { } + ActiveRecord::Associations::Builder::HasMany.define_extensions(model, :association_name) {} end end diff --git a/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb b/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb index ec6ae39835..54fb61d6a5 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 @@ -86,6 +86,10 @@ class DeveloperWithSymbolClassName < Developer has_and_belongs_to_many :projects, class_name: :ProjectWithSymbolsForKeys end +class DeveloperWithConstantClassName < Developer + has_and_belongs_to_many :projects, class_name: ProjectWithSymbolsForKeys +end + class DeveloperWithExtendOption < Developer module NamedExtension def category @@ -169,7 +173,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase active_record = Project.find(1) assert !active_record.developers.empty? assert_equal 3, active_record.developers.size - assert active_record.developers.include?(david) + assert_includes active_record.developers, david end def test_adding_single @@ -249,8 +253,8 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase assert !p.persisted? assert aredridel.save assert aredridel.persisted? - assert_equal no_of_devels+1, Developer.count - assert_equal no_of_projects+1, Project.count + assert_equal no_of_devels + 1, Developer.count + assert_equal no_of_projects + 1, Project.count assert_equal 2, aredridel.projects.size assert_equal 2, aredridel.projects.reload.size end @@ -258,7 +262,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase def test_habtm_saving_multiple_relationships new_project = Project.new("name" => "Grimetime") amount_of_developers = 4 - developers = (0...amount_of_developers).collect {|i| Developer.create(name: "JME #{i}") }.reverse + developers = (0...amount_of_developers).collect { |i| Developer.create(name: "JME #{i}") }.reverse new_project.developer_ids = [developers[0].id, developers[1].id] new_project.developers_with_callback_ids = [developers[2].id, developers[3].id] @@ -379,7 +383,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase dev.projects << projects(:active_record) assert_equal 3, dev.projects.size - assert_equal 1, dev.projects.distinct.size + assert_equal 1, dev.projects.uniq.size end def test_distinct_before_the_fact @@ -544,7 +548,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase assert_no_queries(ignore_none: false) do assert project.developers.loaded? - assert project.developers.include?(developer) + assert_includes project.developers, developer end end @@ -555,7 +559,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase project.reload assert ! project.developers.loaded? assert_queries(1) do - assert project.developers.include?(developer) + assert_includes project.developers, developer end assert ! project.developers.loaded? end @@ -588,7 +592,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase end def test_dynamic_find_all_should_respect_readonly_access - projects(:active_record).readonly_developers.each { |d| assert_raise(ActiveRecord::ReadOnlyRecord) { d.save! } if d.valid?} + projects(:active_record).readonly_developers.each { |d| assert_raise(ActiveRecord::ReadOnlyRecord) { d.save! } if d.valid? } projects(:active_record).readonly_developers.each(&:readonly?) end @@ -600,8 +604,8 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase project.save! project.reload - assert project.developers.include?(jamis) - assert project.developers.include?(david) + assert_includes project.developers, jamis + assert_includes project.developers, david end def test_find_in_association_with_options @@ -628,7 +632,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase david.projects = [projects(:action_controller), Project.new("name" => "ActionWebSearch")] david.save assert_equal 2, david.projects.length - assert !david.projects.include?(projects(:active_record)) + assert_not_includes david.projects, projects(:active_record) end def test_replace_on_new_object @@ -646,9 +650,9 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase developer.special_projects << special_project developer.reload - assert developer.projects.include?(special_project) - assert developer.special_projects.include?(special_project) - assert !developer.special_projects.include?(other_project) + assert_includes developer.projects, special_project + assert_includes developer.special_projects, special_project + assert_not_includes developer.special_projects, other_project end def test_symbol_join_table @@ -700,7 +704,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase assert_equal( 3, Developer.references(:developers_projects_join).merge( - includes: {projects: :developers}, + includes: { projects: :developers }, where: "projects_developers_projects_join.joined_on IS NOT NULL" ).to_a.size ) @@ -720,7 +724,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase assert_equal( 3, Developer.references(:developers_projects_join).merge( - includes: {projects: :developers}, where: "projects_developers_projects_join.joined_on IS NOT NULL", + includes: { projects: :developers }, where: "projects_developers_projects_join.joined_on IS NOT NULL", group: group.join(",") ).to_a.size ) @@ -854,7 +858,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase def test_include_method_in_has_and_belongs_to_many_association_should_return_true_for_instance_added_with_build project = Project.new developer = project.developers.build - assert project.developers.include?(developer) + assert_includes project.developers, developer end def test_destruction_does_not_error_without_primary_key @@ -939,14 +943,16 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase def test_with_symbol_class_name assert_nothing_raised do - DeveloperWithSymbolClassName.new + developer = DeveloperWithSymbolClassName.new + developer.projects end end - def test_association_force_reload_with_only_true_is_deprecated - developer = Developer.find(1) - - assert_deprecated { developer.projects(true) } + def test_with_constant_class_name + assert_nothing_raised do + developer = DeveloperWithConstantClassName.new + developer.projects + end end def test_alternate_database @@ -1000,4 +1006,17 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase user = User.create! assert_nothing_raised { user.jobs_pool.clear } end + + def test_has_and_belongs_to_many_while_partial_writes_false + begin + original_partial_writes = ActiveRecord::Base.partial_writes + ActiveRecord::Base.partial_writes = false + developer = Developer.new(name: "Mehmet Emin İNAÇ") + developer.projects << Project.new(name: "Bounty") + + assert developer.save + ensure + ActiveRecord::Base.partial_writes = original_partial_writes + end + end end diff --git a/activerecord/test/cases/associations/has_many_associations_test.rb b/activerecord/test/cases/associations/has_many_associations_test.rb index 09692fc3a0..cbecfa84ff 100644 --- a/activerecord/test/cases/associations/has_many_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_associations_test.rb @@ -84,7 +84,7 @@ class HasManyAssociationsTestPrimaryKeys < ActiveRecord::TestCase david = people(:david) assert_equal ["A Modest Proposal"], david.essays.map(&:name) - david.essays = [Essay.create!(name: "Remote Work" )] + david.essays = [Essay.create!(name: "Remote Work")] assert_equal ["Remote Work"], david.essays.map(&:name) end @@ -187,7 +187,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase ship.parts.clear part.reload - assert_equal nil, part.ship + assert_nil part.ship assert !part.updated_at_changed? end @@ -305,7 +305,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase test "building the association with an array" do speedometer = Speedometer.new(speedometer_id: "a") - data = [{name: "first"}, {name: "second"}] + data = [{ name: "first" }, { name: "second" }] speedometer.minivans.build(data) assert_equal 2, speedometer.minivans.size @@ -380,47 +380,78 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_no_queries do bulbs.first() - bulbs.first({}) end assert_no_queries do bulbs.second() - bulbs.second({}) end assert_no_queries do bulbs.third() - bulbs.third({}) end assert_no_queries do bulbs.fourth() - bulbs.fourth({}) end assert_no_queries do bulbs.fifth() - bulbs.fifth({}) end assert_no_queries do bulbs.forty_two() - bulbs.forty_two({}) end assert_no_queries do bulbs.third_to_last() - bulbs.third_to_last({}) end assert_no_queries do bulbs.second_to_last() - bulbs.second_to_last({}) end assert_no_queries do bulbs.last() - bulbs.last({}) + end + end + + def test_finder_method_with_dirty_target + company = companies(:first_firm) + new_clients = [] + assert_no_queries(ignore_none: false) do + new_clients << company.clients_of_firm.build(name: "Another Client") + new_clients << company.clients_of_firm.build(name: "Another Client II") + new_clients << company.clients_of_firm.build(name: "Another Client III") + end + + assert_not company.clients_of_firm.loaded? + assert_queries(1) do + assert_same new_clients[0], company.clients_of_firm.third + assert_same new_clients[1], company.clients_of_firm.fourth + assert_same new_clients[2], company.clients_of_firm.fifth + assert_same new_clients[0], company.clients_of_firm.third_to_last + assert_same new_clients[1], company.clients_of_firm.second_to_last + assert_same new_clients[2], company.clients_of_firm.last + end + end + + def test_finder_bang_method_with_dirty_target + company = companies(:first_firm) + new_clients = [] + assert_no_queries(ignore_none: false) do + new_clients << company.clients_of_firm.build(name: "Another Client") + new_clients << company.clients_of_firm.build(name: "Another Client II") + new_clients << company.clients_of_firm.build(name: "Another Client III") + end + + assert_not company.clients_of_firm.loaded? + assert_queries(1) do + assert_same new_clients[0], company.clients_of_firm.third! + assert_same new_clients[1], company.clients_of_firm.fourth! + assert_same new_clients[2], company.clients_of_firm.fifth! + assert_same new_clients[0], company.clients_of_firm.third_to_last! + assert_same new_clients[1], company.clients_of_firm.second_to_last! + assert_same new_clients[2], company.clients_of_firm.last! end end @@ -487,7 +518,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end def test_finding_array_compatibility - assert_equal 3, Firm.order(:id).find{|f| f.id > 0}.clients.length + assert_equal 3, Firm.order(:id).find { |f| f.id > 0 }.clients.length end def test_find_many_with_merged_options @@ -497,7 +528,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end def test_find_should_append_to_association_order - ordered_clients = companies(:first_firm).clients_sorted_desc.order("companies.id") + ordered_clients = companies(:first_firm).clients_sorted_desc.order("companies.id") assert_equal ["id DESC", "companies.id"], ordered_clients.order_values end @@ -525,14 +556,18 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_taking_with_a_number # taking from unloaded Relation bob = Author.find(authors(:bob).id) + new_post = bob.posts.build + assert_not bob.posts.loaded? assert_equal [posts(:misc_by_bob)], bob.posts.take(1) - bob = Author.find(authors(:bob).id) assert_equal [posts(:misc_by_bob), posts(:other_by_bob)], bob.posts.take(2) + assert_equal [posts(:misc_by_bob), posts(:other_by_bob), new_post], bob.posts.take(3) # taking from loaded Relation - bob.posts.to_a - assert_equal [posts(:misc_by_bob)], authors(:bob).posts.take(1) - assert_equal [posts(:misc_by_bob), posts(:other_by_bob)], authors(:bob).posts.take(2) + bob.posts.load + assert bob.posts.loaded? + assert_equal [posts(:misc_by_bob)], bob.posts.take(1) + assert_equal [posts(:misc_by_bob), posts(:other_by_bob)], bob.posts.take(2) + assert_equal [posts(:misc_by_bob), posts(:other_by_bob), new_post], bob.posts.take(3) end def test_taking_with_inverse_of @@ -618,6 +653,18 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_raise(ActiveRecord::RecordNotFound) { firm.clients.find(2, 99) } end + def test_find_one_message_on_primary_key + firm = Firm.all.merge!(order: "id").first + + e = assert_raises(ActiveRecord::RecordNotFound) do + firm.clients.find(0) + end + assert_equal 0, e.id + assert_equal "id", e.primary_key + assert_equal "Client", e.model + assert_match (/\ACouldn't find Client with 'id'=0/), e.message + end + def test_find_ids_and_inverse_of force_signal37_to_load_all_clients_of_firm @@ -644,7 +691,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert ! firm.clients.loaded? assert_queries(4) do - firm.clients.find_each(batch_size: 1) {|c| assert_equal firm.id, c.firm_id } + firm.clients.find_each(batch_size: 1) { |c| assert_equal firm.id, c.firm_id } end assert ! firm.clients.loaded? @@ -670,7 +717,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_queries(2) do firm.clients.find_in_batches(batch_size: 2) do |clients| - clients.each {|c| assert_equal firm.id, c.firm_id } + clients.each { |c| assert_equal firm.id, c.firm_id } end end @@ -701,7 +748,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_find_all_with_include_and_conditions assert_nothing_raised do - Developer.all.merge!(joins: :audit_logs, where: {"audit_logs.message" => nil, :name => "Smith"}).to_a + Developer.all.merge!(joins: :audit_logs, where: { "audit_logs.message" => nil, :name => "Smith" }).to_a end end @@ -823,7 +870,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase rescue Client::RaisedOnSave end - assert !companies(:first_firm).clients_of_firm.reload.include?(good) + assert_not_includes companies(:first_firm).clients_of_firm.reload, good end def test_transactions_when_adding_to_new_record @@ -865,6 +912,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase company.clients_of_firm.build("name" => "Another Client") company.clients_of_firm.build("name" => "Yet Another Client") assert_equal 4, company.clients_of_firm.size + assert_equal 4, company.clients_of_firm.uniq.size end def test_collection_not_empty_after_building @@ -888,7 +936,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_build_many company = companies(:first_firm) - new_clients = assert_no_queries(ignore_none: false) { company.clients_of_firm.build([{"name" => "Another Client"}, {"name" => "Another Client II"}]) } + new_clients = assert_no_queries(ignore_none: false) { company.clients_of_firm.build([{ "name" => "Another Client" }, { "name" => "Another Client II" }]) } assert_equal 2, new_clients.size end @@ -914,7 +962,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_build_via_block company = companies(:first_firm) - new_client = assert_no_queries(ignore_none: false) { company.clients_of_firm.build {|client| client.name = "Another Client" } } + new_client = assert_no_queries(ignore_none: false) { company.clients_of_firm.build { |client| client.name = "Another Client" } } assert !company.clients_of_firm.loaded? assert_equal "Another Client", new_client.name @@ -925,7 +973,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_build_many_via_block company = companies(:first_firm) new_clients = assert_no_queries(ignore_none: false) do - company.clients_of_firm.build([{"name" => "Another Client"}, {"name" => "Another Client II"}]) do |client| + company.clients_of_firm.build([{ "name" => "Another Client" }, { "name" => "Another Client II" }]) do |client| client.name = "changed" end end @@ -936,7 +984,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end def test_create_without_loading_association - first_firm = companies(:first_firm) + first_firm = companies(:first_firm) Firm.column_names Client.column_names @@ -962,7 +1010,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end def test_create_many - companies(:first_firm).clients_of_firm.create([{"name" => "Another Client"}, {"name" => "Another Client II"}]) + companies(:first_firm).clients_of_firm.create([{ "name" => "Another Client" }, { "name" => "Another Client II" }]) assert_equal 4, companies(:first_firm).clients_of_firm.reload.size end @@ -1527,26 +1575,6 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert firm.companies.exists?(name: "child") end - def test_restrict_with_error_is_deprecated_using_key_many - I18n.backend = I18n::Backend::Simple.new - I18n.backend.store_translations :en, activerecord: { errors: { messages: { restrict_dependent_destroy: { many: "message for deprecated key" } } } } - - firm = RestrictedWithErrorFirm.create!(name: "restrict") - firm.companies.create(name: "child") - - assert !firm.companies.empty? - - assert_deprecated { firm.destroy } - - assert !firm.errors.empty? - - assert_equal "message for deprecated key", firm.errors[:base].first - assert RestrictedWithErrorFirm.exists?(name: "restrict") - assert firm.companies.exists?(name: "child") - ensure - I18n.backend.reload! - end - def test_restrict_with_error firm = RestrictedWithErrorFirm.create!(name: "restrict") firm.companies.create(name: "child") @@ -1564,7 +1592,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_restrict_with_error_with_locale I18n.backend = I18n::Backend::Simple.new - I18n.backend.store_translations "en", activerecord: {attributes: {restricted_with_error_firm: {companies: "client companies"}}} + I18n.backend.store_translations "en", activerecord: { attributes: { restricted_with_error_firm: { companies: "client companies" } } } firm = RestrictedWithErrorFirm.create!(name: "restrict") firm.companies.create(name: "child") @@ -1688,6 +1716,11 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert !company.clients.loaded? end + def test_counter_cache_on_unloaded_association + car = Car.create(name: "My AppliCar") + assert_equal car.engines.size, 0 + end + def test_get_ids_ignores_include_option assert_equal [readers(:michael_welcome).id], posts(:welcome).readers_with_person_ids end @@ -1742,7 +1775,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase lambda { authors(:mary).comments = [comments(:greetings), comments(:more_greetings)] }, lambda { authors(:mary).comments << Comment.create!(body: "Yay", post_id: 424242) }, lambda { authors(:mary).comments.delete(authors(:mary).comments.first) }, - ].each {|block| assert_raise(ActiveRecord::HasManyThroughCantAssociateThroughHasOneOrManyReflection, &block) } + ].each { |block| assert_raise(ActiveRecord::HasManyThroughCantAssociateThroughHasOneOrManyReflection, &block) } end def test_dynamic_find_should_respect_association_order_for_through @@ -2244,7 +2277,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 @@ -2414,15 +2447,12 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end test "double insertion of new object to association when same association used in the after create callback of a new object" do - car = Car.create! - car.bulbs << TrickyBulb.new - assert_equal 1, car.bulbs.size - end - - def test_association_force_reload_with_only_true_is_deprecated - company = Company.find(1) - - assert_deprecated { company.clients_of_firm(true) } + reset_callbacks(:save, Bulb) do + Bulb.after_save { |record| record.car.bulbs.load } + car = Car.create! + car.bulbs << Bulb.new + assert_equal 1, car.bulbs.size + end end class AuthorWithErrorDestroyingAssociation < ActiveRecord::Base @@ -2463,9 +2493,34 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_no_queries { car.bulb_ids } end + def test_loading_association_in_validate_callback_doesnt_affect_persistence + reset_callbacks(:validation, Bulb) do + Bulb.after_validation { |m| m.car.bulbs.load } + + car = Car.create!(name: "Car") + bulb = car.bulbs.create! + + assert_equal [bulb], car.bulbs + end + end + private def force_signal37_to_load_all_clients_of_firm companies(:first_firm).clients_of_firm.load_target end + + def reset_callbacks(kind, klass) + old_callbacks = {} + old_callbacks[klass] = klass.send("_#{kind}_callbacks").dup + klass.subclasses.each do |subclass| + old_callbacks[subclass] = subclass.send("_#{kind}_callbacks").dup + end + yield + ensure + klass.send("_#{kind}_callbacks=", old_callbacks[klass]) + klass.subclasses.each do |subclass| + subclass.send("_#{kind}_callbacks=", old_callbacks[subclass]) + end + end end diff --git a/activerecord/test/cases/associations/has_many_through_associations_test.rb b/activerecord/test/cases/associations/has_many_through_associations_test.rb index 90004eb70f..ac005b7010 100644 --- a/activerecord/test/cases/associations/has_many_through_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_through_associations_test.rb @@ -75,7 +75,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase posts = person_prime.includes(:posts).first.posts assert_operator posts.length, :>, 1 - posts.each_cons(2) do |left,right| + posts.each_cons(2) do |left, right| assert_operator left.id, :>, right.id end end @@ -175,7 +175,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase person = Person.new post = Post.new person.posts << post - assert person.posts.include?(post) + assert_includes person.posts, post end def test_associate_existing @@ -187,10 +187,10 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase end assert_queries(1) do - assert post.people.include?(person) + assert_includes post.people, person end - assert post.reload.people.reload.include?(person) + assert_includes post.reload.people.reload, person end def test_delete_all_for_with_dependent_option_destroy @@ -266,7 +266,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase post.people.delete(person) end - assert !post.people.reload.include?(person) + assert_not_includes post.people.reload, person end def test_associating_new @@ -284,10 +284,10 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase end assert_queries(1) do - assert posts(:thinking).people.include?(new_person) + assert_includes posts(:thinking).people, new_person end - assert posts(:thinking).reload.people.reload.include?(new_person) + assert_includes posts(:thinking).reload.people.reload, new_person end def test_associate_new_by_building @@ -300,8 +300,8 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase # Should only need to load the association once assert_queries(1) do - assert posts(:thinking).people.collect(&:first_name).include?("Bob") - assert posts(:thinking).people.collect(&:first_name).include?("Ted") + assert_includes posts(:thinking).people.collect(&:first_name), "Bob" + assert_includes posts(:thinking).people.collect(&:first_name), "Ted" end # 2 queries for each new record (1 to save the record itself, 1 for the join model) @@ -312,8 +312,8 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase posts(:thinking).save end - assert posts(:thinking).reload.people.reload.collect(&:first_name).include?("Bob") - assert posts(:thinking).reload.people.reload.collect(&:first_name).include?("Ted") + assert_includes posts(:thinking).reload.people.reload.collect(&:first_name), "Bob" + assert_includes posts(:thinking).reload.people.reload.collect(&:first_name), "Ted" end def test_build_then_save_with_has_many_inverse @@ -322,7 +322,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase person.save post.reload - assert post.people.include?(person) + assert_includes post.people, person end def test_build_then_save_with_has_one_inverse @@ -331,7 +331,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase person.save post.reload - assert post.single_people.include?(person) + assert_includes post.single_people, person end def test_both_parent_ids_set_when_saving_new @@ -348,7 +348,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase end def test_delete_association - assert_queries(2){posts(:welcome);people(:michael); } + assert_queries(2) { posts(:welcome);people(:michael); } assert_queries(1) do posts(:welcome).people.delete(people(:michael)) @@ -402,7 +402,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase end end - assert_equal nil, reference.reload.job_id + assert_nil reference.reload.job_id ensure Reference.make_comments = false end @@ -423,7 +423,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase end # Check that the destroy callback on Reference did not run - assert_equal nil, person.reload.comments + assert_nil person.reload.comments ensure Reference.make_comments = false end @@ -485,7 +485,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase end references.each do |reference| - assert_equal nil, reference.reload.job_id + assert_nil reference.reload.job_id end end @@ -541,7 +541,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase end def test_replace_association - assert_queries(4){posts(:welcome);people(:david);people(:michael); posts(:welcome).people.reload} + assert_queries(4) { posts(:welcome);people(:david);people(:michael); posts(:welcome).people.reload } # 1 query to delete the existing reader (michael) # 1 query to associate the new reader (david) @@ -549,13 +549,13 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase posts(:welcome).people = [people(:david)] end - assert_queries(0){ - assert posts(:welcome).people.include?(people(:david)) - assert !posts(:welcome).people.include?(people(:michael)) + assert_queries(0) { + assert_includes posts(:welcome).people, people(:david) + assert_not_includes posts(:welcome).people, people(:michael) } - assert posts(:welcome).reload.people.reload.include?(people(:david)) - assert !posts(:welcome).reload.people.reload.include?(people(:michael)) + assert_includes posts(:welcome).reload.people.reload, people(:david) + assert_not_includes posts(:welcome).reload.people.reload, people(:michael) end def test_replace_order_is_preserved @@ -591,10 +591,10 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase # *Now* we actually need the collection so it's loaded assert_queries(1) do - assert posts(:thinking).people.collect(&:first_name).include?("Jeb") + assert_includes posts(:thinking).people.collect(&:first_name), "Jeb" end - assert posts(:thinking).reload.people.reload.collect(&:first_name).include?("Jeb") + assert_includes posts(:thinking).reload.people.reload.collect(&:first_name), "Jeb" end def test_through_record_is_built_when_created_with_where @@ -658,7 +658,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase def test_push_with_invalid_join_record repair_validations(Contract) do - Contract.validate {|r| r.errors[:base] << "Invalid Contract" } + Contract.validate { |r| r.errors[:base] << "Invalid Contract" } firm = companies(:first_firm) lifo = Developer.new(name: "lifo") @@ -702,7 +702,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase [:added, :after, "Bob"], [:added, :before, "Lary"], [:added, :after, "Lary"] - ],log.last(6) + ], log.last(6) post.people_with_callbacks.build(first_name: "Ted") assert_equal [ @@ -716,7 +716,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase [:added, :after, "Sam"] ], log.last(2) - post.people_with_callbacks = [people(:michael),people(:david), Person.new(first_name: "Julian"), Person.create!(first_name: "Roger")] + post.people_with_callbacks = [people(:michael), people(:david), Person.new(first_name: "Julian"), Person.create!(first_name: "Roger")] assert_equal((%w(Ted Bob Sam Lary) * 2).sort, log[-12..-5].collect(&:last).sort) assert_equal [ [:added, :before, "Julian"], @@ -812,7 +812,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase lambda { authors(:david).very_special_comments = [VerySpecialComment.create!(body: "Gorp!", post_id: 1011), VerySpecialComment.create!(body: "Eep!", post_id: 1012)] }, lambda { authors(:david).very_special_comments << VerySpecialComment.create!(body: "Hoohah!", post_id: 1013) }, lambda { authors(:david).very_special_comments.delete(authors(:david).very_special_comments.first) }, - ].each {|block| assert_raise(ActiveRecord::HasManyThroughCantAssociateThroughHasOneOrManyReflection, &block) } + ].each { |block| assert_raise(ActiveRecord::HasManyThroughCantAssociateThroughHasOneOrManyReflection, &block) } end def test_has_many_association_through_a_has_many_association_to_self @@ -832,14 +832,14 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase category = author.named_categories.build(name: "Primary") author.save assert Categorization.exists?(author_id: author.id, named_category_name: category.name) - assert author.named_categories.reload.include?(category) + assert_includes author.named_categories.reload, category end def test_collection_create_with_nonstandard_primary_key_on_belongs_to author = authors(:mary) category = author.named_categories.create(name: "Primary") assert Categorization.exists?(author_id: author.id, named_category_name: category.name) - assert author.named_categories.reload.include?(category) + assert_includes author.named_categories.reload, category end def test_collection_exists @@ -883,10 +883,32 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase 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] - assert_raises(ActiveRecord::AssociationTypeMismatch) {company.developer_ids= ids} + 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) + 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 end def test_build_a_model_from_hm_through_association_with_where_clause @@ -908,14 +930,14 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase person = Person.new reference = person.references.build job = reference.build_job - assert person.jobs.include?(job) + assert_includes person.jobs, job end def test_include_method_in_association_through_should_return_true_for_instance_added_with_nested_builds author = Author.new post = author.posts.build comment = post.comments.build - assert author.comments.include?(comment) + assert_includes author.comments, comment end def test_through_association_readonly_should_be_false @@ -1022,7 +1044,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase post = posts(:welcome) address = author_addresses(:david_address) - assert post.author_addresses.include?(address) + assert_includes post.author_addresses, address post.author_addresses.delete(address) assert post[:author_count].nil? end @@ -1107,7 +1129,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase def test_has_many_through_obeys_order_on_through_association owner = owners(:blackbeard) - assert owner.toys.to_sql.include?("pets.name desc") + assert_includes owner.toys.to_sql, "pets.name desc" assert_equal ["parrot", "bulbul"], owner.toys.map { |r| r.pet.name } end @@ -1182,12 +1204,6 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase assert_nil Club.new.special_favourites.distinct_value end - def test_association_force_reload_with_only_true_is_deprecated - post = Post.find(1) - - assert_deprecated { post.people(true) } - end - def test_has_many_through_do_not_cache_association_reader_if_the_though_method_has_default_scopes member = Member.create! club = Club.create! @@ -1215,4 +1231,12 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase ensure TenantMembership.current_member = nil end + + def test_incorrectly_ordered_through_associations + assert_raises(ActiveRecord::HasManyThroughOrderError) do + DeveloperWithIncorrectlyOrderedHasManyThrough.create( + companies: [Company.create] + ) + end + end end diff --git a/activerecord/test/cases/associations/has_one_associations_test.rb b/activerecord/test/cases/associations/has_one_associations_test.rb index ad0d47920d..aa910ba409 100644 --- a/activerecord/test/cases/associations/has_one_associations_test.rb +++ b/activerecord/test/cases/associations/has_one_associations_test.rb @@ -186,25 +186,6 @@ class HasOneAssociationsTest < ActiveRecord::TestCase assert firm.account.present? end - def test_restrict_with_error_is_deprecated_using_key_one - I18n.backend = I18n::Backend::Simple.new - I18n.backend.store_translations :en, activerecord: { errors: { messages: { restrict_dependent_destroy: { one: "message for deprecated key" } } } } - - firm = RestrictedWithErrorFirm.create!(name: "restrict") - firm.create_account(credit_limit: 10) - - assert_not_nil firm.account - - assert_deprecated { firm.destroy } - - assert !firm.errors.empty? - assert_equal "message for deprecated key", firm.errors[:base].first - assert RestrictedWithErrorFirm.exists?(name: "restrict") - assert firm.account.present? - ensure - I18n.backend.reload! - end - def test_restrict_with_error firm = RestrictedWithErrorFirm.create!(name: "restrict") firm.create_account(credit_limit: 10) @@ -221,7 +202,7 @@ class HasOneAssociationsTest < ActiveRecord::TestCase def test_restrict_with_error_with_locale I18n.backend = I18n::Backend::Simple.new - I18n.backend.store_translations "en", activerecord: {attributes: {restricted_with_error_firm: {account: "firm account"}}} + I18n.backend.store_translations "en", activerecord: { attributes: { restricted_with_error_firm: { account: "firm account" } } } firm = RestrictedWithErrorFirm.create!(name: "restrict") firm.create_account(credit_limit: 10) @@ -326,6 +307,16 @@ class HasOneAssociationsTest < ActiveRecord::TestCase end end + def test_reload_association + odegy = companies(:odegy) + + assert_equal 53, odegy.account.credit_limit + Account.where(id: odegy.account.id).update_all(credit_limit: 80) + assert_equal 53, odegy.account.credit_limit + + assert_equal 80, odegy.reload_account.credit_limit + end + def test_build firm = Firm.new("name" => "GlobalMegaCorp") firm.save @@ -539,21 +530,21 @@ class HasOneAssociationsTest < ActiveRecord::TestCase def test_build_with_block car = Car.create(name: "honda") - bulb = car.build_bulb{ |b| b.color = "Red" } + bulb = car.build_bulb { |b| b.color = "Red" } assert_equal "RED!", bulb.color end def test_create_with_block car = Car.create(name: "honda") - bulb = car.create_bulb{ |b| b.color = "Red" } + bulb = car.create_bulb { |b| b.color = "Red" } assert_equal "RED!", bulb.color end def test_create_bang_with_block car = Car.create(name: "honda") - bulb = car.create_bulb!{ |b| b.color = "Red" } + bulb = car.create_bulb! { |b| b.color = "Red" } assert_equal "RED!", bulb.color end @@ -601,7 +592,7 @@ class HasOneAssociationsTest < ActiveRecord::TestCase new_ship = Ship.create(name: "new name") assert_queries(2) do - # One query for updating name and second query for updating pirate_id + # One query to nullify the old ship, one query to update the new ship pirate.ship = new_ship end @@ -654,15 +645,10 @@ class HasOneAssociationsTest < ActiveRecord::TestCase end end - def test_association_force_reload_with_only_true_is_deprecated - firm = Firm.find(1) - - assert_deprecated { firm.account(true) } - end - class SpecialBook < ActiveRecord::Base self.table_name = "books" belongs_to :author, class_name: "SpecialAuthor" + has_one :subscription, class_name: "SpecialSupscription", foreign_key: "subscriber_id" end class SpecialAuthor < ActiveRecord::Base @@ -670,11 +656,27 @@ class HasOneAssociationsTest < ActiveRecord::TestCase has_one :book, class_name: "SpecialBook", foreign_key: "author_id" end + class SpecialSupscription < ActiveRecord::Base + self.table_name = "subscriptions" + belongs_to :book, class_name: "SpecialBook" + end + def test_assocation_enum_works_properly author = SpecialAuthor.create!(name: "Test") book = SpecialBook.create!(status: "published") author.book = book - refute_equal 0, SpecialAuthor.joins(:book).where(books: { status: "published" } ).count + refute_equal 0, SpecialAuthor.joins(:book).where(books: { status: "published" }).count + end + + def test_assocation_enum_works_properly_with_nested_join + author = SpecialAuthor.create!(name: "Test") + book = SpecialBook.create!(status: "published") + author.book = book + + where_clause = { books: { subscriptions: { subscriber_id: nil } } } + assert_nothing_raised do + SpecialAuthor.joins(book: :subscription).where.not(where_clause) + end end end diff --git a/activerecord/test/cases/associations/has_one_through_associations_test.rb b/activerecord/test/cases/associations/has_one_through_associations_test.rb index 3a7f48fbb7..432c3526a5 100644 --- a/activerecord/test/cases/associations/has_one_through_associations_test.rb +++ b/activerecord/test/cases/associations/has_one_through_associations_test.rb @@ -82,7 +82,7 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase def test_set_record_to_nil_should_delete_association @member.club = nil @member.reload - assert_equal nil, @member.current_membership + assert_nil @member.current_membership assert_nil @member.club end @@ -95,7 +95,7 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase Member.all.merge!(includes: :club, where: ["name = ?", "Groucho Marx"]).to_a end assert_equal 1, members.size - assert_not_nil assert_no_queries {members[0].club} + assert_not_nil assert_no_queries { members[0].club } end def test_has_one_through_eager_loading_through_polymorphic @@ -103,19 +103,19 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase Member.all.merge!(includes: :sponsor_club, where: ["name = ?", "Groucho Marx"]).to_a end assert_equal 1, members.size - assert_not_nil assert_no_queries {members[0].sponsor_club} + assert_not_nil assert_no_queries { members[0].sponsor_club } end def test_has_one_through_with_conditions_eager_loading # conditions on the through table assert_equal clubs(:moustache_club), Member.all.merge!(includes: :favourite_club).find(@member.id).favourite_club memberships(:membership_of_favourite_club).update_columns(favourite: false) - assert_equal nil, Member.all.merge!(includes: :favourite_club).find(@member.id).reload.favourite_club + assert_nil Member.all.merge!(includes: :favourite_club).find(@member.id).reload.favourite_club # conditions on the source table assert_equal clubs(:moustache_club), Member.all.merge!(includes: :hairy_club).find(@member.id).hairy_club clubs(:moustache_club).update_columns(name: "Association of Clean-Shaven Persons") - assert_equal nil, Member.all.merge!(includes: :hairy_club).find(@member.id).reload.hairy_club + assert_nil Member.all.merge!(includes: :hairy_club).find(@member.id).reload.hairy_club end def test_has_one_through_polymorphic_with_source_type @@ -123,9 +123,9 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase end def test_eager_has_one_through_polymorphic_with_source_type - clubs = Club.all.merge!(includes: :sponsored_member, where: ["name = ?","Moustache and Eyebrow Fancier Club"]).to_a + clubs = Club.all.merge!(includes: :sponsored_member, where: ["name = ?", "Moustache and Eyebrow Fancier Club"]).to_a # Only the eyebrow fanciers club has a sponsored_member - assert_not_nil assert_no_queries {clubs[0].sponsored_member} + assert_not_nil assert_no_queries { clubs[0].sponsored_member } end def test_has_one_through_nonpreload_eagerloading @@ -133,7 +133,7 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase 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} + assert_not_nil assert_no_queries { members[0].club } end def test_has_one_through_nonpreload_eager_loading_through_polymorphic @@ -141,7 +141,7 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase 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} + assert_not_nil assert_no_queries { members[0].sponsor_club } end def test_has_one_through_nonpreload_eager_loading_through_polymorphic_with_more_than_one_through_record @@ -182,7 +182,7 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase @member.organization = @organization end assert_equal @organization, @member.organization - assert @organization.members.include?(@member) + assert_includes @organization.members, @member assert_equal "Extra", @member.member_detail.extra_data end @@ -197,16 +197,16 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase end assert_equal @organization, @member.organization assert_equal "Extra", @member.member_detail.extra_data - assert @organization.members.include?(@member) - assert !@new_organization.members.include?(@member) + assert_includes @organization.members, @member + assert_not_includes @new_organization.members, @member assert_no_difference "MemberDetail.count" do @member.organization = @new_organization end assert_equal @new_organization, @member.organization assert_equal "Extra", @member.member_detail.extra_data - assert !@organization.members.include?(@member) - assert @new_organization.members.include?(@member) + assert_not_includes @organization.members, @member + assert_includes @new_organization.members, @member end def test_preloading_has_one_through_on_belongs_to diff --git a/activerecord/test/cases/associations/inner_join_association_test.rb b/activerecord/test/cases/associations/inner_join_association_test.rb index be5c8854be..7414869c8f 100644 --- a/activerecord/test/cases/associations/inner_join_association_test.rb +++ b/activerecord/test/cases/associations/inner_join_association_test.rb @@ -20,7 +20,7 @@ class InnerJoinAssociationTest < ActiveRecord::TestCase def test_construct_finder_sql_does_not_table_name_collide_on_duplicate_associations assert_nothing_raised do - sql = Person.joins(agents: {agents: :agents}).joins(agents: {agents: {primary_contact: :agents}}).to_sql + sql = Person.joins(agents: { agents: :agents }).joins(agents: { agents: { primary_contact: :agents } }).to_sql assert_match(/agents_people_4/i, sql) end end @@ -47,7 +47,7 @@ class InnerJoinAssociationTest < ActiveRecord::TestCase end def test_join_conditions_allow_nil_associations - authors = Author.includes(:essays).where(essays: {id: nil}) + authors = Author.includes(:essays).where(essays: { id: nil }) assert_equal 2, authors.count end @@ -60,13 +60,13 @@ class InnerJoinAssociationTest < ActiveRecord::TestCase def test_find_with_implicit_inner_joins_honors_readonly_with_select authors = Author.joins(:posts).select("authors.*").to_a assert !authors.empty?, "expected authors to be non-empty" - assert authors.all? {|a| !a.readonly? }, "expected no authors to be readonly" + assert authors.all? { |a| !a.readonly? }, "expected no authors to be readonly" end def test_find_with_implicit_inner_joins_honors_readonly_false authors = Author.joins(:posts).readonly(false).to_a assert !authors.empty?, "expected authors to be non-empty" - assert authors.all? {|a| !a.readonly? }, "expected no authors to be readonly" + assert authors.all? { |a| !a.readonly? }, "expected no authors to be readonly" end def test_find_with_implicit_inner_joins_does_not_set_associations @@ -76,17 +76,17 @@ class InnerJoinAssociationTest < ActiveRecord::TestCase end def test_count_honors_implicit_inner_joins - real_count = Author.all.to_a.sum{|a| a.posts.count } + real_count = Author.all.to_a.sum { |a| a.posts.count } assert_equal real_count, Author.joins(:posts).count, "plain inner join count should match the number of referenced posts records" end def test_calculate_honors_implicit_inner_joins - real_count = Author.all.to_a.sum{|a| a.posts.count } + real_count = Author.all.to_a.sum { |a| a.posts.count } assert_equal real_count, Author.joins(:posts).calculate(:count, "authors.id"), "plain inner join count should match the number of referenced posts records" end def test_calculate_honors_implicit_inner_joins_and_distinct_and_conditions - real_count = Author.all.to_a.select {|a| a.posts.any? {|p| p.title.start_with?("Welcome")} }.length + real_count = Author.all.to_a.select { |a| a.posts.any? { |p| p.title.start_with?("Welcome") } }.length authors_with_welcoming_post_titles = Author.all.merge!(joins: :posts, where: "posts.title like 'Welcome%'").distinct.calculate(:count, "authors.id") assert_equal real_count, authors_with_welcoming_post_titles, "inner join and conditions should have only returned authors posting titles starting with 'Welcome'" end diff --git a/activerecord/test/cases/associations/inverse_associations_test.rb b/activerecord/test/cases/associations/inverse_associations_test.rb index 4534cbe664..6fe6ee6783 100644 --- a/activerecord/test/cases/associations/inverse_associations_test.rb +++ b/activerecord/test/cases/associations/inverse_associations_test.rb @@ -227,7 +227,7 @@ class InverseHasOneTests < ActiveRecord::TestCase end def test_parent_instance_should_be_shared_with_eager_loaded_child_on_find - m = Man.all.merge!(where: {name: "Gordon"}, includes: :face).first + m = Man.all.merge!(where: { name: "Gordon" }, includes: :face).first f = m.face assert_equal m.name, f.man.name, "Name of man should be the same before changes to parent instance" m.name = "Bongo" @@ -235,7 +235,7 @@ class InverseHasOneTests < ActiveRecord::TestCase f.man.name = "Mungo" assert_equal m.name, f.man.name, "Name of man should be the same after changes to child-owned instance" - m = Man.all.merge!(where: {name: "Gordon"}, includes: :face, order: "faces.id").first + m = Man.all.merge!(where: { name: "Gordon" }, includes: :face, order: "faces.id").first f = m.face assert_equal m.name, f.man.name, "Name of man should be the same before changes to parent instance" m.name = "Bongo" @@ -310,7 +310,7 @@ class InverseHasManyTests < ActiveRecord::TestCase end def test_parent_instance_should_be_shared_with_eager_loaded_children - m = Man.all.merge!(where: {name: "Gordon"}, includes: :interests).first + m = Man.all.merge!(where: { name: "Gordon" }, includes: :interests).first is = m.interests is.each do |i| assert_equal m.name, i.man.name, "Name of man should be the same before changes to parent instance" @@ -320,7 +320,7 @@ class InverseHasManyTests < ActiveRecord::TestCase assert_equal m.name, i.man.name, "Name of man should be the same after changes to child-owned instance" end - m = Man.all.merge!(where: {name: "Gordon"}, includes: :interests, order: "interests.id").first + m = Man.all.merge!(where: { name: "Gordon" }, includes: :interests, order: "interests.id").first is = m.interests is.each do |i| assert_equal m.name, i.man.name, "Name of man should be the same before changes to parent instance" @@ -333,7 +333,7 @@ class InverseHasManyTests < ActiveRecord::TestCase def test_parent_instance_should_be_shared_with_newly_block_style_built_child m = Man.first - i = m.interests.build {|ii| ii.topic = "Industrial Revolution Re-enactment"} + i = m.interests.build { |ii| ii.topic = "Industrial Revolution Re-enactment" } assert_not_nil i.topic, "Child attributes supplied to build via blocks should be populated" assert_not_nil i.man assert_equal m.name, i.man.name, "Name of man should be the same before changes to parent instance" @@ -356,7 +356,7 @@ class InverseHasManyTests < ActiveRecord::TestCase def test_parent_instance_should_be_shared_with_newly_block_style_created_child m = Man.first - i = m.interests.create {|ii| ii.topic = "Industrial Revolution Re-enactment"} + i = m.interests.create { |ii| ii.topic = "Industrial Revolution Re-enactment" } assert_not_nil i.topic, "Child attributes supplied to create via blocks should be populated" assert_not_nil i.man assert_equal m.name, i.man.name, "Name of man should be the same before changes to parent instance" @@ -494,6 +494,33 @@ class InverseHasManyTests < ActiveRecord::TestCase assert !man.persisted? end + + def test_inverse_instance_should_be_set_before_find_callbacks_are_run + reset_callbacks(Interest, :find) do + Interest.after_find { raise unless association(:man).loaded? && man.present? } + + assert Man.first.interests.reload.any? + assert Man.includes(:interests).first.interests.any? + assert Man.joins(:interests).includes(:interests).first.interests.any? + end + end + + def test_inverse_instance_should_be_set_before_initialize_callbacks_are_run + reset_callbacks(Interest, :initialize) do + Interest.after_initialize { raise unless association(:man).loaded? && man.present? } + + assert Man.first.interests.reload.any? + assert Man.includes(:interests).first.interests.any? + assert Man.joins(:interests).includes(:interests).first.interests.any? + end + end + + def reset_callbacks(target, type) + old_callbacks = target.send(:get_callbacks, type).deep_dup + yield + ensure + target.send(:set_callbacks, type, old_callbacks) if old_callbacks + end end class InverseBelongsToTests < ActiveRecord::TestCase @@ -510,7 +537,7 @@ class InverseBelongsToTests < ActiveRecord::TestCase end def test_eager_loaded_child_instance_should_be_shared_with_parent_on_find - f = Face.all.merge!(includes: :man, where: {description: "trusting"}).first + f = Face.all.merge!(includes: :man, where: { description: "trusting" }).first m = f.man assert_equal f.description, m.face.description, "Description of face should be the same before changes to child instance" f.description = "gormless" @@ -518,7 +545,7 @@ class InverseBelongsToTests < ActiveRecord::TestCase m.face.description = "pleasing" assert_equal f.description, m.face.description, "Description of face should be the same after changes to parent-owned instance" - f = Face.all.merge!(includes: :man, order: "men.id", where: {description: "trusting"}).first + f = Face.all.merge!(includes: :man, order: "men.id", where: { description: "trusting" }).first m = f.man assert_equal f.description, m.face.description, "Description of face should be the same before changes to child instance" f.description = "gormless" @@ -553,7 +580,7 @@ class InverseBelongsToTests < ActiveRecord::TestCase i = interests(:trainspotting) m = i.man assert_not_nil m.interests - iz = m.interests.detect { |_iz| _iz.id == i.id} + iz = m.interests.detect { |_iz| _iz.id == i.id } assert_not_nil iz assert_equal i.topic, iz.topic, "Interest topics should be the same before changes to child" i.topic = "Eating cheese with a spoon" @@ -583,7 +610,7 @@ class InversePolymorphicBelongsToTests < ActiveRecord::TestCase fixtures :men, :faces, :interests def test_child_instance_should_be_shared_with_parent_on_find - f = Face.all.merge!(where: {description: "confused"}).first + f = Face.all.merge!(where: { description: "confused" }).first m = f.polymorphic_man assert_equal f.description, m.polymorphic_face.description, "Description of face should be the same before changes to child instance" f.description = "gormless" @@ -593,7 +620,7 @@ class InversePolymorphicBelongsToTests < ActiveRecord::TestCase end def test_eager_loaded_child_instance_should_be_shared_with_parent_on_find - f = Face.all.merge!(where: {description: "confused"}, includes: :man).first + f = Face.all.merge!(where: { description: "confused" }, includes: :man).first m = f.polymorphic_man assert_equal f.description, m.polymorphic_face.description, "Description of face should be the same before changes to child instance" f.description = "gormless" @@ -601,7 +628,7 @@ class InversePolymorphicBelongsToTests < ActiveRecord::TestCase m.polymorphic_face.description = "pleasing" assert_equal f.description, m.polymorphic_face.description, "Description of face should be the same after changes to parent-owned instance" - f = Face.all.merge!(where: {description: "confused"}, includes: :man, order: "men.id").first + f = Face.all.merge!(where: { description: "confused" }, includes: :man, order: "men.id").first m = f.polymorphic_man assert_equal f.description, m.polymorphic_face.description, "Description of face should be the same before changes to child instance" f.description = "gormless" @@ -654,7 +681,7 @@ class InversePolymorphicBelongsToTests < ActiveRecord::TestCase i = interests(:llama_wrangling) m = i.polymorphic_man assert_not_nil m.polymorphic_interests - iz = m.polymorphic_interests.detect { |_iz| _iz.id == i.id} + iz = m.polymorphic_interests.detect { |_iz| _iz.id == i.id } assert_not_nil iz assert_equal i.topic, iz.topic, "Interest topics should be the same before changes to child" i.topic = "Eating cheese with a spoon" diff --git a/activerecord/test/cases/associations/join_model_test.rb b/activerecord/test/cases/associations/join_model_test.rb index cf08cc9537..a4345f3857 100644 --- a/activerecord/test/cases/associations/join_model_test.rb +++ b/activerecord/test/cases/associations/join_model_test.rb @@ -24,15 +24,15 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase :edges def test_has_many - assert authors(:david).categories.include?(categories(:general)) + assert_includes authors(:david).categories, categories(:general) end def test_has_many_inherited - assert authors(:mary).categories.include?(categories(:sti_test)) + assert_includes authors(:mary).categories, categories(:sti_test) end def test_inherited_has_many - assert categories(:sti_test).authors.include?(authors(:mary)) + assert_includes categories(:sti_test).authors, authors(:mary) end def test_has_many_distinct_through_join_model @@ -155,21 +155,21 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase old_count = posts(:welcome).taggings.count tagging = posts(:welcome).taggings.create(tag: tags(:misc)) assert_equal "Post", tagging.taggable_type - assert_equal old_count+1, posts(:welcome).taggings.count + assert_equal old_count + 1, posts(:welcome).taggings.count end def test_create_bang_polymorphic_with_has_many_scope old_count = posts(:welcome).taggings.count tagging = posts(:welcome).taggings.create!(tag: tags(:misc)) assert_equal "Post", tagging.taggable_type - assert_equal old_count+1, posts(:welcome).taggings.count + assert_equal old_count + 1, posts(:welcome).taggings.count end def test_create_polymorphic_has_one_with_scope old_count = Tagging.count tagging = posts(:welcome).create_tagging(tag: tags(:misc)) assert_equal "Post", tagging.taggable_type - assert_equal old_count+1, Tagging.count + assert_equal old_count + 1, Tagging.count end def test_delete_polymorphic_has_many_with_delete_all @@ -179,7 +179,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase old_count = Tagging.count post.destroy - assert_equal old_count-1, Tagging.count + assert_equal old_count - 1, Tagging.count assert_equal 0, posts(:welcome).taggings.count end @@ -190,7 +190,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase old_count = Tagging.count post.destroy - assert_equal old_count-1, Tagging.count + assert_equal old_count - 1, Tagging.count assert_equal 0, posts(:welcome).taggings.count end @@ -212,7 +212,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase old_count = Tagging.count post.destroy - assert_equal old_count-1, Tagging.count + assert_equal old_count - 1, Tagging.count posts(:welcome).association(:tagging).reload assert_nil posts(:welcome).tagging end @@ -237,7 +237,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase category = categories(:sti_test) ernie = category.authors_with_select.create(name: "Ernie") assert_nothing_raised do - assert_equal ernie, category.authors_with_select.detect {|a| a.name == "Ernie"} + assert_equal ernie, category.authors_with_select.detect { |a| a.name == "Ernie" } end end @@ -402,7 +402,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 end def test_has_many_through_polymorphic_has_many @@ -413,7 +413,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase author = Author.includes(:taggings).find authors(:david).id expected_taggings = taggings(:welcome_general, :thinking_general) assert_no_queries do - assert_equal expected_taggings, author.taggings.distinct.sort_by(&:id) + assert_equal expected_taggings, author.taggings.uniq.sort_by(&:id) end end @@ -421,7 +421,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase author = Author.all.merge!(where: ["name = ?", "David"], includes: :comments, order: "comments.id").first SpecialComment.new; VerySpecialComment.new assert_no_queries do - assert_equal [1,2,3,5,6,7,8,9,10,12], author.comments.collect(&:id) + assert_equal [1, 2, 3, 5, 6, 7, 8, 9, 10, 12], author.comments.collect(&:id) end end @@ -467,10 +467,10 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase saved_post.tags << new_tag assert new_tag.persisted? #consistent with habtm! assert saved_post.persisted? - assert saved_post.tags.include?(new_tag) + assert_includes saved_post.tags, new_tag assert new_tag.persisted? - assert saved_post.reload.tags.reload.include?(new_tag) + assert_includes saved_post.reload.tags.reload, new_tag new_post = Post.new(title: "Association replacement works!", body: "You best believe it.") saved_tag = tags(:general) @@ -478,11 +478,11 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase new_post.tags << saved_tag assert !new_post.persisted? assert saved_tag.persisted? - assert new_post.tags.include?(saved_tag) + assert_includes new_post.tags, saved_tag new_post.save! assert new_post.persisted? - assert new_post.reload.tags.reload.include?(saved_tag) + assert_includes new_post.reload.tags.reload, saved_tag assert !posts(:thinking).tags.build.persisted? assert !posts(:thinking).tags.new.persisted? @@ -493,25 +493,25 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase push = Tag.create!(name: "pushme") post_thinking = posts(:thinking) assert_nothing_raised { post_thinking.tags << push } - assert_nil( wrong = post_thinking.tags.detect { |t| t.class != Tag }, + assert_nil(wrong = post_thinking.tags.detect { |t| t.class != Tag }, message = "Expected a Tag in tags collection, got #{wrong.class}.") - assert_nil( wrong = post_thinking.taggings.detect { |t| t.class != Tagging }, + assert_nil(wrong = post_thinking.taggings.detect { |t| t.class != Tagging }, message = "Expected a Tagging in taggings collection, got #{wrong.class}.") assert_equal(count + 1, post_thinking.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 }, + assert_nil(wrong = post_thinking.tags.detect { |t| t.class != Tag }, message = "Expected a Tag in tags collection, got #{wrong.class}.") - assert_nil( wrong = post_thinking.taggings.detect { |t| t.class != Tagging }, + assert_nil(wrong = post_thinking.taggings.detect { |t| t.class != Tagging }, message = "Expected a Tagging in taggings collection, got #{wrong.class}.") assert_equal(count + 2, post_thinking.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 }, + assert_nil(wrong = post_thinking.tags.detect { |t| t.class != Tag }, message = "Expected a Tag in tags collection, got #{wrong.class}.") - assert_nil( wrong = post_thinking.taggings.detect { |t| t.class != Tagging }, + assert_nil(wrong = post_thinking.taggings.detect { |t| t.class != Tagging }, message = "Expected a Tagging in taggings collection, got #{wrong.class}.") assert_equal(count + 4, post_thinking.reload.tags.size) assert_equal(count + 4, post_thinking.tags.reload.size) @@ -642,20 +642,20 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase def test_polymorphic_has_many expected = taggings(:welcome_general) p = Post.all.merge!(includes: :taggings).find(posts(:welcome).id) - assert_no_queries {assert p.taggings.include?(expected)} - assert posts(:welcome).taggings.include?(taggings(:welcome_general)) + assert_no_queries { assert_includes p.taggings, expected } + assert_includes posts(:welcome).taggings, taggings(:welcome_general) end def test_polymorphic_has_one expected = posts(:welcome) tagging = Tagging.all.merge!(includes: :taggable).find(taggings(:welcome_general).id) - assert_no_queries { assert_equal expected, tagging.taggable} + assert_no_queries { assert_equal expected, tagging.taggable } end def test_polymorphic_belongs_to - p = Post.all.merge!(includes: {taggings: :taggable}).find(posts(:welcome).id) - assert_no_queries {assert_equal posts(:welcome), p.taggings.first.taggable} + p = Post.all.merge!(includes: { taggings: :taggable }).find(posts(:welcome).id) + assert_no_queries { assert_equal posts(:welcome), p.taggings.first.taggable } end def test_preload_polymorphic_has_many_through @@ -675,8 +675,8 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase end taggables = taggings.map(&:taggable) - assert taggables.include?(items(:dvd)) - assert taggables.include?(posts(:welcome)) + assert_includes taggables, items(:dvd) + assert_includes taggables, posts(:welcome) end def test_preload_nil_polymorphic_belongs_to @@ -709,7 +709,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase assert_no_queries do assert david.categories.loaded? - assert david.categories.include?(category) + assert_includes david.categories, category end end @@ -720,7 +720,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase david.reload assert ! david.categories.loaded? assert_queries(1) do - assert david.categories.include?(category) + assert_includes david.categories, category end assert ! david.categories.loaded? end @@ -748,17 +748,17 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase def test_proper_error_message_for_eager_load_and_includes_association_errors includes_error = assert_raises(ActiveRecord::ConfigurationError) { - Post.includes(:nonexistent_relation).where(nonexistent_relation: {name: "Rochester"}).find(1) + Post.includes(:nonexistent_relation).where(nonexistent_relation: { name: "Rochester" }).find(1) } assert_equal("Can't join 'Post' to association named 'nonexistent_relation'; perhaps you misspelled it?", includes_error.message) eager_load_error = assert_raises(ActiveRecord::ConfigurationError) { - Post.eager_load(:nonexistent_relation).where(nonexistent_relation: {name: "Rochester"}).find(1) + Post.eager_load(:nonexistent_relation).where(nonexistent_relation: { name: "Rochester" }).find(1) } assert_equal("Can't join 'Post' to association named 'nonexistent_relation'; perhaps you misspelled it?", eager_load_error.message) includes_and_eager_load_error = assert_raises(ActiveRecord::ConfigurationError) { - Post.eager_load(:nonexistent_relation).includes(:nonexistent_relation).where(nonexistent_relation: {name: "Rochester"}).find(1) + Post.eager_load(:nonexistent_relation).includes(:nonexistent_relation).where(nonexistent_relation: { name: "Rochester" }).find(1) } assert_equal("Can't join 'Post' to association named 'nonexistent_relation'; perhaps you misspelled it?", includes_and_eager_load_error.message) end 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 003240bb26..42dbbad1c8 100644 --- a/activerecord/test/cases/associations/left_outer_join_association_test.rb +++ b/activerecord/test/cases/associations/left_outer_join_association_test.rb @@ -5,7 +5,6 @@ require "models/author" require "models/essay" require "models/categorization" require "models/person" -require "active_support/core_ext/regexp" class LeftOuterJoinAssociationTest < ActiveRecord::TestCase fixtures :authors, :essays, :posts, :comments, :categorizations, :people @@ -18,35 +17,44 @@ class LeftOuterJoinAssociationTest < ActiveRecord::TestCase def test_construct_finder_sql_does_not_table_name_collide_on_duplicate_associations assert_nothing_raised do queries = capture_sql do - Person.left_outer_joins(agents: {agents: :agents}) - .left_outer_joins(agents: {agents: {primary_contact: :agents}}).to_a + Person.left_outer_joins(agents: { agents: :agents }) + .left_outer_joins(agents: { agents: { primary_contact: :agents } }).to_a end assert queries.any? { |sql| /agents_people_4/i.match?(sql) } end end - def test_construct_finder_sql_executes_a_left_outer_join - assert_not_equal Author.count, Author.joins(:posts).count - assert_equal Author.count, Author.left_outer_joins(:posts).count + def test_left_outer_joins_count_is_same_as_size_of_loaded_results + assert_equal 17, Post.left_outer_joins(:comments).to_a.size + assert_equal 17, Post.left_outer_joins(:comments).count end - def test_left_outer_join_by_left_joins - assert_not_equal Author.count, Author.joins(:posts).count - assert_equal Author.count, Author.left_joins(:posts).count + def test_left_joins_aliases_left_outer_joins + assert_equal Post.left_outer_joins(:comments).to_sql, Post.left_joins(:comments).to_sql + end + + def test_left_outer_joins_return_has_value_for_every_comment + all_post_ids = Post.pluck(:id) + assert_equal all_post_ids, all_post_ids & Post.left_outer_joins(:comments).pluck(:id) + end + + def test_left_outer_joins_actually_does_a_left_outer_join + queries = capture_sql { Author.left_outer_joins(:posts).to_a } + assert queries.any? { |sql| /LEFT OUTER JOIN/i.match?(sql) } end def test_construct_finder_sql_ignores_empty_left_outer_joins_hash - queries = capture_sql { Author.left_outer_joins({}) } + queries = capture_sql { Author.left_outer_joins({}).to_a } assert queries.none? { |sql| /LEFT OUTER JOIN/i.match?(sql) } end def test_construct_finder_sql_ignores_empty_left_outer_joins_array - queries = capture_sql { Author.left_outer_joins([]) } + queries = capture_sql { Author.left_outer_joins([]).to_a } assert queries.none? { |sql| /LEFT OUTER JOIN/i.match?(sql) } end def test_left_outer_joins_forbids_to_use_string_as_argument - assert_raise(ArgumentError){ Author.left_outer_joins('LEFT OUTER JOIN "posts" ON "posts"."user_id" = "users"."id"').to_a } + assert_raise(ArgumentError) { Author.left_outer_joins('LEFT OUTER JOIN "posts" ON "posts"."user_id" = "users"."id"').to_a } end def test_join_conditions_added_to_join_clause diff --git a/activerecord/test/cases/associations_test.rb b/activerecord/test/cases/associations_test.rb index 45aefde3e1..a223b4338f 100644 --- a/activerecord/test/cases/associations_test.rb +++ b/activerecord/test/cases/associations_test.rb @@ -57,8 +57,8 @@ class AssociationsTest < ActiveRecord::TestCase end def test_include_with_order_works - assert_nothing_raised {Account.all.merge!(order: "id", includes: :firm).first} - assert_nothing_raised {Account.all.merge!(order: :id, includes: :firm).first} + assert_nothing_raised { Account.all.merge!(order: "id", includes: :firm).first } + assert_nothing_raised { Account.all.merge!(order: :id, includes: :firm).first } end def test_bad_collection_keys @@ -88,10 +88,10 @@ class AssociationsTest < ActiveRecord::TestCase assert firm.clients.empty?, "New firm should have cached no client objects" assert_equal 0, firm.clients.size, "New firm should have cached 0 clients count" - ActiveSupport::Deprecation.silence do - assert !firm.clients(true).empty?, "New firm should have reloaded client objects" - assert_equal 1, firm.clients(true).size, "New firm should have reloaded clients count" - end + firm.clients.reload + + assert !firm.clients.empty?, "New firm should have reloaded client objects" + assert_equal 1, firm.clients.size, "New firm should have reloaded clients count" end def test_using_limitable_reflections_helper @@ -104,19 +104,6 @@ class AssociationsTest < ActiveRecord::TestCase assert !using_limitable_reflections.call(mixed_reflections), "No collection associations (has many style) should pass" end - def test_force_reload_is_uncached - firm = Firm.create!("name" => "A New Firm, Inc") - Client.create!("name" => "TheClient.com", :firm => firm) - - ActiveSupport::Deprecation.silence do - ActiveRecord::Base.cache do - firm.clients.each {} - assert_queries(0) { assert_not_nil firm.clients.each {} } - assert_queries(1) { assert_not_nil firm.clients(true).each {} } - end - end - end - def test_association_with_references firm = companies(:first_firm) assert_includes firm.association_with_references.references_values, "foo" @@ -131,7 +118,7 @@ class AssociationProxyTest < ActiveRecord::TestCase david.posts << (post = Post.new(title: "New on Edge", body: "More cool stuff!")) assert !david.posts.loaded? - assert david.posts.include?(post) + assert_includes david.posts, post end def test_push_has_many_through_does_not_load_target @@ -139,7 +126,7 @@ class AssociationProxyTest < ActiveRecord::TestCase david.categories << categories(:technology) assert !david.categories.loaded? - assert david.categories.include?(categories(:technology)) + assert_includes david.categories, categories(:technology) end def test_push_followed_by_save_does_not_load_target @@ -149,7 +136,7 @@ class AssociationProxyTest < ActiveRecord::TestCase assert !david.posts.loaded? david.save assert !david.posts.loaded? - assert david.posts.include?(post) + assert_includes david.posts, post end def test_push_does_not_lose_additions_to_new_record @@ -199,13 +186,13 @@ class AssociationProxyTest < ActiveRecord::TestCase end def test_create_via_association_with_block - post = authors(:david).posts.create(title: "New on Edge") {|p| p.body = "More cool stuff!"} + post = authors(:david).posts.create(title: "New on Edge") { |p| p.body = "More cool stuff!" } assert_equal post.title, "New on Edge" assert_equal post.body, "More cool stuff!" end def test_create_with_bang_via_association_with_block - post = authors(:david).posts.create!(title: "New on Edge") {|p| p.body = "More cool stuff!"} + post = authors(:david).posts.create!(title: "New on Edge") { |p| p.body = "More cool stuff!" } assert_equal post.title, "New on Edge" assert_equal post.body, "More cool stuff!" end @@ -256,6 +243,13 @@ class AssociationProxyTest < ActiveRecord::TestCase assert_no_queries { david.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) } + end + def test_reset_unloads_target david = authors(:david) david.posts.reload diff --git a/activerecord/test/cases/attribute_methods/read_test.rb b/activerecord/test/cases/attribute_methods/read_test.rb index 2d2ecb4079..978dd93fa4 100644 --- a/activerecord/test/cases/attribute_methods/read_test.rb +++ b/activerecord/test/cases/attribute_methods/read_test.rb @@ -1,5 +1,4 @@ require "cases/helper" -require "thread" module ActiveRecord module AttributeMethods @@ -15,6 +14,7 @@ module ActiveRecord def self.decorate_matching_attribute_types(*); end def self.initialize_generated_modules; end + include ActiveRecord::DefineCallbacks include ActiveRecord::AttributeMethods def self.attribute_names @@ -40,13 +40,13 @@ module ActiveRecord instance = @klass.new @klass.attribute_names.each do |name| - assert !instance.methods.map(&:to_s).include?(name) + assert_not_includes instance.methods.map(&:to_s), name end @klass.define_attribute_methods @klass.attribute_names.each do |name| - assert instance.methods.map(&:to_s).include?(name), "#{name} is not defined" + assert_includes instance.methods.map(&:to_s), name, "#{name} is not defined" end end diff --git a/activerecord/test/cases/attribute_methods_test.rb b/activerecord/test/cases/attribute_methods_test.rb index 7a2041937d..3dc0c0ce53 100644 --- a/activerecord/test/cases/attribute_methods_test.rb +++ b/activerecord/test/cases/attribute_methods_test.rb @@ -92,7 +92,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase test "attribute keys on a new instance" do t = Topic.new - assert_equal nil, t.title, "The topics table has a title column, so it should be nil" + assert_nil t.title, "The topics table has a title column, so it should be nil" assert_raise(NoMethodError) { t.title2 } end @@ -156,7 +156,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase keyboard = Keyboard.create keyboard.key_number = "10" assert_equal "10", keyboard.id_before_type_cast - assert_equal nil, keyboard.read_attribute_before_type_cast("id") + assert_nil keyboard.read_attribute_before_type_cast("id") assert_equal "10", keyboard.read_attribute_before_type_cast("key_number") assert_equal "10", keyboard.read_attribute_before_type_cast(:key_number) end @@ -213,7 +213,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase record.written_on = "345643456" assert_equal "345643456", record.written_on_before_type_cast - assert_equal nil, record.written_on + assert_nil record.written_on record.written_on = "2009-10-11 12:13:14" assert_equal "2009-10-11 12:13:14", record.written_on_before_type_cast @@ -294,7 +294,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase topic = Topic.new(new_topic) assert_equal new_topic[:title], topic.title - topic.attributes= new_topic_values + topic.attributes = new_topic_values assert_equal new_topic_values[:title], topic.title end @@ -319,6 +319,13 @@ class AttributeMethodsTest < ActiveRecord::TestCase assert_equal "Still another topic: part 4", topic.title end + test "write_attribute can write aliased attributes as well" do + topic = Topic.new(title: "Don't change the topic") + topic.write_attribute :heading, "New topic" + + assert_equal "New topic", topic.title + end + test "read_attribute" do topic = Topic.new topic.title = "Don't change the topic" @@ -329,6 +336,16 @@ class AttributeMethodsTest < ActiveRecord::TestCase assert_equal "Don't change the topic", topic[:title] end + test "read_attribute can read aliased attributes as well" do + topic = Topic.new(title: "Don't change the topic") + + assert_equal "Don't change the topic", topic.read_attribute("heading") + assert_equal "Don't change the topic", topic["heading"] + + assert_equal "Don't change the topic", topic.read_attribute(:heading) + assert_equal "Don't change the topic", topic[:heading] + end + test "read_attribute raises ActiveModel::MissingAttributeError when the attribute does not exist" do computer = Computer.select("id").first assert_raises(ActiveModel::MissingAttributeError) { computer[:developer] } @@ -609,7 +626,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase utc_time = Time.utc(2008, 1, 1) cst_time = utc_time.in_time_zone("Central Time (US & Canada)") in_time_zone "Pacific Time (US & Canada)" do - record = @target.new + record = @target.new record.written_on = cst_time assert_equal utc_time, record.written_on assert_equal ActiveSupport::TimeZone["Pacific Time (US & Canada)"], record.written_on.time_zone @@ -633,7 +650,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase (-11..13).each do |timezone_offset| time_string = utc_time.in_time_zone(timezone_offset).to_s in_time_zone "Pacific Time (US & Canada)" do - record = @target.new + record = @target.new record.written_on = time_string assert_equal Time.zone.parse(time_string), record.written_on assert_equal ActiveSupport::TimeZone["Pacific Time (US & Canada)"], record.written_on.time_zone @@ -654,7 +671,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase test "setting a time zone-aware attribute to a blank string returns nil" do in_time_zone "Pacific Time (US & Canada)" do - record = @target.new + record = @target.new record.written_on = " " assert_nil record.written_on assert_nil record[:written_on] @@ -665,7 +682,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase time_string = "Tue Jan 01 00:00:00 2008" (-11..13).each do |timezone_offset| in_time_zone timezone_offset do - record = @target.new + record = @target.new record.written_on = time_string assert_equal Time.zone.parse(time_string), record.written_on assert_equal ActiveSupport::TimeZone[timezone_offset], record.written_on.time_zone @@ -677,7 +694,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase test "setting a time zone-aware datetime in the current time zone" do utc_time = Time.utc(2008, 1, 1) in_time_zone "Pacific Time (US & Canada)" do - record = @target.new + record = @target.new record.written_on = utc_time.in_time_zone assert_equal utc_time, record.written_on assert_equal ActiveSupport::TimeZone["Pacific Time (US & Canada)"], record.written_on.time_zone @@ -737,7 +754,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase test "time zone-aware attributes do not recurse infinitely on invalid values" do in_time_zone "Pacific Time (US & Canada)" do record = @target.new(bonus_time: []) - assert_equal nil, record.bonus_time + assert_nil record.bonus_time end end @@ -755,7 +772,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase topic = @target.new(title: "The pros and cons of programming naked.") assert !topic.respond_to?(:title) exception = assert_raise(NoMethodError) { topic.title } - assert exception.message.include?("private method") + assert_includes exception.message, "private method" assert_equal "I'm private", topic.send(:title) end @@ -765,7 +782,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase topic = @target.new assert !topic.respond_to?(:title=) exception = assert_raise(NoMethodError) { topic.title = "Pants" } - assert exception.message.include?("private method") + assert_includes exception.message, "private method" topic.send(:title=, "Very large pants") end @@ -775,7 +792,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase topic = @target.new(title: "Isaac Newton's pants") assert !topic.respond_to?(:title?) exception = assert_raise(NoMethodError) { topic.title? } - assert exception.message.include?("private method") + assert_includes exception.message, "private method" assert topic.send(:title?) end @@ -1003,10 +1020,10 @@ class AttributeMethodsTest < ActiveRecord::TestCase def privatize(method_signature) @target.class_eval(<<-private_method, __FILE__, __LINE__ + 1) - private - def #{method_signature} - "I'm private" - end - private_method + private + def #{method_signature} + "I'm private" + end + private_method end end diff --git a/activerecord/test/cases/attribute_set_test.rb b/activerecord/test/cases/attribute_set_test.rb index 059b5b2401..bd4b200735 100644 --- a/activerecord/test/cases/attribute_set_test.rb +++ b/activerecord/test/cases/attribute_set_test.rb @@ -25,7 +25,7 @@ module ActiveRecord attributes = builder.build_from_database(foo: "3.3") assert_equal "3.3", attributes[:foo].value_before_type_cast - assert_equal nil, attributes[:bar].value_before_type_cast + assert_nil attributes[:bar].value_before_type_cast assert_equal :bar, attributes[:bar].name end diff --git a/activerecord/test/cases/attributes_test.rb b/activerecord/test/cases/attributes_test.rb index 7ec1f6f18c..3705a6be89 100644 --- a/activerecord/test/cases/attributes_test.rb +++ b/activerecord/test/cases/attributes_test.rb @@ -112,7 +112,7 @@ module ActiveRecord assert_equal 7, klass.attribute_types.length assert_equal 7, klass.column_defaults.length - assert klass.attribute_types.include?("wibble") + assert_includes klass.attribute_types, "wibble" end test "the given default value is cast from user" do @@ -249,5 +249,19 @@ module ActiveRecord model.foo = "lol" refute model.changed? end + + test "attributes not backed by database columns appear in inspect" do + inspection = OverloadedType.new.inspect + + assert_includes inspection, "non_existent_decimal" + end + + test "attributes do not require a type" do + klass = Class.new(OverloadedType) do + attribute :no_type + end + assert_equal 1, klass.new(no_type: 1).no_type + assert_equal "foo", klass.new(no_type: "foo").no_type + end end end diff --git a/activerecord/test/cases/autosave_association_test.rb b/activerecord/test/cases/autosave_association_test.rb index ae5127cc56..2203aa1788 100644 --- a/activerecord/test/cases/autosave_association_test.rb +++ b/activerecord/test/cases/autosave_association_test.rb @@ -1,5 +1,6 @@ require "cases/helper" require "models/bird" +require "models/post" require "models/comment" require "models/company" require "models/customer" @@ -11,7 +12,6 @@ require "models/order" require "models/parrot" require "models/person" require "models/pirate" -require "models/post" require "models/reader" require "models/ship" require "models/ship_part" @@ -36,11 +36,11 @@ class TestAutosaveAssociationsInGeneral < ActiveRecord::TestCase private - def should_be_cool - unless self.first_name == "cool" - errors.add :first_name, "not cool" + def should_be_cool + unless first_name == "cool" + errors.add :first_name, "not cool" + end end - end } reference = Class.new(ActiveRecord::Base) { self.table_name = "references" @@ -196,21 +196,21 @@ class TestDefaultAutosaveAssociationOnAHasOneAssociation < ActiveRecord::TestCas end def test_callbacks_firing_order_on_create - eye = Eye.create(iris_attributes: {color: "honey"}) + eye = Eye.create(iris_attributes: { color: "honey" }) assert_equal [true, false], eye.after_create_callbacks_stack end def test_callbacks_firing_order_on_update - eye = Eye.create(iris_attributes: {color: "honey"}) - eye.update(iris_attributes: {color: "green"}) + eye = Eye.create(iris_attributes: { color: "honey" }) + eye.update(iris_attributes: { color: "green" }) assert_equal [true, false], eye.after_update_callbacks_stack end def test_callbacks_firing_order_on_save - eye = Eye.create(iris_attributes: {color: "honey"}) + eye = Eye.create(iris_attributes: { color: "honey" }) assert_equal [false, false], eye.after_save_callbacks_stack - eye.update(iris_attributes: {color: "blue"}) + eye.update(iris_attributes: { color: "blue" }) assert_equal [false, false, false, false], eye.after_save_callbacks_stack end end @@ -443,7 +443,7 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociationWithAcceptsNestedAttrib assert_not invalid_electron.valid? assert valid_electron.valid? assert_not molecule.valid? - assert_equal [{error: :blank}], molecule.errors.details["electrons.name"] + assert_equal [{ error: :blank }], molecule.errors.details[:"electrons.name"] end def test_errors_details_should_be_indexed_when_passed_as_array @@ -457,8 +457,8 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociationWithAcceptsNestedAttrib assert_not tuning_peg_invalid.valid? assert tuning_peg_valid.valid? assert_not guitar.valid? - assert_equal [{error: :not_a_number, value: nil}] , guitar.errors.details["tuning_pegs[1].pitch"] - assert_equal [], guitar.errors.details["tuning_pegs.pitch"] + assert_equal [{ error: :not_a_number, value: nil }], guitar.errors.details[:"tuning_pegs[1].pitch"] + assert_equal [], guitar.errors.details[:"tuning_pegs.pitch"] end def test_errors_details_should_be_indexed_when_global_flag_is_set @@ -474,8 +474,8 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociationWithAcceptsNestedAttrib assert_not invalid_electron.valid? assert valid_electron.valid? assert_not molecule.valid? - assert_equal [{error: :blank}], molecule.errors.details["electrons[1].name"] - assert_equal [], molecule.errors.details["electrons.name"] + assert_equal [{ error: :blank }], molecule.errors.details[:"electrons[1].name"] + assert_equal [], molecule.errors.details[:"electrons.name"] ensure ActiveRecord::Base.index_nested_attribute_errors = old_attribute_config end @@ -585,7 +585,7 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociation < ActiveRecord::TestCa firm.save firm.reload assert_equal 2, firm.clients.length - assert firm.clients.include?(companies(:second_client)) + assert_includes firm.clients, companies(:second_client) end def test_assign_ids_for_through_a_belongs_to @@ -594,7 +594,7 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociation < ActiveRecord::TestCa post.save post.reload assert_equal 2, post.people.length - assert post.people.include?(people(:david)) + assert_includes post.people, people(:david) end def test_build_before_save @@ -610,7 +610,7 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociation < ActiveRecord::TestCa def test_build_many_before_save company = companies(:first_firm) - assert_no_queries(ignore_none: false) { company.clients_of_firm.build([{"name" => "Another Client"}, {"name" => "Another Client II"}]) } + assert_no_queries(ignore_none: false) { company.clients_of_firm.build([{ "name" => "Another Client" }, { "name" => "Another Client II" }]) } company.name += "-changed" assert_queries(3) { assert company.save } @@ -619,7 +619,7 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociation < ActiveRecord::TestCa def test_build_via_block_before_save company = companies(:first_firm) - new_client = assert_no_queries(ignore_none: false) { company.clients_of_firm.build {|client| client.name = "Another Client" } } + new_client = assert_no_queries(ignore_none: false) { company.clients_of_firm.build { |client| client.name = "Another Client" } } assert !company.clients_of_firm.loaded? company.name += "-changed" @@ -631,7 +631,7 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociation < ActiveRecord::TestCa def test_build_many_via_block_before_save company = companies(:first_firm) assert_no_queries(ignore_none: false) do - company.clients_of_firm.build([{"name" => "Another Client"}, {"name" => "Another Client II"}]) do |client| + company.clients_of_firm.build([{ "name" => "Another Client" }, { "name" => "Another Client II" }]) do |client| client.name = "changed" end end @@ -647,7 +647,7 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociation < ActiveRecord::TestCa assert firm.save firm.reload assert_equal 2, firm.clients.length - assert firm.clients.include?(Client.find_by_name("New Client")) + assert_includes firm.clients, Client.find_by_name("New Client") end end @@ -792,6 +792,7 @@ class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase end @ship.pirate.catchphrase = "Changed Catchphrase" + @ship.name_will_change! assert_raise(RuntimeError) { assert !@pirate.save } assert_not_nil @pirate.reload.ship @@ -1130,7 +1131,7 @@ class TestAutosaveAssociationOnAHasOneAssociation < ActiveRecord::TestCase assert_queries(0) { @ship.save! } @parrot = @pirate.parrots.create(name: "some_name") - @parrot.name="changed_name" + @parrot.name = "changed_name" assert_queries(1) { @ship.save! } assert_queries(0) { @ship.save! } end @@ -1390,6 +1391,14 @@ module AutosaveAssociationOnACollectionAssociationTests assert_equal "Squawky", parrot.reload.name end + def test_should_not_update_children_when_parent_creation_with_no_reason + parrot = Parrot.create!(name: "Polly") + assert_equal 0, parrot.updated_count + + Pirate.create!(parrot_ids: [parrot.id], catchphrase: "Arrrr") + assert_equal 0, parrot.reload.updated_count + end + def test_should_automatically_validate_the_associated_models @pirate.send(@association_name).each { |child| child.name = "" } @@ -1407,9 +1416,9 @@ module AutosaveAssociationOnACollectionAssociationTests end def test_should_default_invalid_error_from_i18n - I18n.backend.store_translations(:en, activerecord: {errors: { models: + I18n.backend.store_translations(:en, activerecord: { errors: { models: { @associated_model_name.to_s.to_sym => { blank: "cannot be blank" } } - }}) + } }) @pirate.send(@association_name).build(name: "") @@ -1698,3 +1707,27 @@ class TestAutosaveAssociationWithTouch < ActiveRecord::TestCase assert_nothing_raised { invoice.line_items.create(amount: 10) } end end + +class TestAutosaveAssociationOnAHasManyAssociationWithInverse < ActiveRecord::TestCase + class Post < ActiveRecord::Base + has_many :comments, inverse_of: :post + end + + class Comment < ActiveRecord::Base + belongs_to :post, inverse_of: :comments + + attr_accessor :post_comments_count + after_save do + self.post_comments_count = post.comments.count + end + end + + def test_after_save_callback_with_autosave + post = Post.new(title: "Test", body: "...") + comment = post.comments.build(body: "...") + post.save! + + assert_equal 1, post.comments.count + assert_equal 1, comment.post_comments_count + end +end diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb index bf2c13cfac..a611cc208c 100644 --- a/activerecord/test/cases/base_test.rb +++ b/activerecord/test/cases/base_test.rb @@ -4,6 +4,7 @@ require "models/author" require "models/topic" require "models/reply" require "models/category" +require "models/categorization" require "models/company" require "models/customer" require "models/developer" @@ -33,8 +34,6 @@ class SecondAbstractClass < FirstAbstractClass self.abstract_class = true end class Photo < SecondAbstractClass; end -class Category < ActiveRecord::Base; end -class Categorization < ActiveRecord::Base; end class Smarts < ActiveRecord::Base; end class CreditCard < ActiveRecord::Base class PinNumber < ActiveRecord::Base @@ -45,8 +44,6 @@ class CreditCard < ActiveRecord::Base class Brand < Category; end end class MasterCreditCard < ActiveRecord::Base; end -class Post < ActiveRecord::Base; end -class Computer < ActiveRecord::Base; end class NonExistentTable < ActiveRecord::Base; end class TestOracleDefault < ActiveRecord::Base; end @@ -56,12 +53,6 @@ end class Weird < ActiveRecord::Base; end -class Boolean < ActiveRecord::Base - def has_fun - super - end -end - class LintTest < ActiveRecord::TestCase include ActiveModel::Lint::Tests @@ -107,14 +98,6 @@ class BasicsTest < ActiveRecord::TestCase assert_nil Edge.primary_key end - unless current_adapter?(:PostgreSQLAdapter, :OracleAdapter, :SQLServerAdapter, :FbAdapter) - def test_limit_with_comma - assert_deprecated do - assert Topic.limit("1,2").to_a - end - end - end - def test_many_mutations car = Car.new name: "<3<3<3" car.engines_count = 0 @@ -144,10 +127,8 @@ class BasicsTest < ActiveRecord::TestCase end def test_limit_should_sanitize_sql_injection_for_limit_with_commas - assert_deprecated do - assert_raises(ArgumentError) do - Topic.limit("1, 7 procedure help()").to_a - end + assert_raises(ArgumentError) do + Topic.limit("1, 7 procedure help()").to_a end end @@ -307,7 +288,7 @@ class BasicsTest < ActiveRecord::TestCase end def test_create_after_initialize_with_block - cb = CustomBulb.create {|c| c.name = "Dude" } + cb = CustomBulb.create { |c| c.name = "Dude" } assert_equal("Dude", cb.name) assert_equal(true, cb.frickinawesome) end @@ -494,12 +475,12 @@ class BasicsTest < ActiveRecord::TestCase def test_utc_as_time_zone_and_new with_timezone_config default: :utc do - attributes = { "bonus_time(1i)"=>"2000", - "bonus_time(2i)"=>"1", - "bonus_time(3i)"=>"1", - "bonus_time(4i)"=>"10", - "bonus_time(5i)"=>"35", - "bonus_time(6i)"=>"50" } + attributes = { "bonus_time(1i)" => "2000", + "bonus_time(2i)" => "1", + "bonus_time(3i)" => "1", + "bonus_time(4i)" => "10", + "bonus_time(5i)" => "35", + "bonus_time(6i)" => "50" } topic = Topic.new(attributes) assert_equal Time.utc(2000, 1, 1, 10, 35, 50), topic.bonus_time end @@ -622,7 +603,7 @@ class BasicsTest < ActiveRecord::TestCase Topic.find(topic.id).destroy end - assert_equal nil, Topic.find_by_id(topic.id) + assert_nil Topic.find_by_id(topic.id) end def test_comparison_with_different_objects @@ -664,7 +645,7 @@ class BasicsTest < ActiveRecord::TestCase Weird.reset_column_information - assert_equal ["EUC-JP"], Weird.columns.map {|c| c.name.encoding.name }.uniq + assert_equal ["EUC-JP"], Weird.columns.map { |c| c.name.encoding.name }.uniq ensure silence_warnings { Encoding.default_internal = old_default_internal } Weird.reset_column_information @@ -898,7 +879,7 @@ class BasicsTest < ActiveRecord::TestCase # fixed dates / times assert_equal Date.new(2004, 1, 1), default.fixed_date - assert_equal Time.local(2004, 1,1,0,0,0,0), default.fixed_time + assert_equal Time.local(2004, 1, 1, 0, 0, 0, 0), default.fixed_time # char types assert_equal "Y", default.char1 @@ -1109,7 +1090,7 @@ class BasicsTest < ActiveRecord::TestCase end def test_set_table_name_with_inheritance - k = Class.new( ActiveRecord::Base ) + k = Class.new(ActiveRecord::Base) def k.name; "Foo"; end def k.table_name; super + "ks"; end assert_equal "foosks", k.table_name @@ -1160,7 +1141,7 @@ class BasicsTest < ActiveRecord::TestCase end def test_find_last - last = Developer.last + last = Developer.last assert_equal last, Developer.all.merge!(order: "id desc").first end @@ -1179,17 +1160,17 @@ class BasicsTest < ActiveRecord::TestCase end def test_find_ordered_last - last = Developer.all.merge!(order: "developers.salary ASC").last + last = Developer.all.merge!(order: "developers.salary ASC").last assert_equal last, Developer.all.merge!(order: "developers.salary ASC").to_a.last end def test_find_reverse_ordered_last - last = Developer.all.merge!(order: "developers.salary DESC").last + last = Developer.all.merge!(order: "developers.salary DESC").last assert_equal last, Developer.all.merge!(order: "developers.salary DESC").to_a.last end def test_find_multiple_ordered_last - last = Developer.all.merge!(order: "developers.name, developers.salary DESC").last + last = Developer.all.merge!(order: "developers.name, developers.salary DESC").last assert_equal last, Developer.all.merge!(order: "developers.name, developers.salary DESC").to_a.last end @@ -1204,7 +1185,7 @@ class BasicsTest < ActiveRecord::TestCase end def test_find_symbol_ordered_last - last = Developer.all.merge!(order: :salary).last + last = Developer.all.merge!(order: :salary).last assert_equal last, Developer.all.merge!(order: :salary).to_a.last end @@ -1278,13 +1259,13 @@ class BasicsTest < ActiveRecord::TestCase ActiveSupport::Dependencies.remove_unloadable_constants! assert_nil ActiveRecord::Scoping::ScopeRegistry.value_for(:current_scope, klass) ensure - Object.class_eval{ remove_const :UnloadablePost } if defined?(UnloadablePost) + Object.class_eval { remove_const :UnloadablePost } if defined?(UnloadablePost) end def test_marshal_round_trip expected = posts(:welcome) marshalled = Marshal.dump(expected) - actual = Marshal.load(marshalled) + actual = Marshal.load(marshalled) assert_equal expected.attributes, actual.attributes end @@ -1379,12 +1360,6 @@ class BasicsTest < ActiveRecord::TestCase end end - def test_uniq_delegates_to_scoped - assert_deprecated do - assert_equal Bird.all.distinct, Bird.uniq - end - end - def test_distinct_delegates_to_scoped assert_equal Bird.all.distinct, Bird.distinct end @@ -1428,6 +1403,16 @@ class BasicsTest < ActiveRecord::TestCase assert_nil hash["firm_name"] end + def test_slice_accepts_array_argument + attrs = { + title: "slice", + author_name: "@Cohen-Carlisle", + content: "accept arrays so I don't have to splat" + }.with_indifferent_access + topic = Topic.new(attrs) + assert_equal attrs, topic.slice(attrs.keys) + end + def test_default_values_are_deeply_dupped company = Company.new company.description << "foo" @@ -1519,7 +1504,7 @@ class BasicsTest < ActiveRecord::TestCase test "ignored columns are not present in columns_hash" do cache_columns = Developer.connection.schema_cache.columns_hash(Developer.table_name) assert_includes cache_columns.keys, "first_name" - refute_includes Developer.columns_hash.keys, "first_name" + assert_not_includes Developer.columns_hash.keys, "first_name" end test "ignored columns have no attribute methods" do diff --git a/activerecord/test/cases/batches_test.rb b/activerecord/test/cases/batches_test.rb index 077e6d0bc0..f7e21faf0f 100644 --- a/activerecord/test/cases/batches_test.rb +++ b/activerecord/test/cases/batches_test.rb @@ -21,7 +21,7 @@ class EachTest < ActiveRecord::TestCase def test_each_should_not_return_query_chain_and_execute_only_one_query assert_queries(1) do - result = Post.find_each(batch_size: 100000){ } + result = Post.find_each(batch_size: 100000) {} assert_nil result end end @@ -135,11 +135,11 @@ class EachTest < ActiveRecord::TestCase def test_find_in_batches_shouldnt_execute_query_unless_needed assert_queries(2) do - Post.find_in_batches(batch_size: @total) {|batch| assert_kind_of Array, batch } + Post.find_in_batches(batch_size: @total) { |batch| assert_kind_of Array, batch } end assert_queries(1) do - Post.find_in_batches(batch_size: @total + 1) {|batch| assert_kind_of Array, batch } + Post.find_in_batches(batch_size: @total + 1) { |batch| assert_kind_of Array, batch } end end @@ -156,7 +156,7 @@ class EachTest < ActiveRecord::TestCase def test_find_in_batches_should_not_use_records_after_yielding_them_in_case_original_array_is_modified not_a_post = "not a post" def not_a_post.id; end - not_a_post.stub(:id, ->{ raise StandardError.new("not_a_post had #id called on it") }) do + not_a_post.stub(:id, -> { raise StandardError.new("not_a_post had #id called on it") }) do assert_nothing_raised do Post.find_in_batches(batch_size: 1) do |batch| assert_kind_of Array, batch @@ -182,7 +182,7 @@ class EachTest < ActiveRecord::TestCase def test_find_in_batches_should_error_on_ignore_the_order assert_raise(ArgumentError) do - PostWithDefaultScope.find_in_batches(error_on_ignore: true){} + PostWithDefaultScope.find_in_batches(error_on_ignore: true) {} end end @@ -191,7 +191,7 @@ class EachTest < ActiveRecord::TestCase prev = ActiveRecord::Base.error_on_ignored_order ActiveRecord::Base.error_on_ignored_order = true assert_nothing_raised do - PostWithDefaultScope.find_in_batches(error_on_ignore: false){} + PostWithDefaultScope.find_in_batches(error_on_ignore: false) {} end ensure # Set back to default @@ -203,7 +203,7 @@ class EachTest < ActiveRecord::TestCase prev = ActiveRecord::Base.error_on_ignored_order ActiveRecord::Base.error_on_ignored_order = true assert_raise(ArgumentError) do - PostWithDefaultScope.find_in_batches(){} + PostWithDefaultScope.find_in_batches() {} end ensure # Set back to default @@ -212,7 +212,7 @@ class EachTest < ActiveRecord::TestCase def test_find_in_batches_should_not_error_by_default assert_nothing_raised do - PostWithDefaultScope.find_in_batches(){} + PostWithDefaultScope.find_in_batches() {} end end @@ -227,7 +227,7 @@ class EachTest < ActiveRecord::TestCase def test_find_in_batches_should_not_modify_passed_options assert_nothing_raised do - Post.find_in_batches({ batch_size: 42, start: 1 }.freeze){} + Post.find_in_batches({ batch_size: 42, start: 1 }.freeze) {} end end @@ -445,7 +445,7 @@ class EachTest < ActiveRecord::TestCase def test_in_batches_should_not_modify_passed_options assert_nothing_raised do - Post.in_batches({ of: 42, start: 1 }.freeze){} + Post.in_batches({ of: 42, start: 1 }.freeze) {} end end diff --git a/activerecord/test/cases/calculations_test.rb b/activerecord/test/cases/calculations_test.rb index 13f9a47980..b29733a676 100644 --- a/activerecord/test/cases/calculations_test.rb +++ b/activerecord/test/cases/calculations_test.rb @@ -13,9 +13,9 @@ require "models/speedometer" require "models/ship_part" require "models/treasure" require "models/developer" +require "models/post" require "models/comment" require "models/rating" -require "models/post" class NumericData < ActiveRecord::Base self.table_name = "numeric_data" @@ -92,21 +92,21 @@ class CalculationsTest < ActiveRecord::TestCase def test_should_group_by_field c = Account.group(:firm_id).sum(:credit_limit) - [1,6,2].each do |firm_id| - assert c.keys.include?(firm_id), "Group #{c.inspect} does not contain firm_id #{firm_id}" + [1, 6, 2].each do |firm_id| + assert_includes c.keys, firm_id, "Group #{c.inspect} does not contain firm_id #{firm_id}" end end def test_should_group_by_arel_attribute c = Account.group(Account.arel_table[:firm_id]).sum(:credit_limit) - [1,6,2].each do |firm_id| - assert c.keys.include?(firm_id), "Group #{c.inspect} does not contain firm_id #{firm_id}" + [1, 6, 2].each do |firm_id| + assert_includes c.keys, firm_id, "Group #{c.inspect} does not contain firm_id #{firm_id}" end end def test_should_group_by_multiple_fields c = Account.group("firm_id", :credit_limit).count(:all) - [ [nil, 50], [1, 50], [6, 50], [6, 55], [9, 53], [2, 60] ].each { |firm_and_limit| assert c.keys.include?(firm_and_limit) } + [ [nil, 50], [1, 50], [6, 50], [6, 55], [9, 53], [2, 60] ].each { |firm_and_limit| assert_includes c.keys, firm_and_limit } end def test_should_group_by_multiple_fields_having_functions @@ -381,7 +381,7 @@ class CalculationsTest < ActiveRecord::TestCase end def test_should_perform_joined_include_when_referencing_included_tables - joined_count = Account.includes(:firm).where(companies: {name: "37signals"}).count + joined_count = Account.includes(:firm).where(companies: { name: "37signals" }).count assert_equal 1, joined_count end @@ -421,10 +421,6 @@ class CalculationsTest < ActiveRecord::TestCase def test_count_with_distinct assert_equal 4, Account.select(:credit_limit).distinct.count - - assert_deprecated do - assert_equal 4, Account.select(:credit_limit).uniq.count - end end def test_count_with_aliased_attribute @@ -453,7 +449,7 @@ class CalculationsTest < ActiveRecord::TestCase def test_should_count_field_in_joined_table_with_group_by c = Account.group("accounts.firm_id").joins(:firm).count("companies.id") - [1,6,2,9].each { |firm_id| assert c.keys.include?(firm_id) } + [1, 6, 2, 9].each { |firm_id| assert_includes c.keys, firm_id } end def test_should_count_field_of_root_table_with_conflicting_group_by_column @@ -572,7 +568,7 @@ class CalculationsTest < ActiveRecord::TestCase end def test_pluck - assert_equal [1,2,3,4,5], Topic.order(:id).pluck(:id) + assert_equal [1, 2, 3, 4, 5], Topic.order(:id).pluck(:id) end def test_pluck_without_column_names @@ -604,11 +600,11 @@ class CalculationsTest < ActiveRecord::TestCase def test_pluck_with_serialization t = Topic.create!(content: { foo: :bar }) - assert_equal [{foo: :bar}], Topic.where(id: t.id).pluck(:content) + assert_equal [{ foo: :bar }], Topic.where(id: t.id).pluck(:content) end def test_pluck_with_qualified_column_name - assert_equal [1,2,3,4,5], Topic.order(:id).pluck("topics.id") + assert_equal [1, 2, 3, 4, 5], Topic.order(:id).pluck("topics.id") end def test_pluck_auto_table_name_prefix @@ -688,7 +684,7 @@ class CalculationsTest < ActiveRecord::TestCase def test_pluck_replaces_select_clause taks_relation = Topic.select(:approved, :id).order(:id) - assert_equal [1,2,3,4,5], taks_relation.pluck(:id) + assert_equal [1, 2, 3, 4, 5], taks_relation.pluck(:id) assert_equal [false, true, true, true, true], taks_relation.pluck(:approved) end diff --git a/activerecord/test/cases/callbacks_test.rb b/activerecord/test/cases/callbacks_test.rb index e2352bd9f9..53ff037de1 100644 --- a/activerecord/test/cases/callbacks_test.rb +++ b/activerecord/test/cases/callbacks_test.rb @@ -57,7 +57,7 @@ end class ParentDeveloper < ActiveRecord::Base self.table_name = "developers" attr_accessor :after_save_called - before_validation {|record| record.after_save_called = true} + before_validation { |record| record.after_save_called = true } end class ChildDeveloper < ParentDeveloper @@ -95,16 +95,16 @@ class OnCallbacksDeveloper < ActiveRecord::Base self.table_name = "developers" before_validation { history << :before_validation } - before_validation(on: :create){ history << :before_validation_on_create } - before_validation(on: :update){ history << :before_validation_on_update } + before_validation(on: :create) { history << :before_validation_on_create } + before_validation(on: :update) { history << :before_validation_on_update } validate do history << :validate end after_validation { history << :after_validation } - after_validation(on: :create){ history << :after_validation_on_create } - after_validation(on: :update){ history << :after_validation_on_update } + after_validation(on: :create) { history << :after_validation_on_create } + after_validation(on: :update) { history << :after_validation_on_update } def history @history ||= [] @@ -125,11 +125,11 @@ class ContextualCallbacksDeveloper < ActiveRecord::Base after_validation :after_validation_on_create_and_update, on: [ :create, :update ] def before_validation_on_create_and_update - history << "before_validation_on_#{self.validation_context}".to_sym + history << "before_validation_on_#{validation_context}".to_sym end def after_validation_on_create_and_update - history << "after_validation_on_#{self.validation_context}".to_sym + history << "after_validation_on_#{validation_context}".to_sym end def history @@ -143,7 +143,7 @@ class CallbackCancellationDeveloper < ActiveRecord::Base attr_reader :after_save_called, :after_create_called, :after_update_called, :after_destroy_called attr_accessor :cancel_before_save, :cancel_before_create, :cancel_before_update, :cancel_before_destroy - before_save {defined?(@cancel_before_save) ? !@cancel_before_save : false} + before_save { defined?(@cancel_before_save) ? !@cancel_before_save : false } before_create { !@cancel_before_create } before_update { !@cancel_before_update } before_destroy { !@cancel_before_destroy } @@ -449,7 +449,7 @@ class CallbacksTest < ActiveRecord::TestCase assert david.valid? assert !david.save exc = assert_raise(ActiveRecord::RecordNotSaved) { david.save! } - assert_equal exc.record, david + assert_equal david, exc.record assert_equal "Failed to save the record", exc.message end @@ -493,7 +493,7 @@ class CallbacksTest < ActiveRecord::TestCase assert_deprecated do assert !david.destroy exc = assert_raise(ActiveRecord::RecordNotDestroyed) { david.destroy! } - assert_equal exc.record, david + assert_equal david, exc.record assert_equal "Failed to destroy the record", exc.message end assert_not_nil ImmutableDeveloper.find_by_id(1) @@ -527,7 +527,7 @@ class CallbacksTest < ActiveRecord::TestCase assert david.valid? assert !david.save exc = assert_raise(ActiveRecord::RecordNotSaved) { david.save! } - assert_equal exc.record, david + assert_equal david, exc.record david = DeveloperWithCanceledCallbacks.find(1) david.salary = 10_000_000 @@ -554,7 +554,7 @@ class CallbacksTest < ActiveRecord::TestCase david = DeveloperWithCanceledCallbacks.find(1) assert !david.destroy exc = assert_raise(ActiveRecord::RecordNotDestroyed) { david.destroy! } - assert_equal exc.record, david + assert_equal david, exc.record assert_not_nil ImmutableDeveloper.find_by_id(1) someone = CallbackHaltedDeveloper.find(1) diff --git a/activerecord/test/cases/collection_cache_key_test.rb b/activerecord/test/cases/collection_cache_key_test.rb index a2874438c1..381a78a8e2 100644 --- a/activerecord/test/cases/collection_cache_key_test.rb +++ b/activerecord/test/cases/collection_cache_key_test.rb @@ -28,20 +28,20 @@ module ActiveRecord end test "it triggers at most one query" do - developers = Developer.where(name: "David") + developers = Developer.where(name: "David") assert_queries(1) { developers.cache_key } assert_queries(0) { developers.cache_key } end test "it doesn't trigger any query if the relation is already loaded" do - developers = Developer.where(name: "David").load + developers = Developer.where(name: "David").load assert_queries(0) { developers.cache_key } end test "relation cache_key changes when the sql query changes" do developers = Developer.where(name: "David") - other_relation = Developer.where(name: "David").where("1 = 1") + other_relation = Developer.where(name: "David").where("1 = 1") assert_not_equal developers.cache_key, other_relation.cache_key end diff --git a/activerecord/test/cases/column_definition_test.rb b/activerecord/test/cases/column_definition_test.rb index 31e618664f..a65bb89052 100644 --- a/activerecord/test/cases/column_definition_test.rb +++ b/activerecord/test/cases/column_definition_test.rb @@ -6,7 +6,7 @@ module ActiveRecord def setup @adapter = AbstractAdapter.new(nil) def @adapter.native_database_types - {string: "varchar"} + { string: "varchar" } end @viz = @adapter.schema_creation end @@ -60,14 +60,13 @@ module ActiveRecord end def test_should_not_set_default_for_blob_and_text_data_types - text_type = MySQL::TypeMetadata.new( - SqlTypeMetadata.new(type: :text)) + text_type = MySQL::TypeMetadata.new(SqlTypeMetadata.new(type: :text)) text_column = MySQL::Column.new("title", nil, text_type) - assert_equal nil, text_column.default + assert_nil text_column.default not_null_text_column = MySQL::Column.new("title", nil, text_type, false) - assert_equal "", not_null_text_column.default + assert_nil not_null_text_column.default end def test_has_default_should_return_false_for_blob_and_text_data_types diff --git a/activerecord/test/cases/comment_test.rb b/activerecord/test/cases/comment_test.rb index 262ad319be..a625299e8d 100644 --- a/activerecord/test/cases/comment_test.rb +++ b/activerecord/test/cases/comment_test.rb @@ -2,7 +2,6 @@ require "cases/helper" require "support/schema_dumping_helper" if ActiveRecord::Base.connection.supports_comments? - class CommentTest < ActiveRecord::TestCase include SchemaDumpingHelper @@ -102,6 +101,7 @@ if ActiveRecord::Base.connection.supports_comments? # Do all the stuff from other tests @connection.add_column :commenteds, :rating, :integer, comment: "I am running out of imagination" @connection.change_column :commenteds, :content, :string, comment: "Whoa, content describes itself!" + @connection.change_column :commenteds, :content, :string @connection.change_column :commenteds, :obvious, :string, comment: nil @connection.add_index :commenteds, :obvious, name: "idx_obvious", comment: "We need to see obvious comments" @@ -135,5 +135,4 @@ if ActiveRecord::Base.connection.supports_comments? assert_no_match %r[t\.string\s+"absent_comment", comment:\n], output end end - end diff --git a/activerecord/test/cases/connection_adapters/connection_handler_test.rb b/activerecord/test/cases/connection_adapters/connection_handler_test.rb index d9bcc0c130..2c33bf22ab 100644 --- a/activerecord/test/cases/connection_adapters/connection_handler_test.rb +++ b/activerecord/test/cases/connection_adapters/connection_handler_test.rb @@ -6,11 +6,11 @@ module ActiveRecord def setup @handler = ConnectionHandler.new @spec_name = "primary" - @pool = @handler.establish_connection(ActiveRecord::Base.configurations["arunit"]) + @pool = @handler.establish_connection(ActiveRecord::Base.configurations["arunit"]) end def test_establish_connection_uses_spec_name - config = {"readonly" => {"adapter" => "sqlite3"}} + config = { "readonly" => { "adapter" => "sqlite3" } } resolver = ConnectionAdapters::ConnectionSpecification::Resolver.new(config) spec = resolver.spec(:readonly) @handler.establish_connection(spec.to_hash) 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 b1ba6f11f2..8faa67255d 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 @@ -27,7 +27,7 @@ module ActiveRecord ENV["DATABASE_URL"] = "postgres://localhost/foo" config = { "not_production" => { "adapter" => "not_postgres", "database" => "not_foo" } } actual = resolve_spec(:default_env, config) - expected = { "adapter"=>"postgresql", "database"=>"foo", "host"=>"localhost", "name"=>"default_env" } + expected = { "adapter" => "postgresql", "database" => "foo", "host" => "localhost", "name" => "default_env" } assert_equal expected, actual end @@ -37,7 +37,7 @@ module ActiveRecord config = { "not_production" => { "adapter" => "not_postgres", "database" => "not_foo" } } actual = resolve_spec(:foo, config) - expected = { "adapter" => "postgresql", "database" => "foo", "host" => "localhost","name"=>"foo" } + expected = { "adapter" => "postgresql", "database" => "foo", "host" => "localhost", "name" => "foo" } assert_equal expected, actual end @@ -47,7 +47,7 @@ module ActiveRecord config = { "not_production" => { "adapter" => "not_postgres", "database" => "not_foo" } } actual = resolve_spec(:foo, config) - expected = { "adapter" => "postgresql", "database" => "foo", "host" => "localhost","name"=>"foo" } + expected = { "adapter" => "postgresql", "database" => "foo", "host" => "localhost", "name" => "foo" } assert_equal expected, actual end @@ -55,13 +55,13 @@ module ActiveRecord ENV["DATABASE_URL"] = "postgres://localhost/foo" config = { "production" => { "adapter" => "not_postgres", "database" => "not_foo", "host" => "localhost" } } actual = resolve_spec(:production, config) - expected = { "adapter"=>"not_postgres", "database"=>"not_foo", "host"=>"localhost", "name"=>"production" } + expected = { "adapter" => "not_postgres", "database" => "not_foo", "host" => "localhost", "name" => "production" } assert_equal expected, actual end def test_resolver_with_database_uri_and_unknown_symbol_key ENV["DATABASE_URL"] = "postgres://localhost/foo" - config = { "not_production" => { "adapter" => "not_postgres", "database" => "not_foo" } } + config = { "not_production" => { "adapter" => "not_postgres", "database" => "not_foo" } } assert_raises AdapterNotSpecified do resolve_spec(:production, config) end @@ -71,7 +71,7 @@ module ActiveRecord ENV["DATABASE_URL"] = "not-postgres://not-localhost/not_foo" config = { "production" => { "adapter" => "also_not_postgres", "database" => "also_not_foo" } } actual = resolve_spec("postgres://localhost/foo", config) - expected = { "adapter"=>"postgresql", "database"=>"foo", "host"=>"localhost" } + expected = { "adapter" => "postgresql", "database" => "foo", "host" => "localhost" } assert_equal expected, actual end @@ -85,7 +85,7 @@ module ActiveRecord ENV["DATABASE_URL"] = "postgres://localhost/foo" config = { "not_default_env" => { "adapter" => "not_postgres", "database" => "not_foo" } } actual = resolve_config(config) - expect_prod = { "adapter"=>"postgresql", "database"=>"foo", "host"=>"localhost" } + expect_prod = { "adapter" => "postgresql", "database" => "foo", "host" => "localhost" } assert_equal expect_prod, actual["default_env"] end @@ -93,7 +93,7 @@ module ActiveRecord ENV["DATABASE_URL"] = "ibm-db://localhost/foo" config = { "default_env" => { "adapter" => "not_postgres", "database" => "not_foo", "host" => "localhost" } } actual = resolve_spec(:default_env, config) - expected = { "adapter"=>"ibm_db", "database"=>"foo", "host"=>"localhost", "name"=>"default_env" } + expected = { "adapter" => "ibm_db", "database" => "foo", "host" => "localhost", "name" => "default_env" } assert_equal expected, actual end @@ -142,13 +142,13 @@ module ActiveRecord "database" => "foo", "host" => "localhost" } assert_equal expected, actual["default_env"] - assert_equal nil, actual["production"] - assert_equal nil, actual["development"] - assert_equal nil, actual["test"] - assert_equal nil, actual[:default_env] - assert_equal nil, actual[:production] - assert_equal nil, actual[:development] - assert_equal nil, actual[:test] + assert_nil actual["production"] + assert_nil actual["development"] + assert_nil actual["test"] + assert_nil actual[:default_env] + assert_nil actual[:production] + assert_nil actual[:development] + assert_nil actual[:test] end def test_blank_with_database_url_with_rails_env @@ -162,15 +162,15 @@ module ActiveRecord "host" => "localhost" } assert_equal expected, actual["not_production"] - assert_equal nil, actual["production"] - assert_equal nil, actual["default_env"] - assert_equal nil, actual["development"] - assert_equal nil, actual["test"] - assert_equal nil, actual[:default_env] - assert_equal nil, actual[:not_production] - assert_equal nil, actual[:production] - assert_equal nil, actual[:development] - assert_equal nil, actual[:test] + assert_nil actual["production"] + assert_nil actual["default_env"] + assert_nil actual["development"] + assert_nil actual["test"] + assert_nil actual[:default_env] + assert_nil actual[:not_production] + assert_nil actual[:production] + assert_nil actual[:development] + assert_nil actual[:test] end def test_blank_with_database_url_with_rack_env @@ -184,15 +184,15 @@ module ActiveRecord "host" => "localhost" } assert_equal expected, actual["not_production"] - assert_equal nil, actual["production"] - assert_equal nil, actual["default_env"] - assert_equal nil, actual["development"] - assert_equal nil, actual["test"] - assert_equal nil, actual[:default_env] - assert_equal nil, actual[:not_production] - assert_equal nil, actual[:production] - assert_equal nil, actual[:development] - assert_equal nil, actual[:test] + assert_nil actual["production"] + assert_nil actual["default_env"] + assert_nil actual["development"] + assert_nil actual["test"] + assert_nil actual[:default_env] + assert_nil actual[:not_production] + assert_nil actual[:production] + assert_nil actual[:development] + assert_nil actual[:test] end def test_database_url_with_ipv6_host_and_port @@ -213,7 +213,7 @@ module ActiveRecord config = { "default_env" => { "url" => "postgres://localhost/foo" } } actual = resolve_config(config) expected = { "default_env" => - { "adapter" => "postgresql", + { "adapter" => "postgresql", "database" => "foo", "host" => "localhost" } @@ -224,7 +224,7 @@ module ActiveRecord def test_merge_no_conflicts_with_database_url ENV["DATABASE_URL"] = "postgres://localhost/foo" - config = {"default_env" => { "pool" => "5" } } + config = { "default_env" => { "pool" => "5" } } actual = resolve_config(config) expected = { "default_env" => { "adapter" => "postgresql", @@ -239,7 +239,7 @@ module ActiveRecord def test_merge_conflicts_with_database_url ENV["DATABASE_URL"] = "postgres://localhost/foo" - config = {"default_env" => { "adapter" => "NOT-POSTGRES", "database" => "NOT-FOO", "pool" => "5" } } + config = { "default_env" => { "adapter" => "NOT-POSTGRES", "database" => "NOT-FOO", "pool" => "5" } } actual = resolve_config(config) expected = { "default_env" => { "adapter" => "postgresql", diff --git a/activerecord/test/cases/connection_adapters/quoting_test.rb b/activerecord/test/cases/connection_adapters/quoting_test.rb deleted file mode 100644 index 59dcb96ebc..0000000000 --- a/activerecord/test/cases/connection_adapters/quoting_test.rb +++ /dev/null @@ -1,13 +0,0 @@ -require "cases/helper" - -module ActiveRecord - module ConnectionAdapters - module Quoting - class QuotingTest < ActiveRecord::TestCase - def test_quoting_classes - assert_equal "'Object'", AbstractAdapter.new(nil).quote(Object) - end - end - end - end -end diff --git a/activerecord/test/cases/connection_adapters/schema_cache_test.rb b/activerecord/test/cases/connection_adapters/schema_cache_test.rb index d4459603af..106323ccc9 100644 --- a/activerecord/test/cases/connection_adapters/schema_cache_test.rb +++ b/activerecord/test/cases/connection_adapters/schema_cache_test.rb @@ -12,6 +12,33 @@ module ActiveRecord assert_equal "id", @cache.primary_keys("posts") end + def test_yaml_dump_and_load + @cache.columns("posts") + @cache.columns_hash("posts") + @cache.data_sources("posts") + @cache.primary_keys("posts") + + new_cache = YAML.load(YAML.dump(@cache)) + assert_no_queries do + assert_equal 11, new_cache.columns("posts").size + assert_equal 11, new_cache.columns_hash("posts").size + assert new_cache.data_sources("posts") + assert_equal "id", new_cache.primary_keys("posts") + end + end + + def test_yaml_loads_5_1_dump + body = File.open(schema_dump_path).read + cache = YAML.load(body) + + assert_no_queries do + assert_equal 11, cache.columns("posts").size + assert_equal 11, cache.columns_hash("posts").size + assert cache.data_sources("posts") + assert_equal "id", cache.primary_keys("posts") + end + end + def test_primary_key_for_non_existent_table assert_nil @cache.primary_keys("omgponies") end @@ -45,17 +72,28 @@ module ActiveRecord @cache = Marshal.load(Marshal.dump(@cache)) - assert_equal 11, @cache.columns("posts").size - assert_equal 11, @cache.columns_hash("posts").size - assert @cache.data_sources("posts") - assert_equal "id", @cache.primary_keys("posts") + assert_no_queries do + assert_equal 11, @cache.columns("posts").size + assert_equal 11, @cache.columns_hash("posts").size + assert @cache.data_sources("posts") + assert_equal "id", @cache.primary_keys("posts") + end end - def test_table_methods_deprecation - assert_deprecated { assert @cache.table_exists?("posts") } - assert_deprecated { assert @cache.tables("posts") } - assert_deprecated { @cache.clear_table_cache!("posts") } + def test_data_source_exist + assert @cache.data_source_exists?("posts") + assert_not @cache.data_source_exists?("foo") end + + def test_clear_data_source_cache + @cache.clear_data_source_cache!("posts") + end + + private + + def schema_dump_path + "test/assets/schema_dump_5_1.yml" + end end end end diff --git a/activerecord/test/cases/connection_management_test.rb b/activerecord/test/cases/connection_management_test.rb index 88620f9f09..d1e946d401 100644 --- a/activerecord/test/cases/connection_management_test.rb +++ b/activerecord/test/cases/connection_management_test.rb @@ -104,7 +104,7 @@ module ActiveRecord def middleware(app) lambda do |env| a, b, c = executor.wrap { app.call(env) } - [a, b, Rack::BodyProxy.new(c) { }] + [a, b, Rack::BodyProxy.new(c) {}] end end end diff --git a/activerecord/test/cases/connection_pool_test.rb b/activerecord/test/cases/connection_pool_test.rb index d7ff9d6880..42600e53fd 100644 --- a/activerecord/test/cases/connection_pool_test.rb +++ b/activerecord/test/cases/connection_pool_test.rb @@ -184,14 +184,14 @@ module ActiveRecord def test_checkout_behaviour pool = ConnectionPool.new ActiveRecord::Base.connection_pool.spec - connection = pool.connection - assert_not_nil connection + main_connection = pool.connection + assert_not_nil main_connection threads = [] 4.times do |i| threads << Thread.new(i) do - connection = pool.connection - assert_not_nil connection - connection.close + thread_connection = pool.connection + assert_not_nil thread_connection + thread_connection.close end end @@ -455,49 +455,63 @@ module ActiveRecord with_single_connection_pool do |pool| [:disconnect, :disconnect!, :clear_reloadable_connections, :clear_reloadable_connections!].each do |group_action_method| conn = pool.connection # drain the only available connection - second_thread_done = Concurrent::CountDownLatch.new - - # create a first_thread and let it get into the FIFO queue first - first_thread = Thread.new do - pool.with_connection { second_thread_done.wait } - end - - # wait for first_thread to get in queue - Thread.pass until pool.num_waiting_in_queue == 1 - - # create a different, later thread, that will attempt to do a "group action", - # but because of the group action semantics it should be able to preempt the - # first_thread when a connection is made available - second_thread = Thread.new do - pool.send(group_action_method) - second_thread_done.count_down - end + second_thread_done = Concurrent::Event.new - # wait for second_thread to get in queue - Thread.pass until pool.num_waiting_in_queue == 2 - - # return the only available connection - pool.checkin(conn) - - # if the second_thread is not able to preempt the first_thread, - # they will temporarily (until either of them timeouts with ConnectionTimeoutError) - # deadlock and a join(2) timeout will be reached - failed = true unless second_thread.join(2) - - #--- post test clean up start - second_thread_done.count_down if failed - - # after `pool.disconnect()` the first thread will be left stuck in queue, no need to wait for - # it to timeout with ConnectionTimeoutError - if (group_action_method == :disconnect || group_action_method == :disconnect!) && pool.num_waiting_in_queue > 0 - pool.with_connection {} # create a new connection in case there are threads still stuck in a queue + begin + # create a first_thread and let it get into the FIFO queue first + first_thread = Thread.new do + pool.with_connection { second_thread_done.wait } + end + + # wait for first_thread to get in queue + Thread.pass until pool.num_waiting_in_queue == 1 + + # create a different, later thread, that will attempt to do a "group action", + # but because of the group action semantics it should be able to preempt the + # first_thread when a connection is made available + second_thread = Thread.new do + pool.send(group_action_method) + second_thread_done.set + end + + # wait for second_thread to get in queue + Thread.pass until pool.num_waiting_in_queue == 2 + + # return the only available connection + pool.checkin(conn) + + # if the second_thread is not able to preempt the first_thread, + # they will temporarily (until either of them timeouts with ConnectionTimeoutError) + # deadlock and a join(2) timeout will be reached + assert second_thread.join(2), "#{group_action_method} is not able to preempt other waiting threads" + + ensure + # post test clean up + failed = !second_thread_done.set? + + if failed + second_thread_done.set + + puts + puts ">>> test_disconnect_and_clear_reloadable_connections_are_able_to_preempt_other_waiting_threads / #{group_action_method}" + p [first_thread, second_thread] + p pool.stat + p pool.connections.map(&:owner) + + first_thread.join(2) + second_thread.join(2) + + puts "---" + p [first_thread, second_thread] + p pool.stat + p pool.connections.map(&:owner) + puts "<<<" + puts + end + + first_thread.join(10) || raise("first_thread got stuck") + second_thread.join(10) || raise("second_thread got stuck") end - - first_thread.join - second_thread.join - #--- post test clean up end - - flunk "#{group_action_method} is not able to preempt other waiting threads" if failed end end end @@ -526,6 +540,26 @@ module ActiveRecord end end + def test_connection_pool_stat + with_single_connection_pool do |pool| + pool.with_connection do |connection| + stats = pool.stat + assert_equal({ size: 1, connections: 1, busy: 1, dead: 0, idle: 0, waiting: 0, checkout_timeout: 5 }, stats) + end + + stats = pool.stat + assert_equal({ size: 1, connections: 1, busy: 0, dead: 0, idle: 1, waiting: 0, checkout_timeout: 5 }, stats) + + Thread.new do + pool.checkout + Thread.current.kill + end.join + + stats = pool.stat + assert_equal({ size: 1, connections: 1, busy: 0, dead: 1, idle: 0, waiting: 0, checkout_timeout: 5 }, stats) + end + end + private def with_single_connection_pool one_conn_spec = ActiveRecord::Base.connection_pool.spec.dup diff --git a/activerecord/test/cases/connection_specification/resolver_test.rb b/activerecord/test/cases/connection_specification/resolver_test.rb index 4bfec47124..13b5bae13c 100644 --- a/activerecord/test/cases/connection_specification/resolver_test.rb +++ b/activerecord/test/cases/connection_specification/resolver_test.rb @@ -4,11 +4,11 @@ module ActiveRecord module ConnectionAdapters class ConnectionSpecification class ResolverTest < ActiveRecord::TestCase - def resolve(spec, config={}) + def resolve(spec, config = {}) Resolver.new(config).resolve(spec) end - def spec(spec, config={}) + def spec(spec, config = {}) Resolver.new(config).spec(spec) end @@ -29,27 +29,27 @@ module ActiveRecord "adapter" => "abstract", "host" => "foo", "encoding" => "utf8", - "name" => "production"}, spec) + "name" => "production" }, spec) end def test_url_sub_key - spec = resolve :production, "production" => {"url" => "abstract://foo?encoding=utf8"} + spec = resolve :production, "production" => { "url" => "abstract://foo?encoding=utf8" } assert_equal({ "adapter" => "abstract", "host" => "foo", "encoding" => "utf8", - "name" => "production"}, spec) + "name" => "production" }, spec) end def test_url_sub_key_merges_correctly - hash = {"url" => "abstract://foo?encoding=utf8&", "adapter" => "sqlite3", "host" => "bar", "pool" => "3"} + hash = { "url" => "abstract://foo?encoding=utf8&", "adapter" => "sqlite3", "host" => "bar", "pool" => "3" } spec = resolve :production, "production" => hash assert_equal({ "adapter" => "abstract", "host" => "foo", "encoding" => "utf8", "pool" => "3", - "name" => "production"}, spec) + "name" => "production" }, spec) end def test_url_host_no_db @@ -112,16 +112,16 @@ module ActiveRecord end def test_url_sub_key_for_sqlite3 - spec = resolve :production, "production" => {"url" => "sqlite3:foo?encoding=utf8"} + spec = resolve :production, "production" => { "url" => "sqlite3:foo?encoding=utf8" } assert_equal({ "adapter" => "sqlite3", "database" => "foo", "encoding" => "utf8", - "name" => "production"}, spec) + "name" => "production" }, spec) end def test_spec_name_on_key_lookup - spec = spec(:readonly, "readonly" => {"adapter" => "sqlite3"}) + spec = spec(:readonly, "readonly" => { "adapter" => "sqlite3" }) assert_equal "readonly", spec.name end diff --git a/activerecord/test/cases/counter_cache_test.rb b/activerecord/test/cases/counter_cache_test.rb index 84f2c3a465..c7d0ba32b4 100644 --- a/activerecord/test/cases/counter_cache_test.rb +++ b/activerecord/test/cases/counter_cache_test.rb @@ -211,4 +211,146 @@ class CounterCacheTest < ActiveRecord::TestCase aircraft.wheels.first.destroy end end + + test "update counters doesn't touch timestamps by default" do + @topic.update_column :updated_at, 5.minutes.ago + previously_updated_at = @topic.updated_at + + Topic.update_counters(@topic.id, replies_count: -1) + + assert_equal previously_updated_at, @topic.updated_at + end + + test "update counters with touch: true" do + assert_touching @topic, :updated_at do + Topic.update_counters(@topic.id, replies_count: -1, touch: true) + end + end + + test "update counters of multiple records with touch: true" do + t1, t2 = topics(:first, :second) + + assert_touching t1, :updated_at do + assert_difference ["t1.reload.replies_count", "t2.reload.replies_count"], 2 do + Topic.update_counters([t1.id, t2.id], replies_count: 2, touch: true) + end + end + end + + test "update multiple counters with touch: true" do + assert_touching @topic, :updated_at do + Topic.update_counters(@topic.id, replies_count: 2, unique_replies_count: 2, touch: true) + end + end + + test "reset counters with touch: true" do + assert_touching @topic, :updated_at do + Topic.reset_counters(@topic.id, :replies, touch: true) + end + end + + test "reset multiple counters with touch: true" do + assert_touching @topic, :updated_at do + Topic.update_counters(@topic.id, replies_count: 1, unique_replies_count: 1) + Topic.reset_counters(@topic.id, :replies, :unique_replies, touch: true) + end + end + + test "increment counters with touch: true" do + assert_touching @topic, :updated_at do + Topic.increment_counter(:replies_count, @topic.id, touch: true) + end + end + + test "decrement counters with touch: true" do + assert_touching @topic, :updated_at do + Topic.decrement_counter(:replies_count, @topic.id, touch: true) + end + end + + test "update counters with touch: :written_on" do + assert_touching @topic, :written_on do + Topic.update_counters(@topic.id, replies_count: -1, touch: :written_on) + end + end + + test "update multiple counters with touch: :written_on" do + assert_touching @topic, :written_on do + Topic.update_counters(@topic.id, replies_count: 2, unique_replies_count: 2, touch: :written_on) + end + end + + test "reset counters with touch: :written_on" do + assert_touching @topic, :written_on do + Topic.reset_counters(@topic.id, :replies, touch: :written_on) + end + end + + test "reset multiple counters with touch: :written_on" do + assert_touching @topic, :written_on do + Topic.update_counters(@topic.id, replies_count: 1, unique_replies_count: 1) + Topic.reset_counters(@topic.id, :replies, :unique_replies, touch: :written_on) + end + end + + test "increment counters with touch: :written_on" do + assert_touching @topic, :written_on do + Topic.increment_counter(:replies_count, @topic.id, touch: :written_on) + end + end + + test "decrement counters with touch: :written_on" do + assert_touching @topic, :written_on do + Topic.decrement_counter(:replies_count, @topic.id, touch: :written_on) + end + end + + test "update counters with touch: %i( updated_at written_on )" do + assert_touching @topic, :updated_at, :written_on do + Topic.update_counters(@topic.id, replies_count: -1, touch: %i( updated_at written_on )) + end + end + + test "update multiple counters with touch: %i( updated_at written_on )" do + assert_touching @topic, :updated_at, :written_on do + Topic.update_counters(@topic.id, replies_count: 2, unique_replies_count: 2, touch: %i( updated_at written_on )) + end + end + + test "reset counters with touch: %i( updated_at written_on )" do + assert_touching @topic, :updated_at, :written_on do + Topic.reset_counters(@topic.id, :replies, touch: %i( updated_at written_on )) + end + end + + test "reset multiple counters with touch: %i( updated_at written_on )" do + assert_touching @topic, :updated_at, :written_on do + Topic.update_counters(@topic.id, replies_count: 1, unique_replies_count: 1) + Topic.reset_counters(@topic.id, :replies, :unique_replies, touch: %i( updated_at written_on )) + end + end + + test "increment counters with touch: %i( updated_at written_on )" do + assert_touching @topic, :updated_at, :written_on do + Topic.increment_counter(:replies_count, @topic.id, touch: %i( updated_at written_on )) + end + end + + test "decrement counters with touch: %i( updated_at written_on )" do + assert_touching @topic, :updated_at, :written_on do + Topic.decrement_counter(:replies_count, @topic.id, touch: %i( updated_at written_on )) + end + end + + private + def assert_touching(record, *attributes) + record.update_columns attributes.map { |attr| [ attr, 5.minutes.ago ] }.to_h + touch_times = attributes.map { |attr| [ attr, record.public_send(attr) ] }.to_h + + yield + + touch_times.each do |attr, previous_touch_time| + assert_operator previous_touch_time, :<, record.reload.public_send(attr) + end + end end diff --git a/activerecord/test/cases/database_statements_test.rb b/activerecord/test/cases/database_statements_test.rb index ac9912d4d1..66035865be 100644 --- a/activerecord/test/cases/database_statements_test.rb +++ b/activerecord/test/cases/database_statements_test.rb @@ -5,6 +5,13 @@ class DatabaseStatementsTest < ActiveRecord::TestCase @connection = ActiveRecord::Base.connection end + unless current_adapter?(:OracleAdapter) + def test_exec_insert + result = @connection.exec_insert("INSERT INTO accounts (firm_id,credit_limit) VALUES (42,5000)", nil, []) + assert_not_nil @connection.send(:last_inserted_id, result) + end + end + def test_insert_should_return_the_inserted_id assert_not_nil return_the_inserted_id(method: :insert) end @@ -13,12 +20,6 @@ class DatabaseStatementsTest < ActiveRecord::TestCase assert_not_nil return_the_inserted_id(method: :create) end - def test_insert_update_delete_sql_is_deprecated - assert_deprecated { @connection.insert_sql("INSERT INTO accounts (firm_id,credit_limit) VALUES (42,5000)") } - assert_deprecated { @connection.update_sql("UPDATE accounts SET credit_limit = 6000 WHERE firm_id = 42") } - assert_deprecated { @connection.delete_sql("DELETE FROM accounts WHERE firm_id = 42") } - end - private def return_the_inserted_id(method:) diff --git a/activerecord/test/cases/defaults_test.rb b/activerecord/test/cases/defaults_test.rb index f94815d34c..6532efcf22 100644 --- a/activerecord/test/cases/defaults_test.rb +++ b/activerecord/test/cases/defaults_test.rb @@ -127,92 +127,66 @@ if current_adapter?(:Mysql2Adapter) ActiveRecord::Base.establish_connection connection end - # MySQL cannot have defaults on text/blob columns. It reports the - # default value as null. + # Strict mode controls how MySQL handles invalid or missing values + # in data-change statements such as INSERT or UPDATE. A value can be + # invalid for several reasons. For example, it might have the wrong + # data type for the column, or it might be out of range. A value is + # missing when a new row to be inserted does not contain a value for + # a non-NULL column that has no explicit DEFAULT clause in its definition. + # (For a NULL column, NULL is inserted if the value is missing.) # - # Despite this, in non-strict mode, MySQL will use an empty string - # as the default value of the field, if no other value is - # specified. + # If strict mode is not in effect, MySQL inserts adjusted values for + # invalid or missing values and produces warnings. In strict mode, + # you can produce this behavior by using INSERT IGNORE or UPDATE IGNORE. # - # Therefore, in non-strict mode, we want column.default to report - # an empty string as its default, to be consistent with that. - # - # In strict mode, column.default should be nil. - def test_mysql_text_not_null_defaults_non_strict + # https://dev.mysql.com/doc/refman/5.7/en/sql-mode.html#sql-mode-strict + def test_mysql_not_null_defaults_non_strict using_strict(false) do - with_text_blob_not_null_table do |klass| + with_mysql_not_null_table do |klass| record = klass.new - assert_equal "", record.non_null_blob - assert_equal "", record.non_null_text - - assert_nil record.null_blob - assert_nil record.null_text + assert_nil record.non_null_integer + assert_nil record.non_null_string + assert_nil record.non_null_text + assert_nil record.non_null_blob record.save! record.reload + assert_equal 0, record.non_null_integer + assert_equal "", record.non_null_string assert_equal "", record.non_null_text assert_equal "", record.non_null_blob - - assert_nil record.null_text - assert_nil record.null_blob end end end - def test_mysql_text_not_null_defaults_strict + def test_mysql_not_null_defaults_strict using_strict(true) do - with_text_blob_not_null_table do |klass| + with_mysql_not_null_table do |klass| record = klass.new - assert_nil record.non_null_blob + assert_nil record.non_null_integer + assert_nil record.non_null_string assert_nil record.non_null_text - assert_nil record.null_blob - assert_nil record.null_text + assert_nil record.non_null_blob - assert_raises(ActiveRecord::StatementInvalid) { klass.create } + assert_raises(ActiveRecord::NotNullViolation) { klass.create } end end end - def with_text_blob_not_null_table + def with_mysql_not_null_table klass = Class.new(ActiveRecord::Base) - klass.table_name = "test_mysql_text_not_null_defaults" + klass.table_name = "test_mysql_not_null_defaults" klass.connection.create_table klass.table_name do |t| - t.column :non_null_text, :text, null: false - t.column :non_null_blob, :blob, null: false - t.column :null_text, :text, null: true - t.column :null_blob, :blob, null: true + t.integer :non_null_integer, null: false + t.string :non_null_string, null: false + t.text :non_null_text, null: false + t.blob :non_null_blob, null: false end yield klass ensure klass.connection.drop_table(klass.table_name) rescue nil end - - # MySQL uses an implicit default 0 rather than NULL unless in strict mode. - # We use an implicit NULL so schema.rb is compatible with other databases. - def test_mysql_integer_not_null_defaults - klass = Class.new(ActiveRecord::Base) - klass.table_name = "test_integer_not_null_default_zero" - klass.connection.create_table klass.table_name do |t| - t.column :zero, :integer, null: false, default: 0 - t.column :omit, :integer, null: false - end - - assert_equal "0", klass.columns_hash["zero"].default - assert !klass.columns_hash["zero"].null - assert_equal nil, klass.columns_hash["omit"].default - assert !klass.columns_hash["omit"].null - - assert_raise(ActiveRecord::StatementInvalid) { klass.create! } - - assert_nothing_raised do - instance = klass.create!(omit: 1) - assert_equal 0, instance.zero - assert_equal 1, instance.omit - end - ensure - klass.connection.drop_table(klass.table_name) rescue nil - end end end diff --git a/activerecord/test/cases/dirty_test.rb b/activerecord/test/cases/dirty_test.rb index 32df15eef3..0e58e65a07 100644 --- a/activerecord/test/cases/dirty_test.rb +++ b/activerecord/test/cases/dirty_test.rb @@ -280,7 +280,7 @@ class DirtyTest < ActiveRecord::TestCase assert pirate.changed? assert_nil pirate.catchphrase_was assert_equal %w(catchphrase), pirate.changed - assert_equal({"catchphrase" => [nil, "arrr"]}, pirate.changes) + assert_equal({ "catchphrase" => [nil, "arrr"] }, pirate.changes) pirate.save assert !pirate.changed? @@ -314,7 +314,7 @@ class DirtyTest < ActiveRecord::TestCase assert !topic.approved_changed? # Coming from web form. - params = {topic: {approved: 1}} + params = { topic: { approved: 1 } } # In the controller. topic.attributes = params[:topic] assert topic.approved? @@ -341,7 +341,7 @@ class DirtyTest < ActiveRecord::TestCase def test_partial_update_with_optimistic_locking person = Person.new(first_name: "foo") - old_lock_version = 1 + old_lock_version = person.lock_version with_partial_writes Person, false do assert_queries(2) { 2.times { person.save! } } @@ -423,7 +423,7 @@ class DirtyTest < ActiveRecord::TestCase def test_save_should_store_serialized_attributes_even_with_partial_writes with_partial_writes(Topic) do - topic = Topic.create!(content: {a: "a"}) + topic = Topic.create!(content: { a: "a" }) assert_not topic.changed? @@ -444,7 +444,7 @@ class DirtyTest < ActiveRecord::TestCase def test_save_always_should_update_timestamps_when_serialized_attributes_are_present with_partial_writes(Topic) do - topic = Topic.create!(content: {a: "a"}) + topic = Topic.create!(content: { a: "a" }) topic.save! updated_at = topic.updated_at @@ -460,7 +460,7 @@ 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.create!(author_name: "Bill", content: { a: "a" }) topic = Topic.select("id, author_name").first topic.update_columns author_name: "John" topic = Topic.first @@ -495,8 +495,8 @@ class DirtyTest < ActiveRecord::TestCase assert_equal 4, pirate.previous_changes.size assert_equal [nil, "arrr"], pirate.previous_changes["catchphrase"] assert_equal [nil, pirate.id], pirate.previous_changes["id"] - assert pirate.previous_changes.include?("updated_on") - assert pirate.previous_changes.include?("created_on") + assert_includes pirate.previous_changes, "updated_on" + assert_includes pirate.previous_changes, "created_on" assert !pirate.previous_changes.key?("parrot_id") pirate.catchphrase = "Yar!!" @@ -631,7 +631,7 @@ class DirtyTest < ActiveRecord::TestCase assert_equal("arrrr", pirate.catchphrase_was) assert pirate.catchphrase_changed?(from: "arrrr") assert_not pirate.catchphrase_changed?(from: "anything else") - assert pirate.changed_attributes.include?(:catchphrase) + assert_includes pirate.changed_attributes, :catchphrase pirate.save! pirate.reload @@ -726,6 +726,89 @@ class DirtyTest < ActiveRecord::TestCase assert person.changed? end + test "saved_change_to_attribute? returns whether a change occurred in the last save" do + person = Person.create!(first_name: "Sean") + + assert person.saved_change_to_first_name? + refute person.saved_change_to_gender? + assert person.saved_change_to_first_name?(from: nil, to: "Sean") + assert person.saved_change_to_first_name?(from: nil) + assert person.saved_change_to_first_name?(to: "Sean") + refute person.saved_change_to_first_name?(from: "Jim", to: "Sean") + refute person.saved_change_to_first_name?(from: "Jim") + refute person.saved_change_to_first_name?(to: "Jim") + end + + test "saved_change_to_attribute returns the change that occurred in the last save" do + person = Person.create!(first_name: "Sean", gender: "M") + + assert_equal [nil, "Sean"], person.saved_change_to_first_name + assert_equal [nil, "M"], person.saved_change_to_gender + + person.update(first_name: "Jim") + + assert_equal ["Sean", "Jim"], person.saved_change_to_first_name + assert_nil person.saved_change_to_gender + end + + test "attribute_before_last_save returns the original value before saving" do + person = Person.create!(first_name: "Sean", gender: "M") + + assert_nil person.first_name_before_last_save + assert_nil person.gender_before_last_save + + person.first_name = "Jim" + + assert_nil person.first_name_before_last_save + assert_nil person.gender_before_last_save + + person.save + + assert_equal "Sean", person.first_name_before_last_save + assert_equal "M", person.gender_before_last_save + end + + test "saved_changes? returns whether the last call to save changed anything" do + person = Person.create!(first_name: "Sean") + + assert person.saved_changes? + + person.save + + refute person.saved_changes? + end + + test "saved_changes returns a hash of all the changes that occurred" do + person = Person.create!(first_name: "Sean", gender: "M") + + assert_equal [nil, "Sean"], person.saved_changes[:first_name] + assert_equal [nil, "M"], person.saved_changes[:gender] + assert_equal %w(id first_name gender created_at updated_at).sort, person.saved_changes.keys.sort + + travel(1.second) do + person.update(first_name: "Jim") + end + + assert_equal ["Sean", "Jim"], person.saved_changes[:first_name] + 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 + 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 "has_changes_to_save? should be false" if has_changes_to_save? + end + end + + person = klass.create!(first_name: "Sean") + refute person.changed? + end + private def with_partial_writes(klass, on = true) old = klass.partial_writes? diff --git a/activerecord/test/cases/enum_test.rb b/activerecord/test/cases/enum_test.rb index c2009843f0..b7641fcf32 100644 --- a/activerecord/test/cases/enum_test.rb +++ b/activerecord/test/cases/enum_test.rb @@ -18,6 +18,7 @@ class EnumTest < ActiveRecord::TestCase assert @book.author_visibility_visible? assert @book.illustrator_visibility_visible? assert @book.with_medium_font_size? + assert @book.medium_to_read? end test "query state with strings" do @@ -26,6 +27,7 @@ class EnumTest < ActiveRecord::TestCase assert_equal "english", @book.language assert_equal "visible", @book.author_visibility assert_equal "visible", @book.illustrator_visibility + assert_equal "medium", @book.difficulty end test "find via scope" do @@ -34,6 +36,7 @@ class EnumTest < ActiveRecord::TestCase assert_equal @book, Book.in_english.first assert_equal @book, Book.author_visibility_visible.first assert_equal @book, Book.illustrator_visibility_visible.first + assert_equal @book, Book.medium_to_read.first end test "find via where with values" do @@ -422,6 +425,43 @@ class EnumTest < ActiveRecord::TestCase assert_not @book.in_french? end + test "query state by predicate with custom suffix" do + assert @book.medium_to_read? + assert_not @book.easy_to_read? + assert_not @book.hard_to_read? + end + + test "enum methods with custom suffix defined" do + assert @book.class.respond_to?(:easy_to_read) + assert @book.class.respond_to?(:medium_to_read) + assert @book.class.respond_to?(:hard_to_read) + + assert @book.respond_to?(:easy_to_read?) + assert @book.respond_to?(:medium_to_read?) + assert @book.respond_to?(:hard_to_read?) + + assert @book.respond_to?(:easy_to_read!) + assert @book.respond_to?(:medium_to_read!) + assert @book.respond_to?(:hard_to_read!) + end + + test "update enum attributes with custom suffix" do + @book.medium_to_read! + assert_not @book.easy_to_read? + assert @book.medium_to_read? + assert_not @book.hard_to_read? + + @book.easy_to_read! + assert @book.easy_to_read? + assert_not @book.medium_to_read? + assert_not @book.hard_to_read? + + @book.hard_to_read! + assert_not @book.easy_to_read? + assert_not @book.medium_to_read? + assert @book.hard_to_read? + end + test "uses default status when no status is provided in fixtures" do book = books(:tlg) assert book.proposed?, "expected fixture to default to proposed status" diff --git a/activerecord/test/cases/errors_test.rb b/activerecord/test/cases/errors_test.rb index 0711a372f2..73feb831d0 100644 --- a/activerecord/test/cases/errors_test.rb +++ b/activerecord/test/cases/errors_test.rb @@ -5,7 +5,7 @@ class ErrorsTest < ActiveRecord::TestCase base = ActiveRecord::ActiveRecordError error_klasses = ObjectSpace.each_object(Class).select { |klass| klass < base } - error_klasses.each do |error_klass| + (error_klasses - [ActiveRecord::AmbiguousSourceReflectionForThroughAssociation]).each do |error_klass| begin error_klass.new.inspect rescue ArgumentError diff --git a/activerecord/test/cases/explain_test.rb b/activerecord/test/cases/explain_test.rb index 43f1d401dd..86fe90ae51 100644 --- a/activerecord/test/cases/explain_test.rb +++ b/activerecord/test/cases/explain_test.rb @@ -40,7 +40,7 @@ if ActiveRecord::Base.connection.supports_explain? queries = sqls.zip(binds) stub_explain_for_query_plans do - expected = sqls.map {|sql| "EXPLAIN for: #{sql}\nquery plan #{sql}"}.join("\n") + expected = sqls.map { |sql| "EXPLAIN for: #{sql}\nquery plan #{sql}" }.join("\n") assert_equal expected, base.exec_explain(queries) end end @@ -75,7 +75,7 @@ if ActiveRecord::Base.connection.supports_explain? def stub_explain_for_query_plans(query_plans = ["query plan foo", "query plan bar"]) explain_called = 0 - connection.stub(:explain, proc{ explain_called += 1; query_plans[explain_called - 1] }) do + connection.stub(:explain, proc { explain_called += 1; query_plans[explain_called - 1] }) do yield end end diff --git a/activerecord/test/cases/finder_respond_to_test.rb b/activerecord/test/cases/finder_respond_to_test.rb index a7af8ac0ea..3eaa993d45 100644 --- a/activerecord/test/cases/finder_respond_to_test.rb +++ b/activerecord/test/cases/finder_respond_to_test.rb @@ -10,7 +10,7 @@ class FinderRespondToTest < ActiveRecord::TestCase end def test_should_preserve_normal_respond_to_behaviour_and_respond_to_newly_added_method - class << Topic; self; end.send(:define_method, :method_added_for_finder_respond_to_test) { } + class << Topic; self; end.send(:define_method, :method_added_for_finder_respond_to_test) {} assert_respond_to Topic, :method_added_for_finder_respond_to_test ensure class << Topic; self; end.send(:remove_method, :method_added_for_finder_respond_to_test) diff --git a/activerecord/test/cases/finder_test.rb b/activerecord/test/cases/finder_test.rb index bf0ce18bb8..e0ad9f5ec1 100644 --- a/activerecord/test/cases/finder_test.rb +++ b/activerecord/test/cases/finder_test.rb @@ -22,13 +22,13 @@ class FinderTest < ActiveRecord::TestCase fixtures :companies, :topics, :entrants, :developers, :developers_projects, :posts, :comments, :accounts, :authors, :author_addresses, :customers, :categories, :categorizations, :cars def test_find_by_id_with_hash - assert_raises(ActiveRecord::StatementInvalid) do + assert_nothing_raised do Post.find_by_id(limit: 1) end end def test_find_by_title_and_id_with_hash - assert_raises(ActiveRecord::StatementInvalid) do + assert_nothing_raised do Post.find_by_title_and_id("foo", limit: 1) end end @@ -49,22 +49,22 @@ class FinderTest < ActiveRecord::TestCase end def test_find_with_ids_returning_ordered - records = Topic.find([4,2,5]) + records = Topic.find([4, 2, 5]) assert_equal "The Fourth Topic of the day", records[0].title assert_equal "The Second Topic of the day", records[1].title assert_equal "The Fifth Topic of the day", records[2].title - records = Topic.find(4,2,5) + records = Topic.find(4, 2, 5) assert_equal "The Fourth Topic of the day", records[0].title assert_equal "The Second Topic of the day", records[1].title assert_equal "The Fifth Topic of the day", records[2].title - records = Topic.find(["4","2","5"]) + records = Topic.find(["4", "2", "5"]) assert_equal "The Fourth Topic of the day", records[0].title assert_equal "The Second Topic of the day", records[1].title assert_equal "The Fifth Topic of the day", records[2].title - records = Topic.find("4","2","5") + records = Topic.find("4", "2", "5") assert_equal "The Fourth Topic of the day", records[0].title assert_equal "The Second Topic of the day", records[1].title assert_equal "The Fifth Topic of the day", records[2].title @@ -72,12 +72,12 @@ class FinderTest < ActiveRecord::TestCase def test_find_with_ids_and_order_clause # The order clause takes precedence over the informed ids - records = Topic.order(:author_name).find([5,3,1]) + records = Topic.order(:author_name).find([5, 3, 1]) assert_equal "The Third Topic of the day", records[0].title assert_equal "The First Topic", records[1].title assert_equal "The Fifth Topic of the day", records[2].title - records = Topic.order(:id).find([5,3,1]) + records = Topic.order(:id).find([5, 3, 1]) assert_equal "The First Topic", records[0].title assert_equal "The Third Topic of the day", records[1].title assert_equal "The Fifth Topic of the day", records[2].title @@ -85,14 +85,14 @@ class FinderTest < ActiveRecord::TestCase def test_find_with_ids_with_limit_and_order_clause # The order clause takes precedence over the informed ids - records = Topic.limit(2).order(:id).find([5,3,1]) + records = Topic.limit(2).order(:id).find([5, 3, 1]) assert_equal 2, records.size assert_equal "The First Topic", records[0].title assert_equal "The Third Topic of the day", records[1].title end def test_find_with_ids_and_limit - records = Topic.limit(3).find([3,2,5,1,4]) + records = Topic.limit(3).find([3, 2, 5, 1, 4]) assert_equal 3, records.size assert_equal "The Third Topic of the day", records[0].title assert_equal "The Second Topic of the day", records[1].title @@ -102,7 +102,7 @@ class FinderTest < ActiveRecord::TestCase def test_find_with_ids_where_and_limit # Please note that Topic 1 is the only not approved so # if it were among the first 3 it would raise an ActiveRecord::RecordNotFound - records = Topic.where(approved: true).limit(3).find([3,2,5,1,4]) + records = Topic.where(approved: true).limit(3).find([3, 2, 5, 1, 4]) assert_equal 3, records.size assert_equal "The Third Topic of the day", records[0].title assert_equal "The Second Topic of the day", records[1].title @@ -110,15 +110,15 @@ class FinderTest < ActiveRecord::TestCase end def test_find_with_ids_and_offset - records = Topic.offset(2).find([3,2,5,1,4]) + records = Topic.offset(2).find([3, 2, 5, 1, 4]) assert_equal 3, records.size assert_equal "The Fifth Topic of the day", records[0].title assert_equal "The First Topic", records[1].title assert_equal "The Fourth Topic of the day", records[2].title end - def test_find_passing_active_record_object_is_deprecated - assert_deprecated do + def test_find_passing_active_record_object_is_not_permitted + assert_raises(ArgumentError) do Topic.find(Topic.last) end end @@ -127,7 +127,7 @@ class FinderTest < ActiveRecord::TestCase gc_disabled = GC.disable Post.where("author_id" => nil) # warm up x = Symbol.all_symbols.count - Post.where("title" => {"xxxqqqq" => "bar"}) + Post.where("title" => { "xxxqqqq" => "bar" }) assert_equal x, Symbol.all_symbols.count ensure GC.enable if gc_disabled == false @@ -136,7 +136,7 @@ class FinderTest < ActiveRecord::TestCase # find should handle strings that come from URLs # (example: Category.find(params[:id])) def test_find_with_string - assert_equal(Topic.find(1).title,Topic.find("1").title) + assert_equal(Topic.find(1).title, Topic.find("1").title) end def test_exists @@ -151,7 +151,7 @@ class FinderTest < ActiveRecord::TestCase assert_equal false, Topic.exists?(45) assert_equal false, Topic.exists?(Topic.new.id) - assert_raise(NoMethodError) { Topic.exists?([1,2]) } + assert_raise(NoMethodError) { Topic.exists?([1, 2]) } end def test_exists_with_polymorphic_relation @@ -167,15 +167,15 @@ class FinderTest < ActiveRecord::TestCase assert_equal false, relation.exists?(false) end - def test_exists_passing_active_record_object_is_deprecated - assert_deprecated do + def test_exists_passing_active_record_object_is_not_permitted + assert_raises(ArgumentError) do Topic.exists?(Topic.new) end end def test_exists_returns_false_when_parameter_has_invalid_type assert_equal false, Topic.exists?("foo") - assert_equal false, Topic.exists?(("9"*53).to_i) # number that's bigger than int + assert_equal false, Topic.exists?(("9" * 53).to_i) # number that's bigger than int end def test_exists_does_not_select_columns_without_alias @@ -258,8 +258,8 @@ class FinderTest < ActiveRecord::TestCase end def test_find_by_ids_with_limit_and_offset - assert_equal 2, Entrant.limit(2).find([1,3,2]).size - entrants = Entrant.limit(3).offset(2).find([1,3,2]) + assert_equal 2, Entrant.limit(2).find([1, 3, 2]).size + entrants = Entrant.limit(3).offset(2).find([1, 3, 2]) assert_equal 1, entrants.size assert_equal "Ruby Guru", entrants.first.name @@ -339,6 +339,11 @@ class FinderTest < ActiveRecord::TestCase assert_equal author.post, Post.find_by(author_id: Author.where(id: author)) end + def test_find_by_and_where_consistency_with_active_record_instance + author = authors(:david) + assert_equal Post.where(author_id: author).take, Post.find_by(author_id: author) + end + def test_take assert_equal topics(:first), Topic.take end @@ -488,12 +493,12 @@ class FinderTest < ActiveRecord::TestCase assert_equal topics(:fourth), Topic.offset(1).second_to_last assert_equal topics(:fourth), Topic.offset(2).second_to_last assert_equal topics(:fourth), Topic.offset(3).second_to_last - assert_equal nil, Topic.offset(4).second_to_last - assert_equal nil, Topic.offset(5).second_to_last + assert_nil Topic.offset(4).second_to_last + assert_nil Topic.offset(5).second_to_last #test with limit - # assert_equal nil, Topic.limit(1).second # TODO: currently failing - assert_equal nil, Topic.limit(1).second_to_last + # assert_nil Topic.limit(1).second # TODO: currently failing + assert_nil Topic.limit(1).second_to_last end def test_second_to_last_have_primary_key_order_by_default @@ -516,15 +521,15 @@ class FinderTest < ActiveRecord::TestCase # test with offset assert_equal topics(:third), Topic.offset(1).third_to_last assert_equal topics(:third), Topic.offset(2).third_to_last - assert_equal nil, Topic.offset(3).third_to_last - assert_equal nil, Topic.offset(4).third_to_last - assert_equal nil, Topic.offset(5).third_to_last + assert_nil Topic.offset(3).third_to_last + assert_nil Topic.offset(4).third_to_last + assert_nil Topic.offset(5).third_to_last # test with limit - # assert_equal nil, Topic.limit(1).third # TODO: currently failing - assert_equal nil, Topic.limit(1).third_to_last - # assert_equal nil, Topic.limit(2).third # TODO: currently failing - assert_equal nil, Topic.limit(2).third_to_last + # assert_nil Topic.limit(1).third # TODO: currently failing + assert_nil Topic.limit(1).third_to_last + # assert_nil Topic.limit(2).third # TODO: currently failing + assert_nil Topic.limit(2).third_to_last end def test_third_to_last_have_primary_key_order_by_default @@ -584,7 +589,7 @@ class FinderTest < ActiveRecord::TestCase end def test_last_on_loaded_relation_should_not_use_sql - relation = Topic.limit(10).load + relation = Topic.limit(10).load assert_no_queries do relation.last relation.last(2) @@ -592,7 +597,7 @@ class FinderTest < ActiveRecord::TestCase end def test_last_with_irreversible_order - assert_deprecated do + assert_raises(ActiveRecord::IrreversibleOrderError) do Topic.order("coalesce(author_name, title)").last end end @@ -627,8 +632,8 @@ class FinderTest < ActiveRecord::TestCase def test_find_only_some_columns topic = Topic.select("author_name").find(1) - assert_raise(ActiveModel::MissingAttributeError) {topic.title} - assert_raise(ActiveModel::MissingAttributeError) {topic.title?} + assert_raise(ActiveModel::MissingAttributeError) { topic.title } + assert_raise(ActiveModel::MissingAttributeError) { topic.title? } assert_nil topic.read_attribute("title") assert_equal "David", topic.author_name assert !topic.attribute_present?("title") @@ -693,27 +698,27 @@ class FinderTest < ActiveRecord::TestCase end def test_find_on_hash_conditions_with_range - assert_equal [1,2], Topic.where(id: 1..2).to_a.map(&:id).sort + assert_equal [1, 2], Topic.where(id: 1..2).to_a.map(&:id).sort assert_raise(ActiveRecord::RecordNotFound) { Topic.where(id: 2..3).find(1) } end def test_find_on_hash_conditions_with_end_exclusive_range - assert_equal [1,2,3], Topic.where(id: 1..3).to_a.map(&:id).sort - assert_equal [1,2], Topic.where(id: 1...3).to_a.map(&:id).sort + assert_equal [1, 2, 3], Topic.where(id: 1..3).to_a.map(&:id).sort + assert_equal [1, 2], Topic.where(id: 1...3).to_a.map(&:id).sort assert_raise(ActiveRecord::RecordNotFound) { Topic.where(id: 2...3).find(3) } end def test_find_on_hash_conditions_with_multiple_ranges - assert_equal [1,2,3], Comment.where(id: 1..3, post_id: 1..2).to_a.map(&:id).sort + assert_equal [1, 2, 3], Comment.where(id: 1..3, post_id: 1..2).to_a.map(&:id).sort assert_equal [1], Comment.where(id: 1..1, post_id: 1..10).to_a.map(&:id).sort end def test_find_on_hash_conditions_with_array_of_integers_and_ranges - assert_equal [1,2,3,5,6,7,8,9], Comment.where(id: [1..2, 3, 5, 6..8, 9]).to_a.map(&:id).sort + assert_equal [1, 2, 3, 5, 6, 7, 8, 9], Comment.where(id: [1..2, 3, 5, 6..8, 9]).to_a.map(&:id).sort end def test_find_on_hash_conditions_with_array_of_ranges - assert_equal [1,2,6,7,8], Comment.where(id: [1..2, 6..8]).to_a.map(&:id).sort + assert_equal [1, 2, 6, 7, 8], Comment.where(id: [1..2, 6..8]).to_a.map(&:id).sort end def test_find_on_multiple_hash_conditions @@ -864,7 +869,7 @@ class FinderTest < ActiveRecord::TestCase def test_named_bind_variables_with_quotes Company.create("name" => "37signals' go'es agains") - assert Company.where(["name = :name", {name: "37signals' go'es agains"}]).first + assert Company.where(["name = :name", { name: "37signals' go'es agains" }]).first end def test_named_bind_variables @@ -874,11 +879,6 @@ class FinderTest < ActiveRecord::TestCase assert_kind_of Time, Topic.where(["id = :id", { id: 1 }]).first.written_on end - def test_string_sanitation - assert_not_equal "'something ' 1=1'", ActiveRecord::Base.sanitize("something ' 1=1") - assert_equal "'something; select table'", ActiveRecord::Base.sanitize("something; select table") - end - def test_count_by_sql assert_equal(0, Entrant.count_by_sql("SELECT COUNT(*) FROM entrants WHERE id > 3")) assert_equal(1, Entrant.count_by_sql(["SELECT COUNT(*) FROM entrants WHERE id > ?", 2])) @@ -988,7 +988,6 @@ class FinderTest < ActiveRecord::TestCase assert_equal devs[2], Developer.offset(2).first assert_equal devs[-3], Developer.offset(2).last - assert_equal devs[-3], Developer.offset(2).last assert_equal devs[-3], Developer.offset(2).order("id DESC").first end @@ -1013,8 +1012,8 @@ class FinderTest < ActiveRecord::TestCase where("project_id=1").to_a assert_equal 3, developers_on_project_one.length developer_names = developers_on_project_one.map(&:name) - assert developer_names.include?("David") - assert developer_names.include?("Jamis") + assert_includes developer_names, "David" + assert_includes developer_names, "Jamis" end def test_joins_dont_clobber_id @@ -1034,7 +1033,7 @@ class FinderTest < ActiveRecord::TestCase def test_find_by_id_with_conditions_with_or assert_nothing_raised do - Post.where("posts.id <= 3 OR posts.#{QUOTED_TYPE} = 'Post'").find([1,2,3]) + Post.where("posts.id <= 3 OR posts.#{QUOTED_TYPE} = 'Post'").find([1, 2, 3]) end end @@ -1066,8 +1065,8 @@ class FinderTest < ActiveRecord::TestCase end def test_select_values - assert_equal ["1","2","3","4","5","6","7","8","9", "10", "11"], Company.connection.select_values("SELECT id FROM companies ORDER BY id").map!(&:to_s) - assert_equal ["37signals","Summit","Microsoft", "Flamboyant Software", "Ex Nihilo", "RailsCore", "Leetsoft", "Jadedpixel", "Odegy", "Ex Nihilo Part Deux", "Apex"], Company.connection.select_values("SELECT name FROM companies ORDER BY id") + assert_equal ["1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11"], Company.connection.select_values("SELECT id FROM companies ORDER BY id").map!(&:to_s) + assert_equal ["37signals", "Summit", "Microsoft", "Flamboyant Software", "Ex Nihilo", "RailsCore", "Leetsoft", "Jadedpixel", "Odegy", "Ex Nihilo Part Deux", "Apex"], Company.connection.select_values("SELECT name FROM companies ORDER BY id") end def test_select_rows @@ -1075,9 +1074,9 @@ class FinderTest < ActiveRecord::TestCase [["1", "1", nil, "37signals"], ["2", "1", "2", "Summit"], ["3", "1", "1", "Microsoft"]], - Company.connection.select_rows("SELECT id, firm_id, client_of, name FROM companies WHERE id IN (1,2,3) ORDER BY id").map! {|i| i.map! {|j| j.to_s unless j.nil?}}) + Company.connection.select_rows("SELECT id, firm_id, client_of, name FROM companies WHERE id IN (1,2,3) ORDER BY id").map! { |i| i.map! { |j| j.to_s unless j.nil? } }) assert_equal [["1", "37signals"], ["2", "Summit"], ["3", "Microsoft"]], - Company.connection.select_rows("SELECT id, name FROM companies WHERE id IN (1,2,3) ORDER BY id").map! {|i| i.map! {|j| j.to_s unless j.nil?}} + Company.connection.select_rows("SELECT id, name FROM companies WHERE id IN (1,2,3) ORDER BY id").map! { |i| i.map! { |j| j.to_s unless j.nil? } } end def test_find_with_order_on_included_associations_with_construct_finder_sql_for_association_limiting_and_is_distinct @@ -1097,7 +1096,7 @@ class FinderTest < ActiveRecord::TestCase order("client_of DESC"). map(&:client_of) - assert client_of.include?(nil) + assert_includes client_of, nil assert_equal [2, 1].sort, client_of.compact.sort end @@ -1119,6 +1118,16 @@ class FinderTest < ActiveRecord::TestCase assert_equal [0, 1, 1], posts.map(&:author_id).sort end + def test_find_one_message_on_primary_key + e = assert_raises(ActiveRecord::RecordNotFound) do + Car.find(0) + end + assert_equal 0, e.id + assert_equal "id", e.primary_key + assert_equal "Car", e.model + assert_equal "Couldn't find Car with 'id'=0", e.message + end + def test_find_one_message_with_custom_primary_key table_with_custom_primary_key do |model| model.primary_key = :name @@ -1162,12 +1171,12 @@ class FinderTest < ActiveRecord::TestCase end test "find_by returns nil if the record is missing" do - assert_equal nil, Post.find_by("1 = 0") + 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 @@ -1216,7 +1225,7 @@ class FinderTest < ActiveRecord::TestCase assert_equal tyre2, zyke.tyres.custom_find_by(id: tyre2.id) end - protected + private def table_with_custom_primary_key yield(Class.new(Toy) do def self.name diff --git a/activerecord/test/cases/fixture_set/file_test.rb b/activerecord/test/cases/fixture_set/file_test.rb index e69c9f303a..533edcc2e0 100644 --- a/activerecord/test/cases/fixture_set/file_test.rb +++ b/activerecord/test/cases/fixture_set/file_test.rb @@ -31,7 +31,7 @@ module ActiveRecord def test_values File.open(::File.join(FIXTURES_ROOT, "accounts.yml")) do |fh| - assert_equal [1,2,3,4,5,6].sort, fh.to_a.map(&:last).map { |x| + assert_equal [1, 2, 3, 4, 5, 6].sort, fh.to_a.map(&:last).map { |x| x["id"] }.sort end @@ -77,7 +77,7 @@ module ActiveRecord yaml = "one:\n name: <%= fixture_helper %>\n" tmp_yaml ["curious", "yml"], yaml do |t| golden = - [["one", {"name" => "Fixture helper"}]] + [["one", { "name" => "Fixture helper" }]] assert_equal golden, File.open(t.path) { |fh| fh.to_a } end ActiveRecord::FixtureSet.context_class.class_eval do diff --git a/activerecord/test/cases/fixtures_test.rb b/activerecord/test/cases/fixtures_test.rb index 24a9894f19..61e596e208 100644 --- a/activerecord/test/cases/fixtures_test.rb +++ b/activerecord/test/cases/fixtures_test.rb @@ -7,17 +7,19 @@ require "models/binary" require "models/book" require "models/bulb" require "models/category" +require "models/post" require "models/comment" require "models/company" require "models/computer" require "models/course" require "models/developer" +require "models/dog" require "models/doubloon" require "models/joke" require "models/matey" +require "models/other_dog" require "models/parrot" require "models/pirate" -require "models/post" require "models/randomly_named_c1" require "models/reply" require "models/ship" @@ -192,28 +194,38 @@ class FixturesTest < ActiveRecord::TestCase end def test_empty_yaml_fixture - assert_not_nil ActiveRecord::FixtureSet.new( Account.connection, "accounts", Account, FIXTURES_ROOT + "/naked/yml/accounts") + assert_not_nil ActiveRecord::FixtureSet.new(Account.connection, "accounts", Account, FIXTURES_ROOT + "/naked/yml/accounts") end def test_empty_yaml_fixture_with_a_comment_in_it - assert_not_nil ActiveRecord::FixtureSet.new( Account.connection, "companies", Company, FIXTURES_ROOT + "/naked/yml/companies") + assert_not_nil ActiveRecord::FixtureSet.new(Account.connection, "companies", Company, FIXTURES_ROOT + "/naked/yml/companies") end def test_nonexistent_fixture_file nonexistent_fixture_path = FIXTURES_ROOT + "/imnothere" #sanity check to make sure that this file never exists - assert Dir[nonexistent_fixture_path+"*"].empty? + assert Dir[nonexistent_fixture_path + "*"].empty? assert_raise(Errno::ENOENT) do - ActiveRecord::FixtureSet.new( Account.connection, "companies", Company, nonexistent_fixture_path) + ActiveRecord::FixtureSet.new(Account.connection, "companies", Company, nonexistent_fixture_path) end end def test_dirty_dirty_yaml_file - assert_raise(ActiveRecord::Fixture::FormatError) do - ActiveRecord::FixtureSet.new( Account.connection, "courses", Course, FIXTURES_ROOT + "/naked/yml/courses") + fixture_path = FIXTURES_ROOT + "/naked/yml/courses" + error = assert_raise(ActiveRecord::Fixture::FormatError) do + ActiveRecord::FixtureSet.new(Account.connection, "courses", Course, fixture_path) + end + assert_equal "fixture is not a hash: #{fixture_path}.yml", error.to_s + end + + def test_yaml_file_with_one_invalid_fixture + fixture_path = FIXTURES_ROOT + "/naked/yml/courses_with_invalid_key" + error = assert_raise(ActiveRecord::Fixture::FormatError) do + ActiveRecord::FixtureSet.new(Account.connection, "courses", Course, fixture_path) end + assert_equal "fixture key is not a hash: #{fixture_path}.yml, keys: [\"two\"]", error.to_s end def test_yaml_file_with_invalid_column @@ -728,7 +740,7 @@ class LoadAllFixturesTest < ActiveRecord::TestCase self.class.fixtures :all if File.symlink? FIXTURES_ROOT + "/all/admin" - assert_equal %w(admin/accounts admin/users developers people tasks), fixture_table_names.sort + assert_equal %w(admin/accounts admin/users developers namespaced/accounts people tasks), fixture_table_names.sort end ensure ActiveRecord::FixtureSet.reset_cache @@ -741,7 +753,7 @@ class LoadAllFixturesWithPathnameTest < ActiveRecord::TestCase self.class.fixtures :all if File.symlink? FIXTURES_ROOT + "/all/admin" - assert_equal %w(admin/accounts admin/users developers people tasks), fixture_table_names.sort + assert_equal %w(admin/accounts admin/users developers namespaced/accounts people tasks), fixture_table_names.sort end ensure ActiveRecord::FixtureSet.reset_cache @@ -923,7 +935,7 @@ class FoxyFixturesTest < ActiveRecord::TestCase end def test_namespaced_models - assert admin_accounts(:signals37).users.include?(admin_users(:david)) + assert_includes admin_accounts(:signals37).users, admin_users(:david) assert_equal 2, admin_accounts(:signals37).users.size end @@ -948,7 +960,7 @@ end class CustomNameForFixtureOrModelTest < ActiveRecord::TestCase ActiveRecord::FixtureSet.reset_cache - set_fixture_class :randomly_named_a9 => + set_fixture_class :randomly_named_a9 => ClassNameThatDoesNotFollowCONVENTIONS, :'admin/randomly_named_a9' => Admin::ClassNameThatDoesNotFollowCONVENTIONS1, @@ -1000,7 +1012,7 @@ end class FixtureClassNamesTest < ActiveRecord::TestCase def setup - @saved_cache = self.fixture_class_names.dup + @saved_cache = fixture_class_names.dup end def teardown @@ -1011,3 +1023,16 @@ class FixtureClassNamesTest < ActiveRecord::TestCase assert_nil fixture_class_names["unregistered_identifier"] end end + +class SameNameDifferentDatabaseFixturesTest < ActiveRecord::TestCase + fixtures :dogs, :other_dogs + + test "fixtures are properly loaded" do + # Force loading the fixtures again to reproduce issue + ActiveRecord::FixtureSet.reset_cache + create_fixtures("dogs", "other_dogs") + + assert_kind_of Dog, dogs(:sophie) + assert_kind_of OtherDog, other_dogs(:lassie) + end +end diff --git a/activerecord/test/cases/forbidden_attributes_protection_test.rb b/activerecord/test/cases/forbidden_attributes_protection_test.rb index b17daafe01..ffa3f63e0d 100644 --- a/activerecord/test/cases/forbidden_attributes_protection_test.rb +++ b/activerecord/test/cases/forbidden_attributes_protection_test.rb @@ -140,11 +140,11 @@ class ForbiddenAttributesProtectionTest < ActiveRecord::TestCase def test_where_not_works_with_permitted_params params = ProtectedParams.new(first_name: "Guille").permit! Person.create!(params) - assert_empty Person.where.not(params).select {|p| p.first_name == "Guille" } + assert_empty Person.where.not(params).select { |p| p.first_name == "Guille" } end def test_strong_params_style_objects_work_with_singular_associations - params = ProtectedParams.new( name: "Stern", ship_attributes: ProtectedParams.new(name: "The Black Rock").permit!).permit! + params = ProtectedParams.new(name: "Stern", ship_attributes: ProtectedParams.new(name: "The Black Rock").permit!).permit! part = ShipPart.new(params) assert_equal "Stern", part.name @@ -155,7 +155,7 @@ class ForbiddenAttributesProtectionTest < ActiveRecord::TestCase params = ProtectedParams.new( trinkets_attributes: ProtectedParams.new( "0" => ProtectedParams.new(name: "Necklace").permit!, - "1" => ProtectedParams.new(name: "Spoon").permit! ) ).permit! + "1" => ProtectedParams.new(name: "Spoon").permit!)).permit! part = ShipPart.new(params) assert_equal "Necklace", part.trinkets[0].name diff --git a/activerecord/test/cases/helper.rb b/activerecord/test/cases/helper.rb index ebd4437a87..1ddcbf0e4f 100644 --- a/activerecord/test/cases/helper.rb +++ b/activerecord/test/cases/helper.rb @@ -1,7 +1,5 @@ require "config" -require "active_support/testing/autorun" -require "active_support/testing/method_call_assertions" require "stringio" require "active_record" @@ -29,9 +27,6 @@ ARTest.connect # Quote "type" if it's a reserved word for the current connection. QUOTED_TYPE = ActiveRecord::Base.connection.quote_column_name("type") -# FIXME: Remove this when the deprecation cycle on TZ aware types by default ends. -ActiveRecord::Base.time_zone_aware_types << :time - def current_adapter?(*types) types.any? do |type| ActiveRecord::ConnectionAdapters.const_defined?(type) && @@ -134,21 +129,6 @@ def disable_extension!(extension, connection) connection.reconnect! end -require "cases/validations_repair_helper" -class ActiveSupport::TestCase - include ActiveRecord::TestFixtures - include ActiveRecord::ValidationsRepairHelper - include ActiveSupport::Testing::MethodCallAssertions - - self.fixture_path = FIXTURES_ROOT - self.use_instantiated_fixtures = false - self.use_transactional_tests = true - - def create_fixtures(*fixture_set_names, &block) - ActiveRecord::FixtureSet.create_fixtures(ActiveSupport::TestCase.fixture_path, fixture_set_names, fixture_class_names, &block) - end -end - def load_schema # silence verbose schema loading original_stdout = $stdout diff --git a/activerecord/test/cases/hot_compatibility_test.rb b/activerecord/test/cases/hot_compatibility_test.rb index e0946544ef..e107ff2362 100644 --- a/activerecord/test/cases/hot_compatibility_test.rb +++ b/activerecord/test/cases/hot_compatibility_test.rb @@ -119,11 +119,11 @@ class HotCompatibilityTest < ActiveRecord::TestCase .instance_variable_get(:@cache)[Process.pid] end - # Rails will automatically clear the prepared statements on the connection - # that runs the migration, so we use two connections to simulate what would - # actually happen on a production system; we'd have one connection running the - # migration from the rake task ("ddl_connection" here), and we'd have another - # connection in a web worker. + # Rails will automatically clear the prepared statements on the connection + # that runs the migration, so we use two connections to simulate what would + # actually happen on a production system; we'd have one connection running the + # migration from the rake task ("ddl_connection" here), and we'd have another + # connection in a web worker. def with_two_connections run_without_connection do |original_connection| ActiveRecord::Base.establish_connection(original_connection.merge(pool_size: 2)) diff --git a/activerecord/test/cases/i18n_test.rb b/activerecord/test/cases/i18n_test.rb index ab3a1132e8..7f03c5b23d 100644 --- a/activerecord/test/cases/i18n_test.rb +++ b/activerecord/test/cases/i18n_test.rb @@ -8,37 +8,37 @@ class ActiveRecordI18nTests < ActiveRecord::TestCase end def test_translated_model_attributes - I18n.backend.store_translations "en", activerecord: {attributes: {topic: {title: "topic title attribute"} } } + I18n.backend.store_translations "en", activerecord: { attributes: { topic: { title: "topic title attribute" } } } assert_equal "topic title attribute", Topic.human_attribute_name("title") end def test_translated_model_attributes_with_symbols - I18n.backend.store_translations "en", activerecord: {attributes: {topic: {title: "topic title attribute"} } } + I18n.backend.store_translations "en", activerecord: { attributes: { topic: { title: "topic title attribute" } } } assert_equal "topic title attribute", Topic.human_attribute_name(:title) end def test_translated_model_attributes_with_sti - I18n.backend.store_translations "en", activerecord: {attributes: {reply: {title: "reply title attribute"} } } + I18n.backend.store_translations "en", activerecord: { attributes: { reply: { title: "reply title attribute" } } } assert_equal "reply title attribute", Reply.human_attribute_name("title") end def test_translated_model_attributes_with_sti_fallback - I18n.backend.store_translations "en", activerecord: {attributes: {topic: {title: "topic title attribute"} } } + I18n.backend.store_translations "en", activerecord: { attributes: { topic: { title: "topic title attribute" } } } assert_equal "topic title attribute", Reply.human_attribute_name("title") end def test_translated_model_names - I18n.backend.store_translations "en", activerecord: {models: {topic: "topic model"} } + I18n.backend.store_translations "en", activerecord: { models: { topic: "topic model" } } assert_equal "topic model", Topic.model_name.human end def test_translated_model_names_with_sti - I18n.backend.store_translations "en", activerecord: {models: {reply: "reply model"} } + I18n.backend.store_translations "en", activerecord: { models: { reply: "reply model" } } assert_equal "reply model", Reply.model_name.human end def test_translated_model_names_with_sti_fallback - I18n.backend.store_translations "en", activerecord: {models: {topic: "topic model"} } + I18n.backend.store_translations "en", activerecord: { models: { topic: "topic model" } } assert_equal "topic model", Reply.model_name.human end end diff --git a/activerecord/test/cases/inheritance_test.rb b/activerecord/test/cases/inheritance_test.rb index 9090184622..e570e9ac1d 100644 --- a/activerecord/test/cases/inheritance_test.rb +++ b/activerecord/test/cases/inheritance_test.rb @@ -58,21 +58,21 @@ class InheritanceTest < ActiveRecord::TestCase end def test_compute_type_success - assert_equal Author, ActiveRecord::Base.send(:compute_type, "Author") + assert_equal Author, Company.send(:compute_type, "Author") end def test_compute_type_nonexistent_constant e = assert_raises NameError do - ActiveRecord::Base.send :compute_type, "NonexistentModel" + Company.send :compute_type, "NonexistentModel" end - assert_equal "uninitialized constant ActiveRecord::Base::NonexistentModel", e.message - assert_equal "ActiveRecord::Base::NonexistentModel", e.name + assert_equal "uninitialized constant Company::NonexistentModel", e.message + assert_equal "Company::NonexistentModel", e.name end def test_compute_type_no_method_error - ActiveSupport::Dependencies.stub(:safe_constantize, proc{ raise NoMethodError }) do + ActiveSupport::Dependencies.stub(:safe_constantize, proc { raise NoMethodError }) do assert_raises NoMethodError do - ActiveRecord::Base.send :compute_type, "InvalidModel" + Company.send :compute_type, "InvalidModel" end end end @@ -87,19 +87,19 @@ class InheritanceTest < ActiveRecord::TestCase error = e end - ActiveSupport::Dependencies.stub(:safe_constantize, proc{ raise e }) do + ActiveSupport::Dependencies.stub(:safe_constantize, proc { raise e }) do exception = assert_raises NameError do - ActiveRecord::Base.send :compute_type, "InvalidModel" + Company.send :compute_type, "InvalidModel" end assert_equal error.message, exception.message end end def test_compute_type_argument_error - ActiveSupport::Dependencies.stub(:safe_constantize, proc{ raise ArgumentError }) do + ActiveSupport::Dependencies.stub(:safe_constantize, proc { raise ArgumentError }) do assert_raises ArgumentError do - ActiveRecord::Base.send :compute_type, "InvalidModel" + Company.send :compute_type, "InvalidModel" end end end diff --git a/activerecord/test/cases/integration_test.rb b/activerecord/test/cases/integration_test.rb index 766917b196..d7aa091623 100644 --- a/activerecord/test/cases/integration_test.rb +++ b/activerecord/test/cases/integration_test.rb @@ -15,7 +15,7 @@ class IntegrationTest < ActiveRecord::TestCase def test_to_param_returns_nil_if_not_persisted client = Client.new - assert_equal nil, client.to_param + assert_nil client.to_param end def test_to_param_returns_id_if_not_persisted_but_id_is_set @@ -89,7 +89,7 @@ class IntegrationTest < ActiveRecord::TestCase def test_to_param_class_method_uses_default_if_not_persisted firm = Firm.new(name: "Fancy Shirts") - assert_equal nil, firm.to_param + assert_nil firm.to_param end def test_to_param_with_no_arguments @@ -172,4 +172,10 @@ class IntegrationTest < ActiveRecord::TestCase owner = owners(:blackbeard) assert_equal "owners/#{owner.id}-#{owner.happy_at.utc.to_s(:usec)}", owner.cache_key(:updated_at, :happy_at) end + + def test_cache_key_when_named_timestamp_is_nil + owner = owners(:blackbeard) + owner.happy_at = nil + assert_equal "owners/#{owner.id}", owner.cache_key(:happy_at) + end end diff --git a/activerecord/test/cases/invertible_migration_test.rb b/activerecord/test/cases/invertible_migration_test.rb index 81e8b0000a..cc3951e2ba 100644 --- a/activerecord/test/cases/invertible_migration_test.rb +++ b/activerecord/test/cases/invertible_migration_test.rb @@ -165,10 +165,8 @@ module ActiveRecord teardown do %w[horses new_horses].each do |table| - ActiveSupport::Deprecation.silence do - if ActiveRecord::Base.connection.table_exists?(table) - ActiveRecord::Base.connection.drop_table(table) - end + if ActiveRecord::Base.connection.table_exists?(table) + ActiveRecord::Base.connection.drop_table(table) end end ActiveRecord::Migration.verbose = @verbose_was @@ -199,14 +197,14 @@ module ActiveRecord def test_migrate_up migration = InvertibleMigration.new migration.migrate(:up) - ActiveSupport::Deprecation.silence { assert migration.connection.table_exists?("horses"), "horses should exist" } + assert migration.connection.table_exists?("horses"), "horses should exist" end def test_migrate_down migration = InvertibleMigration.new migration.migrate :up migration.migrate :down - ActiveSupport::Deprecation.silence { assert !migration.connection.table_exists?("horses") } + assert !migration.connection.table_exists?("horses") end def test_migrate_revert @@ -214,36 +212,30 @@ module ActiveRecord revert = InvertibleRevertMigration.new migration.migrate :up revert.migrate :up - ActiveSupport::Deprecation.silence { assert !migration.connection.table_exists?("horses") } + assert !migration.connection.table_exists?("horses") revert.migrate :down - ActiveSupport::Deprecation.silence { assert migration.connection.table_exists?("horses") } + assert migration.connection.table_exists?("horses") migration.migrate :down - ActiveSupport::Deprecation.silence { assert !migration.connection.table_exists?("horses") } + assert !migration.connection.table_exists?("horses") end def test_migrate_revert_by_part InvertibleMigration.new.migrate :up received = [] migration = InvertibleByPartsMigration.new - migration.test = ->(dir){ - ActiveSupport::Deprecation.silence do - assert migration.connection.table_exists?("horses") - assert migration.connection.table_exists?("new_horses") - end + migration.test = ->(dir) { + assert migration.connection.table_exists?("horses") + assert migration.connection.table_exists?("new_horses") received << dir } migration.migrate :up assert_equal [:both, :up], received - ActiveSupport::Deprecation.silence do - assert !migration.connection.table_exists?("horses") - assert migration.connection.table_exists?("new_horses") - end + assert !migration.connection.table_exists?("horses") + assert migration.connection.table_exists?("new_horses") migration.migrate :down assert_equal [:both, :up, :both, :down], received - ActiveSupport::Deprecation.silence do - assert migration.connection.table_exists?("horses") - assert !migration.connection.table_exists?("new_horses") - end + assert migration.connection.table_exists?("horses") + assert !migration.connection.table_exists?("new_horses") end def test_migrate_revert_whole_migration @@ -252,20 +244,20 @@ module ActiveRecord revert = RevertWholeMigration.new(klass) migration.migrate :up revert.migrate :up - ActiveSupport::Deprecation.silence { assert !migration.connection.table_exists?("horses") } + assert !migration.connection.table_exists?("horses") revert.migrate :down - ActiveSupport::Deprecation.silence { assert migration.connection.table_exists?("horses") } + assert migration.connection.table_exists?("horses") migration.migrate :down - ActiveSupport::Deprecation.silence { assert !migration.connection.table_exists?("horses") } + assert !migration.connection.table_exists?("horses") end end def test_migrate_nested_revert_whole_migration revert = NestedRevertWholeMigration.new(InvertibleRevertMigration) revert.migrate :down - ActiveSupport::Deprecation.silence { assert revert.connection.table_exists?("horses") } + assert revert.connection.table_exists?("horses") revert.migrate :up - ActiveSupport::Deprecation.silence { assert !revert.connection.table_exists?("horses") } + assert !revert.connection.table_exists?("horses") end def test_migrate_revert_change_column_default @@ -305,7 +297,7 @@ module ActiveRecord end def test_revert_order - block = Proc.new{|t| t.string :name } + block = Proc.new { |t| t.string :name } recorder = ActiveRecord::Migration::CommandRecorder.new(ActiveRecord::Base.connection) recorder.instance_eval do create_table("apples", &block) @@ -330,24 +322,24 @@ module ActiveRecord def test_legacy_up LegacyMigration.migrate :up - ActiveSupport::Deprecation.silence { assert ActiveRecord::Base.connection.table_exists?("horses"), "horses should exist" } + assert ActiveRecord::Base.connection.table_exists?("horses"), "horses should exist" end def test_legacy_down LegacyMigration.migrate :up LegacyMigration.migrate :down - ActiveSupport::Deprecation.silence { assert !ActiveRecord::Base.connection.table_exists?("horses"), "horses should not exist" } + assert !ActiveRecord::Base.connection.table_exists?("horses"), "horses should not exist" end def test_up LegacyMigration.up - ActiveSupport::Deprecation.silence { assert ActiveRecord::Base.connection.table_exists?("horses"), "horses should exist" } + assert ActiveRecord::Base.connection.table_exists?("horses"), "horses should exist" end def test_down LegacyMigration.up LegacyMigration.down - ActiveSupport::Deprecation.silence { assert !ActiveRecord::Base.connection.table_exists?("horses"), "horses should not exist" } + assert !ActiveRecord::Base.connection.table_exists?("horses"), "horses should not exist" end def test_migrate_down_with_table_name_prefix @@ -356,7 +348,7 @@ module ActiveRecord migration = InvertibleMigration.new migration.migrate(:up) assert_nothing_raised { migration.migrate(:down) } - ActiveSupport::Deprecation.silence { assert !ActiveRecord::Base.connection.table_exists?("p_horses_s"), "p_horses_s should not exist" } + assert !ActiveRecord::Base.connection.table_exists?("p_horses_s"), "p_horses_s should not exist" ensure ActiveRecord::Base.table_name_prefix = ActiveRecord::Base.table_name_suffix = "" end diff --git a/activerecord/test/cases/json_serialization_test.rb b/activerecord/test/cases/json_serialization_test.rb index 814d3b63b2..155e858822 100644 --- a/activerecord/test/cases/json_serialization_test.rb +++ b/activerecord/test/cases/json_serialization_test.rb @@ -51,7 +51,7 @@ class JsonSerializationTest < ActiveRecord::TestCase assert_match %r{^\{"contact":\{}, json assert_match %r{"name":"Konata Izumi"}, json assert_match %r{"age":16}, json - assert json.include?(%("created_at":#{ActiveSupport::JSON.encode(Time.utc(2006, 8, 1))})) + assert_includes json, %("created_at":#{ActiveSupport::JSON.encode(Time.utc(2006, 8, 1))}) assert_match %r{"awesome":true}, json assert_match %r{"preferences":\{"shows":"anime"\}}, json end @@ -62,7 +62,7 @@ class JsonSerializationTest < ActiveRecord::TestCase assert_match %r{"name":"Konata Izumi"}, json assert_match %r{"age":16}, json - assert json.include?(%("created_at":#{ActiveSupport::JSON.encode(Time.utc(2006, 8, 1))})) + assert_includes json, %("created_at":#{ActiveSupport::JSON.encode(Time.utc(2006, 8, 1))}) assert_match %r{"awesome":true}, json assert_match %r{"preferences":\{"shows":"anime"\}}, json end @@ -73,7 +73,7 @@ class JsonSerializationTest < ActiveRecord::TestCase assert_match %r{"name":"Konata Izumi"}, json assert_match %r{"age":16}, json assert_no_match %r{"awesome":true}, json - assert !json.include?(%("created_at":#{ActiveSupport::JSON.encode(Time.utc(2006, 8, 1))})) + assert_not_includes json, %("created_at":#{ActiveSupport::JSON.encode(Time.utc(2006, 8, 1))}) assert_no_match %r{"preferences":\{"shows":"anime"\}}, json end @@ -83,7 +83,7 @@ class JsonSerializationTest < ActiveRecord::TestCase assert_no_match %r{"name":"Konata Izumi"}, json assert_no_match %r{"age":16}, json assert_match %r{"awesome":true}, json - assert json.include?(%("created_at":#{ActiveSupport::JSON.encode(Time.utc(2006, 8, 1))})) + assert_includes json, %("created_at":#{ActiveSupport::JSON.encode(Time.utc(2006, 8, 1))}) assert_match %r{"preferences":\{"shows":"anime"\}}, json end @@ -102,7 +102,7 @@ class JsonSerializationTest < ActiveRecord::TestCase end def test_uses_serializable_hash_with_only_option - def @contact.serializable_hash(options=nil) + def @contact.serializable_hash(options = nil) super(only: %w(name)) end @@ -113,7 +113,7 @@ class JsonSerializationTest < ActiveRecord::TestCase end def test_uses_serializable_hash_with_except_option - def @contact.serializable_hash(options=nil) + def @contact.serializable_hash(options = nil) super(except: %w(age)) end @@ -137,7 +137,7 @@ class JsonSerializationTest < ActiveRecord::TestCase @contact = ContactSti.new(@contact.attributes) assert_equal "ContactSti", @contact.type - def @contact.serializable_hash(options={}) + def @contact.serializable_hash(options = {}) super({ except: %w(age) }.merge!(options)) end @@ -243,7 +243,7 @@ class DatabaseConnectedJsonEncodingTest < ActiveRecord::TestCase assert !@david.posts.first.respond_to?(:favorite_quote) assert_match %r{"favorite_quote":"Constraints are liberating"}, json - assert_equal %r{"favorite_quote":}.match(json).size, 1 + assert_equal 1, %r{"favorite_quote":}.match(json).size end def test_should_allow_only_option_for_list_of_authors @@ -275,7 +275,7 @@ class DatabaseConnectedJsonEncodingTest < ActiveRecord::TestCase ['"name":"David"', '"posts":[', '{"id":1}', '{"id":2}', '{"id":4}', '{"id":5}', '{"id":6}', '"name":"Mary"', '"posts":[', '{"id":7}', '{"id":9}'].each do |fragment| - assert json.include?(fragment), json + assert_includes json, fragment, json end end diff --git a/activerecord/test/cases/locking_test.rb b/activerecord/test/cases/locking_test.rb index 5c55584ff7..9e42242e55 100644 --- a/activerecord/test/cases/locking_test.rb +++ b/activerecord/test/cases/locking_test.rb @@ -18,7 +18,7 @@ class LockWithoutDefault < ActiveRecord::Base; end class LockWithCustomColumnWithoutDefault < ActiveRecord::Base self.table_name = :lock_without_defaults_cust - self.column_defaults # to test @column_defaults caching. + column_defaults # to test @column_defaults caching. self.locking_column = :custom_lock_version end @@ -161,14 +161,6 @@ class OptimisticLockingTest < ActiveRecord::TestCase assert_equal(error.record.object_id, p2.object_id) end - def test_lock_new_with_nil - p1 = Person.new(first_name: "anika") - p1.save! - p1.lock_version = nil # simulate bad fixture or column with no default - p1.save! - assert_equal 1, p1.lock_version - end - def test_lock_new_when_explicitly_passing_nil p1 = Person.new(first_name: "anika", lock_version: nil) p1.save! @@ -181,6 +173,7 @@ class OptimisticLockingTest < ActiveRecord::TestCase p1.touch assert_equal 1, p1.lock_version + assert_not p1.changed?, "Changes should have been cleared" end def test_touch_stale_object @@ -221,22 +214,149 @@ class OptimisticLockingTest < ActiveRecord::TestCase def test_lock_without_default_sets_version_to_zero t1 = LockWithoutDefault.new + + assert_equal 0, t1.lock_version + assert_nil t1.lock_version_before_type_cast + + t1.save! + t1.reload + assert_equal 0, t1.lock_version + assert_equal 0, t1.lock_version_before_type_cast + end + + def test_lock_without_default_should_work_with_null_in_the_database + ActiveRecord::Base.connection.execute("INSERT INTO lock_without_defaults(title) VALUES('title1')") + t1 = LockWithoutDefault.last + t2 = LockWithoutDefault.last + + assert_equal 0, t1.lock_version + assert_nil t1.lock_version_before_type_cast + assert_equal 0, t2.lock_version + assert_nil t2.lock_version_before_type_cast + + t1.title = "new title1" + t2.title = "new title2" + + assert_nothing_raised { t1.save! } + assert_equal 1, t1.lock_version + assert_equal "new title1", t1.title + + assert_raise(ActiveRecord::StaleObjectError) { t2.save! } + assert_equal 0, t2.lock_version + assert_equal "new title2", t2.title + end + + def test_lock_without_default_should_update_with_lock_col + t1 = LockWithoutDefault.create(title: "title1", lock_version: 6) + + assert_equal 6, t1.lock_version + + t1.update(lock_version: 0) + t1.reload - t1.save - t1 = LockWithoutDefault.find(t1.id) assert_equal 0, t1.lock_version end + def test_lock_without_default_queries_count + t1 = LockWithoutDefault.create(title: "title1") + + assert_equal "title1", t1.title + assert_equal 0, t1.lock_version + + assert_queries(1) { t1.update(title: "title2") } + + t1.reload + assert_equal "title2", t1.title + assert_equal 1, t1.lock_version + + assert_queries(1) { t1.update(title: "title3", lock_version: 6) } + + t1.reload + assert_equal "title3", t1.title + assert_equal 6, t1.lock_version + + t2 = LockWithoutDefault.new(title: "title1") + + assert_queries(1) { t2.save! } + + t2.reload + assert_equal "title1", t2.title + assert_equal 0, t2.lock_version + end + def test_lock_with_custom_column_without_default_sets_version_to_zero t1 = LockWithCustomColumnWithoutDefault.new + assert_equal 0, t1.custom_lock_version assert_nil t1.custom_lock_version_before_type_cast t1.save! t1.reload + + assert_equal 0, t1.custom_lock_version + assert_equal 0, t1.custom_lock_version_before_type_cast + end + + def test_lock_with_custom_column_without_default_should_work_with_null_in_the_database + ActiveRecord::Base.connection.execute("INSERT INTO lock_without_defaults_cust(title) VALUES('title1')") + + t1 = LockWithCustomColumnWithoutDefault.last + t2 = LockWithCustomColumnWithoutDefault.last + assert_equal 0, t1.custom_lock_version - assert [0, "0"].include?(t1.custom_lock_version_before_type_cast) + assert_nil t1.custom_lock_version_before_type_cast + assert_equal 0, t2.custom_lock_version + assert_nil t2.custom_lock_version_before_type_cast + + t1.title = "new title1" + t2.title = "new title2" + + assert_nothing_raised { t1.save! } + assert_equal 1, t1.custom_lock_version + assert_equal "new title1", t1.title + + assert_raise(ActiveRecord::StaleObjectError) { t2.save! } + assert_equal 0, t2.custom_lock_version + assert_equal "new title2", t2.title + end + + def test_lock_with_custom_column_without_default_should_update_with_lock_col + t1 = LockWithCustomColumnWithoutDefault.create(title: "title1", custom_lock_version: 6) + + assert_equal 6, t1.custom_lock_version + + t1.update(custom_lock_version: 0) + t1.reload + + assert_equal 0, t1.custom_lock_version + end + + def test_lock_with_custom_column_without_default_queries_count + t1 = LockWithCustomColumnWithoutDefault.create(title: "title1") + + assert_equal "title1", t1.title + assert_equal 0, t1.custom_lock_version + + assert_queries(1) { t1.update(title: "title2") } + + t1.reload + assert_equal "title2", t1.title + assert_equal 1, t1.custom_lock_version + + assert_queries(1) { t1.update(title: "title3", custom_lock_version: 6) } + + t1.reload + assert_equal "title3", t1.title + assert_equal 6, t1.custom_lock_version + + t2 = LockWithCustomColumnWithoutDefault.new(title: "title1") + + assert_queries(1) { t2.save! } + + t2.reload + assert_equal "title1", t2.title + assert_equal 0, t2.custom_lock_version end def test_readonly_attributes @@ -350,7 +470,7 @@ class OptimisticLockingWithSchemaChangeTest < ActiveRecord::TestCase private - def add_counter_column_to(model, col="test_count") + def add_counter_column_to(model, col = "test_count") model.connection.add_column model.table_name, col, :integer, null: false, default: 0 model.reset_column_information end @@ -459,7 +579,8 @@ unless in_memory_db? assert first.end > second.end end - protected + private + def duel(zzz = 5) t0, t1, t2, t3 = nil, nil, nil, nil diff --git a/activerecord/test/cases/migration/change_schema_test.rb b/activerecord/test/cases/migration/change_schema_test.rb index 0191c9d03b..48cfe89882 100644 --- a/activerecord/test/cases/migration/change_schema_test.rb +++ b/activerecord/test/cases/migration/change_schema_test.rb @@ -43,7 +43,7 @@ module ActiveRecord t.column :foo, :string, null: false end - assert_raises(ActiveRecord::StatementInvalid) do + assert_raises(ActiveRecord::NotNullViolation) do connection.execute "insert into testings (foo) values (NULL)" end end @@ -200,8 +200,8 @@ module ActiveRecord end created_columns = connection.columns(table_name) - created_at_column = created_columns.detect {|c| c.name == "created_at" } - updated_at_column = created_columns.detect {|c| c.name == "updated_at" } + created_at_column = created_columns.detect { |c| c.name == "created_at" } + updated_at_column = created_columns.detect { |c| c.name == "updated_at" } assert !created_at_column.null assert !updated_at_column.null @@ -213,8 +213,8 @@ module ActiveRecord end created_columns = connection.columns(table_name) - created_at_column = created_columns.detect {|c| c.name == "created_at" } - updated_at_column = created_columns.detect {|c| c.name == "updated_at" } + created_at_column = created_columns.detect { |c| c.name == "created_at" } + updated_at_column = created_columns.detect { |c| c.name == "updated_at" } assert created_at_column.null assert updated_at_column.null @@ -233,7 +233,7 @@ module ActiveRecord end connection.add_column :testings, :bar, :string, null: false - assert_raise(ActiveRecord::StatementInvalid) do + assert_raise(ActiveRecord::NotNullViolation) do connection.execute "insert into testings (foo, bar) values ('hello', NULL)" end end @@ -244,12 +244,16 @@ module ActiveRecord t.column :foo, :string end - con = connection - connection.execute "insert into testings (#{con.quote_column_name('id')}, #{con.quote_column_name('foo')}) values (1, 'hello')" - assert_nothing_raised {connection.add_column :testings, :bar, :string, null: false, default: "default" } + quoted_id = connection.quote_column_name("id") + quoted_foo = connection.quote_column_name("foo") + quoted_bar = connection.quote_column_name("bar") + connection.execute("insert into testings (#{quoted_id}, #{quoted_foo}) values (1, 'hello')") + assert_nothing_raised do + connection.add_column :testings, :bar, :string, null: false, default: "default" + end - assert_raises(ActiveRecord::StatementInvalid) do - connection.execute "insert into testings (#{con.quote_column_name('id')}, #{con.quote_column_name('foo')}, #{con.quote_column_name('bar')}) values (2, 'hello', NULL)" + assert_raises(ActiveRecord::NotNullViolation) do + connection.execute("insert into testings (#{quoted_id}, #{quoted_foo}, #{quoted_bar}) values (2, 'hello', NULL)") end end @@ -298,9 +302,9 @@ module ActiveRecord assert_equal false, person_klass.columns_hash["wealth"].null # Oracle needs primary key value from sequence if current_adapter?(:OracleAdapter) - assert_nothing_raised {person_klass.connection.execute("insert into testings (id, title) values (testings_seq.nextval, 'tester')")} + assert_nothing_raised { person_klass.connection.execute("insert into testings (id, title) values (testings_seq.nextval, 'tester')") } else - assert_nothing_raised {person_klass.connection.execute("insert into testings (title) values ('tester')")} + assert_nothing_raised { person_klass.connection.execute("insert into testings (title) values ('tester')") } end # change column default to see that column doesn't lose its not null definition @@ -346,9 +350,9 @@ module ActiveRecord end notnull_migration.new.suppress_messages do notnull_migration.migrate(:up) - assert_equal false, connection.columns(:testings).find{ |c| c.name == "foo"}.null + assert_equal false, connection.columns(:testings).find { |c| c.name == "foo" }.null notnull_migration.migrate(:down) - assert connection.columns(:testings).find{ |c| c.name == "foo"}.null + assert connection.columns(:testings).find { |c| c.name == "foo" }.null end end end @@ -405,9 +409,9 @@ module ActiveRecord def test_drop_table_if_exists connection.create_table(:testings) - ActiveSupport::Deprecation.silence { assert connection.table_exists?(:testings) } + assert connection.table_exists?(:testings) connection.drop_table(:testings, if_exists: true) - ActiveSupport::Deprecation.silence { assert_not connection.table_exists?(:testings) } + assert_not connection.table_exists?(:testings) end def test_drop_table_if_exists_nothing_raised diff --git a/activerecord/test/cases/migration/change_table_test.rb b/activerecord/test/cases/migration/change_table_test.rb index 87da9c90fe..8a4242cf1d 100644 --- a/activerecord/test/cases/migration/change_table_test.rb +++ b/activerecord/test/cases/migration/change_table_test.rb @@ -101,7 +101,12 @@ module ActiveRecord def test_primary_key_creates_primary_key_column with_change_table do |t| - @connection.expect :add_column, nil, [:delete_me, :id, :primary_key, primary_key: true, first: true] + if current_adapter?(:Mysql2Adapter) + @connection.expect :add_column, nil, [:delete_me, :id, :primary_key, { first: true, auto_increment: true, limit: 8, primary_key: true }] + else + @connection.expect :add_column, nil, [:delete_me, :id, :primary_key, primary_key: true, first: true] + end + t.primary_key :id, first: true end end @@ -157,7 +162,7 @@ module ActiveRecord def test_column_creates_column_with_options with_change_table do |t| - @connection.expect :add_column, nil, [:delete_me, :bar, :integer, {null: false}] + @connection.expect :add_column, nil, [:delete_me, :bar, :integer, { null: false }] t.column :bar, :integer, null: false end end @@ -171,7 +176,7 @@ module ActiveRecord def test_index_creates_index_with_options with_change_table do |t| - @connection.expect :add_index, nil, [:delete_me, :bar, {unique: true}] + @connection.expect :add_index, nil, [:delete_me, :bar, { unique: true }] t.index :bar, unique: true end end @@ -185,7 +190,7 @@ module ActiveRecord def test_index_exists_with_options with_change_table do |t| - @connection.expect :index_exists?, nil, [:delete_me, :bar, {unique: true}] + @connection.expect :index_exists?, nil, [:delete_me, :bar, { unique: true }] t.index_exists?(:bar, unique: true) end end @@ -206,7 +211,7 @@ module ActiveRecord def test_change_changes_column_with_options with_change_table do |t| - @connection.expect :change_column, nil, [:delete_me, :bar, :string, {null: true}] + @connection.expect :change_column, nil, [:delete_me, :bar, :string, { null: true }] t.change :bar, :string, null: true end end @@ -234,7 +239,7 @@ module ActiveRecord def test_remove_index_removes_index_with_options with_change_table do |t| - @connection.expect :remove_index, nil, [:delete_me, {unique: true}] + @connection.expect :remove_index, nil, [:delete_me, { unique: true }] t.remove_index unique: true end end diff --git a/activerecord/test/cases/migration/column_attributes_test.rb b/activerecord/test/cases/migration/column_attributes_test.rb index ab8229dbae..48df931543 100644 --- a/activerecord/test/cases/migration/column_attributes_test.rb +++ b/activerecord/test/cases/migration/column_attributes_test.rb @@ -63,8 +63,6 @@ module ActiveRecord # Do a manual insertion if current_adapter?(:OracleAdapter) connection.execute "insert into test_models (id, wealth) values (people_seq.nextval, 12345678901234567890.0123456789)" - elsif current_adapter?(:PostgreSQLAdapter) - connection.execute "insert into test_models (wealth) values (12345678901234567890.0123456789)" else connection.execute "insert into test_models (wealth) values (12345678901234567890.0123456789)" end @@ -74,9 +72,7 @@ module ActiveRecord assert_kind_of BigDecimal, row.wealth # If this assert fails, that means the SELECT is broken! - unless current_adapter?(:SQLite3Adapter) - assert_equal correct_value, row.wealth - end + assert_equal correct_value, row.wealth # Reset to old state TestModel.delete_all @@ -167,7 +163,7 @@ module ActiveRecord assert_raise(ActiveRecordError) { add_column :test_models, :integer_too_big, :integer, limit: 10 } unless current_adapter?(:PostgreSQLAdapter) - assert_raise(ActiveRecordError) { add_column :test_models, :text_too_big, :integer, limit: 0xfffffffff } + assert_raise(ActiveRecordError) { add_column :test_models, :text_too_big, :text, limit: 0xfffffffff } end end end diff --git a/activerecord/test/cases/migration/columns_test.rb b/activerecord/test/cases/migration/columns_test.rb index 70d40ed87a..55c06da411 100644 --- a/activerecord/test/cases/migration/columns_test.rb +++ b/activerecord/test/cases/migration/columns_test.rb @@ -32,7 +32,7 @@ module ActiveRecord rename_column :test_models, :first_name, :nick_name TestModel.reset_column_information - assert TestModel.column_names.include?("nick_name") + assert_includes TestModel.column_names, "nick_name" assert_equal ["foo"], TestModel.all.map(&:nick_name) end @@ -45,7 +45,7 @@ module ActiveRecord rename_column "test_models", "first_name", "nick_name" TestModel.reset_column_information - assert TestModel.column_names.include?("nick_name") + assert_includes TestModel.column_names, "nick_name" assert_equal ["foo"], TestModel.all.map(&:nick_name) end @@ -57,7 +57,7 @@ module ActiveRecord rename_column "test_models", "salary", "annual_salary" - assert TestModel.column_names.include?("annual_salary") + assert_includes TestModel.column_names, "annual_salary" default_after = connection.columns("test_models").find { |c| c.name == "annual_salary" }.default assert_equal "70000", default_after end @@ -88,7 +88,7 @@ module ActiveRecord add_column "test_models", "first_name", :string rename_column "test_models", "first_name", "group" - assert TestModel.column_names.include?("group") + assert_includes TestModel.column_names, "group" end def test_rename_column_with_an_index @@ -192,8 +192,8 @@ module ActiveRecord new_columns = connection.columns(TestModel.table_name) - assert_not new_columns.find { |c| c.name == "age" and c.type == :integer } - assert new_columns.find { |c| c.name == "age" and c.type == :string } + assert_not new_columns.find { |c| c.name == "age" && c.type == :integer } + assert new_columns.find { |c| c.name == "age" && c.type == :string } old_columns = connection.columns(TestModel.table_name) assert old_columns.find { |c| @@ -206,11 +206,11 @@ module ActiveRecord assert_not new_columns.find { |c| default = connection.lookup_cast_type_from_column(c).deserialize(c.default) - c.name == "approved" and c.type == :boolean and default == true + c.name == "approved" && c.type == :boolean && default == true } assert new_columns.find { |c| default = connection.lookup_cast_type_from_column(c).deserialize(c.default) - c.name == "approved" and c.type == :boolean and default == false + c.name == "approved" && c.type == :boolean && default == false } change_column :test_models, :approved, :boolean, default: true end diff --git a/activerecord/test/cases/migration/command_recorder_test.rb b/activerecord/test/cases/migration/command_recorder_test.rb index 39f3fd99c7..802a969cb7 100644 --- a/activerecord/test/cases/migration/command_recorder_test.rb +++ b/activerecord/test/cases/migration/command_recorder_test.rb @@ -44,7 +44,7 @@ module ActiveRecord def test_irreversible_commands_raise_exception assert_raises(ActiveRecord::IrreversibleMigration) do - @recorder.revert{ @recorder.execute "some sql" } + @recorder.revert { @recorder.execute "some sql" } end end @@ -58,12 +58,12 @@ module ActiveRecord @recorder.record :create_table, [:hello] @recorder.record :create_table, [:world] end - tables = @recorder.commands.map{|_cmd, args, _block| args} + tables = @recorder.commands.map { |_cmd, args, _block| args } assert_equal [[:world], [:hello]], tables end def test_revert_order - block = Proc.new{|t| t.string :name } + block = Proc.new { |t| t.string :name } @recorder.instance_eval do create_table("apples", &block) revert do @@ -115,13 +115,13 @@ module ActiveRecord end def test_invert_create_table_with_options_and_block - block = Proc.new{} + block = Proc.new {} drop_table = @recorder.inverse_of :create_table, [:people_reminders, id: false], &block assert_equal [:drop_table, [:people_reminders, id: false], block], drop_table end def test_invert_drop_table - block = Proc.new{} + block = Proc.new {} create_table = @recorder.inverse_of :drop_table, [:people_reminders, id: false], &block assert_equal [:create_table, [:people_reminders, id: false], block], create_table end @@ -143,7 +143,7 @@ module ActiveRecord end def test_invert_drop_join_table - block = Proc.new{} + block = Proc.new {} create_join_table = @recorder.inverse_of :drop_join_table, [:musics, :artists, table_name: :catalog], &block assert_equal [:create_join_table, [:musics, :artists, table_name: :catalog], block], create_join_table end @@ -203,17 +203,17 @@ module ActiveRecord def test_invert_add_index remove = @recorder.inverse_of :add_index, [:table, [:one, :two]] - assert_equal [:remove_index, [:table, {column: [:one, :two]}]], remove + assert_equal [:remove_index, [:table, { column: [:one, :two] }]], remove end def test_invert_add_index_with_name remove = @recorder.inverse_of :add_index, [:table, [:one, :two], name: "new_index"] - assert_equal [:remove_index, [:table, {name: "new_index"}]], remove + assert_equal [:remove_index, [:table, { name: "new_index" }]], remove end def test_invert_add_index_with_no_options remove = @recorder.inverse_of :add_index, [:table, [:one, :two]] - assert_equal [:remove_index, [:table, {column: [:one, :two]}]], remove + assert_equal [:remove_index, [:table, { column: [:one, :two] }]], remove end def test_invert_remove_index @@ -222,17 +222,17 @@ module ActiveRecord end def test_invert_remove_index_with_column - add = @recorder.inverse_of :remove_index, [:table, {column: [:one, :two], options: true}] + add = @recorder.inverse_of :remove_index, [:table, { column: [:one, :two], options: true }] assert_equal [:add_index, [:table, [:one, :two], options: true]], add end def test_invert_remove_index_with_name - add = @recorder.inverse_of :remove_index, [:table, {column: [:one, :two], name: "new_index"}] + add = @recorder.inverse_of :remove_index, [:table, { column: [:one, :two], name: "new_index" }] assert_equal [:add_index, [:table, [:one, :two], name: "new_index"]], add end def test_invert_remove_index_with_no_special_options - add = @recorder.inverse_of :remove_index, [:table, {column: [:one, :two]}] + add = @recorder.inverse_of :remove_index, [:table, { column: [:one, :two] }] assert_equal [:add_index, [:table, [:one, :two], {}]], add end @@ -254,7 +254,7 @@ module ActiveRecord def test_invert_remove_timestamps add = @recorder.inverse_of :remove_timestamps, [:table, { null: true }] - assert_equal [:add_timestamps, [:table, {null: true }], nil], add + assert_equal [:add_timestamps, [:table, { null: true }], nil], add end def test_invert_add_reference diff --git a/activerecord/test/cases/migration/compatibility_test.rb b/activerecord/test/cases/migration/compatibility_test.rb index 0a4b604601..e5a7412bc3 100644 --- a/activerecord/test/cases/migration/compatibility_test.rb +++ b/activerecord/test/cases/migration/compatibility_test.rb @@ -55,7 +55,7 @@ module ActiveRecord end def test_references_does_not_add_index_by_default - migration = Class.new(ActiveRecord::Migration) { + migration = Class.new(ActiveRecord::Migration[4.2]) { def migrate(x) create_table :more_testings do |t| t.references :foo @@ -73,7 +73,7 @@ module ActiveRecord end def test_timestamps_have_null_constraints_if_not_present_in_migration_of_create_table - migration = Class.new(ActiveRecord::Migration) { + migration = Class.new(ActiveRecord::Migration[4.2]) { def migrate(x) create_table :more_testings do |t| t.timestamps @@ -90,7 +90,7 @@ module ActiveRecord end def test_timestamps_have_null_constraints_if_not_present_in_migration_for_adding_timestamps_to_existing_table - migration = Class.new(ActiveRecord::Migration) { + migration = Class.new(ActiveRecord::Migration[4.2]) { def migrate(x) add_timestamps :testings end @@ -102,15 +102,9 @@ module ActiveRecord assert connection.columns(:testings).find { |c| c.name == "updated_at" }.null end - def test_legacy_migrations_get_deprecation_warning_when_run - migration = Class.new(ActiveRecord::Migration) { - def up - add_column :testings, :baz, :string - end - } - - assert_deprecated do - migration.migrate :up + def test_legacy_migrations_raises_exception_when_inherited + assert_raises(StandardError) do + Class.new(ActiveRecord::Migration) 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 372ec40a04..26b1bb4419 100644 --- a/activerecord/test/cases/migration/create_join_table_test.rb +++ b/activerecord/test/cases/migration/create_join_table_test.rb @@ -12,9 +12,7 @@ module ActiveRecord teardown do %w(artists_musics musics_videos catalog).each do |table_name| - ActiveSupport::Deprecation.silence do - connection.drop_table table_name if connection.table_exists?(table_name) - end + connection.drop_table table_name if connection.table_exists?(table_name) end end @@ -61,7 +59,7 @@ module ActiveRecord end def test_create_join_table_with_column_options - connection.create_join_table :artists, :musics, column_options: {null: true} + connection.create_join_table :artists, :musics, column_options: { null: true } assert_equal [true, true], connection.columns(:artists_musics).map(&:null) end @@ -84,51 +82,51 @@ module ActiveRecord connection.create_join_table :artists, :musics connection.drop_join_table :artists, :musics - ActiveSupport::Deprecation.silence { assert !connection.table_exists?("artists_musics") } + assert !connection.table_exists?("artists_musics") end def test_drop_join_table_with_strings connection.create_join_table :artists, :musics connection.drop_join_table "artists", "musics" - ActiveSupport::Deprecation.silence { assert !connection.table_exists?("artists_musics") } + assert !connection.table_exists?("artists_musics") end def test_drop_join_table_with_the_proper_order connection.create_join_table :videos, :musics connection.drop_join_table :videos, :musics - ActiveSupport::Deprecation.silence { assert !connection.table_exists?("musics_videos") } + assert !connection.table_exists?("musics_videos") end def test_drop_join_table_with_the_table_name connection.create_join_table :artists, :musics, table_name: :catalog connection.drop_join_table :artists, :musics, table_name: :catalog - ActiveSupport::Deprecation.silence { assert !connection.table_exists?("catalog") } + assert !connection.table_exists?("catalog") end def test_drop_join_table_with_the_table_name_as_string connection.create_join_table :artists, :musics, table_name: "catalog" connection.drop_join_table :artists, :musics, table_name: "catalog" - ActiveSupport::Deprecation.silence { assert !connection.table_exists?("catalog") } + assert !connection.table_exists?("catalog") end def test_drop_join_table_with_column_options - connection.create_join_table :artists, :musics, column_options: {null: true} - connection.drop_join_table :artists, :musics, column_options: {null: true} + connection.create_join_table :artists, :musics, column_options: { null: true } + connection.drop_join_table :artists, :musics, column_options: { null: true } - ActiveSupport::Deprecation.silence { assert !connection.table_exists?("artists_musics") } + assert !connection.table_exists?("artists_musics") end def test_create_and_drop_join_table_with_common_prefix with_table_cleanup do connection.create_join_table "audio_artists", "audio_musics" - ActiveSupport::Deprecation.silence { assert connection.table_exists?("audio_artists_musics") } + assert connection.table_exists?("audio_artists_musics") connection.drop_join_table "audio_artists", "audio_musics" - ActiveSupport::Deprecation.silence { assert !connection.table_exists?("audio_artists_musics"), "Should have dropped join table, but didn't" } + assert !connection.table_exists?("audio_artists_musics"), "Should have dropped join table, but didn't" end end diff --git a/activerecord/test/cases/migration/foreign_key_test.rb b/activerecord/test/cases/migration/foreign_key_test.rb index cab2069754..9be6667aa1 100644 --- a/activerecord/test/cases/migration/foreign_key_test.rb +++ b/activerecord/test/cases/migration/foreign_key_test.rb @@ -76,7 +76,7 @@ if ActiveRecord::Base.connection.supports_foreign_keys? end def test_add_foreign_key_with_non_standard_primary_key - with_example_table @connection, "space_shuttles", "pk integer PRIMARY KEY" do + with_example_table @connection, "space_shuttles", "pk BIGINT PRIMARY KEY" do @connection.add_foreign_key(:astronauts, :space_shuttles, column: "rocket_id", primary_key: "pk", name: "custom_pk") @@ -101,7 +101,7 @@ if ActiveRecord::Base.connection.supports_foreign_keys? fk = foreign_keys.first if current_adapter?(:Mysql2Adapter) # ON DELETE RESTRICT is the default on MySQL - assert_equal nil, fk.on_delete + assert_nil fk.on_delete else assert_equal :restrict, fk.on_delete end @@ -229,7 +229,7 @@ if ActiveRecord::Base.connection.supports_foreign_keys? create_table("cities") { |t| } create_table("houses") do |t| - t.column :city_id, :integer + t.column :city_id, :bigint end add_foreign_key :houses, :cities, column: "city_id" @@ -261,7 +261,7 @@ if ActiveRecord::Base.connection.supports_foreign_keys? create_table(:schools) create_table(:classes) do |t| - t.column :school_id, :integer + t.column :school_id, :bigint end add_foreign_key :classes, :schools end diff --git a/activerecord/test/cases/migration/index_test.rb b/activerecord/test/cases/migration/index_test.rb index 85e09986ea..0f975026b8 100644 --- a/activerecord/test/cases/migration/index_test.rb +++ b/activerecord/test/cases/migration/index_test.rb @@ -143,7 +143,7 @@ module ActiveRecord end def test_add_index_attribute_length_limit - connection.add_index :testings, [:foo, :bar], length: {foo: 10, bar: nil} + connection.add_index :testings, [:foo, :bar], length: { foo: 10, bar: nil } assert connection.index_exists?(:testings, [:foo, :bar]) end @@ -169,13 +169,13 @@ module ActiveRecord connection.add_index("testings", ["last_name"], length: 10) connection.remove_index("testings", "last_name") - connection.add_index("testings", ["last_name"], length: {last_name: 10}) + connection.add_index("testings", ["last_name"], length: { last_name: 10 }) connection.remove_index("testings", ["last_name"]) connection.add_index("testings", ["last_name", "first_name"], length: 10) connection.remove_index("testings", ["last_name", "first_name"]) - connection.add_index("testings", ["last_name", "first_name"], length: {last_name: 10, first_name: 20}) + connection.add_index("testings", ["last_name", "first_name"], length: { last_name: 10, first_name: 20 }) connection.remove_index("testings", ["last_name", "first_name"]) connection.add_index("testings", ["key"], name: "key_idx", unique: true) @@ -186,11 +186,11 @@ module ActiveRecord # Selected adapters support index sort order if current_adapter?(:SQLite3Adapter, :Mysql2Adapter, :PostgreSQLAdapter) - connection.add_index("testings", ["last_name"], order: {last_name: :desc}) + connection.add_index("testings", ["last_name"], order: { last_name: :desc }) connection.remove_index("testings", ["last_name"]) - connection.add_index("testings", ["last_name", "first_name"], order: {last_name: :desc}) + connection.add_index("testings", ["last_name", "first_name"], order: { last_name: :desc }) connection.remove_index("testings", ["last_name", "first_name"]) - connection.add_index("testings", ["last_name", "first_name"], order: {last_name: :desc, first_name: :asc}) + connection.add_index("testings", ["last_name", "first_name"], order: { last_name: :desc, first_name: :asc }) connection.remove_index("testings", ["last_name", "first_name"]) connection.add_index("testings", ["last_name", "first_name"], order: :desc) connection.remove_index("testings", ["last_name", "first_name"]) diff --git a/activerecord/test/cases/migration/references_foreign_key_test.rb b/activerecord/test/cases/migration/references_foreign_key_test.rb index 8436435170..4957ab8b3d 100644 --- a/activerecord/test/cases/migration/references_foreign_key_test.rb +++ b/activerecord/test/cases/migration/references_foreign_key_test.rb @@ -42,7 +42,7 @@ if ActiveRecord::Base.connection.supports_foreign_keys? test "options hash can be passed" do @connection.change_table :testing_parents do |t| - t.integer :other_id + t.bigint :other_id t.index :other_id, unique: true end @connection.create_table :testings do |t| @@ -59,7 +59,7 @@ if ActiveRecord::Base.connection.supports_foreign_keys? end fks = @connection.foreign_keys("testings") assert_equal([["testings", "testing_parents", "parent_id"]], - fks.map {|fk| [fk.from_table, fk.to_table, fk.column] }) + fks.map { |fk| [fk.from_table, fk.to_table, fk.column] }) end test "foreign keys cannot be added to polymorphic relations when creating the table" do @@ -92,7 +92,7 @@ if ActiveRecord::Base.connection.supports_foreign_keys? test "foreign keys accept options when changing the table" do @connection.change_table :testing_parents do |t| - t.integer :other_id + t.bigint :other_id t.index :other_id, unique: true end @connection.create_table :testings @@ -177,8 +177,8 @@ if ActiveRecord::Base.connection.supports_foreign_keys? test "multiple foreign keys can be added to the same table" do @connection.create_table :testings do |t| - t.integer :col_1 - t.integer :col_2 + t.bigint :col_1 + t.bigint :col_2 t.foreign_key :testing_parents, column: :col_1 t.foreign_key :testing_parents, column: :col_2 @@ -186,7 +186,7 @@ if ActiveRecord::Base.connection.supports_foreign_keys? fks = @connection.foreign_keys("testings") - fk_definitions = fks.map {|fk| [fk.from_table, fk.to_table, fk.column] } + fk_definitions = fks.map { |fk| [fk.from_table, fk.to_table, fk.column] } assert_equal([["testings", "testing_parents", "col_1"], ["testings", "testing_parents", "col_2"]], fk_definitions) end diff --git a/activerecord/test/cases/migration/references_index_test.rb b/activerecord/test/cases/migration/references_index_test.rb index 0d0b0a193b..2866cabab6 100644 --- a/activerecord/test/cases/migration/references_index_test.rb +++ b/activerecord/test/cases/migration/references_index_test.rb @@ -41,8 +41,8 @@ module ActiveRecord def test_creates_index_with_options connection.create_table table_name do |t| - t.references :foo, index: {name: :index_testings_on_yo_momma} - t.references :bar, index: {unique: true} + t.references :foo, index: { name: :index_testings_on_yo_momma } + t.references :bar, index: { unique: true } end assert connection.index_exists?(table_name, :foo_id, name: :index_testings_on_yo_momma) diff --git a/activerecord/test/cases/migration/references_statements_test.rb b/activerecord/test/cases/migration/references_statements_test.rb index 8fbe60f24e..df15d7cb45 100644 --- a/activerecord/test/cases/migration/references_statements_test.rb +++ b/activerecord/test/cases/migration/references_statements_test.rb @@ -57,7 +57,7 @@ module ActiveRecord def test_creates_named_unique_index add_reference table_name, :tag, index: { name: "index_taggings_on_tag_id", unique: true } - assert index_exists?(table_name, :tag_id, name: "index_taggings_on_tag_id", unique: true ) + assert index_exists?(table_name, :tag_id, name: "index_taggings_on_tag_id", unique: true) end def test_creates_reference_id_with_specified_type diff --git a/activerecord/test/cases/migration/rename_table_test.rb b/activerecord/test/cases/migration/rename_table_test.rb index 5474c9ce71..19588d28a2 100644 --- a/activerecord/test/cases/migration/rename_table_test.rb +++ b/activerecord/test/cases/migration/rename_table_test.rb @@ -15,7 +15,7 @@ module ActiveRecord end def teardown - ActiveSupport::Deprecation.silence { rename_table :octopi, :test_models if connection.table_exists? :octopi } + rename_table :octopi, :test_models if connection.table_exists? :octopi super end @@ -57,7 +57,7 @@ module ActiveRecord assert_equal "http://www.foreverflying.com/octopus-black7.jpg", connection.select_value("SELECT url FROM octopi WHERE id=1") index = connection.indexes(:octopi).first - assert index.columns.include?("url") + assert_includes index.columns, "url" assert_equal "index_octopi_on_url", index.name end @@ -82,7 +82,7 @@ module ActiveRecord def test_renaming_table_doesnt_attempt_to_rename_non_existent_sequences connection.create_table :cats, id: :uuid assert_nothing_raised { rename_table :cats, :felines } - ActiveSupport::Deprecation.silence { assert connection.table_exists? :felines } + assert connection.table_exists? :felines ensure connection.drop_table :cats, if_exists: true connection.drop_table :felines, if_exists: true diff --git a/activerecord/test/cases/migration_test.rb b/activerecord/test/cases/migration_test.rb index 5d96569d38..4edb807bbb 100644 --- a/activerecord/test/cases/migration_test.rb +++ b/activerecord/test/cases/migration_test.rb @@ -388,7 +388,7 @@ class MigrationTest < ActiveRecord::TestCase original_rails_env = ENV["RAILS_ENV"] original_rack_env = ENV["RACK_ENV"] ENV["RAILS_ENV"] = ENV["RACK_ENV"] = "foofoo" - new_env = ActiveRecord::ConnectionHandling::DEFAULT_ENV.call + new_env = ActiveRecord::ConnectionHandling::DEFAULT_ENV.call refute_equal current_env, new_env @@ -399,6 +399,7 @@ class MigrationTest < ActiveRecord::TestCase ActiveRecord::Migrator.migrations_paths = old_path ENV["RAILS_ENV"] = original_rails_env ENV["RACK_ENV"] = original_rack_env + ActiveRecord::Migrator.up(migrations_path) end def test_migration_sets_internal_metadata_even_when_fully_migrated @@ -413,7 +414,7 @@ class MigrationTest < ActiveRecord::TestCase original_rails_env = ENV["RAILS_ENV"] original_rack_env = ENV["RACK_ENV"] ENV["RAILS_ENV"] = ENV["RACK_ENV"] = "foofoo" - new_env = ActiveRecord::ConnectionHandling::DEFAULT_ENV.call + new_env = ActiveRecord::ConnectionHandling::DEFAULT_ENV.call refute_equal current_env, new_env @@ -425,18 +426,19 @@ class MigrationTest < ActiveRecord::TestCase ActiveRecord::Migrator.migrations_paths = old_path ENV["RAILS_ENV"] = original_rails_env ENV["RACK_ENV"] = original_rack_env + ActiveRecord::Migrator.up(migrations_path) end def test_internal_metadata_stores_environment_when_other_data_exists ActiveRecord::InternalMetadata.delete_all - ActiveRecord::InternalMetadata[:foo] = "bar" + ActiveRecord::InternalMetadata[:foo] = "bar" current_env = ActiveRecord::ConnectionHandling::DEFAULT_ENV.call migrations_path = MIGRATIONS_ROOT + "/valid" old_path = ActiveRecord::Migrator.migrations_paths ActiveRecord::Migrator.migrations_paths = migrations_path - current_env = ActiveRecord::ConnectionHandling::DEFAULT_ENV.call + current_env = ActiveRecord::ConnectionHandling::DEFAULT_ENV.call ActiveRecord::Migrator.up(migrations_path) assert_equal current_env, ActiveRecord::InternalMetadata[:environment] assert_equal "bar", ActiveRecord::InternalMetadata[:foo] @@ -551,6 +553,23 @@ class MigrationTest < ActiveRecord::TestCase end end + if current_adapter?(:SQLite3Adapter) + def test_allows_sqlite3_rollback_on_invalid_column_type + Person.connection.create_table :something, force: true do |t| + t.column :number, :integer + t.column :name, :string + t.column :foo, :bar + end + assert Person.connection.column_exists?(:something, :foo) + assert_nothing_raised { Person.connection.remove_column :something, :foo, :bar } + assert !Person.connection.column_exists?(:something, :foo) + assert Person.connection.column_exists?(:something, :name) + assert Person.connection.column_exists?(:something, :number) + ensure + Person.connection.drop_table :something, if_exists: true + end + end + if current_adapter? :OracleAdapter def test_create_table_with_custom_sequence_name # table name is 29 chars, the standard sequence name will @@ -688,7 +707,7 @@ class MigrationTest < ActiveRecord::TestCase end end - protected + private # This is needed to isolate class_attribute assignments like `table_name_prefix` # for each test case. def new_isolated_reminder_class @@ -759,7 +778,7 @@ if ActiveRecord::Base.connection.supports_bulk_alter? class BulkAlterTableMigrationsTest < ActiveRecord::TestCase def setup @connection = Person.connection - @connection.create_table(:delete_me, force: true) {|t| } + @connection.create_table(:delete_me, force: true) { |t| } Person.reset_column_information Person.reset_sequence_name end @@ -780,7 +799,7 @@ if ActiveRecord::Base.connection.supports_bulk_alter? end assert_equal 8, columns.size - [:name, :qualification, :experience].each {|s| assert_equal :string, column(s).type } + [:name, :qualification, :experience].each { |s| assert_equal :string, column(s).type } assert_equal "0", column(:age).default end @@ -789,7 +808,7 @@ if ActiveRecord::Base.connection.supports_bulk_alter? t.string :qualification, :experience end - [:qualification, :experience].each {|c| assert column(c) } + [:qualification, :experience].each { |c| assert column(c) } assert_queries(1) do with_bulk_change_table do |t| @@ -798,7 +817,7 @@ if ActiveRecord::Base.connection.supports_bulk_alter? end end - [:qualification, :experience].each {|c| assert ! column(c) } + [:qualification, :experience].each { |c| assert ! column(c) } assert column(:qualification_experience) end @@ -870,7 +889,7 @@ if ActiveRecord::Base.connection.supports_bulk_alter? assert_equal :datetime, column(:birthdate).type end - protected + private def with_bulk_change_table # Reset columns/indexes cache as we're changing the table @@ -882,7 +901,7 @@ if ActiveRecord::Base.connection.supports_bulk_alter? end def column(name) - columns.detect {|c| c.name == name.to_s } + columns.detect { |c| c.name == name.to_s } end def columns @@ -890,7 +909,7 @@ if ActiveRecord::Base.connection.supports_bulk_alter? end def index(name) - indexes.detect {|i| i.name == name.to_s } + indexes.detect { |i| i.name == name.to_s } end def indexes diff --git a/activerecord/test/cases/migrator_test.rb b/activerecord/test/cases/migrator_test.rb index 1ba18bc9c2..224e8d1854 100644 --- a/activerecord/test/cases/migrator_test.rb +++ b/activerecord/test/cases/migrator_test.rb @@ -11,7 +11,7 @@ class MigratorTest < ActiveRecord::TestCase def initialize(name = self.class.name, version = nil) super - @went_up = false + @went_up = false @went_down = false end @@ -290,6 +290,27 @@ class MigratorTest < ActiveRecord::TestCase assert_equal [[:up, 1], [:up, 2], [:up, 3]], calls end + def test_migrator_output_when_running_multiple_migrations + _, migrator = migrator_class(3) + + result = migrator.migrate("valid") + assert_equal(3, result.count) + + # Nothing migrated from duplicate run + result = migrator.migrate("valid") + assert_equal(0, result.count) + + result = migrator.rollback("valid") + assert_equal(1, result.count) + end + + def test_migrator_output_when_running_single_migration + _, migrator = migrator_class(1) + result = migrator.run(:up, "valid", 1) + + assert_equal(1, result.version) + end + def test_migrator_rollback _, migrator = migrator_class(3) @@ -313,9 +334,9 @@ class MigratorTest < ActiveRecord::TestCase _, migrator = migrator_class(3) ActiveRecord::Base.connection.drop_table "schema_migrations", if_exists: true - ActiveSupport::Deprecation.silence { assert_not ActiveRecord::Base.connection.table_exists?("schema_migrations") } + assert_not ActiveRecord::Base.connection.table_exists?("schema_migrations") migrator.migrate("valid", 1) - ActiveSupport::Deprecation.silence { assert ActiveRecord::Base.connection.table_exists?("schema_migrations") } + assert ActiveRecord::Base.connection.table_exists?("schema_migrations") end def test_migrator_forward @@ -344,10 +365,10 @@ class MigratorTest < ActiveRecord::TestCase _, migrator = migrator_class(3) migrator.migrate("valid") - assert_equal([1,2,3], ActiveRecord::Migrator.get_all_versions) + assert_equal([1, 2, 3], ActiveRecord::Migrator.get_all_versions) migrator.rollback("valid") - assert_equal([1,2], ActiveRecord::Migrator.get_all_versions) + assert_equal([1, 2], ActiveRecord::Migrator.get_all_versions) migrator.rollback("valid") assert_equal([1], ActiveRecord::Migrator.get_all_versions) @@ -368,7 +389,7 @@ class MigratorTest < ActiveRecord::TestCase def sensors(count) calls = [] migrations = count.times.map { |i| - m(nil, i + 1) { |c,migration| + m(nil, i + 1) { |c, migration| calls << [c, migration.version] } } diff --git a/activerecord/test/cases/modules_test.rb b/activerecord/test/cases/modules_test.rb index 1b7720e4f8..f8a7bab35f 100644 --- a/activerecord/test/cases/modules_test.rb +++ b/activerecord/test/cases/modules_test.rb @@ -73,7 +73,7 @@ class ModulesTest < ActiveRecord::TestCase clients = [] assert_nothing_raised do - clients << MyApplication::Business::Client.references(:accounts).merge!(includes: {firm: :account}, where: "accounts.id IS NOT NULL").find(3) + clients << MyApplication::Business::Client.references(:accounts).merge!(includes: { firm: :account }, where: "accounts.id IS NOT NULL").find(3) clients << MyApplication::Business::Client.includes(firm: :account).find(3) end diff --git a/activerecord/test/cases/multiparameter_attributes_test.rb b/activerecord/test/cases/multiparameter_attributes_test.rb index b2f76398df..ceb5724377 100644 --- a/activerecord/test/cases/multiparameter_attributes_test.rb +++ b/activerecord/test/cases/multiparameter_attributes_test.rb @@ -260,6 +260,13 @@ class MultiParameterAttributeTest < ActiveRecord::TestCase topic.attributes = attributes assert_equal Time.zone.local(2000, 1, 1, 16, 24, 0), topic.bonus_time assert_not topic.bonus_time.utc? + + attributes = { + "written_on(1i)" => "2000", "written_on(2i)" => "", "written_on(3i)" => "", + "written_on(4i)" => "", "written_on(5i)" => "" + } + topic.attributes = attributes + assert_nil topic.written_on end ensure Topic.reset_column_information @@ -280,14 +287,14 @@ class MultiParameterAttributeTest < ActiveRecord::TestCase unless current_adapter? :OracleAdapter def test_multiparameter_attributes_setting_time_attribute - topic = Topic.new( "bonus_time(4i)"=> "01", "bonus_time(5i)" => "05" ) + 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" ) + topic = Topic.new("written_on(1i)" => "1952", "written_on(2i)" => "3", "written_on(3i)" => "11") assert_equal 1952, topic.written_on.year assert_equal 3, topic.written_on.month assert_equal 11, topic.written_on.day @@ -308,8 +315,8 @@ class MultiParameterAttributeTest < ActiveRecord::TestCase end def test_multiparameter_attributes_setting_time_but_not_date_on_date_field - assert_raise( ActiveRecord::MultiparameterAssignmentErrors ) do - Topic.new( "written_on(4i)" => "13", "written_on(5i)" => "55" ) + assert_raise(ActiveRecord::MultiparameterAssignmentErrors) do + Topic.new("written_on(4i)" => "13", "written_on(5i)" => "55") end end diff --git a/activerecord/test/cases/nested_attributes_test.rb b/activerecord/test/cases/nested_attributes_test.rb index 788277faea..b87419d203 100644 --- a/activerecord/test/cases/nested_attributes_test.rb +++ b/activerecord/test/cases/nested_attributes_test.rb @@ -31,7 +31,7 @@ class TestNestedAttributesInGeneral < ActiveRecord::TestCase def test_should_not_build_a_new_record_using_reject_all_even_if_destroy_is_given pirate = Pirate.create!(catchphrase: "Don' botharrr talkin' like one, savvy?") - pirate.birds_with_reject_all_blank_attributes = [{name: "", color: "", _destroy: "0"}] + pirate.birds_with_reject_all_blank_attributes = [{ name: "", color: "", _destroy: "0" }] pirate.save! assert pirate.birds_with_reject_all_blank.empty? @@ -39,7 +39,7 @@ class TestNestedAttributesInGeneral < ActiveRecord::TestCase def test_should_not_build_a_new_record_if_reject_all_blank_returns_false pirate = Pirate.create!(catchphrase: "Don' botharrr talkin' like one, savvy?") - pirate.birds_with_reject_all_blank_attributes = [{name: "", color: ""}] + pirate.birds_with_reject_all_blank_attributes = [{ name: "", color: "" }] pirate.save! assert pirate.birds_with_reject_all_blank.empty? @@ -47,7 +47,7 @@ class TestNestedAttributesInGeneral < ActiveRecord::TestCase def test_should_build_a_new_record_if_reject_all_blank_does_not_return_false pirate = Pirate.create!(catchphrase: "Don' botharrr talkin' like one, savvy?") - pirate.birds_with_reject_all_blank_attributes = [{name: "Tweetie", color: ""}] + pirate.birds_with_reject_all_blank_attributes = [{ name: "Tweetie", color: "" }] pirate.save! assert_equal 1, pirate.birds_with_reject_all_blank.count @@ -108,7 +108,7 @@ class TestNestedAttributesInGeneral < ActiveRecord::TestCase end def test_reject_if_with_indifferent_keys - Pirate.accepts_nested_attributes_for :ship, reject_if: proc {|attributes| attributes[:name].blank? } + Pirate.accepts_nested_attributes_for :ship, reject_if: proc { |attributes| attributes[:name].blank? } pirate = Pirate.new(catchphrase: "Stop wastin' me time") pirate.ship_attributes = { name: "Hello Pearl" } @@ -116,7 +116,7 @@ class TestNestedAttributesInGeneral < ActiveRecord::TestCase end def test_reject_if_with_a_proc_which_returns_true_always_for_has_one - Pirate.accepts_nested_attributes_for :ship, reject_if: proc {|attributes| true } + Pirate.accepts_nested_attributes_for :ship, reject_if: proc { |attributes| true } pirate = Pirate.new(catchphrase: "Stop wastin' me time") ship = pirate.create_ship(name: "s1") pirate.update(ship_attributes: { name: "s2", id: ship.id }) @@ -138,7 +138,7 @@ class TestNestedAttributesInGeneral < ActiveRecord::TestCase end def test_reject_if_with_a_proc_which_returns_true_always_for_has_many - Man.accepts_nested_attributes_for :interests, reject_if: proc {|attributes| true } + Man.accepts_nested_attributes_for :interests, reject_if: proc { |attributes| true } man = Man.create(name: "John") interest = man.interests.create(topic: "photography") man.update(interests_attributes: { topic: "gardening", id: interest.id }) @@ -146,7 +146,7 @@ class TestNestedAttributesInGeneral < ActiveRecord::TestCase end def test_destroy_works_independent_of_reject_if - Man.accepts_nested_attributes_for :interests, reject_if: proc {|attributes| true }, allow_destroy: true + Man.accepts_nested_attributes_for :interests, reject_if: proc { |attributes| true }, allow_destroy: true man = Man.create(name: "Jon") interest = man.interests.create(topic: "the ladies") man.update(interests_attributes: { _destroy: "1", id: interest.id }) @@ -170,13 +170,13 @@ class TestNestedAttributesInGeneral < ActiveRecord::TestCase Man.accepts_nested_attributes_for(:interests) man = Man.create(name: "John") interest = man.interests.create(topic: "photography") - man.update(interests_attributes: {topic: "gardening", id: interest.id}) + man.update(interests_attributes: { topic: "gardening", id: interest.id }) assert_equal "gardening", interest.reload.topic end def test_reject_if_with_blank_nested_attributes_id # When using a select list to choose an existing 'ship' id, with include_blank: true - Pirate.accepts_nested_attributes_for :ship, reject_if: proc {|attributes| attributes[:id].blank? } + Pirate.accepts_nested_attributes_for :ship, reject_if: proc { |attributes| attributes[:id].blank? } pirate = Pirate.new(catchphrase: "Stop wastin' me time") pirate.ship_attributes = { id: "" } @@ -188,7 +188,7 @@ class TestNestedAttributesInGeneral < ActiveRecord::TestCase man = Man.create(name: "John") interest = man.interests.create topic: "gardening" man = Man.find man.id - man.interests_attributes = [{id: interest.id, topic: "gardening"}] + man.interests_attributes = [{ id: interest.id, topic: "gardening" }] assert_equal man.interests.first.topic, man.interests[0].topic end @@ -225,7 +225,7 @@ class TestNestedAttributesOnAHasOneAssociation < ActiveRecord::TestCase def test_should_raise_argument_error_if_trying_to_build_polymorphic_belongs_to exception = assert_raise ArgumentError do - Treasure.new(name: "pearl", looter_attributes: {catchphrase: "Arrr"}) + Treasure.new(name: "pearl", looter_attributes: { catchphrase: "Arrr" }) end assert_equal "Cannot build association `looter'. Are you trying to build a polymorphic one-to-one association?", exception.message end @@ -700,7 +700,7 @@ module NestedAttributesOnACollectionAssociationTests def test_should_automatically_build_new_associated_models_for_each_entry_in_a_hash_where_the_id_is_missing @pirate.send(@association_name).destroy_all @pirate.reload.attributes = { - association_getter => { "foo" => { name: "Grace OMalley" }, "bar" => { name: "Privateers Greed" }} + association_getter => { "foo" => { name: "Grace OMalley" }, "bar" => { name: "Privateers Greed" } } } assert !@pirate.send(@association_name).first.persisted? @@ -757,7 +757,7 @@ module NestedAttributesOnACollectionAssociationTests def test_should_work_with_update_as_well @pirate.update(catchphrase: "Arr", - association_getter => { "foo" => { id: @child_1.id, name: "Grace OMalley" }}) + association_getter => { "foo" => { id: @child_1.id, name: "Grace OMalley" } }) assert_equal "Grace OMalley", @child_1.reload.name end @@ -813,7 +813,7 @@ module NestedAttributesOnACollectionAssociationTests assert_difference "Man.count" do assert_difference "Interest.count", 2 do man = Man.create!(name: "John", - interests_attributes: [{topic: "Cars"}, {topic: "Sports"}]) + interests_attributes: [{ topic: "Cars" }, { topic: "Sports" }]) assert_equal 2, man.interests.count end end @@ -911,7 +911,7 @@ module NestedAttributesLimitTests assert_raises(ActiveRecord::NestedAttributes::TooManyRecords) do @pirate.attributes = { parrots_attributes: { "foo" => { name: "Lovely Day" }, "bar" => { name: "Blown Away" }, - "car" => { name: "The Happening" }} } + "car" => { name: "The Happening" } } } end end end @@ -971,10 +971,10 @@ class TestNestedAttributesWithNonStandardPrimaryKeys < ActiveRecord::TestCase def test_attr_accessor_of_child_should_be_value_provided_during_update @owner = owners(:ashley) @pet1 = pets(:chew) - attributes = {pets_attributes: { "1"=> { id: @pet1.id, + attributes = { pets_attributes: { "1" => { id: @pet1.id, name: "Foo2", current_user: "John", - _destroy: true }}} + _destroy: true } } } @owner.update(attributes) assert_equal "John", Pet.after_destroy_output end @@ -997,18 +997,18 @@ class TestHasOneAutosaveAssociationWhichItselfHasAutosaveAssociations < ActiveRe end test "when great-grandchild changed via attributes, saving parent should save great-grandchild" do - @pirate.attributes = {ship_attributes: {id: @ship.id, parts_attributes: [{id: @part.id, trinkets_attributes: [{id: @trinket.id, name: "changed"}]}]}} + @pirate.attributes = { ship_attributes: { id: @ship.id, parts_attributes: [{ id: @part.id, trinkets_attributes: [{ id: @trinket.id, name: "changed" }] }] } } @pirate.save assert_equal "changed", @trinket.reload.name end test "when great-grandchild marked_for_destruction via attributes, saving parent should destroy great-grandchild" do - @pirate.attributes = {ship_attributes: {id: @ship.id, parts_attributes: [{id: @part.id, trinkets_attributes: [{id: @trinket.id, _destroy: true}]}]}} + @pirate.attributes = { ship_attributes: { id: @ship.id, parts_attributes: [{ id: @part.id, trinkets_attributes: [{ id: @trinket.id, _destroy: true }] }] } } assert_difference("@part.trinkets.count", -1) { @pirate.save } end test "when great-grandchild added via attributes, saving parent should create great-grandchild" do - @pirate.attributes = {ship_attributes: {id: @ship.id, parts_attributes: [{id: @part.id, trinkets_attributes: [{name: "created"}]}]}} + @pirate.attributes = { ship_attributes: { id: @ship.id, parts_attributes: [{ id: @part.id, trinkets_attributes: [{ name: "created" }] }] } } assert_difference("@part.trinkets.count", 1) { @pirate.save } end @@ -1030,13 +1030,13 @@ class TestHasManyAutosaveAssociationWhichItselfHasAutosaveAssociations < ActiveR end test "if association is not loaded and association record is saved and then in memory record attributes should be saved" do - @ship.parts_attributes=[{id: @part.id,name: "Deck"}] + @ship.parts_attributes = [{ id: @part.id, name: "Deck" }] assert_equal 1, @ship.association(:parts).target.size assert_equal "Deck", @ship.parts[0].name end test "if association is not loaded and child doesn't change and I am saving a grandchild then in memory record should be used" do - @ship.parts_attributes=[{id: @part.id,trinkets_attributes: [{id: @trinket.id, name: "Ruby"}]}] + @ship.parts_attributes = [{ id: @part.id, trinkets_attributes: [{ id: @trinket.id, name: "Ruby" }] }] assert_equal 1, @ship.association(:parts).target.size assert_equal "Mast", @ship.parts[0].name assert_no_difference("@ship.parts[0].association(:trinkets).target.size") do @@ -1054,18 +1054,18 @@ class TestHasManyAutosaveAssociationWhichItselfHasAutosaveAssociations < ActiveR end test "when grandchild changed via attributes, saving parent should save grandchild" do - @ship.attributes = {parts_attributes: [{id: @part.id, trinkets_attributes: [{id: @trinket.id, name: "changed"}]}]} + @ship.attributes = { parts_attributes: [{ id: @part.id, trinkets_attributes: [{ id: @trinket.id, name: "changed" }] }] } @ship.save assert_equal "changed", @trinket.reload.name end test "when grandchild marked_for_destruction via attributes, saving parent should destroy grandchild" do - @ship.attributes = {parts_attributes: [{id: @part.id, trinkets_attributes: [{id: @trinket.id, _destroy: true}]}]} + @ship.attributes = { parts_attributes: [{ id: @part.id, trinkets_attributes: [{ id: @trinket.id, _destroy: true }] }] } assert_difference("@part.trinkets.count", -1) { @ship.save } end test "when grandchild added via attributes, saving parent should create grandchild" do - @ship.attributes = {parts_attributes: [{id: @part.id, trinkets_attributes: [{name: "created"}]}]} + @ship.attributes = { parts_attributes: [{ id: @part.id, trinkets_attributes: [{ name: "created" }] }] } assert_difference("@part.trinkets.count", 1) { @ship.save } end diff --git a/activerecord/test/cases/nested_attributes_with_callbacks_test.rb b/activerecord/test/cases/nested_attributes_with_callbacks_test.rb index d606a6c12d..350a966d40 100644 --- a/activerecord/test/cases/nested_attributes_with_callbacks_test.rb +++ b/activerecord/test/cases/nested_attributes_with_callbacks_test.rb @@ -5,13 +5,13 @@ require "models/bird" class NestedAttributesWithCallbacksTest < ActiveRecord::TestCase Pirate.has_many(:birds_with_add_load, class_name: "Bird", - before_add: proc { |p,b| + before_add: proc { |p, b| @@add_callback_called << b p.birds_with_add_load.to_a }) Pirate.has_many(:birds_with_add, class_name: "Bird", - before_add: proc { |p,b| @@add_callback_called << b }) + before_add: proc { |p, b| @@add_callback_called << b }) Pirate.accepts_nested_attributes_for(:birds_with_add_load, :birds_with_add, @@ -21,7 +21,7 @@ class NestedAttributesWithCallbacksTest < ActiveRecord::TestCase @@add_callback_called = [] @pirate = Pirate.new.tap do |pirate| pirate.catchphrase = "Don't call me!" - pirate.birds_attributes = [{name: "Bird1"},{name: "Bird2"}] + pirate.birds_attributes = [{ name: "Bird1" }, { name: "Bird2" }] pirate.save! end @birds = @pirate.birds.to_a @@ -37,7 +37,7 @@ class NestedAttributesWithCallbacksTest < ActiveRecord::TestCase def existing_birds_attributes @birds.map do |bird| - bird.attributes.slice("id","name") + bird.attributes.slice("id", "name") end end @@ -46,17 +46,17 @@ class NestedAttributesWithCallbacksTest < ActiveRecord::TestCase end def new_bird_attributes - [{"name" => "New Bird"}] + [{ "name" => "New Bird" }] end def destroy_bird_attributes - [{"id" => bird_to_destroy.id.to_s, "_destroy" => true}] + [{ "id" => bird_to_destroy.id.to_s, "_destroy" => true }] end def update_new_and_destroy_bird_attributes - [{"id" => @birds[0].id.to_s, "name" => "New Name"}, - {"name" => "New Bird"}, - {"id" => bird_to_destroy.id.to_s, "_destroy" => true}] + [{ "id" => @birds[0].id.to_s, "name" => "New Name" }, + { "name" => "New Bird" }, + { "id" => bird_to_destroy.id.to_s, "_destroy" => true }] end # Characterizing when :before_add callback is called @@ -136,9 +136,9 @@ class NestedAttributesWithCallbacksTest < ActiveRecord::TestCase def assert_assignment_affects_records_in_target(association_name) association = @pirate.send(association_name) - assert association.detect {|b| b == bird_to_update }.name_changed?, + assert association.detect { |b| b == bird_to_update }.name_changed?, "Update record not updated" - assert association.detect {|b| b == bird_to_destroy }.marked_for_destruction?, + assert association.detect { |b| b == bird_to_destroy }.marked_for_destruction?, "Destroy record not marked for destruction" end end diff --git a/activerecord/test/cases/persistence_test.rb b/activerecord/test/cases/persistence_test.rb index e293770725..3f1da82cb4 100644 --- a/activerecord/test/cases/persistence_test.rb +++ b/activerecord/test/cases/persistence_test.rb @@ -83,13 +83,21 @@ class PersistenceTest < ActiveRecord::TestCase end def test_delete_all_with_joins_and_where_part_is_hash - where_args = {toys: {name: "Bone"}} + where_args = { toys: { name: "Bone" } } count = Pet.joins(:toys).where(where_args).count assert_equal count, 1 assert_equal count, Pet.joins(:toys).where(where_args).delete_all end + def test_delete_all_with_left_joins + where_args = { toys: { name: "Bone" } } + count = Pet.left_joins(:toys).where(where_args).count + + assert_equal count, 1 + assert_equal count, Pet.left_joins(:toys).where(where_args).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 @@ -391,14 +399,14 @@ class PersistenceTest < ActiveRecord::TestCase end topic = klass.create(title: "Another New Topic") assert_queries(0) do - topic.update_attribute(:title, "Another New Topic") + assert topic.update_attribute(:title, "Another New Topic") end end def test_update_does_not_run_sql_if_record_has_not_changed topic = Topic.create(title: "Another New Topic") - assert_queries(0) { topic.update(title: "Another New Topic") } - assert_queries(0) { topic.update_attributes(title: "Another New Topic") } + assert_queries(0) { assert topic.update(title: "Another New Topic") } + assert_queries(0) { assert topic.update_attributes(title: "Another New Topic") } end def test_delete @@ -453,6 +461,20 @@ class PersistenceTest < ActiveRecord::TestCase assert_nil Topic.find(2).last_read end + def test_update_all_with_joins + where_args = { toys: { name: "Bone" } } + count = Pet.left_joins(:toys).where(where_args).count + + assert_equal count, Pet.joins(:toys).where(where_args).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 + + assert_equal count, Pet.left_joins(:toys).where(where_args).update_all(name: "Bob") + end + def test_update_all_with_non_standard_table_name assert_equal 1, WarehouseThing.where(id: 1).update_all(["value = ?", 0]) assert_equal 0, WarehouseThing.find(1).value @@ -967,7 +989,7 @@ class PersistenceTest < ActiveRecord::TestCase self.table_name = :widgets end - instance = widget.create!( + instance = widget.create!( name: "Bob", created_at: 1.day.ago, updated_at: 1.day.ago) diff --git a/activerecord/test/cases/primary_keys_test.rb b/activerecord/test/cases/primary_keys_test.rb index 31d612abd1..1d72899102 100644 --- a/activerecord/test/cases/primary_keys_test.rb +++ b/activerecord/test/cases/primary_keys_test.rb @@ -120,7 +120,7 @@ class PrimaryKeysTest < ActiveRecord::TestCase assert_nothing_raised { MixedCaseMonkey.find(1) } end def test_find_with_multiple_ids_should_quote_pkey - assert_nothing_raised { MixedCaseMonkey.find([1,2]) } + assert_nothing_raised { MixedCaseMonkey.find([1, 2]) } end def test_instance_update_should_quote_pkey assert_nothing_raised { MixedCaseMonkey.find(1).save } @@ -129,12 +129,6 @@ class PrimaryKeysTest < ActiveRecord::TestCase assert_nothing_raised { MixedCaseMonkey.find(1).destroy } end - def test_supports_primary_key - assert_nothing_raised do - ActiveRecord::Base.connection.supports_primary_key? - end - end - if ActiveRecord::Base.connection.supports_primary_key? def test_primary_key_returns_value_if_it_exists klass = Class.new(ActiveRecord::Base) do @@ -154,7 +148,7 @@ class PrimaryKeysTest < ActiveRecord::TestCase end def test_quoted_primary_key_after_set_primary_key - k = Class.new( ActiveRecord::Base ) + k = Class.new(ActiveRecord::Base) assert_equal k.connection.quote_column_name("id"), k.quoted_primary_key k.primary_key = "foo" assert_equal k.connection.quote_column_name("foo"), k.quoted_primary_key @@ -216,6 +210,43 @@ class PrimaryKeyWithNoConnectionTest < ActiveRecord::TestCase end end +class PrimaryKeyWithAutoIncrementTest < ActiveRecord::TestCase + self.use_transactional_tests = false + + class AutoIncrement < ActiveRecord::Base + end + + def setup + @connection = ActiveRecord::Base.connection + end + + def teardown + @connection.drop_table(:auto_increments, if_exists: true) + end + + def test_primary_key_with_auto_increment + @connection.create_table(:auto_increments, id: :integer, auto_increment: true, force: true) + assert_auto_incremented + end + + def test_primary_key_with_auto_increment_and_bigint + @connection.create_table(:auto_increments, id: :bigint, auto_increment: true, force: true) + assert_auto_incremented + end + + private + def assert_auto_incremented + record1 = AutoIncrement.create! + assert_not_nil record1.id + + record1.destroy + + record2 = AutoIncrement.create! + assert_not_nil record2.id + assert_operator record2.id, :>, record1.id + end +end + class PrimaryKeyAnyTypeTest < ActiveRecord::TestCase include SchemaDumpingHelper @@ -289,85 +320,69 @@ class CompositePrimaryKeyTest < ActiveRecord::TestCase end if current_adapter?(:Mysql2Adapter) - class PrimaryKeyBigintNilDefaultTest < ActiveRecord::TestCase + class PrimaryKeyIntegerNilDefaultTest < ActiveRecord::TestCase include SchemaDumpingHelper self.use_transactional_tests = false def setup @connection = ActiveRecord::Base.connection - @connection.create_table(:bigint_defaults, id: :bigint, default: nil, force: true) + @connection.create_table(:int_defaults, id: :integer, default: nil, force: true) end def teardown - @connection.drop_table :bigint_defaults, if_exists: true + @connection.drop_table :int_defaults, if_exists: true end - test "primary key with bigint allows default override via nil" do - column = @connection.columns(:bigint_defaults).find { |c| c.name == "id" } - assert column.bigint? + test "primary key with integer allows default override via nil" do + column = @connection.columns(:int_defaults).find { |c| c.name == "id" } + assert_equal :integer, column.type assert_not column.auto_increment? end - test "schema dump primary key with bigint default nil" do - schema = dump_table_schema "bigint_defaults" - assert_match %r{create_table "bigint_defaults", id: :bigint, default: nil}, schema + test "schema dump primary key with int default nil" do + schema = dump_table_schema "int_defaults" + assert_match %r{create_table "int_defaults", id: :integer, default: nil}, schema end end end -if current_adapter?(:PostgreSQLAdapter, :Mysql2Adapter) - class PrimaryKeyBigSerialTest < ActiveRecord::TestCase - include SchemaDumpingHelper +class PrimaryKeyIntegerTest < ActiveRecord::TestCase + include SchemaDumpingHelper - self.use_transactional_tests = false + self.use_transactional_tests = false - class Widget < ActiveRecord::Base - end + class Widget < ActiveRecord::Base + end - setup do - @connection = ActiveRecord::Base.connection - if current_adapter?(:PostgreSQLAdapter) - @connection.create_table(:widgets, id: :bigserial, force: true) - else - @connection.create_table(:widgets, id: :bigint, force: true) - end - end + setup do + @connection = ActiveRecord::Base.connection + @connection.create_table(:widgets, force: true) + end - teardown do - @connection.drop_table :widgets, if_exists: true - Widget.reset_column_information - end + teardown do + @connection.drop_table :widgets, if_exists: true + Widget.reset_column_information + end - test "primary key column type with bigserial" do - column_type = Widget.type_for_attribute(Widget.primary_key) - assert_equal :integer, column_type.type - assert_equal 8, column_type.limit + if current_adapter?(:PostgreSQLAdapter, :Mysql2Adapter) + test "schema dump primary key with bigserial" do + schema = dump_table_schema "widgets" + assert_match %r{create_table "widgets", force: :cascade}, schema end + end - test "primary key with bigserial are automatically numbered" do - widget = Widget.create! - assert_not_nil widget.id - end + test "primary key column type" do + column_type = Widget.type_for_attribute(Widget.primary_key) + assert_equal :integer, column_type.type - test "schema dump primary key with bigserial" do - schema = dump_table_schema "widgets" - if current_adapter?(:PostgreSQLAdapter) - assert_match %r{create_table "widgets", id: :bigserial, force: :cascade}, schema - else - assert_match %r{create_table "widgets", id: :bigint, force: :cascade}, schema - end + if current_adapter?(:PostgreSQLAdapter, :Mysql2Adapter) + assert_equal 8, column_type.limit end if current_adapter?(:Mysql2Adapter) - test "primary key column type with options" do - @connection.create_table(:widgets, id: :primary_key, limit: 8, unsigned: true, force: true) - column = @connection.columns(:widgets).find { |c| c.name == "id" } - assert column.auto_increment? - assert_equal :integer, column.type - assert_equal 8, column.limit - assert column.unsigned? - end + column = @connection.columns(:widgets).find { |c| c.name == "id" } + assert column.auto_increment? end end end diff --git a/activerecord/test/cases/query_cache_test.rb b/activerecord/test/cases/query_cache_test.rb index d99e8a1dc6..324c2f4388 100644 --- a/activerecord/test/cases/query_cache_test.rb +++ b/activerecord/test/cases/query_cache_test.rb @@ -10,13 +10,34 @@ class QueryCacheTest < ActiveRecord::TestCase fixtures :tasks, :topics, :categories, :posts, :categories_posts - teardown do + class ShouldNotHaveExceptionsLogger < ActiveRecord::LogSubscriber + attr_reader :logger + + def initialize + super + @logger = ::Logger.new File::NULL + @exception = false + end + + def exception? + @exception + end + + def sql(event) + super + rescue + @exception = true + end + end + + def teardown Task.connection.clear_query_cache ActiveRecord::Base.connection.disable_query_cache! + super end def test_exceptional_middleware_clears_and_disables_cache_on_error - assert !ActiveRecord::Base.connection.query_cache_enabled, "cache off" + assert_cache :off mw = middleware { |env| Task.find 1 @@ -26,19 +47,66 @@ class QueryCacheTest < ActiveRecord::TestCase } assert_raises(RuntimeError) { mw.call({}) } - assert_equal 0, ActiveRecord::Base.connection.query_cache.length - assert !ActiveRecord::Base.connection.query_cache_enabled, "cache off" + assert_cache :off end - def test_exceptional_middleware_leaves_enabled_cache_alone - ActiveRecord::Base.connection.enable_query_cache! + def test_query_cache_across_threads + ActiveRecord::Base.connection_pool.connections.each do |conn| + assert_cache :off, conn + end - mw = middleware { |env| - raise "lol borked" - } - assert_raises(RuntimeError) { mw.call({}) } + assert !ActiveRecord::Base.connection.nil? + assert_cache :off + + middleware { + assert_cache :clean + + Task.find 1 + assert_cache :dirty + + thread_1_connection = ActiveRecord::Base.connection + ActiveRecord::Base.clear_active_connections! + assert_cache :off, thread_1_connection + + started = Concurrent::Event.new + checked = Concurrent::Event.new + + thread_2_connection = nil + thread = Thread.new { + thread_2_connection = ActiveRecord::Base.connection + + assert_equal thread_2_connection, thread_1_connection + assert_cache :off - assert ActiveRecord::Base.connection.query_cache_enabled, "cache on" + middleware { + assert_cache :clean + + Task.find 1 + assert_cache :dirty + + started.set + checked.wait + + ActiveRecord::Base.clear_active_connections! + }.call({}) + } + + started.wait + + thread_1_connection = ActiveRecord::Base.connection + assert_not_equal thread_1_connection, thread_2_connection + assert_cache :dirty, thread_2_connection + checked.set + thread.join + + assert_cache :off, thread_2_connection + }.call({}) + + ActiveRecord::Base.connection_pool.connections.each do |conn| + assert_cache :off, conn + end + ensure + ActiveRecord::Base.clear_all_connections! end def test_middleware_delegates @@ -62,10 +130,10 @@ class QueryCacheTest < ActiveRecord::TestCase end def test_cache_enabled_during_call - assert !ActiveRecord::Base.connection.query_cache_enabled, "cache off" + assert_cache :off mw = middleware { |env| - assert ActiveRecord::Base.connection.query_cache_enabled, "cache on" + assert_cache :clean [200, {}, nil] } mw.call({}) @@ -121,6 +189,33 @@ class QueryCacheTest < ActiveRecord::TestCase end end + def test_cache_does_not_raise_exceptions + logger = ShouldNotHaveExceptionsLogger.new + subscriber = ActiveSupport::Notifications.subscribe "sql.active_record", logger + + ActiveRecord::Base.cache do + assert_queries(1) { Task.find(1); Task.find(1) } + end + + assert_not_predicate logger, :exception? + ensure + ActiveSupport::Notifications.unsubscribe subscriber + end + + def test_query_cache_does_not_allow_sql_key_mutation + subscriber = ActiveSupport::Notifications.subscribe("sql.active_record") do |_, _, _, _, payload| + payload[:sql].downcase! + end + + assert_raises RuntimeError do + ActiveRecord::Base.cache do + assert_queries(1) { Task.find(1); Task.find(1) } + end + end + ensure + ActiveSupport::Notifications.unsubscribe subscriber + end + def test_cache_is_flat Task.cache do assert_queries(1) { Topic.find(1); Topic.find(1); } @@ -138,7 +233,7 @@ class QueryCacheTest < ActiveRecord::TestCase 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 Fixnum, Task.connection.select_value("SELECT count(*) AS count_all FROM tasks") + assert_instance_of 0.class, 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 @@ -180,6 +275,27 @@ class QueryCacheTest < ActiveRecord::TestCase Task.connection_specification_name = spec_name end + def test_query_cache_executes_new_queries_within_block + ActiveRecord::Base.connection.enable_query_cache! + + # Warm up the cache by running the query + assert_queries(1) do + assert_equal 0, Post.where(title: "test").to_a.count + end + + # Check that if the same query is run again, no queries are executed + assert_queries(0) do + assert_equal 0, Post.where(title: "test").to_a.count + end + + ActiveRecord::Base.connection.uncached do + # Check that new query is executed, avoiding the cache + assert_queries(1) do + assert_equal 0, Post.where(title: "test").to_a.count + end + end + end + def test_query_cache_doesnt_leak_cached_results_of_rolled_back_queries ActiveRecord::Base.connection.enable_query_cache! post = Post.first @@ -212,12 +328,82 @@ class QueryCacheTest < ActiveRecord::TestCase end end + def test_query_cached_even_when_types_are_reset + Task.cache do + # 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) + + # Clear places where type information is cached + Task.reset_column_information + Task.initialize_find_by_cache + + assert_queries(0) do + Task.find(1) + end + end + end + + def test_query_cache_does_not_establish_connection_if_unconnected + ActiveRecord::Base.clear_active_connections! + refute ActiveRecord::Base.connection_handler.active_connections? # sanity check + + middleware { + refute ActiveRecord::Base.connection_handler.active_connections?, "QueryCache forced ActiveRecord::Base to establish a connection in setup" + }.call({}) + + refute ActiveRecord::Base.connection_handler.active_connections?, "QueryCache forced ActiveRecord::Base to establish a connection in cleanup" + end + + def test_query_cache_is_enabled_on_connections_established_after_middleware_runs + ActiveRecord::Base.clear_active_connections! + refute ActiveRecord::Base.connection_handler.active_connections? # sanity check + + middleware { + assert ActiveRecord::Base.connection.query_cache_enabled, "QueryCache did not get lazily enabled" + }.call({}) + end + + def test_query_caching_is_local_to_the_current_thread + ActiveRecord::Base.clear_active_connections! + + middleware { + assert ActiveRecord::Base.connection_pool.query_cache_enabled + assert ActiveRecord::Base.connection.query_cache_enabled + + Thread.new { + refute ActiveRecord::Base.connection_pool.query_cache_enabled + refute ActiveRecord::Base.connection.query_cache_enabled + }.join + }.call({}) + end + private def middleware(&app) executor = Class.new(ActiveSupport::Executor) ActiveRecord::QueryCache.install_executor_hooks executor lambda { |env| executor.wrap { app.call(env) } } end + + def assert_cache(state, connection = ActiveRecord::Base.connection) + case state + when :off + assert !connection.query_cache_enabled, "cache should be off" + assert connection.query_cache.empty?, "cache should be empty" + when :clean + assert connection.query_cache_enabled, "cache should be on" + assert connection.query_cache.empty?, "cache should be empty" + when :dirty + assert connection.query_cache_enabled, "cache should be on" + assert !connection.query_cache.empty?, "cache should be dirty" + else + raise "unknown state" + end + end end class QueryCacheExpiryTest < ActiveRecord::TestCase diff --git a/activerecord/test/cases/quoting_test.rb b/activerecord/test/cases/quoting_test.rb index 1c276aa765..5ff5e3c735 100644 --- a/activerecord/test/cases/quoting_test.rb +++ b/activerecord/test/cases/quoting_test.rb @@ -82,67 +82,67 @@ module ActiveRecord end def test_quote_with_quoted_id - assert_equal 1, @quoter.quote(Struct.new(:quoted_id).new(1), nil) + assert_equal 1, @quoter.quote(Struct.new(:quoted_id).new(1)) end def test_quote_nil - assert_equal "NULL", @quoter.quote(nil, nil) + assert_equal "NULL", @quoter.quote(nil) end def test_quote_true - assert_equal @quoter.quoted_true, @quoter.quote(true, nil) + assert_equal @quoter.quoted_true, @quoter.quote(true) end def test_quote_false - assert_equal @quoter.quoted_false, @quoter.quote(false, nil) + assert_equal @quoter.quoted_false, @quoter.quote(false) end def test_quote_float float = 1.2 - assert_equal float.to_s, @quoter.quote(float, nil) + assert_equal float.to_s, @quoter.quote(float) end def test_quote_integer integer = 1 - assert_equal integer.to_s, @quoter.quote(integer, nil) + assert_equal integer.to_s, @quoter.quote(integer) end def test_quote_bignum bignum = 1 << 100 - assert_equal bignum.to_s, @quoter.quote(bignum, nil) + assert_equal bignum.to_s, @quoter.quote(bignum) end def test_quote_bigdecimal bigdec = BigDecimal.new((1 << 100).to_s) - assert_equal bigdec.to_s("F"), @quoter.quote(bigdec, nil) + assert_equal bigdec.to_s("F"), @quoter.quote(bigdec) end def test_dates_and_times @quoter.extend(Module.new { def quoted_date(value) "lol" end }) - assert_equal "'lol'", @quoter.quote(Date.today, nil) - assert_equal "'lol'", @quoter.quote(Time.now, nil) - assert_equal "'lol'", @quoter.quote(DateTime.now, nil) + assert_equal "'lol'", @quoter.quote(Date.today) + assert_equal "'lol'", @quoter.quote(Time.now) + assert_equal "'lol'", @quoter.quote(DateTime.now) + end + + def test_quoting_classes + assert_equal "'Object'", @quoter.quote(Object) end def test_crazy_object crazy = Object.new e = assert_raises(TypeError) do - @quoter.quote(crazy, nil) + @quoter.quote(crazy) end assert_equal "can't quote Object", e.message end def test_quote_string_no_column - assert_equal "'lo\\\\l'", @quoter.quote('lo\l', nil) + assert_equal "'lo\\\\l'", @quoter.quote('lo\l') end def test_quote_as_mb_chars_no_column string = ActiveSupport::Multibyte::Chars.new('lo\l') - assert_equal "'lo\\\\l'", @quoter.quote(string, nil) - end - - def test_string_with_crazy_column - assert_equal "'lo\\\\l'", @quoter.quote('lo\l') + assert_equal "'lo\\\\l'", @quoter.quote(string) end def test_quote_duration diff --git a/activerecord/test/cases/reflection_test.rb b/activerecord/test/cases/reflection_test.rb index d71b30af69..0ef51272b9 100644 --- a/activerecord/test/cases/reflection_test.rb +++ b/activerecord/test/cases/reflection_test.rb @@ -86,8 +86,8 @@ class ReflectionTest < ActiveRecord::TestCase column = @first.column_for_attribute("attribute_that_doesnt_exist") assert_instance_of ActiveRecord::ConnectionAdapters::NullColumn, column assert_equal "attribute_that_doesnt_exist", column.name - assert_equal nil, column.sql_type - assert_equal nil, column.type + assert_nil column.sql_type + assert_nil column.type end def test_non_existent_types_are_identity_types @@ -100,7 +100,13 @@ class ReflectionTest < ActiveRecord::TestCase end def test_reflection_klass_for_nested_class_name - reflection = ActiveRecord::Reflection.create(:has_many, nil, nil, { class_name: "MyApplication::Business::Company" }, ActiveRecord::Base) + reflection = ActiveRecord::Reflection.create( + :has_many, + nil, + nil, + { class_name: "MyApplication::Business::Company" }, + Customer + ) assert_nothing_raised do assert_equal MyApplication::Business::Company, reflection.klass end @@ -124,12 +130,12 @@ class ReflectionTest < ActiveRecord::TestCase ) reflection_for_gps_location = AggregateReflection.new( - :gps_location, nil, { }, Customer + :gps_location, nil, {}, Customer ) - assert Customer.reflect_on_all_aggregations.include?(reflection_for_gps_location) - assert Customer.reflect_on_all_aggregations.include?(reflection_for_balance) - assert Customer.reflect_on_all_aggregations.include?(reflection_for_address) + assert_includes Customer.reflect_on_all_aggregations, reflection_for_gps_location + assert_includes Customer.reflect_on_all_aggregations, reflection_for_balance + assert_includes Customer.reflect_on_all_aggregations, reflection_for_address assert_equal reflection_for_address, Customer.reflect_on_aggregation(:address) diff --git a/activerecord/test/cases/relation/merging_test.rb b/activerecord/test/cases/relation/merging_test.rb index bc3b1de697..278dac8171 100644 --- a/activerecord/test/cases/relation/merging_test.rb +++ b/activerecord/test/cases/relation/merging_test.rb @@ -86,9 +86,9 @@ class RelationMergingTest < ActiveRecord::TestCase merged = left.merge(right) assert_equal expected, merged.bound_attributes - assert !merged.to_sql.include?("omg") - assert merged.to_sql.include?("wtf") - assert merged.to_sql.include?("bbq") + assert_not_includes merged.to_sql, "omg" + assert_includes merged.to_sql, "wtf" + assert_includes merged.to_sql, "bbq" end def test_merging_reorders_bind_params diff --git a/activerecord/test/cases/relation/mutation_test.rb b/activerecord/test/cases/relation/mutation_test.rb index ca5d187819..de15eb9187 100644 --- a/activerecord/test/cases/relation/mutation_test.rb +++ b/activerecord/test/cases/relation/mutation_test.rb @@ -71,7 +71,7 @@ module ActiveRecord test "#references!" do assert relation.references!(:foo).equal?(relation) - assert relation.references_values.include?("foo") + assert_includes relation.references_values, "foo" end test "extending!" do @@ -90,7 +90,7 @@ module ActiveRecord assert_equal [], relation.extending_values end - (Relation::SINGLE_VALUE_METHODS - [:lock, :reordering, :reverse_order, :create_with, :uniq]).each do |method| + (Relation::SINGLE_VALUE_METHODS - [:lock, :reordering, :reverse_order, :create_with]).each do |method| test "##{method}!" do assert relation.public_send("#{method}!", :foo).equal?(relation) assert_equal :foo, relation.public_send("#{method}_value") @@ -108,7 +108,7 @@ module ActiveRecord end test "#reorder!" do - @relation = self.relation.order("foo") + @relation = relation.order("foo") assert relation.reorder!("bar").equal?(relation) assert_equal ["bar"], relation.order_values @@ -140,7 +140,7 @@ module ActiveRecord test "create_with!" do assert relation.create_with!(foo: "bar").equal?(relation) - assert_equal({foo: "bar"}, relation.create_with_value) + assert_equal({ foo: "bar" }, relation.create_with_value) end test "test_merge!" do @@ -161,22 +161,6 @@ module ActiveRecord test "distinct!" do relation.distinct! :foo assert_equal :foo, relation.distinct_value - - assert_deprecated do - assert_equal :foo, relation.uniq_value # deprecated access - end - end - - test "uniq! was replaced by distinct!" do - assert_deprecated(/use distinct! instead/) do - relation.uniq! :foo - end - - assert_deprecated(/use distinct_value instead/) do - assert_equal :foo, relation.uniq_value # deprecated access - end - - assert_equal :foo, relation.distinct_value end end end diff --git a/activerecord/test/cases/relation/or_test.rb b/activerecord/test/cases/relation/or_test.rb index 4efc742d5a..abb7ca72dd 100644 --- a/activerecord/test/cases/relation/or_test.rb +++ b/activerecord/test/cases/relation/or_test.rb @@ -61,8 +61,8 @@ module ActiveRecord def test_or_when_grouping groups = Post.where("id < 10").group("body").select("body, COUNT(*) AS c") - expected = groups.having("COUNT(*) > 1 OR body like 'Such%'").to_a.map {|o| [o.body, o.c] } - assert_equal expected, groups.having("COUNT(*) > 1").or(groups.having("body like 'Such%'")).to_a.map {|o| [o.body, o.c] } + expected = groups.having("COUNT(*) > 1 OR body like 'Such%'").to_a.map { |o| [o.body, o.c] } + assert_equal expected, groups.having("COUNT(*) > 1").or(groups.having("body like 'Such%'")).to_a.map { |o| [o.body, o.c] } end def test_or_with_named_scope @@ -79,7 +79,7 @@ module ActiveRecord expected = Post.where("id = 1 or id = 2").to_a p = Post.where("id = 1") p.load - assert_equal p.loaded?, true + assert_equal true, p.loaded? assert_equal expected, p.or(Post.where("id = 2")).to_a end diff --git a/activerecord/test/cases/relation/where_chain_test.rb b/activerecord/test/cases/relation/where_chain_test.rb index 3d93cb7bce..a96d1ae5b5 100644 --- a/activerecord/test/cases/relation/where_chain_test.rb +++ b/activerecord/test/cases/relation/where_chain_test.rb @@ -26,7 +26,7 @@ module ActiveRecord def test_association_not_eq expected = Arel::Nodes::Grouping.new(Comment.arel_table[@name].not_eq(Arel::Nodes::BindParam.new)) - relation = Post.joins(:comments).where.not(comments: {title: "hello"}) + 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_test.rb b/activerecord/test/cases/relation/where_test.rb index ad9008ea4d..ed8ffddcf5 100644 --- a/activerecord/test/cases/relation/where_test.rb +++ b/activerecord/test/cases/relation/where_test.rb @@ -4,10 +4,10 @@ require "models/binary" require "models/cake_designer" require "models/car" require "models/chef" +require "models/post" require "models/comment" require "models/edge" require "models/essay" -require "models/post" require "models/price_estimate" require "models/topic" require "models/treasure" @@ -64,12 +64,12 @@ module ActiveRecord end def test_belongs_to_array_value_where - assert_equal Post.where(author_id: [1,2]).to_sql, Post.where(author: [1,2]).to_sql + assert_equal Post.where(author_id: [1, 2]).to_sql, Post.where(author: [1, 2]).to_sql end def test_belongs_to_nested_relation_where - expected = Post.where(author_id: Author.where(id: [1,2])).to_sql - actual = Post.where(author: Author.where(id: [1,2])).to_sql + expected = Post.where(author_id: Author.where(id: [1, 2])).to_sql + actual = Post.where(author: Author.where(id: [1, 2])).to_sql assert_equal expected, actual end @@ -87,7 +87,7 @@ module ActiveRecord def test_belongs_to_nested_where_with_relation author = authors(:david) - expected = Author.where(id: author ).joins(:posts) + expected = Author.where(id: author).joins(:posts) actual = Author.where(posts: { author_id: Author.where(id: author.id) }).joins(:posts) assert_equal expected.to_a, actual.to_a @@ -127,8 +127,8 @@ module ActiveRecord end def test_polymorphic_nested_relation_where - expected = PriceEstimate.where(estimate_of_type: "Treasure", estimate_of_id: Treasure.where(id: [1,2])) - actual = PriceEstimate.where(estimate_of: Treasure.where(id: [1,2])) + expected = PriceEstimate.where(estimate_of_type: "Treasure", estimate_of_id: Treasure.where(id: [1, 2])) + actual = PriceEstimate.where(estimate_of: Treasure.where(id: [1, 2])) assert_equal expected.to_sql, actual.to_sql end @@ -196,7 +196,7 @@ module ActiveRecord end def test_where_error - assert_raises(ActiveRecord::StatementInvalid) do + assert_nothing_raised do Post.where(id: { "posts.author_id" => 10 }).first end end diff --git a/activerecord/test/cases/relation_test.rb b/activerecord/test/cases/relation_test.rb index 0802afac5a..d5af0cc9a5 100644 --- a/activerecord/test/cases/relation_test.rb +++ b/activerecord/test/cases/relation_test.rb @@ -40,9 +40,10 @@ module ActiveRecord def test_initialize_single_values relation = Relation.new(FakeKlass, :b, nil) - (Relation::SINGLE_VALUE_METHODS - [:create_with]).each do |method| + (Relation::SINGLE_VALUE_METHODS - [:create_with, :readonly]).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,7 +71,7 @@ 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) + assert_equal({ id: 10 }, relation.where_values_hash) end def test_values_wrong_table @@ -108,8 +109,8 @@ module ActiveRecord def test_create_with_value_with_wheres relation = Relation.new(Post, Post.arel_table, Post.predicate_builder) relation.where! relation.table[:id].eq(10) - relation.create_with_value = {hello: "world"} - assert_equal({hello: "world", id: 10}, relation.scope_for_create) + relation.create_with_value = { hello: "world" } + assert_equal({ hello: "world", id: 10 }, relation.scope_for_create) end # FIXME: is this really wanted or expected behavior? @@ -120,7 +121,7 @@ module ActiveRecord relation.where! relation.table[:id].eq(10) assert_equal({}, relation.scope_for_create) - relation.create_with_value = {hello: "world"} + relation.create_with_value = { hello: "world" } assert_equal({}, relation.scope_for_create) end @@ -156,9 +157,9 @@ module ActiveRecord test "merging a hash into a relation" do relation = Relation.new(Post, Post.arel_table, Post.predicate_builder) - relation = relation.merge where: {name: :lol}, readonly: true + relation = relation.merge where: { name: :lol }, readonly: true - assert_equal({"name"=>:lol}, relation.where_clause.to_h) + assert_equal({ "name" => :lol }, relation.where_clause.to_h) assert_equal true, relation.readonly_value end @@ -223,7 +224,7 @@ module ActiveRecord def test_relation_merging_with_merged_joins_as_symbols special_comments_with_ratings = SpecialComment.joins(:ratings) posts_with_special_comments_with_ratings = Post.group("posts.id").joins(:special_comments).merge(special_comments_with_ratings) - assert_equal 3, authors(:david).posts.merge(posts_with_special_comments_with_ratings).count.length + assert_equal({ 2 => 1, 4 => 3, 5 => 1 }, authors(:david).posts.merge(posts_with_special_comments_with_ratings).count) end def test_relation_merging_with_joins_as_join_dependency_pick_proper_parent @@ -273,7 +274,7 @@ module ActiveRecord join_string = "LEFT OUTER JOIN #{Rating.quoted_table_name} ON #{SpecialComment.quoted_table_name}.id = #{Rating.quoted_table_name}.comment_id" special_comments_with_ratings = SpecialComment.joins join_string posts_with_special_comments_with_ratings = Post.group("posts.id").joins(:special_comments).merge(special_comments_with_ratings) - assert_equal 3, authors(:david).posts.merge(posts_with_special_comments_with_ratings).count.length + assert_equal({ 2 => 1, 4 => 3, 5 => 1 }, authors(:david).posts.merge(posts_with_special_comments_with_ratings).count) end class EnsureRoundTripTypeCasting < ActiveRecord::Type::Value @@ -309,10 +310,10 @@ module ActiveRecord def skip_if_sqlite3_version_includes_quoting_bug if sqlite3_version_includes_quoting_bug? skip <<-ERROR.squish - You are using an outdated version of SQLite3 which has a bug in - quoted column names. Please update SQLite3 and rebuild the sqlite3 - ruby gem - ERROR + You are using an outdated version of SQLite3 which has a bug in + quoted column names. Please update SQLite3 and rebuild the sqlite3 + ruby gem + ERROR end end diff --git a/activerecord/test/cases/relations_test.rb b/activerecord/test/cases/relations_test.rb index 8652ef1081..dc6311e8bc 100644 --- a/activerecord/test/cases/relations_test.rb +++ b/activerecord/test/cases/relations_test.rb @@ -184,14 +184,14 @@ class RelationTest < ActiveRecord::TestCase def test_select_with_subquery_in_from_does_not_use_original_table_name relation = Comment.group(:type).select("COUNT(post_id) AS post_count, type") - subquery = Comment.from(relation).select("type","post_count") - assert_equal(relation.map(&:post_count).sort,subquery.map(&:post_count).sort) + subquery = Comment.from(relation).select("type", "post_count") + assert_equal(relation.map(&:post_count).sort, subquery.map(&:post_count).sort) end def test_group_with_subquery_in_from_does_not_use_original_table_name relation = Comment.group(:type).select("COUNT(post_id) AS post_count,type") subquery = Comment.from(relation).group("type").average("post_count") - assert_equal(relation.map(&:post_count).sort,subquery.values.sort) + assert_equal(relation.map(&:post_count).sort, subquery.values.sort) end def test_finding_with_conditions @@ -240,6 +240,15 @@ class RelationTest < ActiveRecord::TestCase assert_raises(ActiveRecord::IrreversibleOrderError) do Topic.order("concat(author_name, title)").reverse_order end + assert_raises(ActiveRecord::IrreversibleOrderError) do + Topic.order("concat(lower(author_name), title)").reverse_order + end + assert_raises(ActiveRecord::IrreversibleOrderError) do + Topic.order("concat(author_name, lower(title))").reverse_order + end + assert_raises(ActiveRecord::IrreversibleOrderError) do + Topic.order("concat(lower(author_name), title, length(title)").reverse_order + end end def test_reverse_order_with_nulls_first_or_last @@ -282,7 +291,7 @@ class RelationTest < ActiveRecord::TestCase assert_includes Topic.order(id: "DESC").to_sql, "DESC" assert_includes Topic.order(id: "desc").to_sql, "DESC" assert_includes Topic.order(id: :DESC).to_sql, "DESC" - assert_includes Topic.order(id: :desc).to_sql,"DESC" + assert_includes Topic.order(id: :desc).to_sql, "DESC" end def test_raising_exception_on_invalid_hash_params @@ -356,7 +365,7 @@ class RelationTest < ActiveRecord::TestCase end def test_finding_with_sanitized_order - query = Tag.order(["field(id, ?)", [1,3,2]]).to_sql + query = Tag.order(["field(id, ?)", [1, 3, 2]]).to_sql assert_match(/field\(id, 1,3,2\)/, query) query = Tag.order(["field(id, ?)", []]).to_sql @@ -384,7 +393,7 @@ class RelationTest < ActiveRecord::TestCase end def test_select_with_block - even_ids = Developer.all.select {|d| d.id % 2 == 0 }.map(&:id) + even_ids = Developer.all.select { |d| d.id % 2 == 0 }.map(&:id) assert_equal [2, 4, 6, 8, 10], even_ids.sort end @@ -433,7 +442,7 @@ class RelationTest < ActiveRecord::TestCase assert_no_queries(ignore_none: false) do assert_equal 0, Developer.none.count assert_equal 0, Developer.none.calculate(:count, nil) - assert_equal nil, Developer.none.calculate(:average, "salary") + assert_nil Developer.none.calculate(:average, "salary") end end @@ -449,55 +458,55 @@ class RelationTest < ActiveRecord::TestCase def test_null_relation_sum ac = Aircraft.new assert_equal Hash.new, ac.engines.group(:id).sum(:id) - assert_equal 0, ac.engines.count + assert_equal 0, ac.engines.count ac.save assert_equal Hash.new, ac.engines.group(:id).sum(:id) - assert_equal 0, ac.engines.count + 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 + assert_equal 0, ac.engines.count ac.save assert_equal Hash.new, ac.engines.group(:id).count - assert_equal 0, ac.engines.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 + assert_equal 0, ac.engines.size ac.save assert_equal Hash.new, ac.engines.group(:id).size - assert_equal 0, ac.engines.size + assert_equal 0, ac.engines.size end def test_null_relation_average ac = Aircraft.new assert_equal Hash.new, ac.engines.group(:car_id).average(:id) - assert_equal nil, ac.engines.average(:id) + assert_nil ac.engines.average(:id) ac.save assert_equal Hash.new, ac.engines.group(:car_id).average(:id) - assert_equal nil, ac.engines.average(:id) + 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_equal nil, ac.engines.minimum(:id) + assert_nil ac.engines.minimum(:id) ac.save assert_equal Hash.new, ac.engines.group(:car_id).minimum(:id) - assert_equal nil, ac.engines.minimum(:id) + 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_equal nil, ac.engines.maximum(:id) + assert_nil ac.engines.maximum(:id) ac.save assert_equal Hash.new, ac.engines.group(:car_id).maximum(:id) - assert_equal nil, ac.engines.maximum(:id) + assert_nil ac.engines.maximum(:id) end def test_null_relation_in_where_condition @@ -521,12 +530,12 @@ class RelationTest < ActiveRecord::TestCase assert_equal 3, developers_on_project_one.length developer_names = developers_on_project_one.map(&:name) - assert developer_names.include?("David") - assert developer_names.include?("Jamis") + assert_includes developer_names, "David" + assert_includes developer_names, "Jamis" end def test_find_on_hash_conditions - assert_equal Topic.all.merge!(where: {approved: false}).to_a, Topic.where(approved: false).to_a + assert_equal Topic.all.merge!(where: { approved: false }).to_a, Topic.where(approved: false).to_a end def test_joins_with_string_array @@ -739,11 +748,11 @@ class RelationTest < ActiveRecord::TestCase posts = Post.preload(:comments) post = posts.find { |p| p.id == 1 } assert_equal 2, post.comments.size - assert post.comments.include?(comments(:greetings)) + assert_includes post.comments, comments(:greetings) post = Post.where("posts.title = 'Welcome to the weblog'").preload(:comments).first assert_equal 2, post.comments.size - assert post.comments.include?(comments(:greetings)) + assert_includes post.comments, comments(:greetings) posts = Post.preload(:last_comment) post = posts.find { |p| p.id == 1 } @@ -776,7 +785,6 @@ class RelationTest < ActiveRecord::TestCase expected_taggings = taggings(:welcome_general, :thinking_general) assert_no_queries do - assert_equal expected_taggings, author.taggings.distinct.sort_by(&:id) assert_equal expected_taggings, author.taggings.uniq.sort_by(&:id) end @@ -832,15 +840,6 @@ class RelationTest < ActiveRecord::TestCase assert_equal author, authors.first end - class Mary < Author; end - - def test_find_by_classname - Author.create!(name: Mary.name) - assert_deprecated do - assert_equal 1, Author.where(name: Mary).size - end - end - def test_find_by_id_with_list_of_ar author = Author.first authors = Author.find_by_id([author]) @@ -970,7 +969,7 @@ class RelationTest < ActiveRecord::TestCase assert ! davids.exists?(42) assert ! davids.exists?(davids.new.id) - fake = Author.where(name: "fake author") + fake = Author.where(name: "fake author") assert ! fake.exists? assert ! fake.exists?(authors(:david).id) end @@ -1005,12 +1004,6 @@ class RelationTest < ActiveRecord::TestCase assert davids.loaded? end - def test_destroy_all_with_conditions_is_deprecated - assert_deprecated do - assert_difference("Author.count", -1) { Author.destroy_all(name: "David") } - end - end - def test_delete_all davids = Author.where(name: "David") @@ -1018,12 +1011,6 @@ class RelationTest < ActiveRecord::TestCase assert ! davids.loaded? end - def test_delete_all_with_conditions_is_deprecated - assert_deprecated do - assert_difference("Author.count", -1) { Author.delete_all(name: "David") } - end - end - def test_delete_all_loaded davids = Author.where(name: "David") @@ -1225,8 +1212,8 @@ class RelationTest < ActiveRecord::TestCase assert posts.any? # Uses COUNT() assert ! posts.where(id: nil).any? - assert posts.any? {|p| p.id > 0 } - assert ! posts.any? {|p| p.id <= 0 } + assert posts.any? { |p| p.id > 0 } + assert ! posts.any? { |p| p.id <= 0 } end assert posts.loaded? @@ -1237,8 +1224,8 @@ class RelationTest < ActiveRecord::TestCase assert_queries(2) do assert posts.many? # Uses COUNT() - assert posts.many? {|p| p.id > 0 } - assert ! posts.many? {|p| p.id < 2 } + assert posts.many? { |p| p.id > 0 } + assert ! posts.many? { |p| p.id < 2 } end assert posts.loaded? @@ -1260,8 +1247,8 @@ class RelationTest < ActiveRecord::TestCase assert ! posts.loaded? assert_queries(1) do - assert posts.none? {|p| p.id < 0 } - assert ! posts.none? {|p| p.id == 1 } + assert posts.none? { |p| p.id < 0 } + assert ! posts.none? { |p| p.id == 1 } end assert posts.loaded? @@ -1276,8 +1263,8 @@ class RelationTest < ActiveRecord::TestCase assert ! posts.loaded? assert_queries(1) do - assert ! posts.one? {|p| p.id < 3 } - assert posts.one? {|p| p.id == 1 } + assert ! posts.one? { |p| p.id < 3 } + assert posts.one? { |p| p.id == 1 } end assert posts.loaded? @@ -1363,11 +1350,11 @@ class RelationTest < ActiveRecord::TestCase end def test_first_or_create_with_array - several_green_birds = Bird.where(color: "green").first_or_create([{name: "parrot"}, {name: "parakeet"}]) + several_green_birds = Bird.where(color: "green").first_or_create([{ name: "parrot" }, { name: "parakeet" }]) assert_kind_of Array, several_green_birds several_green_birds.each { |bird| assert bird.persisted? } - same_parrot = Bird.where(color: "green").first_or_create([{name: "hummingbird"}, {name: "macaw"}]) + same_parrot = Bird.where(color: "green").first_or_create([{ name: "hummingbird" }, { name: "macaw" }]) assert_kind_of Bird, same_parrot assert_equal several_green_birds.first, same_parrot end @@ -1411,17 +1398,17 @@ class RelationTest < ActiveRecord::TestCase end def test_first_or_create_with_valid_array - several_green_birds = Bird.where(color: "green").first_or_create!([{name: "parrot"}, {name: "parakeet"}]) + several_green_birds = Bird.where(color: "green").first_or_create!([{ name: "parrot" }, { name: "parakeet" }]) assert_kind_of Array, several_green_birds several_green_birds.each { |bird| assert bird.persisted? } - same_parrot = Bird.where(color: "green").first_or_create!([{name: "hummingbird"}, {name: "macaw"}]) + same_parrot = Bird.where(color: "green").first_or_create!([{ name: "hummingbird" }, { name: "macaw" }]) assert_kind_of Bird, same_parrot assert_equal several_green_birds.first, same_parrot end def test_first_or_create_with_invalid_array - assert_raises(ActiveRecord::RecordInvalid) { Bird.where(color: "green").first_or_create!([ {name: "parrot"}, {pirate_id: 1} ]) } + assert_raises(ActiveRecord::RecordInvalid) { Bird.where(color: "green").first_or_create!([ { name: "parrot" }, { pirate_id: 1 } ]) } end def test_first_or_initialize @@ -1513,7 +1500,7 @@ class RelationTest < ActiveRecord::TestCase assert_equal Post.where(author_id: 1).to_a, author_posts.to_a all_posts = relation.only(:limit) - assert_equal Post.limit(1).to_a.first, all_posts.first + assert_equal Post.limit(1).to_a, all_posts.to_a end def test_anonymous_extension @@ -1555,8 +1542,8 @@ class RelationTest < ActiveRecord::TestCase end def test_unscoped_block_style - assert_equal "honda", CoolCar.unscoped { CoolCar.order_using_new_style.limit(1).first.name} - assert_equal "honda", FastCar.unscoped { FastCar.order_using_new_style.limit(1).first.name} + assert_equal "honda", CoolCar.unscoped { CoolCar.order_using_new_style.limit(1).first.name } + assert_equal "honda", FastCar.unscoped { FastCar.order_using_new_style.limit(1).first.name } end def test_intersection_with_array @@ -1630,9 +1617,9 @@ class RelationTest < ActiveRecord::TestCase assert_equal "David", topic2.reload.author_name end - def test_update_on_relation_passing_active_record_object_is_deprecated + def test_update_on_relation_passing_active_record_object_is_not_permitted topic = Topic.create!(title: "Foo", author_name: nil) - assert_deprecated(/update/) do + assert_raises(ArgumentError) do Topic.where(id: topic.id).update(topic, title: "Bar") end end @@ -1646,17 +1633,11 @@ class RelationTest < ActiveRecord::TestCase assert_equal ["Foo", "Foo"], query.map(&:name) assert_sql(/DISTINCT/) do assert_equal ["Foo"], query.distinct.map(&:name) - assert_deprecated { assert_equal ["Foo"], query.uniq.map(&:name) } end assert_sql(/DISTINCT/) do assert_equal ["Foo"], query.distinct(true).map(&:name) - assert_deprecated { assert_equal ["Foo"], query.uniq(true).map(&:name) } end assert_equal ["Foo", "Foo"], query.distinct(true).distinct(false).map(&:name) - - assert_deprecated do - assert_equal ["Foo", "Foo"], query.uniq(true).uniq(false).map(&:name) - end end def test_doesnt_add_having_values_if_options_are_blank @@ -1806,7 +1787,7 @@ class RelationTest < ActiveRecord::TestCase end test "find_by returns nil if the record is missing" do - assert_equal nil, Post.all.find_by("1 = 0") + assert_nil Post.all.find_by("1 = 0") end test "find_by doesn't have implicit ordering" do @@ -1955,7 +1936,7 @@ class RelationTest < ActiveRecord::TestCase end def test_unscope_removes_binds - left = Post.where(id: Arel::Nodes::BindParam.new) + left = Post.where(id: Arel::Nodes::BindParam.new) column = Post.columns_hash["id"] left.bind_values += [[column, 20]] @@ -1964,8 +1945,8 @@ class RelationTest < ActiveRecord::TestCase end def test_merging_removes_rhs_bind_parameters - left = Post.where(id: 20) - right = Post.where(id: [1,2,3,4]) + left = Post.where(id: 20) + right = Post.where(id: [1, 2, 3, 4]) merged = left.merge(right) assert_equal [], merged.bind_values diff --git a/activerecord/test/cases/result_test.rb b/activerecord/test/cases/result_test.rb index 8bbd4b3bc5..949086fda0 100644 --- a/activerecord/test/cases/result_test.rb +++ b/activerecord/test/cases/result_test.rb @@ -16,20 +16,20 @@ module ActiveRecord test "to_hash returns row_hashes" do assert_equal [ - {"col_1" => "row 1 col 1", "col_2" => "row 1 col 2"}, - {"col_1" => "row 2 col 1", "col_2" => "row 2 col 2"}, - {"col_1" => "row 3 col 1", "col_2" => "row 3 col 2"}, + { "col_1" => "row 1 col 1", "col_2" => "row 1 col 2" }, + { "col_1" => "row 2 col 1", "col_2" => "row 2 col 2" }, + { "col_1" => "row 3 col 1", "col_2" => "row 3 col 2" }, ], result.to_hash end test "first returns first row as a hash" do assert_equal( - {"col_1" => "row 1 col 1", "col_2" => "row 1 col 2"}, result.first) + { "col_1" => "row 1 col 1", "col_2" => "row 1 col 2" }, result.first) end test "last returns last row as a hash" do assert_equal( - {"col_1" => "row 3 col 1", "col_2" => "row 3 col 2"}, result.last) + { "col_1" => "row 3 col 1", "col_2" => "row 3 col 2" }, result.last) end test "each with block returns row hashes" do diff --git a/activerecord/test/cases/sanitize_test.rb b/activerecord/test/cases/sanitize_test.rb index 464bb12ccb..23bcb0af1e 100644 --- a/activerecord/test/cases/sanitize_test.rb +++ b/activerecord/test/cases/sanitize_test.rb @@ -12,8 +12,8 @@ class SanitizeTest < ActiveRecord::TestCase 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]) 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.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]) end def test_sanitize_sql_array_handles_bind_variables diff --git a/activerecord/test/cases/schema_dumper_test.rb b/activerecord/test/cases/schema_dumper_test.rb index 2ad3da94e4..bea78d2a95 100644 --- a/activerecord/test/cases/schema_dumper_test.rb +++ b/activerecord/test/cases/schema_dumper_test.rb @@ -51,6 +51,7 @@ class SchemaDumperTest < ActiveRecord::TestCase output = standard_dump assert_match %r{create_table "accounts"}, output assert_match %r{create_table "authors"}, output + assert_no_match %r{(?<=, ) do \|t\|}, output assert_no_match %r{create_table "schema_migrations"}, output assert_no_match %r{create_table "ar_internal_metadata"}, output end @@ -70,38 +71,35 @@ class SchemaDumperTest < ActiveRecord::TestCase assert_match %r{create_table "CamelCase"}, output end - def assert_line_up(lines, pattern, required = false) + def assert_no_line_up(lines, pattern) return assert(true) if lines.empty? matches = lines.map { |line| line.match(pattern) } - assert matches.all? if required matches.compact! return assert(true) if matches.empty? - assert_equal 1, matches.map{ |match| match.offset(0).first }.uniq.length + line_matches = lines.map { |line| [line, line.match(pattern)] }.select { |line, match| match } + assert line_matches.all? { |line, match| + start = match.offset(0).first + line[start - 2..start - 1] == ", " + } end def column_definition_lines(output = standard_dump) - output.scan(/^( *)create_table.*?\n(.*?)^\1end/m).map{ |m| m.last.split(/\n/) } + output.scan(/^( *)create_table.*?\n(.*?)^\1end/m).map { |m| m.last.split(/\n/) } end - def test_types_line_up + def test_types_no_line_up column_definition_lines.each do |column_set| next if column_set.empty? - lengths = column_set.map do |column| - if match = column.match(/\bt\.\w+\s+(?="\w+?")/) - match[0].length - end - end.compact - - assert_equal 1, lengths.uniq.length + assert column_set.all? { |column| !column.match(/\bt\.\w+\s{2,}/) } end end - def test_arguments_line_up + def test_arguments_no_line_up column_definition_lines.each do |column_set| - assert_line_up(column_set, /default: /) - assert_line_up(column_set, /limit: /) - assert_line_up(column_set, /null: /) + assert_no_line_up(column_set, /default: /) + assert_no_line_up(column_set, /limit: /) + assert_no_line_up(column_set, /null: /) end end @@ -150,12 +148,7 @@ class SchemaDumperTest < ActiveRecord::TestCase assert_match %r{c_int_4.*limit: 4}, output end - if current_adapter?(:SQLite3Adapter) - assert_match %r{c_int_5.*limit: 5}, output - assert_match %r{c_int_6.*limit: 6}, output - assert_match %r{c_int_7.*limit: 7}, output - assert_match %r{c_int_8.*limit: 8}, output - elsif current_adapter?(:OracleAdapter) + if current_adapter?(:SQLite3Adapter, :OracleAdapter) assert_match %r{c_int_5.*limit: 5}, output assert_match %r{c_int_6.*limit: 6}, output assert_match %r{c_int_7.*limit: 7}, output @@ -186,8 +179,10 @@ class SchemaDumperTest < ActiveRecord::TestCase def test_schema_dumps_index_columns_in_right_order index_definition = standard_dump.split(/\n/).grep(/t\.index.*company_index/).first.strip - if current_adapter?(:Mysql2Adapter, :PostgreSQLAdapter) - assert_equal 't.index ["firm_id", "type", "rating"], name: "company_index", using: :btree', index_definition + if current_adapter?(:PostgreSQLAdapter) + assert_equal 't.index ["firm_id", "type", "rating"], name: "company_index", order: { rating: :desc }, using: :btree', index_definition + elsif current_adapter?(:Mysql2Adapter) + assert_equal 't.index ["firm_id", "type", "rating"], name: "company_index", length: { type: 10 }, using: :btree', index_definition else assert_equal 't.index ["firm_id", "type", "rating"], name: "company_index"', index_definition end @@ -346,7 +341,7 @@ class SchemaDumperTest < ActiveRecord::TestCase create_table("dogs") do |t| t.column :name, :string - t.column :owner_id, :integer + t.column :owner_id, :bigint t.index [:name] t.foreign_key :dog_owners, column: "owner_id" if supports_foreign_keys? end @@ -426,6 +421,13 @@ class SchemaDumperDefaultsTest < ActiveRecord::TestCase t.datetime :datetime_with_default, default: "2014-06-05 07:17:04" t.time :time_with_default, default: "07:17:04" end + + if current_adapter?(:PostgreSQLAdapter) + @connection.create_table :infinity_defaults, force: true do |t| + t.float :float_with_inf_default, default: Float::INFINITY + t.float :float_with_nan_default, default: Float::NAN + end + end end teardown do @@ -441,4 +443,11 @@ class SchemaDumperDefaultsTest < ActiveRecord::TestCase assert_match %r{t\.datetime\s+"datetime_with_default",\s+default: '2014-06-05 07:17:04'}, output assert_match %r{t\.time\s+"time_with_default",\s+default: '2000-01-01 07:17:04'}, output end + + def test_schema_dump_with_float_column_infinity_default + skip unless current_adapter?(:PostgreSQLAdapter) + output = dump_table_schema("infinity_defaults") + assert_match %r{t\.float\s+"float_with_inf_default",\s+default: ::Float::INFINITY}, output + assert_match %r{t\.float\s+"float_with_nan_default",\s+default: ::Float::NAN}, output + end end diff --git a/activerecord/test/cases/schema_loading_test.rb b/activerecord/test/cases/schema_loading_test.rb index 3d92a5e104..362370ac61 100644 --- a/activerecord/test/cases/schema_loading_test.rb +++ b/activerecord/test/cases/schema_loading_test.rb @@ -8,7 +8,7 @@ module SchemaLoadCounter def load_schema! self.load_schema_calls ||= 0 - self.load_schema_calls +=1 + self.load_schema_calls += 1 super end end diff --git a/activerecord/test/cases/scoping/default_scoping_test.rb b/activerecord/test/cases/scoping/default_scoping_test.rb index effecfd294..3a04f4bf7d 100644 --- a/activerecord/test/cases/scoping/default_scoping_test.rb +++ b/activerecord/test/cases/scoping/default_scoping_test.rb @@ -5,7 +5,7 @@ require "models/developer" require "models/computer" require "models/vehicle" require "models/cat" -require "active_support/core_ext/regexp" +require "concurrent/atomic/cyclic_barrier" class DefaultScopingTest < ActiveRecord::TestCase fixtures :developers, :posts, :comments @@ -51,7 +51,7 @@ class DefaultScopingTest < ActiveRecord::TestCase def test_default_scope_with_conditions_string assert_equal Developer.where(name: "David").map(&:id).sort, DeveloperCalledDavid.all.map(&:id).sort - assert_equal nil, DeveloperCalledDavid.create!.name + assert_nil DeveloperCalledDavid.create!.name end def test_default_scope_with_conditions_hash @@ -63,7 +63,7 @@ class DefaultScopingTest < ActiveRecord::TestCase def test_default_scoping_with_threads 2.times do Thread.new { - assert DeveloperOrderedBySalary.all.to_sql.include?("salary DESC") + assert_includes DeveloperOrderedBySalary.all.to_sql, "salary DESC" DeveloperOrderedBySalary.connection.close }.join end @@ -132,7 +132,7 @@ class DefaultScopingTest < ActiveRecord::TestCase assert_equal expected, received expected_2 = Developer.order("salary DESC").collect(&:name) - received_2 = DeveloperOrderedBySalary.select("id").where("name" => "Jamis").unscope({where: :name}, :select).collect(&:name) + received_2 = DeveloperOrderedBySalary.select("id").where("name" => "Jamis").unscope({ where: :name }, :select).collect(&:name) assert_equal expected_2, received_2 expected_3 = Developer.order("salary DESC").collect(&:name) @@ -315,7 +315,7 @@ class DefaultScopingTest < ActiveRecord::TestCase end def test_create_attribute_overwrites_default_values - assert_equal nil, PoorDeveloperCalledJamis.create!(salary: nil).salary + assert_nil PoorDeveloperCalledJamis.create!(salary: nil).salary assert_equal 50000, PoorDeveloperCalledJamis.create!(name: "David").salary end @@ -368,7 +368,7 @@ class DefaultScopingTest < ActiveRecord::TestCase def test_unscoped_with_named_scope_should_not_have_default_scope assert_equal [DeveloperCalledJamis.find(developers(:poor_jamis).id)], DeveloperCalledJamis.poor - assert DeveloperCalledJamis.unscoped.poor.include?(developers(:david).becomes(DeveloperCalledJamis)) + assert_includes DeveloperCalledJamis.unscoped.poor, developers(:david).becomes(DeveloperCalledJamis) assert_equal 11, DeveloperCalledJamis.unscoped.length assert_equal 1, DeveloperCalledJamis.poor.length @@ -438,12 +438,17 @@ class DefaultScopingTest < ActiveRecord::TestCase threads = [] assert_not_equal 1, ThreadsafeDeveloper.unscoped.count + barrier_1 = Concurrent::CyclicBarrier.new(2) + barrier_2 = Concurrent::CyclicBarrier.new(2) + threads << Thread.new do - Thread.current[:long_default_scope] = true + Thread.current[:default_scope_delay] = -> { barrier_1.wait; barrier_2.wait } assert_equal 1, ThreadsafeDeveloper.all.to_a.count ThreadsafeDeveloper.connection.close end threads << Thread.new do + Thread.current[:default_scope_delay] = -> { barrier_2.wait } + barrier_1.wait assert_equal 1, ThreadsafeDeveloper.all.to_a.count ThreadsafeDeveloper.connection.close end diff --git a/activerecord/test/cases/scoping/named_scoping_test.rb b/activerecord/test/cases/scoping/named_scoping_test.rb index 4e799d6dc0..995ff4dfc5 100644 --- a/activerecord/test/cases/scoping/named_scoping_test.rb +++ b/activerecord/test/cases/scoping/named_scoping_test.rb @@ -33,8 +33,8 @@ class NamedScopingTest < ActiveRecord::TestCase all_posts.to_a new_post = Topic.create! - assert !all_posts.include?(new_post) - assert all_posts.reload.include?(new_post) + assert_not_includes all_posts, new_post + assert_includes all_posts.reload, new_post end def test_delegates_finds_and_calculations_to_the_base_class @@ -69,9 +69,9 @@ class NamedScopingTest < ActiveRecord::TestCase end def test_scopes_with_options_limit_finds_to_those_matching_the_criteria_specified - assert !Topic.all.merge!(where: {approved: true}).to_a.empty? + assert !Topic.all.merge!(where: { approved: true }).to_a.empty? - assert_equal Topic.all.merge!(where: {approved: true}).to_a, Topic.approved + assert_equal Topic.all.merge!(where: { approved: true }).to_a, Topic.approved assert_equal Topic.where(approved: true).count, Topic.approved.count end @@ -82,7 +82,7 @@ class NamedScopingTest < ActiveRecord::TestCase end def test_scopes_are_composable - assert_equal((approved = Topic.all.merge!(where: {approved: true}).to_a), Topic.approved) + assert_equal((approved = Topic.all.merge!(where: { approved: true }).to_a), Topic.approved) assert_equal((replied = Topic.all.merge!(where: "replies_count > 0").to_a), Topic.replied) assert !(approved == replied) assert !(approved & replied).empty? @@ -119,8 +119,8 @@ class NamedScopingTest < ActiveRecord::TestCase end def test_scope_with_STI - assert_equal 3,Post.containing_the_letter_a.count - assert_equal 1,SpecialPost.containing_the_letter_a.count + assert_equal 3, Post.containing_the_letter_a.count + assert_equal 1, SpecialPost.containing_the_letter_a.count end def test_has_many_through_associations_have_access_to_scopes @@ -327,12 +327,12 @@ class NamedScopingTest < ActiveRecord::TestCase conflicts.each do |name| e = assert_raises(ArgumentError, "scope `#{name}` should not be allowed") do - klass.class_eval { scope name, ->{ where(approved: true) } } + klass.class_eval { scope name, -> { where(approved: true) } } end assert_match(/You tried to define a scope named \"#{name}\" on the model/, e.message) e = assert_raises(ArgumentError, "scope `#{name}` should not be allowed") do - subklass.class_eval { scope name, ->{ where(approved: true) } } + subklass.class_eval { scope name, -> { where(approved: true) } } end assert_match(/You tried to define a scope named \"#{name}\" on the model/, e.message) end @@ -340,12 +340,12 @@ class NamedScopingTest < ActiveRecord::TestCase non_conflicts.each do |name| assert_nothing_raised do silence_warnings do - klass.class_eval { scope name, ->{ where(approved: true) } } + klass.class_eval { scope name, -> { where(approved: true) } } end end assert_nothing_raised do - subklass.class_eval { scope name, ->{ where(approved: true) } } + subklass.class_eval { scope name, -> { where(approved: true) } } end end end @@ -431,12 +431,12 @@ class NamedScopingTest < ActiveRecord::TestCase assert_equal 4, Topic.approved.count assert_queries(5) do - Topic.approved.find_each(batch_size: 1) {|t| assert t.approved? } + Topic.approved.find_each(batch_size: 1) { |t| assert t.approved? } end assert_queries(3) do Topic.approved.find_in_batches(batch_size: 2) do |group| - group.each {|t| assert t.approved? } + group.each { |t| assert t.approved? } end end end @@ -462,7 +462,7 @@ class NamedScopingTest < ActiveRecord::TestCase [:public_method, :protected_method, :private_method].each do |reserved_method| assert Topic.respond_to?(reserved_method, true) ActiveRecord::Base.logger.expects(:warn) - silence_warnings { Topic.scope(reserved_method, -> { }) } + silence_warnings { Topic.scope(reserved_method, -> {}) } end end diff --git a/activerecord/test/cases/scoping/relation_scoping_test.rb b/activerecord/test/cases/scoping/relation_scoping_test.rb index a46123f451..a1ae57fdbb 100644 --- a/activerecord/test/cases/scoping/relation_scoping_test.rb +++ b/activerecord/test/cases/scoping/relation_scoping_test.rb @@ -28,7 +28,7 @@ class RelationScopingTest < ActiveRecord::TestCase def test_scope_breaks_caching_on_collections author = authors :david ids = author.reload.special_posts_with_default_scope.map(&:id) - assert_equal [1,5,6], ids.sort + assert_equal [1, 5, 6], ids.sort scoped_posts = SpecialPostWithDefaultScope.unscoped do author = authors :david author.reload.special_posts_with_default_scope.to_a @@ -133,8 +133,8 @@ class RelationScopingTest < ActiveRecord::TestCase scoped_developers = Developer.includes(:projects).scoping do Developer.where("projects.id" => 2).to_a end - assert scoped_developers.include?(developers(:david)) - assert !scoped_developers.include?(developers(:jamis)) + assert_includes scoped_developers, developers(:david) + assert_not_includes scoped_developers, developers(:jamis) assert_equal 1, scoped_developers.size end @@ -143,8 +143,8 @@ class RelationScopingTest < ActiveRecord::TestCase Developer.where("developers_projects.project_id = 2").to_a end - assert scoped_developers.include?(developers(:david)) - assert !scoped_developers.include?(developers(:jamis)) + assert_includes scoped_developers, developers(:david) + assert_not_includes scoped_developers, developers(:jamis) assert_equal 1, scoped_developers.size assert_equal developers(:david).attributes, scoped_developers.first.attributes end @@ -155,7 +155,7 @@ class RelationScopingTest < ActiveRecord::TestCase end assert_equal 1, new_comment.post_id - assert Post.find(1).comments.include?(new_comment) + assert_includes Post.find(1).comments, new_comment end def test_scoped_create_with_create_with @@ -164,7 +164,7 @@ class RelationScopingTest < ActiveRecord::TestCase end assert_equal 1, new_comment.post_id - assert Post.find(1).comments.include?(new_comment) + assert_includes Post.find(1).comments, new_comment end def test_scoped_create_with_create_with_has_higher_priority @@ -173,7 +173,7 @@ class RelationScopingTest < ActiveRecord::TestCase end assert_equal 1, new_comment.post_id - assert Post.find(1).comments.include?(new_comment) + assert_includes Post.find(1).comments, new_comment end def test_ensure_that_method_scoping_is_correctly_restored @@ -277,7 +277,7 @@ class NestedRelationScopingTest < ActiveRecord::TestCase assert_equal "David", Developer.first.name Developer.unscoped.where("name = 'Maiha'") do - assert_equal nil, Developer.first + assert_nil Developer.first end # ensure that scoping is restored diff --git a/activerecord/test/cases/secure_token_test.rb b/activerecord/test/cases/secure_token_test.rb index eda0229c26..7b9cbee40a 100644 --- a/activerecord/test/cases/secure_token_test.rb +++ b/activerecord/test/cases/secure_token_test.rb @@ -27,6 +27,6 @@ class SecureTokenTest < ActiveRecord::TestCase @user.token = "custom-secure-token" @user.save - assert_equal @user.token, "custom-secure-token" + assert_equal "custom-secure-token", @user.token end end diff --git a/activerecord/test/cases/serialized_attribute_test.rb b/activerecord/test/cases/serialized_attribute_test.rb index ceb0d5441b..a469da0a5b 100644 --- a/activerecord/test/cases/serialized_attribute_test.rb +++ b/activerecord/test/cases/serialized_attribute_test.rb @@ -107,7 +107,7 @@ class SerializedAttributeTest < ActiveRecord::TestCase end def test_serialized_time_attribute - myobj = Time.local(2008,1,1,1,0) + myobj = Time.local(2008, 1, 1, 1, 0) topic = Topic.create("content" => myobj).reload assert_equal(myobj, topic.content) end @@ -157,6 +157,13 @@ class SerializedAttributeTest < ActiveRecord::TestCase assert_equal(settings, Topic.find(topic.id).content) end + def test_where_by_serialized_attribute_with_hash + settings = { "color" => "green" } + Topic.serialize(:content, Hash) + topic = Topic.create!(content: settings) + assert_equal topic, Topic.where(content: settings).take + end + def test_serialized_default_class Topic.serialize(:content, Hash) topic = Topic.new @@ -178,14 +185,14 @@ class SerializedAttributeTest < ActiveRecord::TestCase topic = Topic.new(content: true) assert topic.save topic = topic.reload - assert_equal topic.content, true + assert_equal true, topic.content end def test_serialized_boolean_value_false topic = Topic.new(content: false) assert topic.save topic = topic.reload - assert_equal topic.content, false + assert_equal false, topic.content end def test_serialize_with_coder @@ -204,7 +211,7 @@ class SerializedAttributeTest < ActiveRecord::TestCase topic.save! topic.reload assert_kind_of some_class, topic.content - assert_equal topic.content, some_class.new("my value") + assert_equal some_class.new("my value"), topic.content end def test_serialize_attribute_via_select_method_when_time_zone_available diff --git a/activerecord/test/cases/suppressor_test.rb b/activerecord/test/cases/suppressor_test.rb index 64f39d8574..a7d16b7cdb 100644 --- a/activerecord/test/cases/suppressor_test.rb +++ b/activerecord/test/cases/suppressor_test.rb @@ -64,7 +64,7 @@ class SuppressorTest < ActiveRecord::TestCase def test_suppresses_when_nested_multiple_times assert_no_difference -> { Notification.count } do Notification.suppress do - Notification.suppress { } + Notification.suppress {} Notification.create Notification.create! Notification.new.save diff --git a/activerecord/test/cases/tasks/database_tasks_test.rb b/activerecord/test/cases/tasks/database_tasks_test.rb index 0519c70ac7..f7c53b5801 100644 --- a/activerecord/test/cases/tasks/database_tasks_test.rb +++ b/activerecord/test/cases/tasks/database_tasks_test.rb @@ -24,13 +24,13 @@ module ActiveRecord sqlite3: :sqlite_tasks } - class DatabaseTasksUtilsTask< ActiveRecord::TestCase + class DatabaseTasksUtilsTask < ActiveRecord::TestCase def test_raises_an_error_when_called_with_protected_environment ActiveRecord::Migrator.stubs(:current_version).returns(1) protected_environments = ActiveRecord::Base.protected_environments.dup current_env = ActiveRecord::Migrator.current_environment - assert !protected_environments.include?(current_env) + assert_not_includes protected_environments, current_env # Assert no error ActiveRecord::Tasks::DatabaseTasks.check_protected_environments! @@ -61,15 +61,15 @@ module ActiveRecord instance = klazz.new klazz.stubs(:new).returns instance - instance.expects(:structure_dump).with("awesome-file.sql") + instance.expects(:structure_dump).with("awesome-file.sql", nil) ActiveRecord::Tasks::DatabaseTasks.register_task(/foo/, klazz) - ActiveRecord::Tasks::DatabaseTasks.structure_dump({"adapter" => :foo}, "awesome-file.sql") + ActiveRecord::Tasks::DatabaseTasks.structure_dump({ "adapter" => :foo }, "awesome-file.sql") end def test_unregistered_task assert_raise(ActiveRecord::Tasks::DatabaseNotSupported) do - ActiveRecord::Tasks::DatabaseTasks.structure_dump({"adapter" => :bar}, "awesome-file.sql") + ActiveRecord::Tasks::DatabaseTasks.structure_dump({ "adapter" => :bar }, "awesome-file.sql") end end end @@ -85,9 +85,19 @@ module ActiveRecord end end + class DatabaseTasksDumpSchemaCacheTest < ActiveRecord::TestCase + def test_dump_schema_cache + path = "/tmp/my_schema_cache.yml" + ActiveRecord::Tasks::DatabaseTasks.dump_schema_cache(ActiveRecord::Base.connection, path) + assert File.file?(path) + ensure + FileUtils.rm_rf(path) + end + end + class DatabaseTasksCreateAllTest < ActiveRecord::TestCase def setup - @configurations = {"development" => {"database" => "my-db"}} + @configurations = { "development" => { "database" => "my-db" } } ActiveRecord::Base.stubs(:configurations).returns(@configurations) end @@ -145,9 +155,9 @@ module ActiveRecord class DatabaseTasksCreateCurrentTest < ActiveRecord::TestCase def setup @configurations = { - "development" => {"database" => "dev-db"}, - "test" => {"database" => "test-db"}, - "production" => {"database" => "prod-db"} + "development" => { "database" => "dev-db" }, + "test" => { "database" => "test-db" }, + "production" => { "database" => "prod-db" } } ActiveRecord::Base.stubs(:configurations).returns(@configurations) @@ -213,7 +223,7 @@ module ActiveRecord class DatabaseTasksDropAllTest < ActiveRecord::TestCase def setup - @configurations = {development: {"database" => "my-db"}} + @configurations = { development: { "database" => "my-db" } } ActiveRecord::Base.stubs(:configurations).returns(@configurations) end @@ -271,9 +281,9 @@ module ActiveRecord class DatabaseTasksDropCurrentTest < ActiveRecord::TestCase def setup @configurations = { - "development" => {"database" => "dev-db"}, - "test" => {"database" => "test-db"}, - "production" => {"database" => "prod-db"} + "development" => { "database" => "dev-db" }, + "test" => { "database" => "test-db" }, + "production" => { "database" => "prod-db" } } ActiveRecord::Base.stubs(:configurations).returns(@configurations) @@ -358,9 +368,9 @@ module ActiveRecord class DatabaseTasksPurgeCurrentTest < ActiveRecord::TestCase def test_purges_current_environment_database configurations = { - "development" => {"database" => "dev-db"}, - "test" => {"database" => "test-db"}, - "production" => {"database" => "prod-db"} + "development" => { "database" => "dev-db" }, + "test" => { "database" => "test-db" }, + "production" => { "database" => "prod-db" } } ActiveRecord::Base.stubs(:configurations).returns(configurations) @@ -374,7 +384,7 @@ module ActiveRecord class DatabaseTasksPurgeAllTest < ActiveRecord::TestCase def test_purge_all_local_configurations - configurations = {development: {"database" => "my-db"}} + configurations = { development: { "database" => "my-db" } } ActiveRecord::Base.stubs(:configurations).returns(configurations) ActiveRecord::Tasks::DatabaseTasks.expects(:purge). @@ -411,8 +421,8 @@ module ActiveRecord ADAPTERS_TASKS.each do |k, v| define_method("test_#{k}_structure_dump") do - eval("@#{v}").expects(:structure_dump).with("awesome-file.sql") - ActiveRecord::Tasks::DatabaseTasks.structure_dump({"adapter" => k}, "awesome-file.sql") + eval("@#{v}").expects(:structure_dump).with("awesome-file.sql", nil) + ActiveRecord::Tasks::DatabaseTasks.structure_dump({ "adapter" => k }, "awesome-file.sql") end end end @@ -422,8 +432,8 @@ module ActiveRecord ADAPTERS_TASKS.each do |k, v| define_method("test_#{k}_structure_load") do - eval("@#{v}").expects(:structure_load).with("awesome-file.sql") - ActiveRecord::Tasks::DatabaseTasks.structure_load({"adapter" => k}, "awesome-file.sql") + eval("@#{v}").expects(:structure_load).with("awesome-file.sql", nil) + ActiveRecord::Tasks::DatabaseTasks.structure_load({ "adapter" => k }, "awesome-file.sql") end end end @@ -443,7 +453,7 @@ module ActiveRecord end class DatabaseTasksCheckSchemaFileSpecifiedFormatsTest < ActiveRecord::TestCase - {ruby: "schema.rb", sql: "structure.sql"}.each_pair do |fmt, filename| + { ruby: "schema.rb", sql: "structure.sql" }.each_pair do |fmt, filename| define_method("test_check_schema_file_for_#{fmt}_format") do ActiveRecord::Tasks::DatabaseTasks.stubs(:db_dir).returns("/tmp") assert_equal "/tmp/#{filename}", ActiveRecord::Tasks::DatabaseTasks.schema_file(fmt) diff --git a/activerecord/test/cases/tasks/mysql_rake_test.rb b/activerecord/test/cases/tasks/mysql_rake_test.rb index dbe935808e..f30e0958c3 100644 --- a/activerecord/test/cases/tasks/mysql_rake_test.rb +++ b/activerecord/test/cases/tasks/mysql_rake_test.rb @@ -59,7 +59,7 @@ if current_adapter?(:Mysql2Adapter) def test_when_database_created_successfully_outputs_info_to_stdout ActiveRecord::Tasks::DatabaseTasks.create @configuration - assert_equal $stdout.string, "Created database 'my-app-db'\n" + assert_equal "Created database 'my-app-db'\n", $stdout.string end def test_create_when_database_exists_outputs_info_to_stderr @@ -69,7 +69,7 @@ if current_adapter?(:Mysql2Adapter) ActiveRecord::Tasks::DatabaseTasks.create @configuration - assert_equal $stderr.string, "Database 'my-app-db' already exists\n" + assert_equal "Database 'my-app-db' already exists\n", $stderr.string end end @@ -205,7 +205,7 @@ if current_adapter?(:Mysql2Adapter) def test_when_database_dropped_successfully_outputs_info_to_stdout ActiveRecord::Tasks::DatabaseTasks.drop @configuration - assert_equal $stdout.string, "Dropped database 'my-app-db'\n" + assert_equal "Dropped database 'my-app-db'\n", $stdout.string end end @@ -294,6 +294,17 @@ if current_adapter?(:Mysql2Adapter) ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, filename) end + def test_structure_dump_with_extra_flags + filename = "awesome-file.sql" + expected_command = ["mysqldump", "--result-file", filename, "--no-data", "--routines", "--skip-comments", "--noop", "test-db"] + + assert_called_with(Kernel, :system, expected_command, returns: true) do + with_structure_dump_flags(["--noop"]) do + ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, filename) + end + end + end + def test_warn_when_external_structure_dump_command_execution_fails filename = "awesome-file.sql" Kernel.expects(:system) @@ -323,6 +334,15 @@ if current_adapter?(:Mysql2Adapter) @configuration.merge("sslca" => "ca.crt"), filename) 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 MySQLStructureLoadTest < ActiveRecord::TestCase @@ -335,11 +355,23 @@ if current_adapter?(:Mysql2Adapter) def test_structure_load filename = "awesome-file.sql" - Kernel.expects(:system).with("mysql", "--execute", %{SET FOREIGN_KEY_CHECKS = 0; SOURCE #{filename}; SET FOREIGN_KEY_CHECKS = 1}, "--database", "test-db") - .returns(true) + expected_command = ["mysql", "--execute", %{SET FOREIGN_KEY_CHECKS = 0; SOURCE #{filename}; SET FOREIGN_KEY_CHECKS = 1}, "--database", "test-db", "--noop"] - ActiveRecord::Tasks::DatabaseTasks.structure_load(@configuration, filename) + assert_called_with(Kernel, :system, expected_command, returns: true) do + with_structure_load_flags(["--noop"]) do + ActiveRecord::Tasks::DatabaseTasks.structure_load(@configuration, filename) + end + end end + + private + def with_structure_load_flags(flags) + old = ActiveRecord::Tasks::DatabaseTasks.structure_load_flags + ActiveRecord::Tasks::DatabaseTasks.structure_load_flags = flags + yield + ensure + ActiveRecord::Tasks::DatabaseTasks.structure_load_flags = old + end end end end diff --git a/activerecord/test/cases/tasks/postgresql_rake_test.rb b/activerecord/test/cases/tasks/postgresql_rake_test.rb index b8c8ec88f0..a23100c32a 100644 --- a/activerecord/test/cases/tasks/postgresql_rake_test.rb +++ b/activerecord/test/cases/tasks/postgresql_rake_test.rb @@ -74,7 +74,7 @@ if current_adapter?(:PostgreSQLAdapter) def test_when_database_created_successfully_outputs_info_to_stdout ActiveRecord::Tasks::DatabaseTasks.create @configuration - assert_equal $stdout.string, "Created database 'my-app-db'\n" + assert_equal "Created database 'my-app-db'\n", $stdout.string end def test_create_when_database_exists_outputs_info_to_stderr @@ -84,7 +84,7 @@ if current_adapter?(:PostgreSQLAdapter) ActiveRecord::Tasks::DatabaseTasks.create @configuration - assert_equal $stderr.string, "Database 'my-app-db' already exists\n" + assert_equal "Database 'my-app-db' already exists\n", $stderr.string end end @@ -126,7 +126,7 @@ if current_adapter?(:PostgreSQLAdapter) def test_when_database_dropped_successfully_outputs_info_to_stdout ActiveRecord::Tasks::DatabaseTasks.drop @configuration - assert_equal $stdout.string, "Dropped database 'my-app-db'\n" + assert_equal "Dropped database 'my-app-db'\n", $stdout.string end end @@ -236,6 +236,16 @@ if current_adapter?(:PostgreSQLAdapter) ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, @filename) end + def test_structure_dump_with_extra_flags + expected_command = ["pg_dump", "-s", "-x", "-O", "-f", @filename, "--noop", "my-app-db"] + + assert_called_with(Kernel, :system, expected_command, returns: true) do + with_structure_dump_flags(["--noop"]) do + ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, @filename) + end + end + end + def test_structure_dump_with_schema_search_path @configuration["schema_search_path"] = "foo,bar" @@ -263,7 +273,6 @@ if current_adapter?(:PostgreSQLAdapter) end private - def with_dump_schemas(value, &block) old_dump_schemas = ActiveRecord::Base.dump_schemas ActiveRecord::Base.dump_schemas = value @@ -271,6 +280,14 @@ if current_adapter?(:PostgreSQLAdapter) ensure ActiveRecord::Base.dump_schemas = old_dump_schemas end + + 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 PostgreSQLStructureLoadTest < ActiveRecord::TestCase @@ -293,12 +310,32 @@ if current_adapter?(:PostgreSQLAdapter) ActiveRecord::Tasks::DatabaseTasks.structure_load(@configuration, filename) end + def test_structure_load_with_extra_flags + filename = "awesome-file.sql" + expected_command = ["psql", "-v", "ON_ERROR_STOP=1", "-q", "-f", filename, "--noop", @configuration["database"]] + + assert_called_with(Kernel, :system, expected_command, returns: true) do + with_structure_load_flags(["--noop"]) do + ActiveRecord::Tasks::DatabaseTasks.structure_load(@configuration, filename) + end + end + end + def test_structure_load_accepts_path_with_spaces filename = "awesome file.sql" Kernel.expects(:system).with("psql", "-v", "ON_ERROR_STOP=1", "-q", "-f", filename, @configuration["database"]).returns(true) ActiveRecord::Tasks::DatabaseTasks.structure_load(@configuration, filename) end + + private + def with_structure_load_flags(flags) + old = ActiveRecord::Tasks::DatabaseTasks.structure_load_flags + ActiveRecord::Tasks::DatabaseTasks.structure_load_flags = flags + yield + ensure + ActiveRecord::Tasks::DatabaseTasks.structure_load_flags = old + end end end end diff --git a/activerecord/test/cases/tasks/sqlite_rake_test.rb b/activerecord/test/cases/tasks/sqlite_rake_test.rb index 141048bfe7..0d917f3f6c 100644 --- a/activerecord/test/cases/tasks/sqlite_rake_test.rb +++ b/activerecord/test/cases/tasks/sqlite_rake_test.rb @@ -34,7 +34,7 @@ if current_adapter?(:SQLite3Adapter) def test_when_db_created_successfully_outputs_info_to_stdout ActiveRecord::Tasks::DatabaseTasks.create @configuration, "/rails/root" - assert_equal $stdout.string, "Created database '#{@database}'\n" + assert_equal "Created database '#{@database}'\n", $stdout.string end def test_db_create_when_file_exists @@ -42,7 +42,7 @@ if current_adapter?(:SQLite3Adapter) ActiveRecord::Tasks::DatabaseTasks.create @configuration, "/rails/root" - assert_equal $stderr.string, "Database '#{@database}' already exists\n" + assert_equal "Database '#{@database}' already exists\n", $stderr.string end def test_db_create_with_file_does_nothing @@ -128,7 +128,7 @@ if current_adapter?(:SQLite3Adapter) def test_when_db_dropped_successfully_outputs_info_to_stdout ActiveRecord::Tasks::DatabaseTasks.drop @configuration, "/rails/root" - assert_equal $stdout.string, "Dropped database '#{@database}'\n" + assert_equal "Dropped database '#{@database}'\n", $stdout.string end end diff --git a/activerecord/test/cases/test_case.rb b/activerecord/test/cases/test_case.rb index b4f5226f2b..31b11c19f7 100644 --- a/activerecord/test/cases/test_case.rb +++ b/activerecord/test/cases/test_case.rb @@ -1,13 +1,28 @@ require "active_support/test_case" +require "active_support/testing/autorun" +require "active_support/testing/method_call_assertions" require "active_support/testing/stream" -require "active_support/core_ext/regexp" +require "active_record/fixtures" + +require "cases/validations_repair_helper" module ActiveRecord # = Active Record Test Case # # Defines some test assertions to test against SQL queries. class TestCase < ActiveSupport::TestCase #:nodoc: + include ActiveSupport::Testing::MethodCallAssertions include ActiveSupport::Testing::Stream + include ActiveRecord::TestFixtures + include ActiveRecord::ValidationsRepairHelper + + self.fixture_path = FIXTURES_ROOT + self.use_instantiated_fixtures = false + self.use_transactional_tests = true + + def create_fixtures(*fixture_set_names, &block) + ActiveRecord::FixtureSet.create_fixtures(ActiveRecord::TestCase.fixture_path, fixture_set_names, fixture_class_names, &block) + end def teardown SQLCounter.clear_log @@ -24,7 +39,7 @@ module ActiveRecord ensure failed_patterns = [] patterns_to_match.each do |pattern| - failed_patterns << pattern unless SQLCounter.log_all.any?{ |sql| pattern === sql } + failed_patterns << pattern unless SQLCounter.log_all.any? { |sql| pattern === sql } end assert failed_patterns.empty?, "Query pattern(s) #{failed_patterns.map(&:inspect).join(', ')} not found.#{SQLCounter.log.size == 0 ? '' : "\nQueries:\n#{SQLCounter.log.join("\n")}"}" end @@ -48,11 +63,11 @@ module ActiveRecord assert_queries(0, options, &block) end - def assert_column(model, column_name, msg=nil) + def assert_column(model, column_name, msg = nil) assert has_column?(model, column_name), msg end - def assert_no_column(model, column_name, msg=nil) + def assert_no_column(model, column_name, msg = nil) assert_not has_column?(model, column_name), msg end @@ -109,12 +124,9 @@ module ActiveRecord end def call(name, start, finish, message_id, values) - sql = values[:sql] - - # FIXME: this seems bad. we should probably have a better way to indicate - # the query was cached - return if "CACHE" == values[:name] + return if values[:cached] + sql = values[:sql] self.class.log_all << sql self.class.log << sql unless ignore.match?(sql) end diff --git a/activerecord/test/cases/test_fixtures_test.rb b/activerecord/test/cases/test_fixtures_test.rb index 14a5faa85e..58d3bea3a2 100644 --- a/activerecord/test/cases/test_fixtures_test.rb +++ b/activerecord/test/cases/test_fixtures_test.rb @@ -3,28 +3,10 @@ require "cases/helper" class TestFixturesTest < ActiveRecord::TestCase setup do @klass = Class.new - @klass.send(:include, ActiveRecord::TestFixtures) - end - - def test_deprecated_use_transactional_fixtures= - assert_deprecated "use use_transactional_tests= instead" do - @klass.use_transactional_fixtures = true - end - end - - def test_use_transactional_tests_prefers_use_transactional_fixtures - ActiveSupport::Deprecation.silence do - @klass.use_transactional_fixtures = false - end - - assert_equal false, @klass.use_transactional_tests + @klass.include(ActiveRecord::TestFixtures) end def test_use_transactional_tests_defaults_to_true - ActiveSupport::Deprecation.silence do - @klass.use_transactional_fixtures = nil - end - assert_equal true, @klass.use_transactional_tests end diff --git a/activerecord/test/cases/timestamp_test.rb b/activerecord/test/cases/timestamp_test.rb index 519d9db601..39b40e3411 100644 --- a/activerecord/test/cases/timestamp_test.rb +++ b/activerecord/test/cases/timestamp_test.rb @@ -422,7 +422,7 @@ class TimestampTest < ActiveRecord::TestCase self.table_name = "people" before_create do - self.born_at = self.created_at + self.born_at = created_at end end @@ -430,39 +430,24 @@ class TimestampTest < ActiveRecord::TestCase assert_not_equal person.born_at, nil end - def test_timestamp_attributes_for_create - toy = Toy.first - assert_equal [:created_at, :created_on], toy.send(:timestamp_attributes_for_create) - end - - def test_timestamp_attributes_for_update - toy = Toy.first - assert_equal [:updated_at, :updated_on], toy.send(:timestamp_attributes_for_update) - end - - def test_all_timestamp_attributes - toy = Toy.first - assert_equal [:created_at, :created_on, :updated_at, :updated_on], toy.send(:all_timestamp_attributes) - end - def test_timestamp_attributes_for_create_in_model toy = Toy.first - assert_equal [:created_at], toy.send(:timestamp_attributes_for_create_in_model) + assert_equal ["created_at"], toy.send(:timestamp_attributes_for_create_in_model) end def test_timestamp_attributes_for_update_in_model toy = Toy.first - assert_equal [:updated_at], toy.send(:timestamp_attributes_for_update_in_model) + assert_equal ["updated_at"], toy.send(:timestamp_attributes_for_update_in_model) end def test_all_timestamp_attributes_in_model toy = Toy.first - assert_equal [:created_at, :updated_at], toy.send(:all_timestamp_attributes_in_model) + 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(:foos, null: true, index: true) + t.timestamps null: true, index: true end indexes = ActiveRecord::Base.connection.indexes("foos") diff --git a/activerecord/test/cases/touch_later_test.rb b/activerecord/test/cases/touch_later_test.rb index 697447a4f3..d1e8c649d9 100644 --- a/activerecord/test/cases/touch_later_test.rb +++ b/activerecord/test/cases/touch_later_test.rb @@ -24,6 +24,15 @@ class TouchLaterTest < ActiveRecord::TestCase assert_not invoice.changed? end + def test_touch_later_respects_no_touching_policy + time = Time.now.utc - 25.days + topic = Topic.create!(updated_at: time, created_at: time) + Topic.no_touching do + topic.touch_later + end + assert_equal time.to_i, topic.updated_at.to_i + end + def test_touch_later_update_the_attributes time = Time.now.utc - 25.days topic = Topic.create!(updated_at: time, created_at: time) diff --git a/activerecord/test/cases/transaction_callbacks_test.rb b/activerecord/test/cases/transaction_callbacks_test.rb index 6a7d48871c..391bbe8877 100644 --- a/activerecord/test/cases/transaction_callbacks_test.rb +++ b/activerecord/test/cases/transaction_callbacks_test.rb @@ -68,17 +68,17 @@ class TransactionCallbacksTest < ActiveRecord::TestCase def do_before_commit(on) blocks = @before_commit[on] if defined?(@before_commit) - blocks.each{|b| b.call(self)} if blocks + blocks.each { |b| b.call(self) } if blocks end def do_after_commit(on) blocks = @after_commit[on] if defined?(@after_commit) - blocks.each{|b| b.call(self)} if blocks + blocks.each { |b| b.call(self) } if blocks end def do_after_rollback(on) blocks = @after_rollback[on] if defined?(@after_rollback) - blocks.each{|b| b.call(self)} if blocks + blocks.each { |b| b.call(self) } if blocks end end @@ -101,8 +101,8 @@ class TransactionCallbacksTest < ActiveRecord::TestCase end def test_call_after_commit_after_transaction_commits - @first.after_commit_block{|r| r.history << :after_commit} - @first.after_rollback_block{|r| r.history << :after_rollback} + @first.after_commit_block { |r| r.history << :after_commit } + @first.after_rollback_block { |r| r.history << :after_rollback } @first.save! assert_equal [:after_commit], @first.history @@ -155,7 +155,7 @@ class TransactionCallbacksTest < ActiveRecord::TestCase end def test_only_call_after_commit_on_top_level_transactions - @first.after_commit_block{|r| r.history << :after_commit} + @first.after_commit_block { |r| r.history << :after_commit } assert @first.history.empty? @first.transaction do @@ -168,8 +168,8 @@ class TransactionCallbacksTest < ActiveRecord::TestCase end def test_call_after_rollback_after_transaction_rollsback - @first.after_commit_block{|r| r.history << :after_commit} - @first.after_rollback_block{|r| r.history << :after_rollback} + @first.after_commit_block { |r| r.history << :after_commit } + @first.after_rollback_block { |r| r.history << :after_rollback } Topic.transaction do @first.save! @@ -243,16 +243,16 @@ class TransactionCallbacksTest < ActiveRecord::TestCase end def test_only_call_after_rollback_on_records_rolled_back_to_a_savepoint - def @first.rollbacks(i=0); @rollbacks ||= 0; @rollbacks += i if i; end - def @first.commits(i=0); @commits ||= 0; @commits += i if i; end - @first.after_rollback_block{|r| r.rollbacks(1)} - @first.after_commit_block{|r| r.commits(1)} + def @first.rollbacks(i = 0); @rollbacks ||= 0; @rollbacks += i if i; end + def @first.commits(i = 0); @commits ||= 0; @commits += i if i; end + @first.after_rollback_block { |r| r.rollbacks(1) } + @first.after_commit_block { |r| r.commits(1) } second = TopicWithCallbacks.find(3) - def second.rollbacks(i=0); @rollbacks ||= 0; @rollbacks += i if i; end - def second.commits(i=0); @commits ||= 0; @commits += i if i; end - second.after_rollback_block{|r| r.rollbacks(1)} - second.after_commit_block{|r| r.commits(1)} + def second.rollbacks(i = 0); @rollbacks ||= 0; @rollbacks += i if i; end + def second.commits(i = 0); @commits ||= 0; @commits += i if i; end + second.after_rollback_block { |r| r.rollbacks(1) } + second.after_commit_block { |r| r.commits(1) } Topic.transaction do @first.save! @@ -269,11 +269,11 @@ class TransactionCallbacksTest < ActiveRecord::TestCase end def test_only_call_after_rollback_on_records_rolled_back_to_a_savepoint_when_release_savepoint_fails - def @first.rollbacks(i=0); @rollbacks ||= 0; @rollbacks += i if i; end - def @first.commits(i=0); @commits ||= 0; @commits += i if i; end + def @first.rollbacks(i = 0); @rollbacks ||= 0; @rollbacks += i if i; end + def @first.commits(i = 0); @commits ||= 0; @commits += i if i; end - @first.after_rollback_block{|r| r.rollbacks(1)} - @first.after_commit_block{|r| r.commits(1)} + @first.after_rollback_block { |r| r.rollbacks(1) } + @first.after_commit_block { |r| r.commits(1) } Topic.transaction do @first.save @@ -292,7 +292,7 @@ class TransactionCallbacksTest < ActiveRecord::TestCase end def test_after_commit_callback_should_not_swallow_errors - @first.after_commit_block{ fail "boom" } + @first.after_commit_block { fail "boom" } assert_raises(RuntimeError) do Topic.transaction do @first.save! @@ -303,8 +303,8 @@ class TransactionCallbacksTest < ActiveRecord::TestCase def test_after_commit_callback_when_raise_should_not_restore_state first = TopicWithCallbacks.new second = TopicWithCallbacks.new - first.after_commit_block{ fail "boom" } - second.after_commit_block{ fail "boom" } + first.after_commit_block { fail "boom" } + second.after_commit_block { fail "boom" } begin Topic.transaction do @@ -322,7 +322,7 @@ class TransactionCallbacksTest < ActiveRecord::TestCase def test_after_rollback_callback_should_not_swallow_errors_when_set_to_raise error_class = Class.new(StandardError) - @first.after_rollback_block{ raise error_class } + @first.after_rollback_block { raise error_class } assert_raises(error_class) do Topic.transaction do @first.save! @@ -336,8 +336,8 @@ class TransactionCallbacksTest < ActiveRecord::TestCase first = TopicWithCallbacks.new second = TopicWithCallbacks.new - first.after_rollback_block{ raise error_class } - second.after_rollback_block{ raise error_class } + first.after_rollback_block { raise error_class } + second.after_rollback_block { raise error_class } begin Topic.transaction do @@ -449,6 +449,51 @@ class CallbacksOnMultipleActionsTest < ActiveRecord::TestCase end end +class CallbacksOnDestroyUpdateActionRaceTest < ActiveRecord::TestCase + class TopicWithHistory < ActiveRecord::Base + self.table_name = :topics + + def self.clear_history + @@history = [] + end + + def self.history + @@history ||= [] + end + end + + class TopicWithCallbacksOnDestroy < TopicWithHistory + after_commit(on: :destroy) { |record| record.class.history << :destroy } + end + + class TopicWithCallbacksOnUpdate < TopicWithHistory + after_commit(on: :update) { |record| record.class.history << :update } + end + + def test_trigger_once_on_multiple_deletions + TopicWithCallbacksOnDestroy.clear_history + topic = TopicWithCallbacksOnDestroy.new + topic.save + topic_clone = TopicWithCallbacksOnDestroy.find(topic.id) + topic.destroy + topic_clone.destroy + + assert_equal [:destroy], TopicWithCallbacksOnDestroy.history + end + + def test_trigger_on_update_where_row_was_deleted + TopicWithCallbacksOnUpdate.clear_history + topic = TopicWithCallbacksOnUpdate.new + topic.save + topic_clone = TopicWithCallbacksOnUpdate.find(topic.id) + topic.destroy + topic_clone.author_name = "Test Author" + topic_clone.save + + assert_equal [], TopicWithCallbacksOnUpdate.history + end +end + class TransactionEnrollmentCallbacksTest < ActiveRecord::TestCase class TopicWithoutTransactionalEnrollmentCallbacks < ActiveRecord::Base self.table_name = :topics diff --git a/activerecord/test/cases/transaction_isolation_test.rb b/activerecord/test/cases/transaction_isolation_test.rb index f177f0644a..58abfadaf4 100644 --- a/activerecord/test/cases/transaction_isolation_test.rb +++ b/activerecord/test/cases/transaction_isolation_test.rb @@ -9,7 +9,7 @@ unless ActiveRecord::Base.connection.supports_transaction_isolation? test "setting the isolation level raises an error" do assert_raises(ActiveRecord::TransactionIsolationError) do - Tag.transaction(isolation: :serializable) { } + Tag.transaction(isolation: :serializable) {} end end end @@ -90,7 +90,7 @@ if ActiveRecord::Base.connection.supports_transaction_isolation? test "setting isolation when joining a transaction raises an error" do Tag.transaction do assert_raises(ActiveRecord::TransactionIsolationError) do - Tag.transaction(isolation: :serializable) { } + Tag.transaction(isolation: :serializable) {} end end end @@ -98,7 +98,7 @@ if ActiveRecord::Base.connection.supports_transaction_isolation? test "setting isolation when starting a nested transaction raises error" do Tag.transaction do assert_raises(ActiveRecord::TransactionIsolationError) do - Tag.transaction(requires_new: true, isolation: :serializable) { } + Tag.transaction(requires_new: true, isolation: :serializable) {} end end end diff --git a/activerecord/test/cases/transactions_test.rb b/activerecord/test/cases/transactions_test.rb index c8963ada63..9b1cca8583 100644 --- a/activerecord/test/cases/transactions_test.rb +++ b/activerecord/test/cases/transactions_test.rb @@ -98,7 +98,7 @@ class TransactionTest < ActiveRecord::TestCase end Topic.transaction do - @first.approved = true + @first.approved = true @first.save! end @@ -160,7 +160,7 @@ class TransactionTest < ActiveRecord::TestCase assert !@first.approved Topic.transaction do - @first.approved = true + @first.approved = true @first.save! end assert !Topic.find(@first.id).approved?, "Should not commit the approved flag" @@ -443,16 +443,16 @@ class TransactionTest < ActiveRecord::TestCase def test_using_named_savepoints Topic.transaction do - @first.approved = true + @first.approved = true @first.save! Topic.connection.create_savepoint("first") - @first.approved = false + @first.approved = false @first.save! Topic.connection.rollback_to_savepoint("first") assert @first.reload.approved? - @first.approved = false + @first.approved = false @first.save! Topic.connection.release_savepoint("first") assert_not @first.reload.approved? @@ -493,7 +493,7 @@ class TransactionTest < ActiveRecord::TestCase def test_rollback_when_commit_raises assert_called(Topic.connection, :begin_db_transaction) do - Topic.connection.stub(:commit_db_transaction, ->{ raise("OH NOES") }) do + Topic.connection.stub(:commit_db_transaction, -> { raise("OH NOES") }) do assert_called(Topic.connection, :rollback_db_transaction) do e = assert_raise RuntimeError do @@ -511,9 +511,10 @@ class TransactionTest < ActiveRecord::TestCase topic = Topic.new(title: "test") topic.freeze e = assert_raise(RuntimeError) { topic.save } - assert_match(/frozen/i, e.message) # Not good enough, but we can't do much - # about it since there is no specific error - # for frozen objects. + # Not good enough, but we can't do much + # about it since there is no specific error + # for frozen objects. + assert_match(/frozen/i, e.message) assert !topic.persisted?, "not persisted" assert_nil topic.id assert topic.frozen?, "not frozen" @@ -630,11 +631,11 @@ class TransactionTest < ActiveRecord::TestCase assert_nothing_raised do Topic.reset_column_information Topic.connection.add_column("topics", "stuff", :string) - assert Topic.column_names.include?("stuff") + assert_includes Topic.column_names, "stuff" Topic.reset_column_information Topic.connection.remove_column("topics", "stuff") - assert !Topic.column_names.include?("stuff") + assert_not_includes Topic.column_names, "stuff" end if Topic.connection.supports_ddl_transactions? @@ -692,7 +693,7 @@ class TransactionTest < ActiveRecord::TestCase klass = Class.new(ActiveRecord::Base) do self.table_name = "transaction_without_primary_keys" - after_commit { } # necessary to trigger the has_transactional_callbacks branch + after_commit {} # necessary to trigger the has_transactional_callbacks branch end assert_no_difference(-> { klass.count }) do diff --git a/activerecord/test/cases/type/date_time_test.rb b/activerecord/test/cases/type/date_time_test.rb index bc4900e1c2..6848619ece 100644 --- a/activerecord/test/cases/type/date_time_test.rb +++ b/activerecord/test/cases/type/date_time_test.rb @@ -3,7 +3,7 @@ require "models/task" module ActiveRecord module Type - class IntegerTest < ActiveRecord::TestCase + class DateTimeTest < ActiveRecord::TestCase def test_datetime_seconds_precision_applied_to_timestamp skip "This test is invalid if subsecond precision isn't supported" unless subsecond_precision_supported? p = Task.create!(starting: ::Time.now) diff --git a/activerecord/test/cases/type/unsigned_integer_test.rb b/activerecord/test/cases/type/unsigned_integer_test.rb new file mode 100644 index 0000000000..1cd4dbc2c5 --- /dev/null +++ b/activerecord/test/cases/type/unsigned_integer_test.rb @@ -0,0 +1,17 @@ +require "cases/helper" + +module ActiveRecord + module Type + class UnsignedIntegerTest < ActiveRecord::TestCase + test "unsigned int max value is in range" do + assert_equal(4294967295, UnsignedInteger.new.serialize(4294967295)) + end + + test "minus value is out of range" do + assert_raises(ActiveModel::RangeError) do + UnsignedInteger.new.serialize(-1) + end + end + end + end +end diff --git a/activerecord/test/cases/validations/association_validation_test.rb b/activerecord/test/cases/validations/association_validation_test.rb index 66d6ecb928..f5ceb27d97 100644 --- a/activerecord/test/cases/validations/association_validation_test.rb +++ b/activerecord/test/cases/validations/association_validation_test.rb @@ -26,7 +26,7 @@ class AssociationValidationTest < ActiveRecord::TestCase def test_validates_associated_one Reply.validates :topic, associated: true - Topic.validates_presence_of( :content ) + Topic.validates_presence_of(:content) r = Reply.new("title" => "A reply", "content" => "with content!") r.topic = Topic.create("title" => "uhohuhoh") assert !r.valid? 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 d73d287073..a57065ba75 100644 --- a/activerecord/test/cases/validations/i18n_generate_message_validation_test.rb +++ b/activerecord/test/cases/validations/i18n_generate_message_validation_test.rb @@ -47,7 +47,7 @@ class I18nGenerateMessageValidationTest < ActiveRecord::TestCase test "RecordInvalid exception translation falls back to the :errors namespace" do reset_i18n_load_path do - I18n.backend.store_translations "en", errors: {messages: {record_invalid: "fallback message"}} + I18n.backend.store_translations "en", errors: { messages: { record_invalid: "fallback message" } } topic = Topic.new topic.errors.add(:title, :blank) assert_equal "fallback message", ActiveRecord::RecordInvalid.new(topic).message @@ -56,28 +56,28 @@ class I18nGenerateMessageValidationTest < ActiveRecord::TestCase test "translation for 'taken' can be overridden" do reset_i18n_load_path do - I18n.backend.store_translations "en", errors: {attributes: {title: {taken: "Custom taken message" }}} + I18n.backend.store_translations "en", errors: { attributes: { title: { taken: "Custom taken message" } } } assert_equal "Custom taken message", @topic.errors.generate_message(:title, :taken, value: "title") end end test "translation for 'taken' can be overridden in activerecord scope" do reset_i18n_load_path do - I18n.backend.store_translations "en", activerecord: {errors: {messages: {taken: "Custom taken message" }}} + I18n.backend.store_translations "en", activerecord: { errors: { messages: { taken: "Custom taken message" } } } assert_equal "Custom taken message", @topic.errors.generate_message(:title, :taken, value: "title") end end test "translation for 'taken' can be overridden in activerecord model scope" do reset_i18n_load_path do - I18n.backend.store_translations "en", activerecord: {errors: {models: {topic: {taken: "Custom taken message" }}}} + I18n.backend.store_translations "en", activerecord: { errors: { models: { topic: { taken: "Custom taken message" } } } } assert_equal "Custom taken message", @topic.errors.generate_message(:title, :taken, value: "title") end end test "translation for 'taken' can be overridden in activerecord attributes scope" do reset_i18n_load_path do - I18n.backend.store_translations "en", activerecord: {errors: {models: {topic: {attributes: {title: {taken: "Custom taken message" }}}}}} + I18n.backend.store_translations "en", activerecord: { errors: { models: { topic: { attributes: { title: { taken: "Custom taken message" } } } } } } assert_equal "Custom taken message", @topic.errors.generate_message(:title, :taken, value: "title") end end diff --git a/activerecord/test/cases/validations/i18n_validation_test.rb b/activerecord/test/cases/validations/i18n_validation_test.rb index cbb5362f3f..fd88a3ea67 100644 --- a/activerecord/test/cases/validations/i18n_validation_test.rb +++ b/activerecord/test/cases/validations/i18n_validation_test.rb @@ -12,7 +12,7 @@ class I18nValidationTest < ActiveRecord::TestCase @old_load_path, @old_backend = I18n.load_path.dup, I18n.backend I18n.load_path.clear I18n.backend = I18n::Backend::Simple.new - I18n.backend.store_translations("en", errors: {messages: {custom: nil}}) + I18n.backend.store_translations("en", errors: { messages: { custom: nil } }) end teardown do @@ -36,13 +36,13 @@ class I18nValidationTest < ActiveRecord::TestCase # are used to generate tests to keep things DRY # COMMON_CASES = [ - # [ case, validation_options, generate_message_options] + # [ case, validation_options, generate_message_options] [ "given no options", {}, {}], - [ "given custom message", {message: "custom"}, {message: "custom"}], - [ "given if condition", {if: lambda { true }}, {}], - [ "given unless condition", {unless: lambda { false }}, {}], - [ "given option that is not reserved", {format: "jpg"}, {format: "jpg" }], - [ "given on condition", {on: [:create, :update] }, {}] + [ "given custom message", { message: "custom" }, { message: "custom" }], + [ "given if condition", { if: lambda { true } }, {}], + [ "given unless condition", { unless: lambda { false } }, {}], + [ "given option that is not reserved", { format: "jpg" }, { format: "jpg" }], + [ "given on condition", { on: [:create, :update] }, {}] ] COMMON_CASES.each do |name, validation_options, generate_message_options| @@ -65,8 +65,8 @@ class I18nValidationTest < ActiveRecord::TestCase end def test_validates_associated_finds_custom_model_key_translation - I18n.backend.store_translations "en", activerecord: {errors: {models: {topic: {attributes: {replies: {invalid: "custom message"}}}}}} - I18n.backend.store_translations "en", activerecord: {errors: {messages: {invalid: "global message"}}} + I18n.backend.store_translations "en", activerecord: { errors: { models: { topic: { attributes: { replies: { invalid: "custom message" } } } } } } + I18n.backend.store_translations "en", activerecord: { errors: { messages: { invalid: "global message" } } } Topic.validates_associated :replies replied_topic.valid? @@ -74,7 +74,7 @@ class I18nValidationTest < ActiveRecord::TestCase end def test_validates_associated_finds_global_default_translation - I18n.backend.store_translations "en", activerecord: {errors: {messages: {invalid: "global message"}}} + I18n.backend.store_translations "en", activerecord: { errors: { messages: { invalid: "global message" } } } Topic.validates_associated :replies replied_topic.valid? diff --git a/activerecord/test/cases/validations/length_validation_test.rb b/activerecord/test/cases/validations/length_validation_test.rb index 4fcb3e2eb6..ba45c6dcc1 100644 --- a/activerecord/test/cases/validations/length_validation_test.rb +++ b/activerecord/test/cases/validations/length_validation_test.rb @@ -54,7 +54,7 @@ class LengthValidationTest < ActiveRecord::TestCase assert owner.save pet_count = Pet.count - assert_not owner.update_attributes pets_attributes: [ {_destroy: 1, id: pet.id} ] + assert_not owner.update_attributes pets_attributes: [ { _destroy: 1, id: pet.id } ] assert_not owner.valid? assert owner.errors[:pets].any? assert_equal pet_count, Pet.count diff --git a/activerecord/test/cases/validations/uniqueness_validation_test.rb b/activerecord/test/cases/validations/uniqueness_validation_test.rb index 44b4e28777..6d22638592 100644 --- a/activerecord/test/cases/validations/uniqueness_validation_test.rb +++ b/activerecord/test/cases/validations/uniqueness_validation_test.rb @@ -6,6 +6,9 @@ require "models/guid" require "models/event" require "models/dashboard" require "models/uuid_item" +require "models/author" +require "models/person" +require "models/essay" class Wizard < ActiveRecord::Base self.abstract_class = true @@ -163,6 +166,19 @@ class UniquenessValidationTest < ActiveRecord::TestCase assert !r2.valid?, "Saving r2 first time" end + def test_validate_uniqueness_with_polymorphic_object_scope + Essay.validates_uniqueness_of(:name, scope: :writer) + + a = Author.create(name: "Sergey") + p = Person.create(first_name: "Sergey") + + e1 = a.essays.create(name: "Essay") + assert e1.valid?, "Saving e1" + + e2 = p.essays.create(name: "Essay") + assert e2.valid?, "Saving e2" + end + def test_validate_uniqueness_with_composed_attribute_scope r1 = ReplyWithTitleObject.create "title" => "r1", "content" => "hello world" assert r1.valid?, "Saving r1" diff --git a/activerecord/test/cases/view_test.rb b/activerecord/test/cases/view_test.rb index 5f85c5ff8b..d055968a56 100644 --- a/activerecord/test/cases/view_test.rb +++ b/activerecord/test/cases/view_test.rb @@ -11,20 +11,21 @@ module ViewBehavior end class Ebook < ActiveRecord::Base + self.table_name = "ebooks'" self.primary_key = "id" end def setup super @connection = ActiveRecord::Base.connection - create_view "ebooks", <<-SQL + create_view "ebooks'", <<-SQL SELECT id, name, status FROM books WHERE format = 'ebook' SQL end def teardown super - drop_view "ebooks" + drop_view "ebooks'" end def test_reading @@ -44,8 +45,7 @@ module ViewBehavior def test_table_exists view_name = Ebook.table_name - # TODO: switch this assertion around once we changed #tables to not return views. - ActiveSupport::Deprecation.silence { assert @connection.table_exists?(view_name), "'#{view_name}' table should exist" } + assert_not @connection.table_exists?(view_name), "'#{view_name}' table should not exist" end def test_views_ara_valid_data_sources @@ -60,21 +60,26 @@ module ViewBehavior end def test_attributes - assert_equal({"id" => 2, "name" => "Ruby for Rails", "status" => 0}, + assert_equal({ "id" => 2, "name" => "Ruby for Rails", "status" => 0 }, Ebook.first.attributes) end def test_does_not_assume_id_column_as_primary_key model = Class.new(ActiveRecord::Base) do - self.table_name = "ebooks" + self.table_name = "ebooks'" end assert_nil model.primary_key end def test_does_not_dump_view_as_table - schema = dump_table_schema "ebooks" - assert_no_match %r{create_table "ebooks"}, schema + schema = dump_table_schema "ebooks'" + assert_no_match %r{create_table "ebooks'"}, schema end + + private + def quote_table_name(name) + @connection.quote_table_name(name) + end end if ActiveRecord::Base.connection.supports_views? @@ -83,11 +88,11 @@ if ActiveRecord::Base.connection.supports_views? private def create_view(name, query) - @connection.execute "CREATE VIEW #{name} AS #{query}" + @connection.execute "CREATE VIEW #{quote_table_name(name)} AS #{query}" end def drop_view(name) - @connection.execute "DROP VIEW #{name}" if @connection.view_exists? name + @connection.execute "DROP VIEW #{quote_table_name(name)}" if @connection.view_exists? name end end @@ -100,9 +105,9 @@ if ActiveRecord::Base.connection.supports_views? setup do @connection = ActiveRecord::Base.connection @connection.execute <<-SQL - CREATE VIEW paperbacks - AS SELECT name, status FROM books WHERE format = 'paperback' - SQL + CREATE VIEW paperbacks + AS SELECT name, status FROM books WHERE format = 'paperback' + SQL end teardown do @@ -125,8 +130,7 @@ if ActiveRecord::Base.connection.supports_views? def test_table_exists view_name = Paperback.table_name - # TODO: switch this assertion around once we changed #tables to not return views. - ActiveSupport::Deprecation.silence { assert @connection.table_exists?(view_name), "'#{view_name}' table should exist" } + assert_not @connection.table_exists?(view_name), "'#{view_name}' table should not exist" end def test_column_definitions @@ -135,7 +139,7 @@ if ActiveRecord::Base.connection.supports_views? end def test_attributes - assert_equal({"name" => "Agile Web Development with Rails", "status" => 2}, + assert_equal({ "name" => "Agile Web Development with Rails", "status" => 2 }, Paperback.first.attributes) end @@ -149,7 +153,7 @@ if ActiveRecord::Base.connection.supports_views? end end -# sqlite dose not support CREATE, INSERT, and DELETE for VIEW + # sqlite dose not support CREATE, INSERT, and DELETE for VIEW if current_adapter?(:Mysql2Adapter, :PostgreSQLAdapter) class UpdateableViewTest < ActiveRecord::TestCase self.use_transactional_tests = false @@ -162,9 +166,9 @@ if ActiveRecord::Base.connection.supports_views? setup do @connection = ActiveRecord::Base.connection @connection.execute <<-SQL - CREATE VIEW printed_books - AS SELECT id, name, status, format FROM books WHERE format = 'paperback' - SQL + CREATE VIEW printed_books + AS SELECT id, name, status, format FROM books WHERE format = 'paperback' + SQL end teardown do @@ -196,8 +200,8 @@ if ActiveRecord::Base.connection.supports_views? end end end - end # end fo `if current_adapter?(:Mysql2Adapter, :PostgreSQLAdapter)` -end # end fo `if ActiveRecord::Base.connection.supports_views?` + end # end of `if current_adapter?(:Mysql2Adapter, :PostgreSQLAdapter)` +end # end of `if ActiveRecord::Base.connection.supports_views?` if ActiveRecord::Base.connection.respond_to?(:supports_materialized_views?) && ActiveRecord::Base.connection.supports_materialized_views? @@ -206,11 +210,11 @@ if ActiveRecord::Base.connection.respond_to?(:supports_materialized_views?) && private def create_view(name, query) - @connection.execute "CREATE MATERIALIZED VIEW #{name} AS #{query}" + @connection.execute "CREATE MATERIALIZED VIEW #{quote_table_name(name)} AS #{query}" end def drop_view(name) - @connection.execute "DROP MATERIALIZED VIEW #{name}" if @connection.view_exists? name + @connection.execute "DROP MATERIALIZED VIEW #{quote_table_name(name)}" if @connection.view_exists? name end end end diff --git a/activerecord/test/cases/yaml_serialization_test.rb b/activerecord/test/cases/yaml_serialization_test.rb index ade6e6095b..1571b31329 100644 --- a/activerecord/test/cases/yaml_serialization_test.rb +++ b/activerecord/test/cases/yaml_serialization_test.rb @@ -22,8 +22,8 @@ class YamlSerializationTest < ActiveRecord::TestCase end def test_roundtrip_serialized_column - topic = Topic.new(content: {omg: :lol}) - assert_equal({omg: :lol}, YAML.load(YAML.dump(topic)).content) + topic = Topic.new(content: { omg: :lol }) + assert_equal({ omg: :lol }, YAML.load(YAML.dump(topic)).content) end def test_psych_roundtrip @@ -95,7 +95,7 @@ class YamlSerializationTest < ActiveRecord::TestCase topic = YAML.load(yaml_fixture("rails_4_1")) assert topic.new_record? - assert_equal nil, topic.id + assert_nil topic.id assert_equal "The First Topic", topic.title assert_equal({ omg: :lol }, topic.content) end diff --git a/activerecord/test/config.example.yml b/activerecord/test/config.example.yml index 58e2d45748..4bcb2aeea6 100644 --- a/activerecord/test/config.example.yml +++ b/activerecord/test/config.example.yml @@ -77,6 +77,9 @@ connections: postgresql: arunit: min_messages: warning + arunit_without_prepared_statements: + min_messages: warning + prepared_statements: false arunit2: min_messages: warning diff --git a/activerecord/test/fixtures/all/namespaced/accounts.yml b/activerecord/test/fixtures/all/namespaced/accounts.yml new file mode 100644 index 0000000000..9e341a15af --- /dev/null +++ b/activerecord/test/fixtures/all/namespaced/accounts.yml @@ -0,0 +1,2 @@ +signals37: + name: 37signals diff --git a/activerecord/test/fixtures/books.yml b/activerecord/test/fixtures/books.yml index a304fba399..b3625ee72e 100644 --- a/activerecord/test/fixtures/books.yml +++ b/activerecord/test/fixtures/books.yml @@ -9,6 +9,7 @@ awdr: author_visibility: :visible illustrator_visibility: :visible font_size: :medium + difficulty: :medium rfr: author_id: 1 diff --git a/activerecord/test/fixtures/naked/yml/courses_with_invalid_key.yml b/activerecord/test/fixtures/naked/yml/courses_with_invalid_key.yml new file mode 100644 index 0000000000..6f9da79b45 --- /dev/null +++ b/activerecord/test/fixtures/naked/yml/courses_with_invalid_key.yml @@ -0,0 +1,3 @@ +one: + id: 1 +two: ['not a hash'] diff --git a/activerecord/test/fixtures/other_dogs.yml b/activerecord/test/fixtures/other_dogs.yml new file mode 100644 index 0000000000..b576861929 --- /dev/null +++ b/activerecord/test/fixtures/other_dogs.yml @@ -0,0 +1,2 @@ +lassie: + id: 1 diff --git a/activerecord/test/models/admin/user.rb b/activerecord/test/models/admin/user.rb index 2e703f6219..a76e4b6795 100644 --- a/activerecord/test/models/admin/user.rb +++ b/activerecord/test/models/admin/user.rb @@ -22,11 +22,11 @@ class Admin::User < ActiveRecord::Base store :json_data_empty, accessors: [ :is_a_good_guy ], coder: Coder.new def phone_number - read_store_attribute(:settings, :phone_number).gsub(/(\d{3})(\d{3})(\d{4})/,'(\1) \2-\3') + read_store_attribute(:settings, :phone_number).gsub(/(\d{3})(\d{3})(\d{4})/, '(\1) \2-\3') end def phone_number=(value) - write_store_attribute(:settings, :phone_number, value && value.gsub(/[^\d]/,"")) + write_store_attribute(:settings, :phone_number, value && value.gsub(/[^\d]/, "")) end def color diff --git a/activerecord/test/models/author.rb b/activerecord/test/models/author.rb index 8633504d54..fab613afd1 100644 --- a/activerecord/test/models/author.rb +++ b/activerecord/test/models/author.rb @@ -67,13 +67,13 @@ class Author < ActiveRecord::Base before_remove: :log_before_removing, after_remove: :log_after_removing has_many :posts_with_proc_callbacks, class_name: "Post", - before_add: Proc.new {|o, r| o.post_log << "before_adding#{r.id || '<new>'}"}, - after_add: Proc.new {|o, r| o.post_log << "after_adding#{r.id || '<new>'}"}, - before_remove: Proc.new {|o, r| o.post_log << "before_removing#{r.id}"}, - after_remove: Proc.new {|o, r| o.post_log << "after_removing#{r.id}"} + before_add: Proc.new { |o, r| o.post_log << "before_adding#{r.id || '<new>'}" }, + after_add: Proc.new { |o, r| o.post_log << "after_adding#{r.id || '<new>'}" }, + before_remove: Proc.new { |o, r| o.post_log << "before_removing#{r.id}" }, + after_remove: Proc.new { |o, r| o.post_log << "after_removing#{r.id}" } has_many :posts_with_multiple_callbacks, class_name: "Post", - before_add: [:log_before_adding, Proc.new {|o, r| o.post_log << "before_adding_proc#{r.id || '<new>'}"}], - after_add: [:log_after_adding, Proc.new {|o, r| o.post_log << "after_adding_proc#{r.id || '<new>'}"}] + before_add: [:log_before_adding, Proc.new { |o, r| o.post_log << "before_adding_proc#{r.id || '<new>'}" }], + 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 diff --git a/activerecord/test/models/book.rb b/activerecord/test/models/book.rb index edd0281127..17bf3fbcb4 100644 --- a/activerecord/test/models/book.rb +++ b/activerecord/test/models/book.rb @@ -8,12 +8,13 @@ class Book < ActiveRecord::Base has_many :subscribers, through: :subscriptions enum status: [:proposed, :written, :published] - enum read_status: {unread: 0, reading: 2, read: 3} + enum read_status: { unread: 0, reading: 2, read: 3 } enum nullable_status: [:single, :married] enum language: [:english, :spanish, :french], _prefix: :in enum author_visibility: [:visible, :invisible], _prefix: true enum illustrator_visibility: [:visible, :invisible], _prefix: true enum font_size: [:small, :medium, :large], _prefix: :with, _suffix: true + enum difficulty: [:easy, :medium, :hard], _suffix: :to_read enum cover: { hard: "hard", soft: "soft" } def published! diff --git a/activerecord/test/models/boolean.rb b/activerecord/test/models/boolean.rb index 7bae22e5f9..0da228aac2 100644 --- a/activerecord/test/models/boolean.rb +++ b/activerecord/test/models/boolean.rb @@ -1,2 +1,5 @@ class Boolean < ActiveRecord::Base + def has_fun + super + end end diff --git a/activerecord/test/models/bulb.rb b/activerecord/test/models/bulb.rb index 3196207ac9..113d21cb84 100644 --- a/activerecord/test/models/bulb.rb +++ b/activerecord/test/models/bulb.rb @@ -50,9 +50,3 @@ class FailedBulb < Bulb throw(:abort) end end - -class TrickyBulb < Bulb - after_create do |record| - record.car.bulbs.to_a - end -end diff --git a/activerecord/test/models/comment.rb b/activerecord/test/models/comment.rb index 7315a1af8e..a4b81d56e0 100644 --- a/activerecord/test/models/comment.rb +++ b/activerecord/test/models/comment.rb @@ -1,5 +1,5 @@ class Comment < ActiveRecord::Base - scope :limit_by, lambda {|l| limit(l) } + scope :limit_by, lambda { |l| limit(l) } scope :containing_the_letter_e, -> { where("comments.body LIKE '%e%'") } scope :not_again, -> { where("comments.body NOT LIKE '%again%'") } scope :for_first_post, -> { where(post_id: 1) } @@ -55,6 +55,6 @@ class CommentThatAutomaticallyAltersPostBody < Comment end class CommentWithDefaultScopeReferencesAssociation < Comment - default_scope ->{ includes(:developer).order("developers.name").references(:developer) } + default_scope -> { includes(:developer).order("developers.name").references(:developer) } belongs_to :developer end diff --git a/activerecord/test/models/company.rb b/activerecord/test/models/company.rb index 025630087c..20e37710e7 100644 --- a/activerecord/test/models/company.rb +++ b/activerecord/test/models/company.rb @@ -151,7 +151,7 @@ class Client < Company # is calling client.destroy, deleting from the database, or setting # foreign keys to NULL. def self.destroyed_client_ids - @destroyed_client_ids ||= Hash.new { |h,k| h[k] = [] } + @destroyed_client_ids ||= Hash.new { |h, k| h[k] = [] } end before_destroy do |client| @@ -199,7 +199,7 @@ class Account < ActiveRecord::Base alias_attribute :available_credit, :credit_limit def self.destroyed_account_ids - @destroyed_account_ids ||= Hash.new { |h,k| h[k] = [] } + @destroyed_account_ids ||= Hash.new { |h, k| h[k] = [] } end # Test private kernel method through collection proxy using has_many. @@ -216,14 +216,12 @@ class Account < ActiveRecord::Base validate :check_empty_credit_limit - protected + private def check_empty_credit_limit errors.add("credit_limit", :blank) if credit_limit.blank? end - private - def private_method "Sir, yes sir!" end diff --git a/activerecord/test/models/company_in_module.rb b/activerecord/test/models/company_in_module.rb index 682f99e365..0782c1eff4 100644 --- a/activerecord/test/models/company_in_module.rb +++ b/activerecord/test/models/company_in_module.rb @@ -88,7 +88,7 @@ module MyApplication validate :check_empty_credit_limit - protected + private def check_empty_credit_limit errors.add("credit_card", :blank) if credit_card.blank? diff --git a/activerecord/test/models/customer.rb b/activerecord/test/models/customer.rb index 71afd0866e..3d40cb1ace 100644 --- a/activerecord/test/models/customer.rb +++ b/activerecord/test/models/customer.rb @@ -5,7 +5,7 @@ class Customer < ActiveRecord::Base composed_of :balance, class_name: "Money", mapping: %w(balance amount), converter: Proc.new(&:to_money) composed_of :gps_location, allow_nil: true composed_of :non_blank_gps_location, class_name: "GpsLocation", allow_nil: true, mapping: %w(gps_location gps_location), - converter: lambda { |gps| self.gps_conversion_was_run = true; gps.blank? ? nil : GpsLocation.new(gps)} + converter: lambda { |gps| self.gps_conversion_was_run = true; gps.blank? ? nil : GpsLocation.new(gps) } composed_of :fullname, mapping: %w(name to_s), constructor: Proc.new { |name| Fullname.parse(name) }, converter: :parse composed_of :fullname_no_converter, mapping: %w(name to_s), class_name: "Fullname" end @@ -56,7 +56,7 @@ class GpsLocation end def ==(other) - self.latitude == other.latitude && self.longitude == other.longitude + latitude == other.latitude && longitude == other.longitude end end diff --git a/activerecord/test/models/developer.rb b/activerecord/test/models/developer.rb index 5ca1d37f6d..654830ba11 100644 --- a/activerecord/test/models/developer.rb +++ b/activerecord/test/models/developer.rb @@ -251,7 +251,7 @@ class ThreadsafeDeveloper < ActiveRecord::Base self.table_name = "developers" def self.default_scope - sleep 0.05 if Thread.current[:long_default_scope] + Thread.current[:default_scope_delay].call limit(1) end end @@ -260,3 +260,9 @@ class CachedDeveloper < ActiveRecord::Base self.table_name = "developers" self.cache_timestamp_format = :number end + +class DeveloperWithIncorrectlyOrderedHasManyThrough < ActiveRecord::Base + self.table_name = "developers" + has_many :companies, through: :contracts + has_many :contracts, foreign_key: :developer_id +end diff --git a/activerecord/test/models/eye.rb b/activerecord/test/models/eye.rb index ab3b3eacf3..f53c34e4b1 100644 --- a/activerecord/test/models/eye.rb +++ b/activerecord/test/models/eye.rb @@ -22,12 +22,12 @@ class Eye < ActiveRecord::Base alias trace_after_create2 trace_after_create def trace_after_update - (@after_update_callbacks_stack ||= []) << iris.changed? + (@after_update_callbacks_stack ||= []) << iris.has_changes_to_save? end alias trace_after_update2 trace_after_update def trace_after_save - (@after_save_callbacks_stack ||= []) << iris.changed? + (@after_save_callbacks_stack ||= []) << iris.has_changes_to_save? end alias trace_after_save2 trace_after_save end diff --git a/activerecord/test/models/invoice.rb b/activerecord/test/models/invoice.rb index 44ee717187..4be5a00193 100644 --- a/activerecord/test/models/invoice.rb +++ b/activerecord/test/models/invoice.rb @@ -1,4 +1,4 @@ class Invoice < ActiveRecord::Base has_many :line_items, autosave: true - before_save {|record| record.balance = record.line_items.map(&:amount).sum } + before_save { |record| record.balance = record.line_items.map(&:amount).sum } end diff --git a/activerecord/test/models/member.rb b/activerecord/test/models/member.rb index 93dfc8253f..36f2461b84 100644 --- a/activerecord/test/models/member.rb +++ b/activerecord/test/models/member.rb @@ -5,7 +5,7 @@ class Member < ActiveRecord::Base has_one :club, through: :current_membership has_one :selected_club, through: :selected_membership, source: :club has_one :favourite_club, -> { where "memberships.favourite = ?", true }, through: :membership, source: :club - has_one :hairy_club, -> { where clubs: {name: "Moustache and Eyebrow Fancier Club"} }, through: :membership, source: :club + has_one :hairy_club, -> { where clubs: { name: "Moustache and Eyebrow Fancier Club" } }, through: :membership, source: :club has_one :sponsor, as: :sponsorable has_one :sponsor_club, through: :sponsor has_one :member_detail, inverse_of: false diff --git a/activerecord/test/models/other_dog.rb b/activerecord/test/models/other_dog.rb new file mode 100644 index 0000000000..418caf34be --- /dev/null +++ b/activerecord/test/models/other_dog.rb @@ -0,0 +1,5 @@ +require_dependency "models/arunit2_model" + +class OtherDog < ARUnit2Model + self.table_name = "dogs" +end diff --git a/activerecord/test/models/parrot.rb b/activerecord/test/models/parrot.rb index 5b693664d4..1e5f9285a8 100644 --- a/activerecord/test/models/parrot.rb +++ b/activerecord/test/models/parrot.rb @@ -13,6 +13,11 @@ class Parrot < ActiveRecord::Base def cancel_save_callback_method throw(:abort) end + + before_update :increment_updated_count + def increment_updated_count + self.updated_count += 1 + end end class LiveParrot < Parrot diff --git a/activerecord/test/models/pirate.rb b/activerecord/test/models/pirate.rb index 80a941874b..c532ab426e 100644 --- a/activerecord/test/models/pirate.rb +++ b/activerecord/test/models/pirate.rb @@ -9,10 +9,10 @@ class Pirate < ActiveRecord::Base before_remove: :log_before_remove, after_remove: :log_after_remove has_and_belongs_to_many :parrots_with_proc_callbacks, class_name: "Parrot", - before_add: proc {|p,pa| p.ship_log << "before_adding_proc_parrot_#{pa.id || '<new>'}"}, - after_add: proc {|p,pa| p.ship_log << "after_adding_proc_parrot_#{pa.id || '<new>'}"}, - before_remove: proc {|p,pa| p.ship_log << "before_removing_proc_parrot_#{pa.id}"}, - after_remove: proc {|p,pa| p.ship_log << "after_removing_proc_parrot_#{pa.id}"} + before_add: proc { |p, pa| p.ship_log << "before_adding_proc_parrot_#{pa.id || '<new>'}" }, + after_add: proc { |p, pa| p.ship_log << "after_adding_proc_parrot_#{pa.id || '<new>'}" }, + before_remove: proc { |p, pa| p.ship_log << "before_removing_proc_parrot_#{pa.id}" }, + after_remove: proc { |p, pa| p.ship_log << "after_removing_proc_parrot_#{pa.id}" } has_and_belongs_to_many :autosaved_parrots, class_name: "Parrot", autosave: true has_many :treasures, as: :looter @@ -28,10 +28,10 @@ class Pirate < ActiveRecord::Base before_remove: :log_before_remove, after_remove: :log_after_remove has_many :birds_with_proc_callbacks, class_name: "Bird", - before_add: proc {|p,b| p.ship_log << "before_adding_proc_bird_#{b.id || '<new>'}"}, - after_add: proc {|p,b| p.ship_log << "after_adding_proc_bird_#{b.id || '<new>'}"}, - before_remove: proc {|p,b| p.ship_log << "before_removing_proc_bird_#{b.id}"}, - after_remove: proc {|p,b| p.ship_log << "after_removing_proc_bird_#{b.id}"} + before_add: proc { |p, b| p.ship_log << "before_adding_proc_bird_#{b.id || '<new>'}" }, + after_add: proc { |p, b| p.ship_log << "after_adding_proc_bird_#{b.id || '<new>'}" }, + before_remove: proc { |p, b| p.ship_log << "before_removing_proc_bird_#{b.id}" }, + after_remove: proc { |p, b| p.ship_log << "after_removing_proc_bird_#{b.id}" } has_many :birds_with_reject_all_blank, class_name: "Bird" has_one :foo_bulb, -> { where name: "foo" }, foreign_key: :car_id, class_name: "Bulb" diff --git a/activerecord/test/models/post.rb b/activerecord/test/models/post.rb index 8bfd79d907..e74aedb814 100644 --- a/activerecord/test/models/post.rb +++ b/activerecord/test/models/post.rb @@ -21,9 +21,10 @@ class Post < ActiveRecord::Base scope :titled_with_an_apostrophe, -> { where("title LIKE '%''%'") } scope :ranked_by_comments, -> { order("comments_count DESC") } - scope :limit_by, lambda {|l| limit(l) } + scope :limit_by, lambda { |l| limit(l) } belongs_to :author + belongs_to :readonly_author, -> { readonly }, class_name: "Author", foreign_key: :author_id belongs_to :author_with_posts, -> { includes(:posts) }, class_name: "Author", foreign_key: :author_id belongs_to :author_with_address, -> { includes(:author_address) }, class_name: "Author", foreign_key: :author_id @@ -34,8 +35,8 @@ class Post < ActiveRecord::Base has_one :first_comment, -> { order("id ASC") }, class_name: "Comment" has_one :last_comment, -> { order("id desc") }, class_name: "Comment" - scope :with_special_comments, -> { joins(:comments).where(comments: {type: "SpecialComment"}) } - scope :with_very_special_comments, -> { joins(:comments).where(comments: {type: "VerySpecialComment"}) } + scope :with_special_comments, -> { joins(:comments).where(comments: { type: "SpecialComment" }) } + scope :with_very_special_comments, -> { joins(:comments).where(comments: { type: "VerySpecialComment" }) } scope :with_post, ->(post_id) { joins(:comments).where(comments: { post_id: post_id }) } scope :with_comments, -> { preload(:comments) } @@ -46,7 +47,7 @@ class Post < ActiveRecord::Base scope :typographically_interesting, -> { containing_the_letter_a.or(titled_with_an_apostrophe) } - has_many :comments do + has_many :comments do def find_most_recent order("id DESC").first end @@ -141,10 +142,10 @@ class Post < ActiveRecord::Base has_many :people, through: :readers has_many :single_people, through: :readers has_many :people_with_callbacks, source: :person, through: :readers, - before_add: lambda {|owner, reader| log(:added, :before, reader.first_name) }, - after_add: lambda {|owner, reader| log(:added, :after, reader.first_name) }, - before_remove: lambda {|owner, reader| log(:removed, :before, reader.first_name) }, - after_remove: lambda {|owner, reader| log(:removed, :after, reader.first_name) } + before_add: lambda { |owner, reader| log(:added, :before, reader.first_name) }, + after_add: lambda { |owner, reader| log(:added, :after, reader.first_name) }, + before_remove: lambda { |owner, reader| log(:removed, :before, reader.first_name) }, + after_remove: lambda { |owner, reader| log(:removed, :after, reader.first_name) } has_many :skimmers, -> { where skimmer: true }, class_name: "Reader" has_many :impatient_people, through: :skimmers, source: :person @@ -167,7 +168,7 @@ class Post < ActiveRecord::Base @log = [] end - def self.log(message=nil, side=nil, new_record=nil) + def self.log(message = nil, side = nil, new_record = nil) return @log if message.nil? @log << [message, side, new_record] end @@ -230,7 +231,7 @@ end class SpecialPostWithDefaultScope < ActiveRecord::Base self.inheritance_column = :disabled self.table_name = "posts" - default_scope { where(id: [1, 5,6]) } + default_scope { where(id: [1, 5, 6]) } end class PostThatLoadsCommentsInAnAfterSaveHook < ActiveRecord::Base diff --git a/activerecord/test/models/project.rb b/activerecord/test/models/project.rb index 4d6b269731..5009f8f54b 100644 --- a/activerecord/test/models/project.rb +++ b/activerecord/test/models/project.rb @@ -7,10 +7,10 @@ class Project < ActiveRecord::Base has_and_belongs_to_many :developers_named_david, -> { where("name = 'David'").distinct }, class_name: "Developer" has_and_belongs_to_many :developers_named_david_with_hash_conditions, -> { where(name: "David").distinct }, class_name: "Developer" has_and_belongs_to_many :salaried_developers, -> { where "salary > 0" }, class_name: "Developer" - has_and_belongs_to_many :developers_with_callbacks, class_name: "Developer", before_add: Proc.new {|o, r| o.developers_log << "before_adding#{r.id || '<new>'}"}, - after_add: Proc.new {|o, r| o.developers_log << "after_adding#{r.id || '<new>'}"}, - before_remove: Proc.new {|o, r| o.developers_log << "before_removing#{r.id}"}, - after_remove: Proc.new {|o, r| o.developers_log << "after_removing#{r.id}"} + has_and_belongs_to_many :developers_with_callbacks, class_name: "Developer", before_add: Proc.new { |o, r| o.developers_log << "before_adding#{r.id || '<new>'}" }, + after_add: Proc.new { |o, r| o.developers_log << "after_adding#{r.id || '<new>'}" }, + before_remove: Proc.new { |o, r| o.developers_log << "before_removing#{r.id}" }, + after_remove: Proc.new { |o, r| o.developers_log << "after_removing#{r.id}" } has_and_belongs_to_many :well_payed_salary_groups, -> { group("developers.salary").having("SUM(salary) > 10000").select("SUM(salary) as salary") }, class_name: "Developer" belongs_to :firm has_one :lead_developer, through: :firm, inverse_of: :contracted_projects diff --git a/activerecord/test/models/sponsor.rb b/activerecord/test/models/sponsor.rb index 175933ccf1..3f142b25fe 100644 --- a/activerecord/test/models/sponsor.rb +++ b/activerecord/test/models/sponsor.rb @@ -2,6 +2,6 @@ class Sponsor < ActiveRecord::Base belongs_to :sponsor_club, class_name: "Club", foreign_key: "club_id" belongs_to :sponsorable, polymorphic: true belongs_to :thing, polymorphic: true, foreign_type: :sponsorable_type, foreign_key: :sponsorable_id - belongs_to :sponsorable_with_conditions, -> { where name: "Ernie"}, polymorphic: true, + belongs_to :sponsorable_with_conditions, -> { where name: "Ernie" }, polymorphic: true, foreign_type: "sponsorable_type", foreign_key: "sponsorable_id" end diff --git a/activerecord/test/models/subject.rb b/activerecord/test/models/subject.rb index 29e290825e..504f68a296 100644 --- a/activerecord/test/models/subject.rb +++ b/activerecord/test/models/subject.rb @@ -5,7 +5,7 @@ class Subject < ActiveRecord::Base # as otherwise synonym test was failing after_initialize :set_email_address - protected + private def set_email_address unless persisted? self.author_email_address = "test@test.com" diff --git a/activerecord/test/models/topic.rb b/activerecord/test/models/topic.rb index db04735d01..0420e2d15c 100644 --- a/activerecord/test/models/topic.rb +++ b/activerecord/test/models/topic.rb @@ -73,7 +73,7 @@ class Topic < ActiveRecord::Base write_attribute(:approved, val) end - protected + private def default_written_on self.written_on = Time.now unless attribute_present?("written_on") diff --git a/activerecord/test/schema/postgresql_specific_schema.rb b/activerecord/test/schema/postgresql_specific_schema.rb index f00b858ea6..15ba2d67ab 100644 --- a/activerecord/test/schema/postgresql_specific_schema.rb +++ b/activerecord/test/schema/postgresql_specific_schema.rb @@ -1,6 +1,7 @@ ActiveRecord::Schema.define do enable_extension!("uuid-ossp", ActiveRecord::Base.connection) + enable_extension!("pgcrypto", ActiveRecord::Base.connection) if ActiveRecord::Base.connection.supports_pgcrypto_uuid? create_table :uuid_parents, id: :uuid, force: true do |t| t.string :name diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb index 558c21b8d2..ba6f5de894 100644 --- a/activerecord/test/schema/schema.rb +++ b/activerecord/test/schema/schema.rb @@ -54,7 +54,7 @@ ActiveRecord::Schema.define do create_table :authors, force: true do |t| t.string :name, null: false - t.integer :author_address_id + t.bigint :author_address_id t.integer :author_address_extra_id t.string :organization_id t.string :owned_essay_id @@ -98,6 +98,7 @@ ActiveRecord::Schema.define do t.column :author_visibility, :integer, default: 0 t.column :illustrator_visibility, :integer, default: 0 t.column :font_size, :integer, default: 0 + t.column :difficulty, :integer, default: 0 t.column :cover, :string, default: "hard" end @@ -125,6 +126,9 @@ ActiveRecord::Schema.define do t.timestamps null: false end + create_table :old_cars, id: :integer, force: true do |t| + end + create_table :carriers, force: true create_table :categories, force: true do |t| @@ -196,7 +200,7 @@ ActiveRecord::Schema.define do t.integer :rating, default: 1 t.integer :account_id t.string :description, default: "" - t.index [:firm_id, :type, :rating], name: "company_index" + t.index [:firm_id, :type, :rating], name: "company_index", length: { type: 10 }, order: { rating: :desc } t.index [:firm_id, :type], name: "company_partial_index", where: "rating > 10" t.index :name, name: "company_name_index", using: :btree t.index "lower(name)", name: "company_expression_index" if supports_expression_index? @@ -302,7 +306,7 @@ ActiveRecord::Schema.define do end create_table :engines, force: true do |t| - t.integer :car_id + t.bigint :car_id end create_table :entrants, force: true do |t| @@ -435,10 +439,12 @@ ActiveRecord::Schema.define do end create_table :lock_without_defaults, force: true do |t| + t.column :title, :string t.column :lock_version, :integer end create_table :lock_without_defaults_cust, force: true do |t| + t.column :title, :string t.column :custom_lock_version, :integer end @@ -568,6 +574,7 @@ ActiveRecord::Schema.define do t.column :color, :string t.column :parrot_sti_class, :string t.column :killer_id, :integer + t.column :updated_count, :integer, default: 0 if subsecond_precision_supported? t.column :created_at, :datetime, precision: 0 t.column :created_on, :datetime, precision: 0 @@ -899,10 +906,9 @@ ActiveRecord::Schema.define do end [:circles, :squares, :triangles, :non_poly_ones, :non_poly_twos].each do |t| - create_table(t, force: true) { } + create_table(t, force: true) {} end - # NOTE - the following 4 tables are used by models that have :inverse_of options on the associations create_table :men, force: true do |t| t.string :name end @@ -926,14 +932,14 @@ ActiveRecord::Schema.define do t.integer :zine_id end - create_table :wheels, force: true do |t| - t.references :wheelable, polymorphic: true - end - create_table :zines, force: true do |t| t.string :title end + create_table :wheels, force: true do |t| + t.references :wheelable, polymorphic: true + end + create_table :countries, force: true, id: false, primary_key: "country_id" do |t| t.string :country_id t.string :name @@ -1002,7 +1008,7 @@ ActiveRecord::Schema.define do if supports_foreign_keys? # fk_test_has_fk should be before fk_test_has_pk create_table :fk_test_has_fk, force: true do |t| - t.integer :fk_id, null: false + t.bigint :fk_id, null: false end create_table :fk_test_has_pk, force: true, primary_key: "pk_id" do |t| @@ -1045,3 +1051,5 @@ Professor.connection.create_table :courses_professors, id: false, force: true do t.references :course t.references :professor end + +OtherDog.connection.create_table :dogs, force: true diff --git a/activerecord/test/support/config.rb b/activerecord/test/support/config.rb index d0717f7b34..aaff408b41 100644 --- a/activerecord/test/support/config.rb +++ b/activerecord/test/support/config.rb @@ -1,5 +1,5 @@ require "yaml" -require "erubis" +require "erb" require "fileutils" require "pathname" @@ -20,13 +20,14 @@ module ARTest FileUtils.cp TEST_ROOT + "/config.example.yml", config_file end - erb = Erubis::Eruby.new(config_file.read) + erb = ERB.new(config_file.read) expand_config(YAML.parse(erb.result(binding)).transform) end def expand_config(config) config["connections"].each do |adapter, connection| - dbs = [["arunit", "activerecord_unittest"], ["arunit2", "activerecord_unittest2"]] + dbs = [["arunit", "activerecord_unittest"], ["arunit2", "activerecord_unittest2"], + ["arunit_without_prepared_statements", "activerecord_unittest"]] dbs.each do |name, dbname| unless connection[name].is_a?(Hash) connection[name] = { "database" => connection[name] } diff --git a/activerecord/test/support/connection.rb b/activerecord/test/support/connection.rb index c9260398e2..bc5af36a28 100644 --- a/activerecord/test/support/connection.rb +++ b/activerecord/test/support/connection.rb @@ -2,6 +2,7 @@ require "active_support/logger" require "models/college" require "models/course" require "models/professor" +require "models/other_dog" module ARTest def self.connection_name |