diff options
Diffstat (limited to 'activerecord')
397 files changed, 9154 insertions, 5862 deletions
diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index 4d0c1a4178..1f8163db12 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,242 +1,64 @@ -* Serialize JSON attribute value `nil` as SQL `NULL`, not JSON `null` +* Deprecate passing arguments and block at the same time to `count` and `sum` in `ActiveRecord::Calculations`. - *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* + *Ryuta Kamizono* -* PostgreSQL array columns will now respect the encoding of strings contained - in the array. +* Loading model schema from database is now thread-safe. - Fixes #26326. + Fixes #28589. - *Sean Griffin* + *Vikrant Chaudhary*, *David Abdemoulaie* -* Inverse association instances will now be set before `after_find` or - `after_initialize` callbacks are run. +* Add `ActiveRecord::Base#cache_version` to support recyclable cache keys via the new versioned entries + in `ActiveSupport::Cache`. This also means that `ActiveRecord::Base#cache_key` will now return a stable key + that does not include a timestamp any more. - Fixes #26320. + NOTE: This feature is turned off by default, and `#cache_key` will still return cache keys with timestamps + until you set `ActiveRecord::Base.cache_versioning = true`. That's the setting for all new apps on Rails 5.2+ - *Sean Griffin* + *DHH* -* Remove unnecessarily association load when a `belongs_to` association has already been - loaded then the foreign key is changed directly and the record saved. +* Respect `SchemaDumper.ignore_tables` in rake tasks for databases structure dump - *James Coleman* + *Rusty Geldmacher*, *Guillermo Iguaran* -* Remove standardized column types/arguments spaces in schema dump. +* Add type caster to `RuntimeReflection#alias_name` - *Tim Petricola* + Fixes #28959. -* Avoid loading records from database when they are already loaded using - the `pluck` method on a collection. + *Jon Moss* - Fixes #25921. +* Deprecate `supports_statement_cache?`. *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. +* Quote database name in `db:create` grant statement (when database user does not have access to create the database). - 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 + *Rune Philosof* - record.save! - record.reload +* Raise error `UnknownMigrationVersionError` on the movement of migrations + when the current migration does not exist. - 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 + *bogdanvlviv* - https://dev.mysql.com/doc/refman/5.7/en/sql-mode.html#sql-mode-strict - - *Ryuta Kamizono* +* Fix `bin/rails db:forward` first migration. -* 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. + *bogdanvlviv* - Fixes #26087 +* Support Descending Indexes for MySQL. - *Travis O'Neill* - -* Deprecate `sanitize_conditions`. Use `sanitize_sql` instead. + MySQL 8.0.1 and higher supports descending indexes: `DESC` in an index definition is no longer ignored. + See https://dev.mysql.com/doc/refman/8.0/en/descending-indexes.html. *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`. - - Fixes #25978. - - *Sean Griffin* - -* Fix the SELECT statement in `#table_comment` for MySQL. - - *Takeshi Akima* - -* Virtual attributes will no longer raise when read on models loaded from the - database - - *Sean Griffin* - -* Support calling the method `merge` in `scope`'s lambda. - - *Yasuhiro Sugino* - -* Fixes multi-parameter attributes conversion with invalid params. - - *Hiroyuki Ishii* - -* Add newline between each migration in `structure.sql`. - - Keeps schema migration inserts as a single commit, but allows for easier - git diffing. +* Fix inconsistency with changed attributes when overriding AR attribute reader. - Fixes #25504. + *bogdanvlviv* - *Grey Baker*, *Norberto Lopes* - -* The flag `error_on_ignored_order_or_limit` has been deprecated in favor of - the current `error_on_ignored_order`. - - *Xavier Noria* - -* Batch processing methods support `limit`: - - Post.limit(10_000).find_each do |post| - # ... - end - - It also works in `find_in_batches` and `in_batches`. - - *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`. - - Fixes #25595. - - *Sean Griffin* - -* Fix the generated `#to_param` method to use `omission: ''` so that - the resulting output is actually up to 20 characters, not - effectively 17 to leave room for the default "...". - Also call `#parameterize` before `#truncate` and make the - `separator: /-/` to maximize the information included in the - output. - - Fixes #23635. - - *Rob Biedenharn* - -* Ensure concurrent invocations of the connection reaper cannot allocate the - same connection to two threads. - - Fixes #25585. - - *Matthew Draper* - -* Inspecting an object with an associated array of over 10 elements no longer - truncates the array, preventing `inspect` from looping infinitely in some - cases. +* When calling the dynamic fixture accessor method with no arguments it now returns all fixtures of this type. + Previously this method always returned an empty array. *Kevin McPhillips* -* Removed the unused methods `ActiveRecord::Base.connection_id` and - `ActiveRecord::Base.connection_id=`. - - *Sean Griffin* - -* Ensure hashes can be assigned to attributes created using `composed_of`. - - Fixes #25210. - - *Sean Griffin* - -* Fix logging edge case where if an attribute was of the binary type and - was provided as a Hash. - - *Jon Moss* - -* Handle JSON deserialization correctly if the column default from database - adapter returns `''` instead of `nil`. - - *Johannes Opper* - -* Introduce `ActiveRecord::TransactionSerializationError` for catching - transaction serialization failures or deadlocks. - - *Erol Fornoles* - -* PostgreSQL: Fix `db:structure:load` silent failure on SQL error. - - The command line flag `-v ON_ERROR_STOP=1` should be used - when invoking `psql` to make sure errors are not suppressed. - - Example: - - psql -v ON_ERROR_STOP=1 -q -f awesome-file.sql my-app-db - - Fixes #23818. - - *Ralin Chimev* - -Please check [5-0-stable](https://github.com/rails/rails/blob/5-0-stable/activerecord/CHANGELOG.md) for previous changes. +Please check [5-1-stable](https://github.com/rails/rails/blob/5-1-stable/activerecord/CHANGELOG.md) for previous changes. 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 e077d345d6..2d0d5bd657 100644 --- a/activerecord/Rakefile +++ b/activerecord/Rakefile @@ -1,7 +1,7 @@ require "rake/testtask" -require File.expand_path(File.dirname(__FILE__)) + "/test/config" -require File.expand_path(File.dirname(__FILE__)) + "/test/support/config" +require File.expand_path("test/config", __dir__) +require File.expand_path("test/support/config", __dir__) def run_without_aborting(*tasks) errors = [] @@ -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,7 +66,7 @@ end (Dir["test/cases/**/*_test.rb"].reject { |x| x.include?("/adapters/") } + Dir["test/cases/adapters/#{adapter_short}/**/*_test.rb"]).all? do |file| - sh(Gem.ruby, "-w" ,"-Itest", file) + sh(Gem.ruby, "-w" , "-Itest", file) end || raise("Failures") end end @@ -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" @@ -139,7 +134,7 @@ task drop_postgresql_databases: "db:postgresql:drop" task rebuild_postgresql_databases: "db:postgresql:rebuild" task :lines do - load File.expand_path("..", File.dirname(__FILE__)) + "/tools/line_statistics" + load File.expand_path("../tools/line_statistics", __dir__) files = FileList["lib/active_record/**/*.rb"] CodeTools::LineStatistics.new(files).print_loc end diff --git a/activerecord/activerecord.gemspec b/activerecord/activerecord.gemspec index 2cd8f179dd..450ec0bba9 100644 --- a/activerecord/activerecord.gemspec +++ b/activerecord/activerecord.gemspec @@ -1,4 +1,4 @@ -version = File.read(File.expand_path("../../RAILS_VERSION", __FILE__)).strip +version = File.read(File.expand_path("../RAILS_VERSION", __dir__)).strip Gem::Specification.new do |s| s.platform = Gem::Platform::RUBY @@ -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 3b5dab16cb..29f49c6195 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 @@ -44,7 +44,6 @@ module ActiveRecord autoload :Explain autoload :Inheritance autoload :Integration - autoload :LegacyYamlAdapter autoload :Migration autoload :Migrator, "active_record/migration" autoload :ModelSchema @@ -85,6 +84,8 @@ module ActiveRecord autoload :AttributeMethods autoload :AutosaveAssociation + autoload :LegacyYamlAdapter + autoload :Relation autoload :AssociationRelation autoload :NullRelation @@ -176,5 +177,5 @@ ActiveSupport.on_load(:active_record) do end ActiveSupport.on_load(:i18n) do - I18n.load_path << File.dirname(__FILE__) + "/active_record/locale/en.yml" + I18n.load_path << File.expand_path("active_record/locale/en.yml", __dir__) end diff --git a/activerecord/lib/active_record/aggregations.rb b/activerecord/lib/active_record/aggregations.rb index 5ca8fe576e..10cbd5429c 100644 --- a/activerecord/lib/active_record/aggregations.rb +++ b/activerecord/lib/active_record/aggregations.rb @@ -15,11 +15,11 @@ 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 @@ -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) diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index b5f1f1980a..7c37132d3a 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 @@ -349,23 +379,23 @@ module ActiveRecord # # === 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: + # 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 # - # 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. + # 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 # @@ -1246,7 +1276,7 @@ module ActiveRecord # Scope examples: # has_many :comments, -> { where(author_id: 1) } # has_many :employees, -> { joins(:address) } - # has_many :posts, ->(post) { where("max_post_length > ?", post.length) } + # has_many :posts, ->(blog) { where("max_post_length > ?", blog.max_post_length) } # # === Extensions # @@ -1413,7 +1443,7 @@ module ActiveRecord # Scope examples: # has_one :author, -> { where(comment_id: 1) } # has_one :employer, -> { joins(:company) } - # has_one :dob, ->(dob) { where("Date.new(2000, 01, 01) > ?", dob) } + # has_one :latest_post, ->(blog) { where("created_at > ?", blog.enabled_at) } # # === Options # @@ -1543,7 +1573,7 @@ module ActiveRecord # Scope examples: # belongs_to :firm, -> { where(id: 2) } # belongs_to :user, -> { joins(:friends) } - # belongs_to :level, ->(level) { where("game_level > ?", level.current) } + # belongs_to :level, ->(game) { where("game_level > ?", game.current_level) } # # === Options # @@ -1617,6 +1647,9 @@ module ActiveRecord # +:inverse_of+ to avoid an extra query during validation. # NOTE: <tt>required</tt> is set to <tt>true</tt> by default and is deprecated. If # you don't want to have association presence validated, use <tt>optional: true</tt>. + # [:default] + # Provide a callable (i.e. proc or lambda) to specify that the association should + # be initialized with a particular record before validation. # # Option examples: # belongs_to :firm, foreign_key: "client_of" @@ -1630,6 +1663,7 @@ module ActiveRecord # belongs_to :comment, touch: true # belongs_to :company, touch: :employees_last_updated_at # belongs_to :user, optional: true + # belongs_to :account, default: -> { company.account } def belongs_to(name, scope = nil, options = {}) reflection = Builder::BelongsTo.build(self, name, scope, options) Reflection.add_reflection self, name, reflection @@ -1735,9 +1769,8 @@ module ActiveRecord # # Scope examples: # has_and_belongs_to_many :projects, -> { includes(:milestones, :manager) } - # has_and_belongs_to_many :categories, ->(category) { - # where("default_category = ?", category.name) - # } + # has_and_belongs_to_many :categories, ->(post) { + # where("default_category = ?", post.default_category) # # === Extensions # @@ -1797,7 +1830,7 @@ module ActiveRecord builder = Builder::HasAndBelongsToMany.new name, self, options - join_model = builder.through_model + join_model = ActiveSupport::Deprecation.silence { builder.through_model } const_set join_model.name, join_model private_constant join_model.name @@ -1826,8 +1859,8 @@ module ActiveRecord hm_options[k] = options[k] if options.key? k end - has_many name, scope, hm_options, &extension - self._reflections[name.to_s].parent_reflection = habtm_reflection + ActiveSupport::Deprecation.silence { has_many name, scope, hm_options, &extension } + _reflections[name.to_s].parent_reflection = habtm_reflection end end end diff --git a/activerecord/lib/active_record/associations/alias_tracker.rb b/activerecord/lib/active_record/associations/alias_tracker.rb index 3963008a76..4a5c821607 100644 --- a/activerecord/lib/active_record/associations/alias_tracker.rb +++ b/activerecord/lib/active_record/associations/alias_tracker.rb @@ -4,8 +4,6 @@ module ActiveRecord module Associations # Keeps track of table aliases for ActiveRecord::Associations::JoinDependency class AliasTracker # :nodoc: - attr_reader :aliases - def self.create(connection, initial_table, type_caster) aliases = Hash.new(0) aliases[initial_table] = 1 @@ -80,6 +78,11 @@ module ActiveRecord end end + # TODO Change this to private once we've dropped Ruby 2.2 support. + # Workaround for Ruby 2.2 "private attribute?" warning. + protected + attr_reader :aliases + private def truncate(name) diff --git a/activerecord/lib/active_record/associations/association.rb b/activerecord/lib/active_record/associations/association.rb index f506614591..ee2e79889f 100644 --- a/activerecord/lib/active_record/associations/association.rb +++ b/activerecord/lib/active_record/associations/association.rb @@ -83,7 +83,7 @@ module ActiveRecord end def scope - target_scope.merge(association_scope) + target_scope.merge!(association_scope) end # The scope for this association. @@ -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 @@ -124,6 +133,16 @@ module ActiveRecord AssociationRelation.create(klass, klass.arel_table, klass.predicate_builder, self).merge!(klass.all) end + def extensions + extensions = klass.default_extensions | reflection.extensions + + if scope = reflection.scope + extensions |= klass.unscoped.instance_exec(owner, &scope).extensions + end + + extensions + end + # Loads the \target if needed and returns it. # # This method is abstract in the sense that it relies on +find_target+, @@ -143,14 +162,6 @@ module ActiveRecord reset end - def interpolate(sql, record = nil) - if sql.respond_to?(:to_proc) - owner.instance_exec(record, &sql) - else - sql - end - end - # We can't dump @reflection since it contains the scope proc def marshal_dump ivars = (instance_variables - [:@reflection]).map { |name| [name, instance_variable_get(name)] } @@ -166,7 +177,7 @@ 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) @@ -254,7 +265,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..1593b94f0c 100644 --- a/activerecord/lib/active_record/associations/association_scope.rb +++ b/activerecord/lib/active_record/associations/association_scope.rb @@ -24,8 +24,8 @@ module ActiveRecord alias_tracker = AliasTracker.create connection, association.klass.table_name, klass.type_caster chain_head, chain_tail = get_chain(reflection, association, alias_tracker) - scope.extending! Array(reflection.options[:extend]) - add_constraints(scope, owner, klass, reflection, chain_head, chain_tail) + scope.extending! reflection.extensions + add_constraints(scope, owner, reflection, chain_head, chain_tail) end def join_type @@ -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 @@ -58,8 +60,8 @@ module ActiveRecord table.create_join(table, table.create_on(constraint), join_type) end - def last_chain_scope(scope, table, reflection, owner, association_klass) - join_keys = reflection.join_keys(association_klass) + def last_chain_scope(scope, table, reflection, owner) + join_keys = reflection.join_keys key = join_keys.key foreign_key = join_keys.foreign_key @@ -78,8 +80,8 @@ module ActiveRecord value_transformation.call(value) end - def next_chain_scope(scope, table, reflection, association_klass, foreign_table, next_reflection) - join_keys = reflection.join_keys(association_klass) + def next_chain_scope(scope, table, reflection, foreign_table, next_reflection) + join_keys = reflection.join_keys key = join_keys.key foreign_key = join_keys.foreign_key @@ -118,25 +120,25 @@ module ActiveRecord [runtime_reflection, previous_reflection] end - def add_constraints(scope, owner, association_klass, refl, chain_head, chain_tail) + def add_constraints(scope, owner, refl, chain_head, chain_tail) owner_reflection = chain_tail table = owner_reflection.alias_name - scope = last_chain_scope(scope, table, owner_reflection, owner, association_klass) + scope = last_chain_scope(scope, table, owner_reflection, owner) reflection = chain_head while reflection table = reflection.alias_name + next_reflection = reflection.next unless reflection == chain_tail - next_reflection = reflection.next foreign_table = next_reflection.alias_name - scope = next_chain_scope(scope, table, reflection, association_klass, foreign_table, next_reflection) + scope = next_chain_scope(scope, table, reflection, foreign_table, next_reflection) end # Exclude the scope of the association itself, because that # was already merged in the #scope method. reflection.constraints.each do |scope_chain_item| - item = eval_scope(reflection.klass, scope_chain_item, owner) + item = eval_scope(reflection.klass, table, scope_chain_item, owner) if scope_chain_item == refl.scope scope.merge! item.except(:where, :includes) @@ -151,14 +153,15 @@ module ActiveRecord scope.order_values |= item.order_values end - reflection = reflection.next + reflection = next_reflection end scope end - def eval_scope(klass, scope, owner) - klass.unscoped.instance_exec(owner, &scope) + def eval_scope(klass, table, scope, owner) + predicate_builder = PredicateBuilder.new(TableMetadata.new(klass, table)) + ActiveRecord::Relation.create(klass, table, predicate_builder).instance_exec(owner, &scope) end end end diff --git a/activerecord/lib/active_record/associations/belongs_to_association.rb b/activerecord/lib/active_record/associations/belongs_to_association.rb index 64b2311911..0e61dbfb00 100644 --- a/activerecord/lib/active_record/associations/belongs_to_association.rb +++ b/activerecord/lib/active_record/associations/belongs_to_association.rb @@ -21,6 +21,10 @@ module ActiveRecord self.target = record end + def default(&block) + writer(owner.instance_exec(&block)) if reader.nil? + end + def reset super @updated = false diff --git a/activerecord/lib/active_record/associations/builder/belongs_to.rb b/activerecord/lib/active_record/associations/builder/belongs_to.rb index 3121e70a04..2b9dd8aae8 100644 --- a/activerecord/lib/active_record/associations/builder/belongs_to.rb +++ b/activerecord/lib/active_record/associations/builder/belongs_to.rb @@ -5,7 +5,7 @@ module ActiveRecord::Associations::Builder # :nodoc: end def self.valid_options(options) - super + [:polymorphic, :touch, :counter_cache, :optional] + super + [:polymorphic, :touch, :counter_cache, :optional, :default] end def self.valid_dependent_options @@ -16,6 +16,7 @@ module ActiveRecord::Associations::Builder # :nodoc: super add_counter_cache_callbacks(model, reflection) if reflection.options[:counter_cache] add_touch_callbacks(model, reflection) if reflection.options[:touch] + add_default_callbacks(model, reflection) if reflection.options[:default] end def self.define_accessors(mixin, reflection) @@ -35,17 +36,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 +71,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 +110,19 @@ 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.(:saved_changes), if: :saved_changes? + model.after_touch callback.(:changes_to_save) + model.after_destroy callback.(:changes_to_save) + end - model.after_save callback, if: :changed? - model.after_touch callback - model.after_destroy callback + def self.add_default_callbacks(model, reflection) + model.before_validation lambda { |o| + o.association(reflection.name).default(&reflection.options[:default]) + } 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 e5b3af8252..edc53e2517 100644 --- a/activerecord/lib/active_record/associations/collection_association.rb +++ b/activerecord/lib/active_record/associations/collection_association.rb @@ -25,26 +25,13 @@ 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 - if null_scope? - # Cache the proxy separately before the owner has an id - # or else a post-save proxy will still lack the id - @null_proxy ||= CollectionProxy.create(klass, self) - else - @proxy ||= CollectionProxy.create(klass, self) - end + @proxy ||= CollectionProxy.create(klass, self) + @proxy.reset_scope end # Implements the writer method, e.g. foo.items= for Foo.has_many :items @@ -55,9 +42,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 +53,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 @@ -192,11 +181,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 @@ -222,11 +208,7 @@ 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 + target.size elsif !association_scope.group_values.empty? load_target.size elsif !association_scope.distinct_value && target.is_a?(Array) @@ -253,13 +235,6 @@ module ActiveRecord end end - def distinct - seen = {} - load_target.find_all do |record| - seen[record.id] = true unless seen.key?(record.id) - end - end - # Replace this collection with +other_array+. This will perform a diff # and delete/add only records that have changed. def replace(other_array) @@ -306,29 +281,9 @@ module ActiveRecord replace_on_target(record, index, skip_callbacks, &block) end - def replace_on_target(record, index, skip_callbacks) - callback(:before_add, record) unless skip_callbacks - - was_loaded = loaded? - yield(record) if block_given? - - unless !was_loaded && loaded? - if index - @target[index] = record - else - @target << record - end - end - - callback(:after_add, record) unless skip_callbacks - set_inverse_instance(record) - - record - end - - def scope(opts = {}) - scope = super() - scope.none! if opts.fetch(:nullify, true) && null_scope? + def scope + scope = super + scope.none! if null_scope? scope end @@ -378,7 +333,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 @@ -402,15 +357,19 @@ module ActiveRecord transaction do add_to_target(build_record(attributes)) do |record| yield(record) if block_given? - insert_record(record, true, raise) + insert_record(record, true, raise) { @_was_loaded = loaded? } end end end end # Do the relevant stuff to insert the given record into the association collection. - def insert_record(record, validate = true, raise = false) - raise NotImplementedError + def insert_record(record, validate = true, raise = false, &block) + if raise + record.save!(validate: validate, &block) + else + record.save(validate: validate, &block) + end end def create_scope @@ -438,8 +397,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 @@ -464,19 +424,41 @@ module ActiveRecord end end - def concat_records(records, should_raise = false) + def concat_records(records, raise = false) result = true records.each do |record| raise_on_type_mismatch!(record) - add_to_target(record) do |rec| - result &&= insert_record(rec, true, should_raise) unless owner.new_record? + add_to_target(record) do + result &&= insert_record(record, true, raise) { @_was_loaded = loaded? } unless owner.new_record? end end result && records end + def replace_on_target(record, index, skip_callbacks) + callback(:before_add, record) unless skip_callbacks + + set_inverse_instance(record) + + @_was_loaded = true + + yield(record) if block_given? + + if index + target[index] = record + elsif @_was_loaded || !loaded? + target << record + end + + callback(:after_add, record) unless skip_callbacks + + record + ensure + @_was_loaded = nil + end + def callback(method, record) callbacks_for(method).each do |callback| callback.call(method, owner, record) diff --git a/activerecord/lib/active_record/associations/collection_proxy.rb b/activerecord/lib/active_record/associations/collection_proxy.rb index dda240585e..8cdb508c43 100644 --- a/activerecord/lib/active_record/associations/collection_proxy.rb +++ b/activerecord/lib/active_record/associations/collection_proxy.rb @@ -28,12 +28,12 @@ module ActiveRecord # is computed directly through SQL and does not trigger by itself the # instantiation of the actual post records. class CollectionProxy < Relation - delegate :exists?, :update_all, :arel, to: :scope - def initialize(klass, association) #:nodoc: @association = association super klass, klass.arel_table, klass.predicate_builder - merge! association.scope(nullify: false) + + extensions = association.extensions + extend(*extensions) if extensions.any? end def target @@ -81,7 +81,7 @@ module ActiveRecord # # #<Pet id: nil, name: "Choo-Choo"> # # ] # - # person.pets.select(:id, :name ) + # person.pets.select(:id, :name) # # => [ # # #<Pet id: 1, name: "Fancy-Fancy">, # # #<Pet id: 2, name: "Spook">, @@ -106,12 +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"> - # # ] # Finds an object in the collection responding to the +id+. Uses the same # rules as ActiveRecord::Base.find. Returns ActiveRecord::RecordNotFound @@ -724,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 @@ -738,11 +738,14 @@ module ActiveRecord # # person.pets.select(:name).distinct # # => [#<Pet name: "Fancy-Fancy">] - def distinct - @association.distinct - end - alias uniq distinct + # + # person.pets.select(:name).distinct.distinct(false) + # # => [ + # # #<Pet name: "Fancy-Fancy">, + # # #<Pet name: "Fancy-Fancy"> + # # ] + #-- def calculate(operation, column_name) null_scope? ? scope.calculate(operation, column_name) : super end @@ -949,19 +952,10 @@ module ActiveRecord @association end - # We don't want this object to be put on the scoping stack, because - # that could create an infinite loop where we call an @association - # method, which gets the current scope, which is this object, which - # delegates to @association, and so on. - def scoping - @association.scope.scoping { yield } - end - # Returns a <tt>Relation</tt> object for the records in this association def scope - @association.scope + @scope ||= @association.scope end - alias spawn scope # Equivalent to <tt>Array#==</tt>. Returns +true+ if the two arrays # contain the same number of elements and if each element is equal @@ -1094,7 +1088,7 @@ module ActiveRecord # # => [#<Pet id: 1, name: "Snoop", group: "dogs", person_id: 1>] def reload proxy_association.reload - self + reset_scope end # Unloads the association. Returns +self+. @@ -1116,10 +1110,24 @@ module ActiveRecord def reset proxy_association.reset proxy_association.reset_scope + reset_scope + end + + def reset_scope # :nodoc: + @scope = nil self end - protected + delegate_methods = [ + QueryMethods, + SpawnMethods, + ].flat_map { |klass| + klass.public_instance_methods(false) + } - self.public_instance_methods(false) - [:select] + [:scoping] + + delegate(*delegate_methods, to: :scope) + + private def find_nth_with_limit(index, limit) load_target if find_from_target? @@ -1131,8 +1139,6 @@ module ActiveRecord super end - private - def null_scope? @association.null_scope? end diff --git a/activerecord/lib/active_record/associations/has_many_association.rb b/activerecord/lib/active_record/associations/has_many_association.rb index d1d0cc4c49..10ca0e47ff 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,13 +31,7 @@ module ActiveRecord def insert_record(record, validate = true, raise = false) set_owner_attributes(record) - set_inverse_instance(record) - - if raise - record.save!(validate: validate) - else - record.save(validate: validate) - end + super end def empty? @@ -72,7 +59,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 @@ -108,7 +95,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..53ffb3b68d 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,8 @@ 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? + return unless super end save_through_record(record) @@ -86,7 +84,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..8458253ff8 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,12 +79,13 @@ 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) - raise RecordNotSaved, "Failed to remove the existing associated #{reflection.name}. " + + raise RecordNotSaved, "Failed to remove the existing associated #{reflection.name}. " \ "The record failed to save after its foreign key was set to nil." end end diff --git a/activerecord/lib/active_record/associations/has_one_through_association.rb b/activerecord/lib/active_record/associations/has_one_through_association.rb index 604904abcc..1183bdf6f4 100644 --- a/activerecord/lib/active_record/associations/has_one_through_association.rb +++ b/activerecord/lib/active_record/associations/has_one_through_association.rb @@ -22,6 +22,10 @@ module ActiveRecord elsif record attributes = construct_join_attributes(record) + if through_record && through_record.destroyed? + through_record = through_proxy.tap(&:reload).target + end + if through_record through_record.update(attributes) elsif owner.new_record? diff --git a/activerecord/lib/active_record/associations/join_dependency.rb b/activerecord/lib/active_record/associations/join_dependency.rb index c26c469c1e..643226267c 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] } @@ -32,7 +32,7 @@ module ActiveRecord @alias_cache[node][column] end - class Table < Struct.new(:node, :columns) # :nodoc: + Table = Struct.new(:node, :columns) do # :nodoc: def table Arel::Nodes::TableAlias.new node.table, node.aliased_table_name end @@ -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 @@ -106,12 +106,7 @@ module ActiveRecord def join_constraints(outer_joins, join_type) joins = join_root.children.flat_map { |child| - - if join_type == Arel::Nodes::OuterJoin - make_left_outer_joins join_root, child - else - make_inner_joins join_root, child - end + make_join_constraints(join_root, child, join_type) } joins.concat outer_joins.flat_map { |oj| @@ -126,8 +121,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 +138,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 @@ -171,31 +166,19 @@ module ActiveRecord chain = child.reflection.chain foreign_table = parent.table foreign_klass = parent.base_klass - child.join_constraints(foreign_table, foreign_klass, child, join_type, tables, child.reflection.scope_chain, chain) + child.join_constraints(foreign_table, foreign_klass, join_type, tables, chain) end def make_outer_joins(parent, child) - tables = table_aliases_for(parent, child) - join_type = Arel::Nodes::OuterJoin - info = make_constraints parent, child, tables, join_type - - [info] + child.children.flat_map { |c| make_outer_joins(child, c) } - end - - def make_left_outer_joins(parent, child) - tables = child.tables join_type = Arel::Nodes::OuterJoin - info = make_constraints parent, child, tables, join_type - - [info] + child.children.flat_map { |c| make_left_outer_joins(child, c) } + make_join_constraints(parent, child, join_type, true) end - def make_inner_joins(parent, child) - tables = child.tables - join_type = Arel::Nodes::InnerJoin - info = make_constraints parent, child, tables, join_type + def make_join_constraints(parent, child, join_type, aliasing = false) + tables = aliasing ? table_aliases_for(parent, child) : child.tables + info = make_constraints(parent, child, tables, join_type) - [info] + child.children.flat_map { |c| make_inner_joins(child, c) } + [info] + child.children.flat_map { |c| make_join_constraints(child, c, join_type, aliasing) } end def table_aliases_for(parent, node) @@ -223,8 +206,8 @@ module ActiveRecord [left.children.find { |node2| node1.match? node2 }, node1] }.partition(&:first) - ojs = missing.flat_map { |_,n| make_outer_joins left, n } - intersection.flat_map { |l,r| walk l, r }.concat ojs + 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) diff --git a/activerecord/lib/active_record/associations/join_dependency/join_association.rb b/activerecord/lib/active_record/associations/join_dependency/join_association.rb index a5705951f3..97cfec0302 100644 --- a/activerecord/lib/active_record/associations/join_dependency/join_association.rb +++ b/activerecord/lib/active_record/associations/join_dependency/join_association.rb @@ -23,50 +23,27 @@ module ActiveRecord JoinInformation = Struct.new :joins, :binds - def join_constraints(foreign_table, foreign_klass, node, join_type, tables, scope_chain, chain) + def join_constraints(foreign_table, foreign_klass, join_type, tables, chain) joins = [] binds = [] tables = tables.reverse - scope_chain_index = 0 - scope_chain = scope_chain.reverse - # The chain starts with the target table, but we want to end with it here (makes # more sense in this context), so we reverse chain.reverse_each do |reflection| table = tables.shift klass = reflection.klass - join_keys = reflection.join_keys(klass) + join_keys = reflection.join_keys key = join_keys.key foreign_key = join_keys.foreign_key constraint = build_constraint(klass, table, key, foreign_table, foreign_key) predicate_builder = PredicateBuilder.new(TableMetadata.new(klass, table)) - scope_chain_items = scope_chain[scope_chain_index].map do |item| - if item.is_a?(Relation) - item - else - ActiveRecord::Relation.create(klass, table, predicate_builder) - .instance_exec(node, &item) - end - end - scope_chain_index += 1 - - klass_scope = - if klass.current_scope - klass.current_scope.clone.tap { |scope| - scope.joins_values = [] - } - else - relation = ActiveRecord::Relation.create( - klass, - table, - predicate_builder, - ) - klass.send(:build_default_scope, relation) - end + scope_chain_items = reflection.join_scopes(table, predicate_builder) + klass_scope = reflection.klass_join_scope(table, predicate_builder) + scope_chain_items.concat [klass_scope].compact rel = scope_chain_items.inject(scope_chain_items.shift) do |left, right| 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 61cec5403a..80c9fde5d1 100644 --- a/activerecord/lib/active_record/associations/join_dependency/join_part.rb +++ b/activerecord/lib/active_record/associations/join_dependency/join_part.rb @@ -22,10 +22,6 @@ module ActiveRecord @children = children end - def name - reflection.name - end - def match?(other) self.class == other.class end diff --git a/activerecord/lib/active_record/associations/preloader/association.rb b/activerecord/lib/active_record/associations/preloader/association.rb index c79efca920..63ef3f2d8c 100644 --- a/activerecord/lib/active_record/associations/preloader/association.rb +++ b/activerecord/lib/active_record/associations/preloader/association.rb @@ -51,11 +51,10 @@ module ActiveRecord raise NotImplementedError end - def options - reflection.options - end - private + def options + reflection.options + end def associated_records_by_owner(preloader) records = load_records do |record| @@ -113,7 +112,7 @@ module ActiveRecord 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).load(&block) end diff --git a/activerecord/lib/active_record/associations/preloader/belongs_to.rb b/activerecord/lib/active_record/associations/preloader/belongs_to.rb index 38e231826c..c20145770f 100644 --- a/activerecord/lib/active_record/associations/preloader/belongs_to.rb +++ b/activerecord/lib/active_record/associations/preloader/belongs_to.rb @@ -3,7 +3,7 @@ module ActiveRecord class Preloader class BelongsTo < SingularAssociation #:nodoc: def association_key_name - reflection.options[:primary_key] || klass && klass.primary_key + options[:primary_key] || klass && klass.primary_key end def owner_key_name diff --git a/activerecord/lib/active_record/associations/preloader/through_association.rb b/activerecord/lib/active_record/associations/preloader/through_association.rb index be9dfe7686..8b954138cd 100644 --- a/activerecord/lib/active_record/associations/preloader/through_association.rb +++ b/activerecord/lib/active_record/associations/preloader/through_association.rb @@ -16,15 +16,13 @@ module ActiveRecord through_scope) through_records = owners.map do |owner| - association = owner.association through_reflection.name - - center = target_records_from_association(association) + center = owner.association(through_reflection.name).target [owner, Array(center)] end reset_association owners, through_reflection.name - middle_records = through_records.flat_map { |(_,rec)| rec } + middle_records = through_records.flat_map(&:last) preloaders = preloader.preload(middle_records, source_reflection.name, @@ -32,20 +30,18 @@ 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| rhs_records = middles.flat_map { |r| - association = r.association source_reflection.name - - target_records_from_association(association) + r.association(source_reflection.name).target }.compact # Respect the order on `reflection_scope` if it exists, else use the natural order. @@ -69,7 +65,7 @@ module ActiveRecord def reset_association(owners, association_name) should_reset = (through_scope != through_reflection.klass.unscoped) || - (reflection.options[:source_type] && through_reflection.collection?) + (options[:source_type] && through_reflection.collection?) # Don't cache the association - we would only be caching a subset if should_reset @@ -98,10 +94,6 @@ module ActiveRecord scope end - - def target_records_from_association(association) - association.loaded? ? association.target : association.reader - end end end end diff --git a/activerecord/lib/active_record/associations/singular_association.rb b/activerecord/lib/active_record/associations/singular_association.rb index e386cc0e4c..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 @@ -30,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 @@ -51,6 +50,8 @@ module ActiveRecord sc.execute(binds, klass, conn) do |record| set_inverse_instance record 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 0b08c2a39b..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 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 9843e0ca66..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 = {} diff --git a/activerecord/lib/active_record/attribute_decorators.rb b/activerecord/lib/active_record/attribute_decorators.rb index 340dfe11cf..5bc8527745 100644 --- a/activerecord/lib/active_record/attribute_decorators.rb +++ b/activerecord/lib/active_record/attribute_decorators.rb @@ -3,17 +3,38 @@ module ActiveRecord extend ActiveSupport::Concern included do - class_attribute :attribute_type_decorations, instance_accessor: false # :internal: - self.attribute_type_decorations = TypeDecorator.new + class_attribute :attribute_type_decorations, instance_accessor: false, default: TypeDecorator.new # :internal: 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 1ed1deec55..83c61fad19 100644 --- a/activerecord/lib/active_record/attribute_methods.rb +++ b/activerecord/lib/active_record/attribute_methods.rb @@ -62,7 +62,6 @@ module ActiveRecord super(attribute_names) @attribute_methods_generated = true end - true end def undefine_attribute_methods # :nodoc: @@ -394,28 +393,21 @@ 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. def arel_attributes_with_values(attribute_names) diff --git a/activerecord/lib/active_record/attribute_methods/dirty.rb b/activerecord/lib/active_record/attribute_methods/dirty.rb index c9638bf70b..76987fb8f4 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" @@ -13,8 +14,19 @@ module ActiveRecord raise "You cannot include Dirty after Timestamp" end - class_attribute :partial_writes, instance_writer: false - self.partial_writes = true + class_attribute :partial_writes, instance_writer: false, default: 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,9 +47,9 @@ module ActiveRecord # <tt>reload</tt> the record and clears changed attributes. def reload(*) super.tap do - @mutation_tracker = nil @previous_mutation_tracker = nil - @changed_attributes = HashWithIndifferentAccess.new + clear_mutation_trackers + @changed_attributes = ActiveSupport::HashWithIndifferentAccess.new end end @@ -46,19 +58,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 + @changed_attributes = ActiveSupport::HashWithIndifferentAccess.new + clear_mutation_trackers end def clear_changes_information @previous_mutation_tracker = nil - @changed_attributes = HashWithIndifferentAccess.new - store_original_attributes + @changed_attributes = ActiveSupport::HashWithIndifferentAccess.new + forget_attribute_assignments + clear_mutation_trackers end def raw_write_attribute(attr_name, *) @@ -80,17 +99,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 +127,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 +244,47 @@ 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 + if self.class.has_attribute?(attr_name) + mutations_from_database.force_change(attr_name) + else + ActiveSupport::Deprecation.warn(<<-EOW.squish) + #{attr_name} is not an attribute known to Active Record. + This behavior is deprecated and will be removed in the next + version of Rails. If you'd like #{attr_name} to be managed + by Active Record, add `attribute :#{attr_name}` to your class. + EOW + mutations_from_database.deprecated_force_change(attr_name) + end end def _update_record(*) @@ -124,18 +296,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 6243398a52..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) diff --git a/activerecord/lib/active_record/attribute_methods/read.rb b/activerecord/lib/active_record/attribute_methods/read.rb index 30f7750884..fdc4bf6621 100644 --- a/activerecord/lib/active_record/attribute_methods/read.rb +++ b/activerecord/lib/active_record/attribute_methods/read.rb @@ -4,7 +4,7 @@ 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. @@ -48,8 +48,13 @@ 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 = self.class.primary_key if name == "id".freeze + 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 && self.class.primary_key _read_attribute(name, &block) end diff --git a/activerecord/lib/active_record/attribute_methods/serialization.rb b/activerecord/lib/active_record/attribute_methods/serialization.rb index c70178cd2c..4d9aff76cc 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 @@ -54,7 +54,7 @@ module ActiveRecord elsif [:load, :dump].all? { |x| class_name_or_coder.respond_to?(x) } class_name_or_coder else - Coders::YAMLColumn.new(class_name_or_coder) + Coders::YAMLColumn.new(attr_name, class_name_or_coder) end decorate_attribute_type(attr_name, :serialize) do |type| 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 bea1514cdf..4a8d231503 100644 --- a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb +++ b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb @@ -1,5 +1,3 @@ -require "active_support/core_ext/string/strip" - module ActiveRecord module AttributeMethods module TimeZoneConversion @@ -39,7 +37,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) @@ -59,17 +57,15 @@ module ActiveRecord mattr_accessor :time_zone_aware_attributes, instance_writer: false self.time_zone_aware_attributes = false - class_attribute :skip_time_zone_conversion_for_attributes, instance_writer: false - 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] + class_attribute :skip_time_zone_conversion_for_attributes, instance_writer: false, default: [] + class_attribute :time_zone_aware_types, instance_writer: false, default: [ :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 +76,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 f65c297e01..fe0e01db28 100644 --- a/activerecord/lib/active_record/attribute_methods/write.rb +++ b/activerecord/lib/active_record/attribute_methods/write.rb @@ -8,7 +8,7 @@ module ActiveRecord end module ClassMethods - protected + private def define_method_attribute=(name) safe_name = name.unpack("h*".freeze).first @@ -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: diff --git a/activerecord/lib/active_record/attribute_mutation_tracker.rb b/activerecord/lib/active_record/attribute_mutation_tracker.rb index c257aef52f..4de993e169 100644 --- a/activerecord/lib/active_record/attribute_mutation_tracker.rb +++ b/activerecord/lib/active_record/attribute_mutation_tracker.rb @@ -1,7 +1,11 @@ module ActiveRecord class AttributeMutationTracker # :nodoc: + OPTION_NOT_GIVEN = Object.new + def initialize(attributes) @attributes = attributes + @forced_changes = Set.new + @deprecated_forced_changes = Set.new end def changed_values @@ -14,15 +18,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) } || deprecated_forced_changes.any? + 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 +50,26 @@ 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 + def deprecated_force_change(attr_name) + deprecated_forced_changes << attr_name.to_s + end + + # TODO Change this to private once we've dropped Ruby 2.2 support. + # Workaround for Ruby 2.2 "private attribute?" warning. protected - attr_reader :attributes + attr_reader :attributes, :forced_changes, :deprecated_forced_changes private @@ -48,14 +81,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 +106,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..01f9d815d5 100644 --- a/activerecord/lib/active_record/attribute_set.rb +++ b/activerecord/lib/active_record/attribute_set.rb @@ -64,7 +64,7 @@ module ActiveRecord end def deep_dup - dup.tap do |copy| + self.class.allocate.tap do |copy| copy.instance_variable_set(:@attributes, attributes.deep_dup) end end @@ -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..475b9beec4 100644 --- a/activerecord/lib/active_record/attributes.rb +++ b/activerecord/lib/active_record/attributes.rb @@ -6,8 +6,7 @@ module ActiveRecord extend ActiveSupport::Concern included do - class_attribute :attributes_to_define_after_schema_loads, instance_accessor: false # :internal: - self.attributes_to_define_after_schema_loads = {} + class_attribute :attributes_to_define_after_schema_loads, instance_accessor: false, default: {} # :internal: end module ClassMethods @@ -116,7 +115,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 +142,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 +189,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 d3e0dee731..829a4f6e86 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 @@ -181,6 +181,7 @@ module ActiveRecord if reflection.collection? before_save :before_save_collection_association + after_save :after_save_collection_association define_non_cyclic_method(save_method) { save_collection_association(reflection) } # Doesn't use after_save as that would save associations added in after_create/after_update twice @@ -215,13 +216,7 @@ module ActiveRecord method = :validate_single_association end - define_non_cyclic_method(validation_method) do - send(method, reflection) - # TODO: remove the following line as soon as the return value of - # callbacks is ignored, that is, returning `false` does not - # display a deprecation warning or halts the callback chain. - true - end + define_non_cyclic_method(validation_method) { send(method, reflection) } validate validation_method after_validation :_ensure_no_duplicate_errors end @@ -267,7 +262,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,12 +320,12 @@ 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) + context = validation_context unless [:create, :update].include?(validation_context) - unless valid = record.valid?(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) @@ -368,7 +363,10 @@ module ActiveRecord # association whether or not the parent was a new record before saving. def before_save_collection_association @new_record_before_save = new_record? - true + end + + def after_save_collection_association + @new_record_before_save = false end # Saves any new associated records, or all loaded autosave associations if @@ -383,6 +381,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 records = associated_records_to_validate_or_save(association, @new_record_before_save, autosave) if autosave records_to_destroy = records.select(&:marked_for_destruction?) @@ -408,9 +409,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 @@ -451,7 +449,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. diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index 1e7e939097..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 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..9c52a31b95 100644 --- a/activerecord/lib/active_record/coders/yaml_column.rb +++ b/activerecord/lib/active_record/coders/yaml_column.rb @@ -1,12 +1,12 @@ require "yaml" -require "active_support/core_ext/regexp" module ActiveRecord module Coders # :nodoc: class YAMLColumn # :nodoc: attr_accessor :object_class - def initialize(object_class = Object) + def initialize(attr_name, object_class = Object) + @attr_name = attr_name @object_class = object_class check_arity_of_constructor end @@ -14,7 +14,7 @@ module ActiveRecord def dump(obj) return if obj.nil? - assert_valid_value(obj) + assert_valid_value(obj, action: "dump") YAML.dump obj end @@ -23,27 +23,25 @@ module ActiveRecord return yaml unless yaml.is_a?(String) && /^---/.match?(yaml) obj = YAML.load(yaml) - assert_valid_value(obj) + assert_valid_value(obj, action: "load") obj ||= object_class.new if object_class != Object obj end - def assert_valid_value(obj) + def assert_valid_value(obj, action:) unless obj.nil? || obj.is_a?(object_class) raise SerializationTypeMismatch, - "Attribute was supposed to be a #{object_class}, but was a #{obj.class}. -- #{obj.inspect}" + "can't #{action} `#{@attr_name}`: was supposed to be a #{object_class}, but was a #{obj.class}. -- #{obj.inspect}" end end private def check_arity_of_constructor - begin - load(nil) - rescue ArgumentError - raise ArgumentError, "Cannot serialize #{object_class}. Classes passed to `serialize` must have a 0 argument constructor." - end + load(nil) + rescue ArgumentError + raise ArgumentError, "Cannot serialize #{object_class}. Classes passed to `serialize` must have a 0 argument constructor." end end end diff --git a/activerecord/lib/active_record/collection_cache_key.rb b/activerecord/lib/active_record/collection_cache_key.rb index 43784b70e3..8b937b6703 100644 --- a/activerecord/lib/active_record/collection_cache_key.rb +++ b/activerecord/lib/active_record/collection_cache_key.rb @@ -7,17 +7,27 @@ module ActiveRecord if collection.loaded? size = collection.size if size > 0 - timestamp = collection.max_by(×tamp_column).public_send(timestamp_column) + timestamp = collection.max_by(×tamp_column)._read_attribute(timestamp_column) end else column_type = type_for_attribute(timestamp_column.to_s) column = "#{connection.quote_table_name(collection.table_name)}.#{connection.quote_column_name(timestamp_column)}" + select_values = "COUNT(*) AS #{connection.quote_column_name("size")}, MAX(%s) AS timestamp" - query = collection - .unscope(:select) - .select("COUNT(*) AS #{connection.quote_column_name("size")}", "MAX(#{column}) AS timestamp") - .unscope(:order) - result = connection.select_one(query) + if collection.limit_value || collection.offset_value + query = collection.spawn + query.select_values = [column] + subquery_alias = "subquery_for_cache_key" + subquery_column = "#{subquery_alias}.#{timestamp_column}" + subquery = query.arel.as(subquery_alias) + arel = Arel::SelectManager.new(subquery).project(select_values % subquery_column) + else + query = collection.unscope(:order) + query.select_values = [select_values % column] + arel = query.arel + end + + result = connection.select_one(arel, nil, query.bound_attributes) if result.blank? size = 0 diff --git a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb index 2d62fd8d50..61bf5477aa 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 @@ -171,14 +171,14 @@ module ActiveRecord @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. + # threads currently waiting. Otherwise, return +nil+. def no_wait_poll remove if can_remove_no_wait? end @@ -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,10 +350,19 @@ 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 + + @lock_thread = false + end + + def lock_thread=(lock_thread) + if lock_thread + @lock_thread = Thread.current + else + @lock_thread = nil + end end # Retrieve the connection associated with the current thread, or call @@ -361,7 +371,7 @@ module ActiveRecord # #connection can be called any number of times; the connection is # held in a cache keyed by a thread. def connection - @thread_cached_conns[connection_cache_key(Thread.current)] ||= checkout + @thread_cached_conns[connection_cache_key(@lock_thread || Thread.current)] ||= checkout end # Returns true if there is an open connection being used for the current thread. @@ -445,8 +455,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 +465,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 @@ -513,14 +506,16 @@ module ActiveRecord # +conn+: an AbstractAdapter object, which was obtained by earlier by # calling #checkout on this pool. def checkin(conn) - synchronize do - remove_connection_from_thread_cache conn + conn.lock.synchronize do + synchronize do + remove_connection_from_thread_cache conn - conn._run_checkin_callbacks do - conn.expire - end + conn._run_checkin_callbacks do + conn.expire + end - @available.add conn + @available.add conn + end end end @@ -581,6 +576,24 @@ 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 @@ -681,13 +694,32 @@ 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 @@ -739,7 +771,7 @@ module ActiveRecord # 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 @@ -795,7 +827,7 @@ module ActiveRecord # end # # class Book < ActiveRecord::Base - # establish_connection "library_db" + # establish_connection :library_db # end # # class ScaryBook < Book @@ -827,13 +859,13 @@ module ActiveRecord # All Active Record models use this handler to determine the connection pool that they # should use. # - # The ConnectionHandler class is not coupled with the Active models, as it has no knowlodge + # The ConnectionHandler class is not coupled with the Active models, as it has no knowledge # about the model. The model needs to pass a specification name to the handler, - # in order to lookup the correct connection pool. + # in order to look up the correct connection pool. 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 @@ -947,7 +979,7 @@ module ActiveRecord end def pool_from_any_process_for(spec_name) - owner_to_pool = @owner_to_pool.values.find { |v| v[spec_name] } + owner_to_pool = @owner_to_pool.values.reverse.find { |v| v[spec_name] } owner_to_pool && owner_to_pool[spec_name] 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 aa2dfdd573..c6811a4802 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 @@ -115,7 +113,7 @@ 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. # @@ -126,28 +124,23 @@ module ActiveRecord 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+ - def supports_statement_cache? - false + def supports_statement_cache? # :nodoc: + true end + deprecate :supports_statement_cache? # Runs the given block in a database transaction, and returns the result # of the block. @@ -334,17 +327,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 +348,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) 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 6ca53c72ce..e53ba4e666 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. @@ -58,14 +83,16 @@ module ActiveRecord # the same SQL query and repeatedly return the same result each time, silently # undermining the randomness you were expecting. def clear_query_cache - @query_cache.clear + @lock.synchronize do + @query_cache.clear + end end def select_all(arel, name = nil, binds = [], preparable: nil) if @query_cache_enabled && !locked?(arel) arel, binds = binds_from_relation arel, binds sql = to_sql(arel, binds) - 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,16 +100,24 @@ module ActiveRecord private - def cache_sql(sql, binds) - result = - if @query_cache[sql].key?(binds) - ActiveSupport::Notifications.instrument("sql.active_record", - sql: sql, binds: binds, name: "CACHE", connection_id: object_id) - @query_cache[sql][binds] - else - @query_cache[sql][binds] = yield - end - result.dup + def cache_sql(sql, name, binds) + @lock.synchronize do + result = + if @query_cache[sql].key?(binds) + 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 + end + result.dup + end end # If arel is locked this is a SELECT ... FOR UPDATE or somesuch. Such @@ -90,6 +125,10 @@ module ActiveRecord 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..61233dcc51 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb @@ -1,22 +1,25 @@ require "active_support/core_ext/big_decimal/conversions" +require "active_support/multibyte/chars" module ActiveRecord module ConnectionAdapters # :nodoc: module Quoting # Quotes the column value to help prevent # {SQL injection attacks}[http://en.wikipedia.org/wiki/SQL_injection]. - def quote(value, column = nil) - # records are quoted as their primary key - return value.quoted_id if value.respond_to?(:quoted_id) + def quote(value) + value = id_value_for_database(value) if value.is_a?(Base) - 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) + if value.respond_to?(:quoted_id) + at = value.method(:quoted_id).source_location + at &&= " at %s:%d" % at + + owner = value.method(:quoted_id).owner.to_s + klass = value.class.to_s + klass += "(#{owner})" unless owner == klass + + ActiveSupport::Deprecation.warn \ + "Defining #quoted_id is deprecated and will be ignored in Rails 5.2. (defined on #{klass}#{at})" + return value.quoted_id end _quote(value) @@ -26,6 +29,8 @@ module ActiveRecord # SQLite does not understand dates, so this method will convert a Date # to a String. def type_cast(value, column = nil) + value = id_value_for_database(value) if value.is_a?(Base) + if value.respond_to?(:quoted_id) && value.respond_to?(:id) return value.id end @@ -63,17 +68,6 @@ module ActiveRecord lookup_cast_type(column.sql_type) end - def fetch_type_metadata(sql_type) - cast_type = lookup_cast_type(sql_type) - SqlTypeMetadata.new( - sql_type: sql_type, - type: cast_type.type, - limit: cast_type.limit, - precision: cast_type.precision, - scale: cast_type.scale, - ) - end - # Quotes a string, escaping any ' (single quote) and \ (backslash) # characters. def quote_string(s) @@ -150,11 +144,28 @@ module ActiveRecord quoted_date(value).sub(/\A2000-01-01 /, "") end - private + def quoted_binary(value) # :nodoc: + "'#{quote_string(value.to_s)}'" + end - def type_casted_binds(binds) + def type_casted_binds(binds) # :nodoc: + if binds.first.is_a?(Array) + binds.map { |column, value| type_cast(value, column) } + else binds.map { |attr| type_cast(attr.value_for_database) } end + end + + private + def lookup_cast_type(sql_type) + type_map.lookup(sql_type) + end + + def id_value_for_database(value) + if primary_key = value.class.primary_key + value.instance_variable_get(:@attributes)[primary_key].value_for_database + end + end def types_which_need_no_typecasting [nil, Numeric, String] @@ -162,7 +173,7 @@ module ActiveRecord def _quote(value) case value - when String, ActiveSupport::Multibyte::Chars, Type::Binary::Data + when String, ActiveSupport::Multibyte::Chars "'#{quote_string(value.to_s)}'" when true then quoted_true when false then quoted_false @@ -170,6 +181,7 @@ module ActiveRecord # BigDecimals need to be put in a non-normalized form and quoted. when BigDecimal then value.to_s("F") when Numeric, ActiveSupport::Duration then value.to_s + when Type::Binary::Data then quoted_binary(value) when Type::Time::Value then "'#{quoted_time(value)}'" when Date, Time then "'#{quoted_date(value)}'" when Symbol then "'#{quote_string(value.to_s)}'" diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb index 322684672f..a4fecc4a8e 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb @@ -15,9 +15,9 @@ module ActiveRecord end delegate :quote_column_name, :quote_table_name, :quote_default_expression, :type_to_sql, - :options_include_default?, :supports_indexes_in_create?, :supports_foreign_keys?, :foreign_key_options, to: :@conn + :options_include_default?, :supports_indexes_in_create?, :supports_foreign_keys_in_create?, :foreign_key_options, to: :@conn private :quote_column_name, :quote_table_name, :quote_default_expression, :type_to_sql, - :options_include_default?, :supports_indexes_in_create?, :supports_foreign_keys?, :foreign_key_options + :options_include_default?, :supports_indexes_in_create?, :supports_foreign_keys_in_create?, :foreign_key_options private @@ -29,7 +29,7 @@ module ActiveRecord end def visit_ColumnDefinition(o) - o.sql_type ||= type_to_sql(o.type, o.limit, o.precision, o.scale) + o.sql_type = type_to_sql(o.type, o.options) column_sql = "#{quote_column_name(o.name)} #{o.sql_type}" add_column_options!(column_sql, column_options(o)) unless o.type == :primary_key column_sql @@ -49,7 +49,7 @@ module ActiveRecord statements.concat(o.indexes.map { |column_name, options| index_in_create(o.name, column_name, options) }) end - if supports_foreign_keys? + if supports_foreign_keys_in_create? statements.concat(o.foreign_keys.map { |to_table, options| foreign_key_in_create(o.name, to_table, options) }) end @@ -96,17 +96,7 @@ module ActiveRecord end def column_options(o) - column_options = {} - column_options[:null] = o.null unless o.null.nil? - column_options[:default] = o.default unless o.default.nil? - column_options[:column] = o - column_options[:first] = o.first - column_options[:after] = o.after - column_options[:auto_increment] = o.auto_increment - column_options[:primary_key] = o.primary_key - column_options[:collation] = o.collation - column_options[:comment] = o.comment - column_options + o.options.merge(column: o) end def add_column_options!(sql, options) @@ -143,5 +133,6 @@ module ActiveRecord end end end + SchemaCreation = AbstractAdapter::SchemaCreation # :nodoc: end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb index ffde4f2c93..a30fbe0e05 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb @@ -2,30 +2,63 @@ module ActiveRecord module ConnectionAdapters #:nodoc: # Abstract representation of an index definition on a table. Instances of # this type are typically created and returned by methods in database - # adapters. e.g. ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter#indexes - class IndexDefinition < Struct.new(:table, :name, :unique, :columns, :lengths, :orders, :where, :type, :using, :comment) #:nodoc: + # adapters. e.g. ActiveRecord::ConnectionAdapters::MySQL::SchemaStatements#indexes + class IndexDefinition # :nodoc: + attr_reader :table, :name, :unique, :columns, :lengths, :orders, :where, :type, :using, :comment + + def initialize( + table, name, + unique = false, + columns = [], + lengths: {}, + orders: {}, + where: nil, + type: nil, + using: nil, + comment: nil + ) + @table = table + @name = name + @unique = unique + @columns = columns + @lengths = lengths + @orders = orders + @where = where + @type = type + @using = using + @comment = comment + end end # Abstract representation of a column definition. Instances of this type # are typically created by methods in TableDefinition, and added to the # +columns+ attribute of said TableDefinition object, in order to be used # for generating a number of table creation or table changing SQL statements. - class ColumnDefinition < Struct.new(:name, :type, :limit, :precision, :scale, :default, :null, :first, :after, :auto_increment, :primary_key, :collation, :sql_type, :comment) #:nodoc: + ColumnDefinition = Struct.new(:name, :type, :options, :sql_type) do # :nodoc: def primary_key? - primary_key || type.to_sym == :primary_key + options[:primary_key] end - end - class AddColumnDefinition < Struct.new(:column) # :nodoc: - end + [:limit, :precision, :scale, :default, :null, :collation, :comment].each do |option_name| + module_eval <<-CODE, __FILE__, __LINE__ + 1 + def #{option_name} + options[:#{option_name}] + end - class ChangeColumnDefinition < Struct.new(:column, :name) #:nodoc: + def #{option_name}=(value) + options[:#{option_name}] = value + end + CODE + end end - class PrimaryKeyDefinition < Struct.new(:name) # :nodoc: - end + AddColumnDefinition = Struct.new(:column) # :nodoc: - class ForeignKeyDefinition < Struct.new(:from_table, :to_table, :options) #:nodoc: + ChangeColumnDefinition = Struct.new(:column, :name) #:nodoc: + + PrimaryKeyDefinition = Struct.new(:name) # :nodoc: + + ForeignKeyDefinition = Struct.new(:from_table, :to_table, :options) do #:nodoc: def name options[:name] end @@ -71,7 +104,7 @@ module ActiveRecord polymorphic: false, index: true, foreign_key: false, - type: :integer, + type: :bigint, **options ) @name = name @@ -100,22 +133,20 @@ module ActiveRecord end end + # TODO Change this to private once we've dropped Ruby 2.2 support. + # Workaround for Ruby 2.2 "private attribute?" warning. protected attr_reader :name, :polymorphic, :index, :foreign_key, :type, :options private - def as_options(value, default = {}) - if value.is_a?(Hash) - value - else - default - end + def as_options(value) + value.is_a?(Hash) ? value : {} end def polymorphic_options - as_options(polymorphic, options) + as_options(polymorphic).merge(null: options[:null]) end def index_options @@ -175,6 +206,7 @@ module ActiveRecord :text, :time, :timestamp, + :virtual, ].each do |column_type| module_eval <<-CODE, __FILE__, __LINE__ + 1 def #{column_type}(*args, **options) @@ -355,33 +387,22 @@ module ActiveRecord # # See {connection.add_reference}[rdoc-ref:SchemaStatements#add_reference] for details of the options you can use. def references(*args, **options) - args.each do |col| - ReferenceDefinition.new(col, **options).add_to(self) + args.each do |ref_name| + ReferenceDefinition.new(ref_name, options).add_to(self) end end alias :belongs_to :references - def new_column_definition(name, type, options) # :nodoc: + def new_column_definition(name, type, **options) # :nodoc: type = aliased_types(type.to_s, type) - column = create_column_definition name, type - - column.limit = options[:limit] - column.precision = options[:precision] - column.scale = options[:scale] - column.default = options[:default] - column.null = options[:null] - column.first = options[:first] - column.after = options[:after] - column.auto_increment = options[:auto_increment] - column.primary_key = type == :primary_key || options[:primary_key] - column.collation = options[:collation] - column.comment = options[:comment] - column + options[:primary_key] ||= type == :primary_key + options[:null] = false if options[:primary_key] + create_column_definition(name, type, options) end private - def create_column_definition(name, type) - ColumnDefinition.new name, type + def create_column_definition(name, type, options) + ColumnDefinition.new(name, type, options) end def aliased_types(name, fallback) @@ -475,7 +496,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 = {}) @@ -496,9 +517,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 = {}) @@ -589,8 +610,7 @@ module ActiveRecord # t.belongs_to(:supplier, foreign_key: true) # # See {connection.add_reference}[rdoc-ref:SchemaStatements#add_reference] for details of the options you can use. - def references(*args) - options = args.extract_options! + def references(*args, **options) args.each do |ref_name| @base.add_reference(name, ref_name, options) end @@ -603,8 +623,7 @@ module ActiveRecord # t.remove_belongs_to(:supplier, polymorphic: true) # # See {connection.remove_reference}[rdoc-ref:SchemaStatements#remove_reference] - def remove_references(*args) - options = args.extract_options! + def remove_references(*args, **options) args.each do |ref_name| @base.remove_reference(name, ref_name, options) end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb index 8bb7362c2e..34036d8a1d 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb @@ -7,16 +7,15 @@ 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_with_virtual(column), prepare_column_options(column)] end def column_spec_for_primary_key(column) return {} if default_primary_key?(column) spec = { id: schema_type(column).inspect } spec.merge!(prepare_column_options(column).except!(:null)) + spec[:default] ||= "nil" if explicit_primary_key_default?(column) + spec end # This can be overridden on an Adapter level basis to support other @@ -38,7 +37,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 @@ -52,14 +51,27 @@ module ActiveRecord end # Lists the valid migration options - def migration_keys - [:name, :limit, :precision, :scale, :default, :null, :collation, :comment] + def migration_keys # :nodoc: + column_options_keys end + deprecate :migration_keys private def default_primary_key?(column) - schema_type(column) == :integer + schema_type(column) == :bigint + end + + def explicit_primary_key_default?(column) + false + end + + def schema_type_with_virtual(column) + if supports_virtual_columns? && column.virtual? + :virtual + else + schema_type(column) + end 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 29520ed9c8..16a398f631 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb @@ -31,6 +31,8 @@ module ActiveRecord # Returns the relation names useable to back Active Record models. # For most adapters this means all #tables and #views. def data_sources + select_values(data_source_sql, "SCHEMA") + rescue NotImplementedError tables | views end @@ -39,12 +41,14 @@ module ActiveRecord # data_source_exists?(:ebooks) # def data_source_exists?(name) + select_values(data_source_sql(name), "SCHEMA").any? if name.present? + rescue NotImplementedError data_sources.include?(name.to_s) end # Returns an array of table names defined in the database. - def tables(name = nil) - raise NotImplementedError, "#tables is not implemented" + def tables + select_values(data_source_sql(type: "BASE TABLE"), "SCHEMA") end # Checks to see if the table +table_name+ exists on the database. @@ -52,12 +56,14 @@ module ActiveRecord # table_exists?(:developers) # def table_exists?(table_name) + select_values(data_source_sql(table_name, type: "BASE TABLE"), "SCHEMA").any? if table_name.present? + rescue NotImplementedError tables.include?(table_name.to_s) end # Returns an array of view names defined in the database. def views - raise NotImplementedError, "#views is not implemented" + select_values(data_source_sql(type: "VIEW"), "SCHEMA") end # Checks to see if the view +view_name+ exists on the database. @@ -65,11 +71,15 @@ module ActiveRecord # view_exists?(:ebooks) # def view_exists?(view_name) + select_values(data_source_sql(view_name, type: "VIEW"), "SCHEMA").any? if view_name.present? + rescue NotImplementedError views.include?(view_name.to_s) 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. # @@ -95,10 +105,12 @@ module ActiveRecord indexes(table_name).any? { |i| checks.all? { |check| check[i] } } end - # Returns an array of Column objects for the table specified by +table_name+. - # See the concrete implementation for details on the expected parameter values. + # Returns an array of +Column+ objects for the table specified by +table_name+. def columns(table_name) - raise NotImplementedError, "#columns is not implemented" + table_name = table_name.to_s + column_definitions(table_name).map do |field| + new_column_from_field(table_name, field) + end end # Checks to see if a column exists in a given table. @@ -120,7 +132,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| + column_options_keys.each do |attr| checks << lambda { |c| c.send(attr) == options[attr] } if options.key?(attr) end @@ -271,8 +283,8 @@ module ActiveRecord yield td if block_given? - if options[:force] && data_source_exists?(table_name) - drop_table(table_name, options) + if options[:force] + drop_table(table_name, **options, if_exists: true) end result = execute schema_creation.accept td @@ -284,10 +296,10 @@ module ActiveRecord 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 @@ -332,18 +344,16 @@ module ActiveRecord # part_id int NOT NULL, # ) ENGINE=InnoDB DEFAULT CHARSET=utf8 # - def create_join_table(table_1, table_2, options = {}) + def create_join_table(table_1, table_2, column_options: {}, **options) join_table_name = find_join_table_name(table_1, table_2, options) - column_options = options.delete(:column_options) || {} - column_options.reverse_merge!(null: false) - type = column_options.delete(:type) || :integer + column_options.reverse_merge!(null: false, index: false) - t1_column, t2_column = [table_1, table_2].map { |t| t.to_s.singularize.foreign_key } + t1_ref, t2_ref = [table_1, table_2].map { |t| t.to_s.singularize } create_table(join_table_name, options.merge!(id: false)) do |td| - td.send type, t1_column, column_options - td.send type, t2_column, column_options + td.references t1_ref, column_options + td.references t2_ref, column_options yield td if block_given? end end @@ -766,16 +776,17 @@ module ActiveRecord raise ArgumentError, "You must specify the index name" end else - index_name(table_name, column: options) + index_name(table_name, index_name_options(options)) end end # Verifies the existence of an index with a given name. - # - # The default argument is returned if the underlying implementation does not define the indexes method, - # as there's no way to determine the correct answer in that case. - def index_name_exists?(table_name, index_name, default) - return default unless respond_to?(:indexes) + def index_name_exists?(table_name, index_name, default = nil) + unless default.nil? + ActiveSupport::Deprecation.warn(<<-MSG.squish) + Passing default to #index_name_exists? is deprecated without replacement. + MSG + end index_name = index_name.to_s indexes(table_name).detect { |i| i.name == index_name } end @@ -826,8 +837,8 @@ module ActiveRecord # # add_reference(:products, :supplier, foreign_key: {to_table: :firms}) # - def add_reference(table_name, *args) - ReferenceDefinition.new(*args).add_to(update_table_definition(table_name, self)) + def add_reference(table_name, ref_name, **options) + ReferenceDefinition.new(ref_name, options).add_to(update_table_definition(table_name, self)) end alias :add_belongs_to :add_reference @@ -854,6 +865,7 @@ module ActiveRecord else foreign_key_options = { to_table: reference_name } end + foreign_key_options[:column] ||= "#{ref_name}_id" remove_foreign_key(table_name, **foreign_key_options) end @@ -964,16 +976,6 @@ module ActiveRecord foreign_key_for(from_table, options_or_to_table).present? end - 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 } - end - - def foreign_key_for!(from_table, options_or_to_table = {}) # :nodoc: - 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: prefix = Base.table_name_prefix suffix = Base.table_name_suffix @@ -989,34 +991,19 @@ module ActiveRecord end def dump_schema_information #:nodoc: - versions = ActiveRecord::SchemaMigration.order("version").pluck(:version) + versions = ActiveRecord::SchemaMigration.all_versions insert_versions_sql(versions) end - def insert_versions_sql(versions) # :nodoc: - sm_table = ActiveRecord::Migrator.schema_migrations_table_name - - if supports_multi_insert? - sql = "INSERT INTO #{sm_table} (version) VALUES\n" - sql << versions.map { |v| "('#{v}')" }.join(",\n") - sql << ";\n\n" - sql - else - versions.map { |version| - "INSERT INTO #{sm_table} (version) VALUES ('#{version}');" - }.join "\n\n" - end - end - - # Should not be called normally, but this operation is non-destructive. - # The migrations module handles this automatically. - def initialize_schema_migrations_table + def initialize_schema_migrations_table # :nodoc: ActiveRecord::SchemaMigration.create_table end + deprecate :initialize_schema_migrations_table - def initialize_internal_metadata_table + def initialize_internal_metadata_table # :nodoc: ActiveRecord::InternalMetadata.create_table end + deprecate :initialize_internal_metadata_table def internal_string_options_for_primary_key # :nodoc: { primary_key: true } @@ -1025,16 +1012,15 @@ module ActiveRecord def assume_migrated_upto_version(version, migrations_paths) migrations_paths = Array(migrations_paths) version = version.to_i - sm_table = quote_table_name(ActiveRecord::Migrator.schema_migrations_table_name) + sm_table = quote_table_name(ActiveRecord::SchemaMigration.table_name) - migrated = select_values("SELECT version FROM #{sm_table}").map(&:to_i) - paths = migrations_paths.map { |p| "#{p}/[0-9]*_*.rb" } - versions = Dir[*paths].map do |filename| - filename.split("/").last.split("_").first.to_i + migrated = ActiveRecord::SchemaMigration.all_versions.map(&:to_i) + versions = ActiveRecord::Migrator.migration_files(migrations_paths).map do |file| + ActiveRecord::Migrator.parse_migration_filename(file).first.to_i 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 } @@ -1042,11 +1028,17 @@ module ActiveRecord 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: + def type_to_sql(type, limit: nil, precision: nil, scale: nil, **) # :nodoc: type = type.to_sym if type if native = native_database_types[type] column_type_sql = (native.is_a?(Hash) ? native[:name] : native).dup @@ -1064,7 +1056,7 @@ module ActiveRecord raise ArgumentError, "Error adding decimal column: precision cannot be empty if scale is specified" end - elsif [:datetime, :time].include?(type) && precision ||= native[:precision] + elsif [:datetime, :timestamp, :time, :interval].include?(type) && precision ||= native[:precision] if (0..6) === precision column_type_sql << "(#{precision})" else @@ -1116,18 +1108,14 @@ module ActiveRecord end def add_index_options(table_name, column_name, comment: nil, **options) # :nodoc: - if column_name.is_a?(String) && /\W/ === column_name - column_names = column_name - else - column_names = Array(column_name) - end + column_names = index_column_names(column_name) options.assert_valid_keys(:unique, :order, :name, :where, :length, :internal, :using, :algorithm, :type) index_type = options[:type].to_s if options.key?(:type) index_type ||= options[:unique] ? "UNIQUE" : "" index_name = options[:name].to_s if options.key?(:name) - index_name ||= index_name(table_name, index_name_options(column_names)) + index_name ||= index_name(table_name, column_names) if options.key?(:algorithm) algorithm = index_algorithms.fetch(options[:algorithm]) { @@ -1143,7 +1131,7 @@ module ActiveRecord validate_index_length!(table_name, index_name, options.fetch(:internal, false)) - if data_source_exists?(table_name) && index_name_exists?(table_name, index_name, false) + if data_source_exists?(table_name) && index_name_exists?(table_name, index_name) raise ArgumentError, "Index name '#{index_name}' on table '#{table_name}' already exists" end index_columns = quoted_columns_for_index(column_names, options).join(", ") @@ -1165,12 +1153,16 @@ module ActiveRecord raise NotImplementedError, "#{self.class} does not support changing column comments" end - protected + private + def column_options_keys + [:limit, :precision, :scale, :default, :null, :collation, :comment] + end def add_index_sort_order(quoted_columns, **options) if order = options[:order] case order when Hash + order = order.symbolize_keys quoted_columns.each { |name, column| column << " #{order[name].upcase}" if order[name].present? } when String quoted_columns.each { |name, column| column << " #{order.upcase}" if order.present? } @@ -1192,28 +1184,24 @@ module ActiveRecord 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, quote_column_name(name).dup] }] + 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) checks << lambda { |i| i.name == options[:name].to_s } if options.key?(:name) - column_names = Array(options[:column]).map(&:to_s) + column_names = index_column_names(options[:column]) else - column_names = Array(options).map(&:to_s) + column_names = index_column_names(options) end - if column_names.any? - checks << lambda { |i| i.columns.join("_and_") == column_names.join("_and_") } + if column_names.present? + checks << lambda { |i| index_name(table_name, i.columns) == index_name(table_name, column_names) } end raise ArgumentError, "No name or columns specified" if checks.none? @@ -1252,7 +1240,10 @@ module ActiveRecord end end - private + def schema_creation + SchemaCreation.new(self) + end + def create_table_definition(*args) TableDefinition.new(*args) end @@ -1261,23 +1252,61 @@ module ActiveRecord AlterTable.new create_table_definition(name) end - def index_name_options(column_names) # :nodoc: - if column_names.is_a?(String) + def fetch_type_metadata(sql_type) + cast_type = lookup_cast_type(sql_type) + SqlTypeMetadata.new( + sql_type: sql_type, + type: cast_type.type, + limit: cast_type.limit, + precision: cast_type.precision, + scale: cast_type.scale, + ) + end + + def index_column_names(column_names) + if column_names.is_a?(String) && /\W/.match?(column_names) + column_names + else + Array(column_names) + end + end + + def index_name_options(column_names) + if column_names.is_a?(String) && /\W/.match?(column_names) column_names = column_names.scan(/\w+/).join("_") end { column: column_names } end - def foreign_key_name(table_name, options) # :nodoc: - identifier = "#{table_name}_#{options.fetch(:column)}_fk" - hashed_identifier = Digest::SHA256.hexdigest(identifier).first(10) + def foreign_key_name(table_name, options) options.fetch(:name) do + identifier = "#{table_name}_#{options.fetch(:column)}_fk" + hashed_identifier = Digest::SHA256.hexdigest(identifier).first(10) + "fk_rails_#{hashed_identifier}" end end - def validate_index_length!(table_name, new_name, internal = false) # :nodoc: + def foreign_key_for(from_table, options_or_to_table = {}) + return unless supports_foreign_keys? + foreign_keys(from_table).detect { |fk| fk.defined_for? options_or_to_table } + end + + def foreign_key_for!(from_table, 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 extract_foreign_key_action(specifier) + case specifier + when "CASCADE"; :cascade + when "SET NULL"; :nullify + when "RESTRICT"; :restrict + end + end + + 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 @@ -1296,6 +1325,27 @@ module ActiveRecord def can_remove_index_by_name?(options) options.is_a?(Hash) && options.key?(:name) && options.except(:name, :algorithm).empty? end + + def insert_versions_sql(versions) + sm_table = quote_table_name(ActiveRecord::SchemaMigration.table_name) + + if versions.is_a?(Array) + sql = "INSERT INTO #{sm_table} (version) VALUES\n" + sql << versions.map { |v| "(#{quote(v)})" }.join(",\n") + sql << ";\n\n" + sql + else + "INSERT INTO #{sm_table} (version) VALUES (#{quote(versions)});" + end + end + + def data_source_sql(name = nil, type: nil) + raise NotImplementedError + end + + def quoted_scope(name = nil, type: nil) + raise NotImplementedError + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb b/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb index 6bb072dd73..19b7821494 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb @@ -149,57 +149,67 @@ module ActiveRecord end def begin_transaction(options = {}) - run_commit_callbacks = !current_transaction.joinable? - transaction = - if @stack.empty? - RealTransaction.new(@connection, options, run_commit_callbacks: run_commit_callbacks) - else - SavepointTransaction.new(@connection, "active_record_#{@stack.size}", options, - run_commit_callbacks: run_commit_callbacks) - end + @connection.lock.synchronize do + run_commit_callbacks = !current_transaction.joinable? + transaction = + if @stack.empty? + RealTransaction.new(@connection, options, run_commit_callbacks: run_commit_callbacks) + else + SavepointTransaction.new(@connection, "active_record_#{@stack.size}", options, + run_commit_callbacks: run_commit_callbacks) + end - @stack.push(transaction) - transaction + @stack.push(transaction) + transaction + end end def commit_transaction - transaction = @stack.last + @connection.lock.synchronize do + transaction = @stack.last - begin - transaction.before_commit_records - ensure - @stack.pop - end + begin + transaction.before_commit_records + ensure + @stack.pop + end - transaction.commit - transaction.commit_records + transaction.commit + transaction.commit_records + end end def rollback_transaction(transaction = nil) - transaction ||= @stack.pop - transaction.rollback - transaction.rollback_records + @connection.lock.synchronize do + transaction ||= @stack.pop + transaction.rollback + transaction.rollback_records + end end def within_new_transaction(options = {}) - transaction = begin_transaction options - yield - rescue Exception => error - if transaction - rollback_transaction - after_failure_actions(transaction, error) - end - raise - ensure - unless error - if Thread.current.status == "aborting" - rollback_transaction if transaction - else - begin - commit_transaction - rescue Exception - rollback_transaction(transaction) unless transaction.state.completed? - raise + @connection.lock.synchronize do + begin + transaction = begin_transaction options + yield + rescue Exception => error + if transaction + rollback_transaction + after_failure_actions(transaction, error) + end + raise + ensure + unless error + if Thread.current.status == "aborting" + rollback_transaction if transaction + else + begin + commit_transaction + rescue Exception + rollback_transaction(transaction) unless transaction.state.completed? + raise + end + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb index 0c7197a002..85d6fbe8b3 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb @@ -62,19 +62,19 @@ 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 + attr_reader :schema_cache, :owner, :logger, :prepared_statements, :lock alias :in_use? :owner def self.type_cast_config_to_integer(config) @@ -93,8 +93,6 @@ module ActiveRecord end end - attr_reader :prepared_statements - def initialize(connection, logger = nil, config = {}) # :nodoc: super() @@ -106,7 +104,8 @@ module ActiveRecord @pool = nil @schema_cache = SchemaCache.new self @quoted_column_names, @quoted_table_names = {}, {} - @visitor = arel_visitor + @visitor = arel_visitor + @lock = Monitor.new if self.class.type_cast_config_to_boolean(config.fetch(:prepared_statements) { true }) @prepared_statements = true @@ -141,24 +140,8 @@ module ActiveRecord end end - def collector - if prepared_statements - SQLString.new - else - BindCollector.new - end - end - - def arel_visitor # :nodoc: - Arel::Visitors::ToSql.new(self) - end - - def valid_type?(type) - false - end - - def schema_creation - SchemaCreation.new self + def valid_type?(type) # :nodoc: + !native_database_types[type].nil? end # this method must only be called while holding connection pool's mutex @@ -168,7 +151,7 @@ module ActiveRecord if @owner == Thread.current msg << "it is already leased by the current thread." else - msg << "it is already in use by a different thread: #{@owner}. " << + msg << "it is already in use by a different thread: #{@owner}. " \ "Current thread: #{Thread.current}." end raise ActiveRecordError, msg @@ -186,8 +169,8 @@ module ActiveRecord def expire if in_use? if @owner != Thread.current - raise ActiveRecordError, "Cannot expire connection, " << - "it is owned by a different thread: #{@owner}. " << + raise ActiveRecordError, "Cannot expire connection, " \ + "it is owned by a different thread: #{@owner}. " \ "Current thread: #{Thread.current}." end @@ -223,16 +206,15 @@ module ActiveRecord self.class::ADAPTER_NAME end - # Does this adapter support migrations? - def supports_migrations? - false + def supports_migrations? # :nodoc: + true end + deprecate :supports_migrations? - # Can this adapter determine the primary key for tables not attached - # to an Active Record class, such as join tables? - def supports_primary_key? - false + def supports_primary_key? # :nodoc: + true end + deprecate :supports_primary_key? # Does this adapter support DDL rollbacks in transactions? That is, would # CREATE TABLE or ALTER TABLE get rolled back by a transaction? @@ -302,6 +284,12 @@ module ActiveRecord false end + # Does this adapter support creating foreign key constraints + # in the same statement as creating the table? + def supports_foreign_keys_in_create? + supports_foreign_keys? + end + # Does this adapter support views? def supports_views? false @@ -332,6 +320,11 @@ module ActiveRecord true end + # Does this adapter support virtual columns? + def supports_virtual_columns? + false + end + # This is meant to be implemented by the adapters that support extensions def disable_extension(name) end @@ -420,12 +413,15 @@ module ActiveRecord # This is done under the hood by calling #active?. If the connection # is no longer active, then this method will reconnect to the database. def verify!(*ignored) + if ignored.size > 0 + ActiveSupport::Deprecation.warn("Passing arguments to #verify method of the connection has no effect and has been deprecated. Please remove all arguments from the #verify method call.") + end reconnect! unless active? end # Provides access to the underlying database driver for this adapter. For # example, this method returns a Mysql2::Client object in case of Mysql2Adapter, - # and a PGconn object in case of PostgreSQLAdapter. + # and a PG::Connection object in case of PostgreSQLAdapter. # # This is useful for when you need to call a proprietary method such as # PostgreSQL's lo_* methods. @@ -433,15 +429,15 @@ module ActiveRecord @connection end - def case_sensitive_comparison(table, attribute, column, value) - table[attribute].eq(Arel::Nodes::BindParam.new) + def case_sensitive_comparison(table, attribute, column, value) # :nodoc: + table[attribute].eq(value) end - def case_insensitive_comparison(table, attribute, column, value) + def case_insensitive_comparison(table, attribute, column, value) # :nodoc: if can_perform_case_insensitive_comparison_for?(column) - table[attribute].lower.eq(table.lower(Arel::Nodes::BindParam.new)) + table[attribute].lower.eq(table.lower(value)) else - table[attribute].eq(Arel::Nodes::BindParam.new) + table[attribute].eq(value) end end @@ -461,14 +457,6 @@ module ActiveRecord end end - def new_column(name, default, sql_type_metadata, null, table_name, default_function = nil, collation = nil) # :nodoc: - Column.new(name, default, sql_type_metadata, null, table_name, default_function, collation) - end - - def lookup_cast_type(sql_type) # :nodoc: - type_map.lookup(sql_type) - end - def column_name_for_operation(operation, node) # :nodoc: visitor.accept(node, collector).value end @@ -491,9 +479,13 @@ module ActiveRecord result end - protected + def default_index_type?(index) # :nodoc: + index.using.nil? + end + + 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 +516,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 +567,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, @@ -583,25 +575,46 @@ module ActiveRecord binds: binds, type_casted_binds: type_casted_binds, statement_name: statement_name, - connection_id: object_id) { yield } + connection_id: object_id) do + @lock.synchronize do + yield + end + end rescue => e raise translate_exception_class(e, sql) end 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}") end + + def collector + if prepared_statements + SQLString.new + else + BindCollector.new + end + end + + def arel_visitor + Arel::Visitors::ToSql.new(self) + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb index be8511f119..01599985ca 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb @@ -6,29 +6,22 @@ require "active_record/connection_adapters/mysql/quoting" require "active_record/connection_adapters/mysql/schema_creation" require "active_record/connection_adapters/mysql/schema_definitions" require "active_record/connection_adapters/mysql/schema_dumper" +require "active_record/connection_adapters/mysql/schema_statements" require "active_record/connection_adapters/mysql/type_metadata" require "active_support/core_ext/string/strip" -require "active_support/core_ext/regexp" module ActiveRecord module ConnectionAdapters class AbstractMysqlAdapter < AbstractAdapter include MySQL::Quoting include MySQL::ColumnDumper + include MySQL::SchemaStatements def update_table_definition(table_name, base) # :nodoc: MySQL::Table.new(table_name, base) end - def schema_creation # :nodoc: - MySQL::SchemaCreation.new(self) - end - - def arel_visitor # :nodoc: - Arel::Visitors::MySQL.new(self) - end - ## # :singleton-method: # By default, the Mysql2Adapter will consider all columns of type <tt>tinyint(1)</tt> @@ -36,17 +29,17 @@ module ActiveRecord # to your application.rb file: # # ActiveRecord::ConnectionAdapters::Mysql2Adapter.emulate_booleans = false - class_attribute :emulate_booleans - self.emulate_booleans = true + class_attribute :emulate_booleans, default: 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 }, float: { name: "float" }, decimal: { name: "decimal" }, datetime: { name: "datetime" }, + timestamp: { name: "timestamp" }, time: { name: "time" }, date: { name: "date" }, binary: { name: "blob", limit: 65535 }, @@ -54,9 +47,6 @@ module ActiveRecord json: { name: "json" }, } - INDEX_TYPES = [:fulltext, :spatial] - INDEX_USINGS = [:btree, :hash] - class StatementPool < ConnectionAdapters::StatementPool private def dealloc(stmt) stmt[:stmt].close @@ -68,19 +58,11 @@ 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 - CHARSETS_OF_4BYTES_MAXLEN = ["utf8mb4", "utf16", "utf16le", "utf32"] - - def internal_string_options_for_primary_key # :nodoc: - super.tap { |options| - options[:collation] = collation.sub(/\A[^_]+/, "utf8") if CHARSETS_OF_4BYTES_MAXLEN.include?(charset) - } - end - def version #:nodoc: @version ||= Version.new(full_version.match(/^\d+\.\d+\.\d+/)[0]) end @@ -89,29 +71,12 @@ module ActiveRecord /mariadb/i.match?(full_version) end - # Returns true, since this connection adapter supports migrations. - def supports_migrations? - true - end - - def supports_primary_key? - true - end - def supports_bulk_alter? #:nodoc: true end - # Returns true, since this connection adapter supports prepared statement - # caching. - def supports_statement_cache? - true - end - - # Technically MySQL allows to create indexes with the sort order syntax - # but at the moment (5.5) it doesn't yet implement them def supports_index_sort_order? - true + !mariadb? && version >= "8.0.1" end def supports_transaction_isolation? @@ -142,6 +107,14 @@ module ActiveRecord end end + def supports_virtual_columns? + if mariadb? + version >= "5.2.0" + else + version >= "5.7.5" + end + end + def supports_advisory_locks? true end @@ -170,10 +143,6 @@ module ActiveRecord raise NotImplementedError end - def new_column(*args) #:nodoc: - MySQL::Column.new(*args) - end - # Must return the MySQL error number from the exception, if the exception has an # error number. def error_number(exception) # :nodoc: @@ -216,7 +185,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,116 +280,18 @@ 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 - - if name - ActiveSupport::Deprecation.warn(<<-MSG.squish) - Passing arguments to #tables is deprecated without replacement. - MSG - end - - data_sources - end - - def data_sources - 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) - # 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 - - data_source_exists?(table_name) - end - - def data_source_exists?(table_name) - return false unless table_name.present? - - schema, name = extract_schema_qualified_name(table_name) - - sql = "SELECT table_name FROM information_schema.tables " - sql << "WHERE table_schema = #{quote(schema)} AND table_name = #{quote(name)}" - - 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? - - schema, name = extract_schema_qualified_name(view_name) - - sql = "SELECT table_name FROM information_schema.tables WHERE table_type = 'VIEW'" - sql << " AND table_schema = #{quote(schema)} AND table_name = #{quote(name)}" - - select_values(sql, "SCHEMA").any? - end - - # Returns an array of indexes for the given table. - def indexes(table_name, name = nil) #:nodoc: - indexes = [] - current_index = nil - execute_and_free("SHOW KEYS FROM #{quote_table_name(table_name)}", "SCHEMA") do |result| - each_hash(result) do |row| - if current_index != row[:Key_name] - next if row[:Key_name] == "PRIMARY" # skip the primary key - current_index = row[:Key_name] - - 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) - end - - indexes.last.columns << row[:Column_name] - indexes.last.lengths << 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) - end - end - def table_comment(table_name) # :nodoc: - schema, name = extract_schema_qualified_name(table_name) + scope = quoted_scope(table_name) select_value(<<-SQL.strip_heredoc, "SCHEMA") SELECT table_comment FROM information_schema.tables - WHERE table_schema = #{quote(schema)} - AND table_name = #{quote(name)} + WHERE table_schema = #{scope[:schema]} + AND table_name = #{scope[:name]} SQL end @@ -509,14 +384,14 @@ 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 def foreign_keys(table_name) raise ArgumentError unless table_name.present? - schema, name = extract_schema_qualified_name(table_name) + scope = quoted_scope(table_name) fk_info = select_all(<<-SQL.strip_heredoc, "SCHEMA") SELECT fk.referenced_table_name AS 'to_table', @@ -529,8 +404,9 @@ module ActiveRecord JOIN information_schema.referential_constraints rc USING (constraint_schema, constraint_name) WHERE fk.referenced_column_name IS NOT NULL - AND fk.table_schema = #{quote(schema)} - AND fk.table_name = #{quote(name)} + AND fk.table_schema = #{scope[:schema]} + AND fk.table_name = #{scope[:name]} + AND rc.table_name = #{scope[:name]} SQL fk_info.map do |row| @@ -569,7 +445,7 @@ module ActiveRecord end # Maps logical Rails types to MySQL-specific data types. - def type_to_sql(type, limit = nil, precision = nil, scale = nil, unsigned = nil) + def type_to_sql(type, limit: nil, precision: nil, scale: nil, unsigned: nil, **) # :nodoc: sql = \ case type.to_s when "integer" @@ -585,7 +461,7 @@ module ActiveRecord binary_to_sql(limit) end else - super(type, limit, precision, scale) + super end sql << " unsigned" if unsigned && type != :primary_key @@ -602,21 +478,21 @@ module ActiveRecord def primary_keys(table_name) # :nodoc: raise ArgumentError unless table_name.present? - schema, name = extract_schema_qualified_name(table_name) + scope = quoted_scope(table_name) select_values(<<-SQL.strip_heredoc, "SCHEMA") SELECT column_name FROM information_schema.key_column_usage WHERE constraint_name = 'PRIMARY' - AND table_schema = #{quote(schema)} - AND table_name = #{quote(name)} + AND table_schema = #{scope[:schema]} + AND table_name = #{scope[:name]} ORDER BY ordinal_position SQL end - def case_sensitive_comparison(table, attribute, column, value) + def case_sensitive_comparison(table, attribute, column, value) # :nodoc: if column.collation && !column.case_sensitive? - table[attribute].eq(Arel::Nodes::Bin.new(Arel::Nodes::BindParam.new)) + table[attribute].eq(Arel::Nodes::Bin.new(value)) else super end @@ -646,13 +522,13 @@ module ActiveRecord self.class.type_cast_config_to_boolean(@config.fetch(:strict, true)) end - def valid_type?(type) - !native_database_types[type].nil? + def default_index_type?(index) # :nodoc: + index.using == :btree || super end - protected + private - def initialize_type_map(m) # :nodoc: + def initialize_type_map(m) super register_class_with_limit m, %r(char)i, MysqlString @@ -692,9 +568,9 @@ module ActiveRecord 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) @@ -703,21 +579,18 @@ module ActiveRecord end def extract_precision(sql_type) - if /time/ === sql_type + if /\A(?:date)?time(?:stamp)?\b/.match?(sql_type) super || 0 else super end end - def fetch_type_metadata(sql_type, extra = "") - MySQL::TypeMetadata.new(super(sql_type), extra: extra) - end - def add_index_length(quoted_columns, **options) if length = options[:length] case length when Hash + length = length.symbolize_keys quoted_columns.each { |name, column| column << "(#{length[name]})" if length[name].present? } when Integer quoted_columns.each { |name, column| column << "(#{length})" } @@ -734,9 +607,14 @@ module ActiveRecord # 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) @@ -744,8 +622,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 @@ -762,14 +652,18 @@ module ActiveRecord def change_column_sql(table_name, column_name, type, options = {}) column = column_for(table_name, column_name) - unless options_include_default?(options) + unless options.key?(:default) options[:default] = column.default end - unless options.has_key?(:null) + unless options.key?(:null) 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)) @@ -816,21 +710,17 @@ 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! def subquery_for(key, select) - subsubselect = select.clone - subsubselect.projections = [key] + subselect = select.clone + subselect.projections = [key] # Materialize subquery by adding distinct # to work with MySQL 5.7.6 which sets optimizer_switch='derived_merge=on' - subsubselect.distinct unless select.limit || select.offset || select.orders.any? + subselect.distinct unless select.limit || select.offset || select.orders.any? - subselect = Arel::SelectManager.new(select.engine) - subselect.project Arel.sql(key.name) - subselect.from subsubselect.as("__active_record_temp") + Arel::SelectManager.new(subselect.as("__active_record_temp")).project(Arel.sql(key.name)) end def supports_rename_index? @@ -844,9 +734,9 @@ module ActiveRecord variables["sql_auto_is_null"] = 0 # Increase timeout so the server doesn't disconnect us. - wait_timeout = @config[:wait_timeout] + wait_timeout = self.class.type_cast_config_to_integer(@config[:wait_timeout]) wait_timeout = 2147483 unless wait_timeout.is_a?(Integer) - variables["wait_timeout"] = self.class.type_cast_config_to_integer(wait_timeout) + variables["wait_timeout"] = wait_timeout defaults = [":default", :default].to_set @@ -887,7 +777,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: @@ -896,25 +786,24 @@ module ActiveRecord end end - def extract_foreign_key_action(specifier) # :nodoc: - case specifier - when "CASCADE"; :cascade - when "SET NULL"; :nullify - end - end - def create_table_info(table_name) # :nodoc: select_one("SHOW CREATE TABLE #{quote_table_name(table_name)}")["Create Table"] end - def create_table_definition(*args) # :nodoc: - MySQL::TableDefinition.new(*args) + def arel_visitor + Arel::Visitors::MySQL.new(self) end - def extract_schema_qualified_name(string) # :nodoc: - schema, name = string.to_s.scan(/[^`.\s]+|`[^`]*`/) - schema, name = @config[:database], schema unless name - [schema, name] + 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 integer_to_sql(limit) # :nodoc: @@ -949,11 +838,6 @@ module ActiveRecord end class MysqlJson < Type::Internal::AbstractJson # :nodoc: - def changed_in_place?(raw_old_value, new_value) - # Normalization is required because MySQL JSON data format includes - # the space between the elements. - super(serialize(deserialize(raw_old_value)), new_value) - end end class MysqlString < Type::String # :nodoc: 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 849130ba43..3e4ea28f63 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 @@ -149,9 +149,18 @@ module ActiveRecord # Expands each key in @configurations hash into fully resolved hash def resolve_all config = configurations.dup + + if env = ActiveRecord::ConnectionHandling::DEFAULT_ENV.call + env_config = config[env] if config[env].is_a?(Hash) && !(config[env].key?("adapter") || config[env].key?("url")) + end + + config.reject! { |k, v| v.is_a?(Hash) && !(v.key?("adapter") || v.key?("url")) } + config.merge! env_config if env_config + config.each do |key, value| config[key] = resolve(value) if value end + config end diff --git a/activerecord/lib/active_record/connection_adapters/mysql/column.rb b/activerecord/lib/active_record/connection_adapters/mysql/column.rb index 296d9a15f8..c9ad47c035 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql/column.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql/column.rb @@ -5,16 +5,20 @@ module ActiveRecord 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 + + def virtual? + /\b(?:VIRTUAL|STORED|PERSISTENT)\b/.match?(extra) + 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 56800f7590..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 @@ -52,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 @@ -86,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 925555703d..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 @@ -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/quoting.rb b/activerecord/lib/active_record/connection_adapters/mysql/quoting.rb index 9d11ad28d4..d4f5906b33 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql/quoting.rb @@ -36,15 +36,9 @@ module ActiveRecord end end - private - - def _quote(value) - if value.is_a?(Type::Binary::Data) - "x'#{value.hex}'" - else - super - end - end + def quoted_binary(value) + "x'#{value.hex}'" + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/mysql/schema_creation.rb b/activerecord/lib/active_record/connection_adapters/mysql/schema_creation.rb index d808b50332..083cd6340f 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql/schema_creation.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql/schema_creation.rb @@ -1,9 +1,9 @@ module ActiveRecord module ConnectionAdapters module MySQL - class SchemaCreation < AbstractAdapter::SchemaCreation - delegate :add_sql_comment!, to: :@conn - private :add_sql_comment! + class SchemaCreation < AbstractAdapter::SchemaCreation # :nodoc: + delegate :add_sql_comment!, :mariadb?, to: :@conn + private :add_sql_comment!, :mariadb? private @@ -11,11 +11,6 @@ module ActiveRecord "DROP FOREIGN KEY #{name}" end - def visit_ColumnDefinition(o) - o.sql_type = type_to_sql(o.type, o.limit, o.precision, o.scale, o.unsigned) - super - end - def visit_AddColumnDefinition(o) add_column_position!(super, column_options(o.column)) end @@ -29,13 +24,15 @@ module ActiveRecord add_sql_comment!(super, options[:comment]) end - def column_options(o) - column_options = super - column_options[:charset] = o.charset - column_options - end - def add_column_options!(sql, options) + # By default, TIMESTAMP columns are NOT NULL, cannot contain NULL values, + # and assigning NULL assigns the current timestamp. To permit a TIMESTAMP + # column to contain NULL, explicitly declare it with the NULL attribute. + # See http://dev.mysql.com/doc/refman/5.7/en/timestamp-initialization.html + if /\Atimestamp\b/.match?(options[:column].sql_type) && !options[:primary_key] + sql << " NULL" unless options[:null] == false || options_include_default?(options) + end + if charset = options[:charset] sql << " CHARACTER SET #{charset}" end @@ -44,6 +41,13 @@ module ActiveRecord sql << " COLLATE #{collation}" end + if as = options[:as] + sql << " AS (#{as})" + if options[:stored] + sql << (mariadb? ? " PERSISTENT" : " STORED") + end + end + add_sql_comment!(super, options[:comment]) end diff --git a/activerecord/lib/active_record/connection_adapters/mysql/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/mysql/schema_definitions.rb index ce773ed75b..6d88c14d50 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,7 @@ module ActiveRecord module MySQL module ColumnMethods def primary_key(name, type = :primary_key, **options) - options[:auto_increment] = true if type == :bigint && !options.key?(:default) + options[:auto_increment] = true if [:integer, :bigint].include?(type) && !options.key?(:default) super end @@ -56,32 +56,29 @@ module ActiveRecord end end - class ColumnDefinition < ActiveRecord::ConnectionAdapters::ColumnDefinition - attr_accessor :charset, :unsigned - end - class TableDefinition < ActiveRecord::ConnectionAdapters::TableDefinition include ColumnMethods - def new_column_definition(name, type, options) # :nodoc: - column = super - case column.type + def new_column_definition(name, type, **options) # :nodoc: + case type + when :virtual + type = options[:type] when :primary_key - column.type = :integer - column.auto_increment = true + type = :integer + options[:limit] ||= 8 + options[:auto_increment] = true + options[:primary_key] = true when /\Aunsigned_(?<type>.+)\z/ - column.type = $~[:type].to_sym - column.unsigned = true + type = $~[:type].to_sym + options[:unsigned] = true end - column.unsigned ||= options[:unsigned] - column.charset = options[:charset] - column + + super end private - - def create_column_definition(name, type) - MySQL::ColumnDefinition.new(name, type) + def aliased_types(name, fallback) + fallback end 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..e2ba0ba1a0 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql/schema_dumper.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql/schema_dumper.rb @@ -1,21 +1,17 @@ module ActiveRecord module ConnectionAdapters 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 - end - spec[:unsigned] = "true" if column.unsigned? - spec - end - + module ColumnDumper # :nodoc: def prepare_column_options(column) spec = super spec[:unsigned] = "true" if column.unsigned? + + if supports_virtual_columns? && column.virtual? + spec[:as] = extract_expression_for_virtual_column(column) + spec[:stored] = "true" if /\b(?:STORED|PERSISTENT)\b/.match?(column.extra) + spec = { type: schema_type(column).inspect }.merge!(spec) + end + spec end @@ -26,11 +22,18 @@ module ActiveRecord private def default_primary_key?(column) - super && column.auto_increment? + super && column.auto_increment? && !column.unsigned? + end + + def explicit_primary_key_default?(column) + column.type == :integer && !column.auto_increment? end def schema_type(column) - if column.sql_type == "tinyblob" + case column.sql_type + when /\Atimestamp\b/ + :timestamp + when "tinyblob" :blob else super @@ -38,7 +41,7 @@ module ActiveRecord end def schema_precision(column) - super unless /time/ === column.sql_type && column.precision == 0 + super unless /\A(?:date)?time(?:stamp)?\b/.match?(column.sql_type) && column.precision == 0 end def schema_collation(column) @@ -48,6 +51,22 @@ module ActiveRecord column.collation.inspect if column.collation != @table_collation_cache[table_name] end end + + def extract_expression_for_virtual_column(column) + if mariadb? + create_table_info = create_table_info(column.table_name) + if %r/#{quote_column_name(column.name)} #{Regexp.quote(column.sql_type)}(?: COLLATE \w+)? AS \((?<expression>.+?)\) #{column.extra}/ =~ create_table_info + $~[:expression].inspect + end + else + scope = quoted_scope(column.table_name) + sql = "SELECT generation_expression FROM information_schema.columns" \ + " WHERE table_schema = #{scope[:schema]}" \ + " AND table_name = #{scope[:name]}" \ + " AND column_name = #{quote(column.name)}" + select_value(sql, "SCHEMA").inspect + end + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/mysql/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/mysql/schema_statements.rb new file mode 100644 index 0000000000..f9e1e046ea --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/mysql/schema_statements.rb @@ -0,0 +1,122 @@ +module ActiveRecord + module ConnectionAdapters + module MySQL + module SchemaStatements # :nodoc: + # Returns an array of indexes for the given table. + def indexes(table_name, name = nil) + if name + ActiveSupport::Deprecation.warn(<<-MSG.squish) + Passing name to #indexes is deprecated without replacement. + MSG + end + + indexes = [] + current_index = nil + execute_and_free("SHOW KEYS FROM #{quote_table_name(table_name)}", "SCHEMA") do |result| + each_hash(result) do |row| + if current_index != row[:Key_name] + next if row[:Key_name] == "PRIMARY" # skip the primary key + current_index = row[:Key_name] + + mysql_index_type = row[:Index_type].downcase.to_sym + case mysql_index_type + when :fulltext, :spatial + index_type = mysql_index_type + when :btree, :hash + index_using = mysql_index_type + end + + indexes << IndexDefinition.new( + row[:Table], + row[:Key_name], + row[:Non_unique].to_i == 0, + type: index_type, + using: index_using, + comment: row[:Index_comment].presence + ) + end + + indexes.last.columns << row[:Column_name] + indexes.last.lengths.merge!(row[:Column_name] => row[:Sub_part].to_i) if row[:Sub_part] + indexes.last.orders.merge!(row[:Column_name] => :desc) if row[:Collation] == "D" + end + end + + indexes + end + + def internal_string_options_for_primary_key + super.tap do |options| + if CHARSETS_OF_4BYTES_MAXLEN.include?(charset) && (mariadb? || version < "8.0.0") + options[:collation] = collation.sub(/\A[^_]+/, "utf8") + end + end + end + + private + CHARSETS_OF_4BYTES_MAXLEN = ["utf8mb4", "utf16", "utf16le", "utf32"] + + def schema_creation + MySQL::SchemaCreation.new(self) + end + + def create_table_definition(*args) + MySQL::TableDefinition.new(*args) + end + + def new_column_from_field(table_name, field) + type_metadata = fetch_type_metadata(field[:Type], field[:Extra]) + if type_metadata.type == :datetime && field[:Default] == "CURRENT_TIMESTAMP" + default, default_function = nil, field[:Default] + else + default, default_function = field[:Default], nil + end + + MySQL::Column.new( + field[:Field], + default, + type_metadata, + field[:Null] == "YES", + table_name, + default_function, + field[:Collation], + comment: field[:Comment].presence + ) + end + + def fetch_type_metadata(sql_type, extra = "") + MySQL::TypeMetadata.new(super(sql_type), extra: extra) + end + + def extract_foreign_key_action(specifier) + super unless specifier == "RESTRICT" + end + + def data_source_sql(name = nil, type: nil) + scope = quoted_scope(name, type: type) + + sql = "SELECT table_name FROM information_schema.tables" + sql << " WHERE table_schema = #{scope[:schema]}" + sql << " AND table_name = #{scope[:name]}" if scope[:name] + sql << " AND table_type = #{scope[:type]}" if scope[:type] + sql + end + + def quoted_scope(name = nil, type: nil) + schema, name = extract_schema_qualified_name(name) + scope = {} + scope[:schema] = schema ? quote(schema) : "database()" + scope[:name] = quote(name) if name + scope[:type] = quote(type) if type + scope + end + + def extract_schema_qualified_name(string) + schema, name = string.to_s.scan(/[^`.\s]+|`[^`]*`/) + schema, name = nil, schema unless name + [schema, name] + end + end + end + end +end 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 24dcf852e1..9ad6a6c0d0 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql/type_metadata.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql/type_metadata.rb @@ -2,6 +2,8 @@ module ActiveRecord module ConnectionAdapters module MySQL class TypeMetadata < DelegateClass(SqlTypeMetadata) # :nodoc: + undef to_yaml if method_defined?(:to_yaml) + attr_reader :extra def initialize(type_metadata, extra: "") diff --git a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb index a3e2c913c5..af55cfe2f6 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb @@ -10,16 +10,12 @@ module ActiveRecord # Establishes a connection to the database that's used by all Active Record objects. def mysql2_connection(config) config = config.symbolize_keys - - 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) 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 092543259f..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,7 +81,9 @@ 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 @@ -95,7 +93,9 @@ module ActiveRecord # 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 @@ -130,7 +130,7 @@ module ActiveRecord super end - protected :sql_for_insert + private :sql_for_insert def exec_insert(sql, name = nil, binds = [], pk = nil, sequence_name = nil) if use_insert_returning? || pk == false @@ -175,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/oid.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb index 0e526f6201..4098250f3e 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb @@ -11,6 +11,7 @@ require "active_record/connection_adapters/postgresql/oid/inet" 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/oid" require "active_record/connection_adapters/postgresql/oid/point" require "active_record/connection_adapters/postgresql/oid/legacy_point" require "active_record/connection_adapters/postgresql/oid/range" 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 b969503178..a73a8c1726 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 + type_cast_array(value.values, :deserialize) else super end @@ -33,11 +38,8 @@ module ActiveRecord def serialize(value) if value.is_a?(::Array) - result = @pg_encoder.encode(type_cast_array(value, :serialize)) - if encoding = determine_encoding_of_strings(value) - result.encode!(encoding) - end - result + casted_values = type_cast_array(value, :serialize) + Data.new(@pg_encoder, casted_values) else super end @@ -58,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) @@ -67,13 +73,6 @@ module ActiveRecord @subtype.public_send(method, value) end end - - def determine_encoding_of_strings(value) - case value - when ::Array then determine_encoding_of_strings(value.first) - when ::String then value.encoding - end - end end end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit.rb index 74bff229ea..0a505f46a7 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit.rb @@ -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/bytea.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/bytea.rb index 8f9d6e7f9b..702fa8175c 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/bytea.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/bytea.rb @@ -6,7 +6,7 @@ module ActiveRecord def deserialize(value) return if value.nil? return value.to_s if value.is_a?(Type::Binary::Data) - PGconn.unescape_bytea(super) + PG::Connection.unescape_bytea(super) end end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/hstore.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/hstore.rb 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/jsonb.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/jsonb.rb index 87391b5dc7..705cb7f0b3 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/jsonb.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/jsonb.rb @@ -6,16 +6,6 @@ module ActiveRecord def type :jsonb end - - def changed_in_place?(raw_old_value, new_value) - # Postgres does not preserve insignificant whitespaces when - # round-tripping jsonb columns. This causes some false positives for - # the comparison here. Therefore, we need to parse and re-dump the - # raw value here to ensure the insignificant whitespaces are - # consistent with our encoder's output. - raw_old_value = serialize(deserialize(raw_old_value)) - super(raw_old_value, new_value) - end end end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/oid.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/oid.rb new file mode 100644 index 0000000000..9c2ac08b30 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/oid.rb @@ -0,0 +1,13 @@ +module ActiveRecord + module ConnectionAdapters + module PostgreSQL + module OID # :nodoc: + class Oid < Type::Integer # :nodoc: + def type + :oid + end + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb index 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/oid/specialized_string.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/specialized_string.rb index 2d2fede4e8..564e82a4ac 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/specialized_string.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/specialized_string.rb @@ -5,8 +5,9 @@ module ActiveRecord class SpecializedString < Type::String # :nodoc: attr_reader :type - def initialize(type) + def initialize(type, **options) @type = type + super(options) end end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb index b5031d890f..44eb666965 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb @@ -33,7 +33,7 @@ module ActiveRecord # Quotes schema names for use in SQL queries. def quote_schema_name(name) - PGconn.quote_ident(name) + PG::Connection.quote_ident(name) end def quote_table_name_for_assignment(table, attr) @@ -42,7 +42,7 @@ module ActiveRecord # Quotes column names for use in SQL queries. def quote_column_name(name) # :nodoc: - @quoted_column_names[name] ||= PGconn.quote_ident(super).freeze + @quoted_column_names[name] ||= PG::Connection.quote_ident(super).freeze end # Quote date/time values for use in SQL input. @@ -55,10 +55,14 @@ module ActiveRecord end end + def quoted_binary(value) # :nodoc: + "'#{escape_bytea(value.to_s)}'" + end + def quote_default_expression(value, column) # :nodoc: if value.is_a?(Proc) value.call - elsif column.type == :uuid && value.include?("()") + elsif column.type == :uuid && /\(\)/.match?(value) value # Does not quote function default values for UUID columns elsif column.respond_to?(:array?) value = type_cast_from_column(column, value) @@ -73,11 +77,12 @@ module ActiveRecord end private + def lookup_cast_type(sql_type) + super(select_value("SELECT #{quote(sql_type)}::regtype::oid", "SCHEMA").to_i) + end def _quote(value) case value - when Type::Binary::Data - "'#{escape_bytea(value.to_s)}'" when OID::Xml::Data "xml '#{quote_string(value.to_s)}'" when OID::Bit::Data @@ -92,6 +97,8 @@ module ActiveRecord else super end + when OID::Array::Data + _quote(encode_array(value)) else super end @@ -101,15 +108,42 @@ module ActiveRecord case value when Type::Binary::Data # Return a bind param hash with format as binary. - # See http://deveiate.org/code/pg/PGconn.html#method-i-exec_prepared-doc + # See https://deveiate.org/code/pg/PG/Connection.html#method-i-exec_prepared-doc # for more information { value: value.to_s, format: 1 } when OID::Xml::Data, OID::Bit::Data value.to_s + 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_creation.rb b/activerecord/lib/active_record/connection_adapters/postgresql/schema_creation.rb new file mode 100644 index 0000000000..e1d5089115 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_creation.rb @@ -0,0 +1,15 @@ +module ActiveRecord + module ConnectionAdapters + module PostgreSQL + class SchemaCreation < AbstractAdapter::SchemaCreation # :nodoc: + private + def add_column_options!(sql, options) + if options[:collation] + sql << " COLLATE \"#{options[:collation]}\"" + end + super + 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..11ea1e5144 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,25 @@ 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 + options[:auto_increment] = true if [:integer, :bigint].include?(type) && !options.key?(:default) + if type == :uuid + options[:default] = options.fetch(:default, "gen_random_uuid()") + elsif options.delete(:auto_increment) == true && %i(integer bigint).include?(type) + type = if type == :bigint || options[:limit] == 8 + :bigserial + else + :serial + end + end + super end @@ -67,6 +88,10 @@ module ActiveRecord args.each { |name| column(name, :inet, options) } end + def interval(*args, **options) + args.each { |name| column(name, :interval, options) } + end + def int4range(*args, **options) args.each { |name| column(name, :int4range, options) } end @@ -99,6 +124,10 @@ module ActiveRecord args.each { |name| column(name, :numrange, options) } end + def oid(*args, **options) + args.each { |name| column(name, :oid, options) } + end + def point(*args, **options) args.each { |name| column(name, :point, options) } end @@ -152,24 +181,8 @@ module ActiveRecord end end - class ColumnDefinition < ActiveRecord::ConnectionAdapters::ColumnDefinition - attr_accessor :array - end - class TableDefinition < ActiveRecord::ConnectionAdapters::TableDefinition include ColumnMethods - - def new_column_definition(name, type, options) # :nodoc: - column = super - column.array = options[:array] - column - end - - private - - def create_column_definition(name, type) - PostgreSQL::ColumnDefinition.new name, type - end end class Table < ActiveRecord::ConnectionAdapters::Table 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..5555a46b6b 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_dumper.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_dumper.rb @@ -1,15 +1,7 @@ module ActiveRecord module ConnectionAdapters module PostgreSQL - module ColumnDumper - def column_spec_for_primary_key(column) - spec = super - if schema_type(column) == :uuid - spec[:default] ||= "nil" - end - spec - end - + module ColumnDumper # :nodoc: # Adds +:array+ option to the default set def prepare_column_options(column) spec = super @@ -25,7 +17,11 @@ module ActiveRecord private def default_primary_key?(column) - schema_type(column) == :serial + schema_type(column) == :bigserial + end + + def explicit_primary_key_default?(column) + column.type == :uuid || (column.type == :integer && !column.serial?) 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 29a77580f5..cb45d7ba6b 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb @@ -3,22 +3,6 @@ require "active_support/core_ext/string/strip" module ActiveRecord module ConnectionAdapters module PostgreSQL - class SchemaCreation < AbstractAdapter::SchemaCreation - private - - def visit_ColumnDefinition(o) - o.sql_type = type_to_sql(o.type, o.limit, o.precision, o.scale, o.array) - super - end - - def add_column_options!(sql, options) - if options[:collation] - sql << " COLLATE \"#{options[:collation]}\"" - end - super - end - end - module SchemaStatements # Drops the database specified on the +name+ attribute # and creates it again using the provided +options+. @@ -70,91 +54,24 @@ module ActiveRecord execute "DROP DATABASE IF EXISTS #{quote_table_name(name)}" 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 - - select_values("SELECT tablename FROM pg_tables WHERE schemaname = ANY(current_schemas(false))", "SCHEMA") - end - - def data_sources # :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 ('r', 'v','m') -- (r)elation/table, (v)iew, (m)aterialized view - AND n.nspname = ANY (current_schemas(false)) - SQL - end - - # Returns true if table exists. - # 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))'} - 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 - - def view_exists?(view_name) # :nodoc: - name = Utils.extract_schema_qualified_name(view_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 c.relname = '#{name.identifier}' - AND n.nspname = #{name.schema ? "'#{name.schema}'" : 'ANY (current_schemas(false))'} - SQL - end - def drop_table(table_name, options = {}) # :nodoc: execute "DROP TABLE#{' IF EXISTS' if options[:if_exists]} #{quote_table_name(table_name)}#{' CASCADE' if options[:force] == :cascade}" end # Returns true if schema exists. def schema_exists?(name) - select_value("SELECT COUNT(*) FROM pg_namespace WHERE nspname = '#{name}'", "SCHEMA").to_i > 0 + select_value("SELECT COUNT(*) FROM pg_namespace WHERE nspname = #{quote(name)}", "SCHEMA").to_i > 0 end # Verifies existence of an index with a given name. - def index_name_exists?(table_name, index_name, default) - table = Utils.extract_schema_qualified_name(table_name.to_s) - index = Utils.extract_schema_qualified_name(index_name.to_s) + def index_name_exists?(table_name, index_name, default = nil) + unless default.nil? + ActiveSupport::Deprecation.warn(<<-MSG.squish) + Passing default to #index_name_exists? is deprecated without replacement. + MSG + end + table = quoted_scope(table_name) + index = quoted_scope(index_name) select_value(<<-SQL, "SCHEMA").to_i > 0 SELECT COUNT(*) @@ -163,15 +80,21 @@ module ActiveRecord INNER JOIN pg_class i ON d.indexrelid = i.oid LEFT JOIN pg_namespace n ON n.oid = i.relnamespace WHERE i.relkind = 'i' - AND i.relname = '#{index.identifier}' - AND t.relname = '#{table.identifier}' - AND n.nspname = #{index.schema ? "'#{index.schema}'" : 'ANY (current_schemas(false))'} + AND i.relname = #{index[:name]} + AND t.relname = #{table[:name]} + AND n.nspname = #{index[:schema]} SQL end # Returns an array of indexes for the given table. - def indexes(table_name, name = nil) - table = Utils.extract_schema_qualified_name(table_name.to_s) + def indexes(table_name, name = nil) # :nodoc: + if name + ActiveSupport::Deprecation.warn(<<-MSG.squish) + Passing name to #indexes is deprecated without replacement. + MSG + end + + scope = quoted_scope(table_name) result = query(<<-SQL, "SCHEMA") SELECT distinct i.relname, d.indisunique, d.indkey, pg_get_indexdef(d.indexrelid), t.oid, @@ -185,8 +108,8 @@ module ActiveRecord LEFT JOIN pg_namespace n ON n.oid = i.relnamespace WHERE i.relkind = 'i' AND d.indisprimary = 'f' - AND t.relname = '#{table.identifier}' - AND n.nspname = #{table.schema ? "'#{table.schema}'" : 'ANY (current_schemas(false))'} + AND t.relname = #{scope[:name]} + AND n.nspname = #{scope[:schema]} ORDER BY i.relname SQL @@ -217,49 +140,43 @@ module ActiveRecord ] end - IndexDefinition.new(table_name, index_name, unique, columns, [], orders, where, nil, using.to_sym, comment.presence) - 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) + IndexDefinition.new( + table_name, + index_name, + unique, + columns, + orders: orders, + where: where, + using: using.to_sym, + comment: comment.presence + ) end end - def new_column(*args) # :nodoc: - PostgreSQLColumn.new(*args) - 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 def table_comment(table_name) # :nodoc: - name = Utils.extract_schema_qualified_name(table_name.to_s) - if name.identifier + scope = quoted_scope(table_name, type: "BASE TABLE") + if scope[:name] select_value(<<-SQL.strip_heredoc, "SCHEMA") SELECT pg_catalog.obj_description(c.oid, 'pg_class') FROM pg_catalog.pg_class c LEFT JOIN pg_namespace n ON n.oid = c.relnamespace - WHERE c.relname = #{quote(name.identifier)} - AND c.relkind IN ('r') -- (r)elation/table - AND n.nspname = #{name.schema ? quote(name.schema) : 'ANY (current_schemas(false))'} + WHERE c.relname = #{scope[:name]} + AND c.relkind IN (#{scope[:type]}) + AND n.nspname = #{scope[:schema]} SQL end end # Returns the current database name. def current_database - select_value("select current_database()", "SCHEMA") + select_value("SELECT current_database()", "SCHEMA") end # Returns the current schema name. @@ -269,17 +186,17 @@ module ActiveRecord # Returns the current database encoding format. def encoding - select_value("SELECT pg_encoding_to_char(encoding) FROM pg_database WHERE datname LIKE '#{current_database}'", "SCHEMA") + select_value("SELECT pg_encoding_to_char(encoding) FROM pg_database WHERE datname = current_database()", "SCHEMA") end # Returns the current database collation. def collation - select_value("SELECT datcollate FROM pg_database WHERE datname LIKE '#{current_database}'", "SCHEMA") + select_value("SELECT datcollate FROM pg_database WHERE datname = current_database()", "SCHEMA") end # Returns the current database ctype. def ctype - select_value("SELECT datctype FROM pg_database WHERE datname LIKE '#{current_database}'", "SCHEMA") + select_value("SELECT datctype FROM pg_database WHERE datname = current_database()", "SCHEMA") end # Returns an array of schema names. @@ -373,9 +290,17 @@ module ActiveRecord if pk && sequence quoted_sequence = quote_table_name(sequence) + max_pk = select_value("select MAX(#{quote_column_name pk}) from #{quote_table_name(table)}") + if max_pk.nil? + if postgresql_version >= 100000 + minvalue = select_value("SELECT seqmin from pg_sequence where seqrelid = '#{quoted_sequence}'::regclass") + else + minvalue = select_value("SELECT min_value FROM #{quoted_sequence}") + end + end select_value(<<-end_sql, "SCHEMA") - SELECT setval('#{quoted_sequence}', (SELECT COALESCE(MAX(#{quote_column_name pk})+(SELECT increment_by FROM #{quoted_sequence}), (SELECT min_value FROM #{quoted_sequence})) FROM #{quote_table_name(table)}), false) + SELECT setval('#{quoted_sequence}', #{max_pk ? max_pk : minvalue}, #{max_pk ? true : false}) end_sql end end @@ -436,16 +361,17 @@ module ActiveRecord def primary_keys(table_name) # :nodoc: select_values(<<-SQL.strip_heredoc, "SCHEMA") - WITH pk_constraint AS ( - SELECT conrelid, unnest(conkey) AS connum FROM pg_constraint - WHERE contype = 'p' - AND conrelid = '#{quote_table_name(table_name)}'::regclass - ), cons AS ( - SELECT conrelid, connum, row_number() OVER() AS rownum FROM pk_constraint - ) - SELECT attr.attname FROM pg_attribute attr - INNER JOIN cons ON attr.attrelid = cons.conrelid AND attr.attnum = cons.connum - ORDER BY cons.rownum + SELECT a.attname + FROM ( + SELECT indrelid, indkey, generate_subscripts(indkey, 1) idx + FROM pg_index + WHERE indrelid = #{quote(quote_table_name(table_name))}::regclass + AND indisprimary + ) i + JOIN pg_attribute a + ON a.attrelid = i.indrelid + AND a.attnum = i.indkey[i.idx] + ORDER BY i.idx SQL end @@ -459,14 +385,15 @@ module ActiveRecord clear_cache! execute "ALTER TABLE #{quote_table_name(table_name)} RENAME TO #{quote_table_name(new_name)}" pk, seq = pk_and_sequence_for(new_name) - if seq && seq.identifier == "#{table_name}_#{pk}_seq" - new_seq = "#{new_name}_#{pk}_seq" + if pk idx = "#{table_name}_pkey" new_idx = "#{new_name}_pkey" - execute "ALTER TABLE #{seq.quoted} RENAME TO #{quote_table_name(new_seq)}" execute "ALTER INDEX #{quote_table_name(idx)} RENAME TO #{quote_table_name(new_idx)}" + if seq && seq.identifier == "#{table_name}_#{pk}_seq" + new_seq = "#{new_name}_#{pk}_seq" + execute "ALTER TABLE #{seq.quoted} RENAME TO #{quote_table_name(new_seq)}" + end end - rename_table_indexes(table_name, new_name) end @@ -480,7 +407,7 @@ module ActiveRecord clear_cache! quoted_table_name = quote_table_name(table_name) quoted_column_name = quote_column_name(column_name) - sql_type = type_to_sql(type, options[:limit], options[:precision], options[:scale], options[:array]) + sql_type = type_to_sql(type, options) sql = "ALTER TABLE #{quoted_table_name} ALTER COLUMN #{quoted_column_name} TYPE #{sql_type}" if options[:collation] sql << " COLLATE \"#{options[:collation]}\"" @@ -488,12 +415,12 @@ module ActiveRecord if options[:using] sql << " USING #{options[:using]}" elsif options[:cast_as] - cast_as_type = type_to_sql(options[:cast_as], options[:limit], options[:precision], options[:scale], options[:array]) + cast_as_type = type_to_sql(options[:cast_as], options) sql << " USING CAST(#{quoted_column_name} AS #{cast_as_type})" end execute sql - change_column_default(table_name, column_name, options[:default]) if options_include_default?(options) + change_column_default(table_name, column_name, options[:default]) if options.key?(:default) change_column_null(table_name, column_name, options[:null], options[:default]) if options.key?(:null) change_column_comment(table_name, column_name, options[:comment]) if options.key?(:comment) end @@ -583,6 +510,7 @@ module ActiveRecord end def foreign_keys(table_name) + scope = quoted_scope(table_name) fk_info = select_all(<<-SQL.strip_heredoc, "SCHEMA") SELECT t2.oid::regclass::text AS to_table, a1.attname AS column, a2.attname AS primary_key, c.conname AS name, c.confupdtype AS on_update, c.confdeltype AS on_delete FROM pg_constraint c @@ -592,8 +520,8 @@ module ActiveRecord JOIN pg_attribute a2 ON a2.attnum = c.confkey[1] AND a2.attrelid = t2.oid JOIN pg_namespace t3 ON c.connamespace = t3.oid WHERE c.contype = 'f' - AND t1.relname = #{quote(table_name)} - AND t3.nspname = ANY (current_schemas(false)) + AND t1.relname = #{scope[:name]} + AND t3.nspname = #{scope[:schema]} ORDER BY c.conname SQL @@ -611,20 +539,8 @@ module ActiveRecord end end - def extract_foreign_key_action(specifier) # :nodoc: - case specifier - when "c"; :cascade - when "n"; :nullify - when "r"; :restrict - end - end - - def index_name_length - 63 - end - # Maps logical Rails types to PostgreSQL-specific data types. - def type_to_sql(type, limit = nil, precision = nil, scale = nil, array = nil) + def type_to_sql(type, limit: nil, precision: nil, scale: nil, array: nil, **) # :nodoc: sql = \ case type.to_s when "binary" @@ -649,7 +565,7 @@ module ActiveRecord else raise(ActiveRecordError, "No integer type has byte size #{limit}. Use a numeric with scale 0 instead.") end else - super(type, limit, precision, scale) + super end sql << "[]" if array && type != :primary_key @@ -670,17 +586,84 @@ module ActiveRecord [super, *order_columns].join(", ") end - def fetch_type_metadata(column_name, sql_type, oid, fmod) - cast_type = get_oid_type(oid, fmod, column_name, sql_type) - simple_type = SqlTypeMetadata.new( - sql_type: sql_type, - type: cast_type.type, - limit: cast_type.limit, - precision: cast_type.precision, - scale: cast_type.scale, - ) - PostgreSQLTypeMetadata.new(simple_type, oid: oid, fmod: fmod) - end + private + def schema_creation + PostgreSQL::SchemaCreation.new(self) + end + + def create_table_definition(*args) + PostgreSQL::TableDefinition.new(*args) + end + + def new_column_from_field(table_name, field) + column_name, type, default, notnull, oid, fmod, collation, comment = field + type_metadata = fetch_type_metadata(column_name, type, oid.to_i, fmod.to_i) + 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 fetch_type_metadata(column_name, sql_type, oid, fmod) + cast_type = get_oid_type(oid, fmod, column_name, sql_type) + simple_type = SqlTypeMetadata.new( + sql_type: sql_type, + type: cast_type.type, + limit: cast_type.limit, + precision: cast_type.precision, + scale: cast_type.scale, + ) + PostgreSQLTypeMetadata.new(simple_type, oid: oid, fmod: fmod) + end + + def extract_foreign_key_action(specifier) + case specifier + when "c"; :cascade + when "n"; :nullify + when "r"; :restrict + end + end + + def data_source_sql(name = nil, type: nil) + scope = quoted_scope(name, type: type) + scope[:type] ||= "'r','v','m'" # (r)elation/table, (v)iew, (m)aterialized view + + sql = "SELECT c.relname FROM pg_class c LEFT JOIN pg_namespace n ON n.oid = c.relnamespace" + sql << " WHERE n.nspname = #{scope[:schema]}" + sql << " AND c.relname = #{scope[:name]}" if scope[:name] + sql << " AND c.relkind IN (#{scope[:type]})" + sql + end + + def quoted_scope(name = nil, type: nil) + schema, name = extract_schema_qualified_name(name) + type = \ + case type + when "BASE TABLE" + "'r'" + when "VIEW" + "'v','m'" + end + scope = {} + scope[:schema] = schema ? quote(schema) : "ANY (current_schemas(false))" + scope[:name] = quote(name) if name + scope[:type] = type if type + scope + end + + def extract_schema_qualified_name(string) + name = Utils.extract_schema_qualified_name(string.to_s) + [name.schema, name.identifier] + end end end end 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..f57179ae59 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/type_metadata.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/type_metadata.rb @@ -1,6 +1,8 @@ module ActiveRecord module ConnectionAdapters class PostgreSQLTypeMetadata < DelegateClass(SqlTypeMetadata) + undef to_yaml if method_defined?(:to_yaml) + attr_reader :oid, :fmod, :array def initialize(type_metadata, oid: nil, fmod: nil) @@ -8,7 +10,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..aa7940188a 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/utils.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/utils.rb @@ -19,9 +19,9 @@ module ActiveRecord def quoted if schema - PGconn.quote_ident(schema) << SEPARATOR << PGconn.quote_ident(identifier) + PG::Connection.quote_ident(schema) << SEPARATOR << PG::Connection.quote_ident(identifier) else - PGconn.quote_ident(identifier) + PG::Connection.quote_ident(identifier) end end @@ -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 8001c0dd53..f74f966fd9 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -3,18 +3,19 @@ gem "pg", "~> 0.18" require "pg" require "active_record/connection_adapters/abstract_adapter" +require "active_record/connection_adapters/statement_pool" require "active_record/connection_adapters/postgresql/column" require "active_record/connection_adapters/postgresql/database_statements" require "active_record/connection_adapters/postgresql/explain_pretty_printer" require "active_record/connection_adapters/postgresql/oid" require "active_record/connection_adapters/postgresql/quoting" require "active_record/connection_adapters/postgresql/referential_integrity" +require "active_record/connection_adapters/postgresql/schema_creation" require "active_record/connection_adapters/postgresql/schema_definitions" require "active_record/connection_adapters/postgresql/schema_dumper" require "active_record/connection_adapters/postgresql/schema_statements" require "active_record/connection_adapters/postgresql/type_metadata" require "active_record/connection_adapters/postgresql/utils" -require "active_record/connection_adapters/statement_pool" module ActiveRecord module ConnectionHandling # :nodoc: @@ -28,11 +29,11 @@ module ActiveRecord conn_params[:user] = conn_params.delete(:username) if conn_params[:username] conn_params[:dbname] = conn_params.delete(:database) if conn_params[:database] - # Forward only valid config params to PGconn.connect. - valid_conn_param_keys = PGconn.conndefaults_hash.keys + [:requiressl] + # Forward only valid config params to PG::Connection.connect. + valid_conn_param_keys = PG::Connection.conndefaults_hash.keys + [:requiressl] conn_params.slice!(*valid_conn_param_keys) - # The postgres drivers don't allow the creation of an unconnected PGconn object, + # The postgres drivers don't allow the creation of an unconnected PG::Connection object, # so just pass a nil connection object for the time being. ConnectionAdapters::PostgreSQLAdapter.new(nil, logger, conn_params, config) end @@ -70,7 +71,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" }, @@ -108,6 +109,8 @@ module ActiveRecord bit: { name: "bit" }, bit_varying: { name: "bit varying" }, money: { name: "money" }, + interval: { name: "interval" }, + oid: { name: "oid" }, } OID = PostgreSQL::OID #:nodoc: @@ -118,20 +121,6 @@ module ActiveRecord include PostgreSQL::DatabaseStatements include PostgreSQL::ColumnDumper - def schema_creation # :nodoc: - PostgreSQL::SchemaCreation.new self - end - - def arel_visitor # :nodoc: - Arel::Visitors::PostgreSQL.new(self) - end - - # Returns true, since this connection adapter supports prepared statement - # caching. - def supports_statement_cache? - true - end - def supports_index_sort_order? true end @@ -198,8 +187,8 @@ module ActiveRecord end def connection_active? - @connection.status == PGconn::CONNECTION_OK - rescue PGError + @connection.status == PG::CONNECTION_OK + rescue PG::Error false end end @@ -212,7 +201,7 @@ module ActiveRecord # @local_tz is initialized as nil to avoid warnings when connect tries to use it @local_tz = nil - @table_alias_length = nil + @max_identifier_length = nil connect add_pg_encoders @@ -233,7 +222,9 @@ module ActiveRecord # Clears the prepared statements cache. def clear_cache! - @statements.clear + @lock.synchronize do + @statements.clear + end end def truncate(table_name, name = nil) @@ -242,50 +233,48 @@ module ActiveRecord # Is this connection alive and ready for queries? def active? - @connection.query "SELECT 1" + @lock.synchronize do + @connection.query "SELECT 1" + end true - rescue PGError + rescue PG::Error false end # Close then reopen the connection. def reconnect! - super - @connection.reset - configure_connection + @lock.synchronize do + super + @connection.reset + configure_connection + end end def reset! - clear_cache! - reset_transaction - unless @connection.transaction_status == ::PG::PQTRANS_IDLE - @connection.query "ROLLBACK" + @lock.synchronize do + clear_cache! + reset_transaction + unless @connection.transaction_status == ::PG::PQTRANS_IDLE + @connection.query "ROLLBACK" + end + @connection.query "DISCARD ALL" + configure_connection end - @connection.query "DISCARD ALL" - configure_connection end # Disconnects from the database if already connected. Otherwise, this # method does nothing. def disconnect! - super - @connection.close rescue nil + @lock.synchronize do + super + @connection.close rescue nil + end end def native_database_types #:nodoc: NATIVE_DATABASE_TYPES end - # Returns true, since this connection adapter supports migrations. - def supports_migrations? - true - end - - # Does PostgreSQL support finding primary key on non-Active Record tables? - def supports_primary_key? #:nodoc: - true - end - def set_standard_conforming_strings execute("SET standard_conforming_strings = on", "SCHEMA") end @@ -306,8 +295,8 @@ module ActiveRecord true end - # Range datatypes weren't introduced until PostgreSQL 9.2 def supports_ranges? + # Range datatypes weren't introduced until PostgreSQL 9.2 postgresql_version >= 90200 end @@ -315,6 +304,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") @@ -359,8 +352,9 @@ module ActiveRecord # Returns the configured supported identifier length supported by PostgreSQL def table_alias_length - @table_alias_length ||= query("SHOW max_identifier_length", "SCHEMA")[0][0].to_i + @max_identifier_length ||= select_value("SHOW max_identifier_length", "SCHEMA").to_i end + alias index_name_length table_alias_length # Set the authorized user for this session def session_auth=(user) @@ -372,19 +366,10 @@ module ActiveRecord @use_insert_returning end - def valid_type?(type) - !native_database_types[type].nil? - end - def update_table_definition(table_name, base) #:nodoc: PostgreSQL::Table.new(table_name, base) end - def lookup_cast_type(sql_type) # :nodoc: - oid = execute("SELECT #{quote(sql_type)}::regtype::oid", "SCHEMA").first["oid"].to_i - super(oid) - end - def column_name_for_operation(operation, node) # :nodoc: OPERATION_ALIASES.fetch(operation) { operation.downcase } end @@ -400,10 +385,16 @@ module ActiveRecord @connection.server_version end - protected + def default_index_type?(index) # :nodoc: + index.using == :btree || super + end + + 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" @@ -412,13 +403,17 @@ module ActiveRecord def translate_exception(exception, message) return exception unless exception.respond_to?(:result) - case exception.result.try(:error_field, PGresult::PG_DIAG_SQLSTATE) + case exception.result.try(:error_field, PG::PG_DIAG_SQLSTATE) when UNIQUE_VIOLATION RecordNotUnique.new(message) when FOREIGN_KEY_VIOLATION 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,9 +423,7 @@ 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 = "".freeze) if !type_map.key?(oid) load_additional_types(type_map, [oid]) end @@ -443,11 +436,11 @@ module ActiveRecord } 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 - m.alias_type "oid", "int2" + m.register_type "oid", OID::Oid.new m.register_type "float4", Type::Float.new m.alias_type "float8", "float4" m.register_type "text", Type::Text.new @@ -482,8 +475,10 @@ module ActiveRecord m.register_type "polygon", OID::SpecializedString.new(:polygon) m.register_type "circle", OID::SpecializedString.new(:circle) - # FIXME: why are we keeping these types as strings? - m.alias_type "interval", "varchar" + m.register_type "interval" do |_, _, sql_type| + precision = extract_precision(sql_type) + OID::SpecializedString.new(:interval, precision: precision) + end register_class_with_precision m, "time", Type::Time register_class_with_precision m, "timestamp", OID::DateTime @@ -511,7 +506,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,7 +518,7 @@ 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 @@ -549,15 +544,15 @@ module ActiveRecord 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: - !default_value && (%r{\w+\(.*\)|\(.*\)::\w+} === default) + def has_default_function?(default_value, default) + !default_value && %r{\w+\(.*\)|\(.*\)::\w+|CURRENT_DATE|CURRENT_TIMESTAMP}.match?(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 +596,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 +608,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) @@ -619,8 +620,10 @@ module ActiveRecord if in_transaction? raise ActiveRecord::PreparedStatementCacheExpired.new(e.cause.message) else - # outside of transactions we can simply flush this query and retry - @statements.delete sql_key(sql) + @lock.synchronize do + # outside of transactions we can simply flush this query and retry + @statements.delete sql_key(sql) + end retry end end @@ -637,7 +640,7 @@ module ActiveRecord CACHED_PLAN_HEURISTIC = "cached plan must not change result type".freeze def is_cached_plan_failure?(e) pgerror = e.cause - code = pgerror.result.result_error_field(PGresult::PG_DIAG_SQLSTATE) + code = pgerror.result.result_error_field(PG::PG_DIAG_SQLSTATE) code == FEATURE_NOT_SUPPORTED && pgerror.message.include?(CACHED_PLAN_HEURISTIC) rescue false @@ -656,25 +659,27 @@ module ActiveRecord # Prepare the statement if it hasn't been prepared, return # the statement key. def prepare_statement(sql) - sql_key = sql_key(sql) - unless @statements.key? sql_key - nextkey = @statements.next_key - begin - @connection.prepare nextkey, sql - rescue => e - raise translate_exception_class(e, sql) + @lock.synchronize do + sql_key = sql_key(sql) + unless @statements.key? sql_key + nextkey = @statements.next_key + begin + @connection.prepare nextkey, sql + rescue => e + raise translate_exception_class(e, sql) + end + # Clear the queue + @connection.get_last_result + @statements[sql_key] = nextkey end - # Clear the queue - @connection.get_last_result - @statements[sql_key] = nextkey + @statements[sql_key] end - @statements[sql_key] end # Connects to a PostgreSQL server and sets up the adapter depending on the # connected server's characteristics. def connect - @connection = PGconn.connect(@connection_parameters) + @connection = PG.connect(@connection_parameters) configure_connection rescue ::PG::Error => error if error.message.include?("does not exist") @@ -719,7 +724,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,28 +746,28 @@ 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, - (SELECT c.collname FROM pg_collation c, pg_type t - WHERE c.oid = a.attcollation AND t.oid = a.atttypid AND a.attcollation <> t.typcollation), - 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 + c.collname, 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 + LEFT JOIN pg_type t ON a.atttypid = t.oid + LEFT JOIN pg_collation c ON a.attcollation = c.oid AND a.attcollation <> t.typcollation + 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: - PostgreSQL::TableDefinition.new(*args) + def arel_visitor + Arel::Visitors::PostgreSQL.new(self) end def can_perform_case_insensitive_comparison_for?(column) @@ -771,10 +776,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 +797,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 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/quoting.rb b/activerecord/lib/active_record/connection_adapters/sqlite3/quoting.rb index f01ed67b0f..7276a65098 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3/quoting.rb @@ -18,15 +18,11 @@ module ActiveRecord quoted_date(value) end - private + def quoted_binary(value) + "x'#{value.hex}'" + end - def _quote(value) - if value.is_a?(Type::Binary::Data) - "x'#{value.hex}'" - else - super - end - end + private def _type_cast(value) case value diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3/schema_creation.rb b/activerecord/lib/active_record/connection_adapters/sqlite3/schema_creation.rb index 70c0d28830..bc798d1dbb 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3/schema_creation.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3/schema_creation.rb @@ -1,15 +1,8 @@ module ActiveRecord module ConnectionAdapters module SQLite3 - class SchemaCreation < AbstractAdapter::SchemaCreation + class SchemaCreation < AbstractAdapter::SchemaCreation # :nodoc: private - - def column_options(o) - options = super - options[:null] = false if o.primary_key - options - end - def add_column_options!(sql, options) if options[:collation] sql << " COLLATE \"#{options[:collation]}\"" 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..e157e4b218 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/sqlite3/schema_definitions.rb @@ -0,0 +1,28 @@ +module ActiveRecord + module ConnectionAdapters + module SQLite3 + module ColumnMethods + def primary_key(name, type = :primary_key, **options) + if %i(integer bigint).include?(type) && (options.delete(:auto_increment) == true || !options.key?(:default)) + type = :primary_key + end + + super + end + end + + class TableDefinition < ActiveRecord::ConnectionAdapters::TableDefinition + include ColumnMethods + + def references(*args, **options) + super(*args, type: :integer, **options) + end + alias :belongs_to :references + end + + class Table < ActiveRecord::ConnectionAdapters::Table + include ColumnMethods + 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..eec018eda3 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/sqlite3/schema_dumper.rb @@ -0,0 +1,17 @@ +module ActiveRecord + module ConnectionAdapters + module SQLite3 + module ColumnDumper # :nodoc: + private + + def default_primary_key?(column) + schema_type(column) == :integer + end + + def explicit_primary_key_default?(column) + column.bigint? + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/sqlite3/schema_statements.rb new file mode 100644 index 0000000000..e02491edb6 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/sqlite3/schema_statements.rb @@ -0,0 +1,92 @@ +module ActiveRecord + module ConnectionAdapters + module SQLite3 + module SchemaStatements # :nodoc: + # Returns an array of indexes for the given table. + def indexes(table_name, name = nil) + if name + ActiveSupport::Deprecation.warn(<<-MSG.squish) + Passing name to #indexes is deprecated without replacement. + MSG + end + + exec_query("PRAGMA index_list(#{quote_table_name(table_name)})", "SCHEMA").map do |row| + index_sql = select_value(<<-SQL, "SCHEMA") + SELECT sql + FROM sqlite_master + WHERE name = #{quote(row['name'])} AND type = 'index' + UNION ALL + SELECT sql + FROM sqlite_temp_master + WHERE name = #{quote(row['name'])} AND type = 'index' + SQL + + /\sWHERE\s+(?<where>.+)$/i =~ index_sql + + columns = exec_query("PRAGMA index_info(#{quote(row['name'])})", "SCHEMA").map do |col| + col["name"] + end + + IndexDefinition.new( + table_name, + row["name"], + row["unique"] != 0, + columns, + where: where + ) + end + end + + private + def schema_creation + SQLite3::SchemaCreation.new(self) + end + + def create_table_definition(*args) + SQLite3::TableDefinition.new(*args) + end + + def new_column_from_field(table_name, field) + default = \ + case field["dflt_value"] + when /^null$/i + nil + when /^'(.*)'$/m + $1.gsub("''", "'") + when /^"(.*)"$/m + $1.gsub('""', '"') + else + field["dflt_value"] + end + + type_metadata = fetch_type_metadata(field["type"]) + Column.new(field["name"], default, type_metadata, field["notnull"].to_i == 0, table_name, nil, field["collation"]) + end + + def data_source_sql(name = nil, type: nil) + scope = quoted_scope(name, type: type) + scope[:type] ||= "'table','view'" + + sql = "SELECT name FROM sqlite_master WHERE name <> 'sqlite_sequence'" + sql << " AND name = #{scope[:name]}" if scope[:name] + sql << " AND type IN (#{scope[:type]})" + sql + end + + def quoted_scope(name = nil, type: nil) + type = \ + case type + when "BASE TABLE" + "'table'" + when "VIEW" + "'view'" + end + scope = {} + scope[:name] = quote(name) if name + scope[:type] = type if type + scope + 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 e2b534b511..7233325d5a 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb @@ -3,6 +3,9 @@ require "active_record/connection_adapters/statement_pool" require "active_record/connection_adapters/sqlite3/explain_pretty_printer" require "active_record/connection_adapters/sqlite3/quoting" require "active_record/connection_adapters/sqlite3/schema_creation" +require "active_record/connection_adapters/sqlite3/schema_definitions" +require "active_record/connection_adapters/sqlite3/schema_dumper" +require "active_record/connection_adapters/sqlite3/schema_statements" gem "sqlite3", "~> 1.3.6" require "sqlite3" @@ -52,6 +55,8 @@ module ActiveRecord ADAPTER_NAME = "SQLite".freeze include SQLite3::Quoting + include SQLite3::ColumnDumper + include SQLite3::SchemaStatements NATIVE_DATABASE_TYPES = { primary_key: "INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL", @@ -75,12 +80,8 @@ module ActiveRecord end end - def schema_creation # :nodoc: - SQLite3::SchemaCreation.new self - end - - def arel_visitor # :nodoc: - Arel::Visitors::SQLite.new(self) + def update_table_definition(table_name, base) # :nodoc: + SQLite3::Table.new(table_name, base) end def initialize(connection, logger, connection_options, config) @@ -88,6 +89,8 @@ module ActiveRecord @active = nil @statements = StatementPool.new(self.class.type_cast_config_to_integer(config[:statement_limit])) + + configure_connection end def supports_ddl_transactions? @@ -102,23 +105,12 @@ module ActiveRecord sqlite_version >= "3.8.0" end - # Returns true, since this connection adapter supports prepared statement - # caching. - def supports_statement_cache? - true - end - - # Returns true, since this connection adapter supports migrations. - def supports_migrations? #:nodoc: - true - end - - def supports_primary_key? #:nodoc: + def requires_reloading? true end - def requires_reloading? - true + def supports_foreign_keys_in_create? + sqlite_version >= "3.6.19" end def supports_views? @@ -154,10 +146,6 @@ module ActiveRecord true end - def valid_type?(type) - true - end - # Returns 62. SQLite supports index names up to 64 # characters. The rest is used by Rails internally to perform # temporary rename operations @@ -178,6 +166,19 @@ module ActiveRecord true end + # REFERENTIAL INTEGRITY ==================================== + + def disable_referential_integrity # :nodoc: + old = select_value("PRAGMA foreign_keys") + + begin + execute("PRAGMA foreign_keys = OFF") + yield + ensure + execute("PRAGMA foreign_keys = #{old}") + end + end + #-- # DATABASE STATEMENTS ====================================== #++ @@ -191,30 +192,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,120 +232,27 @@ 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 - end - - def data_sources - 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) - end - - def data_source_exists?(table_name) - 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 views # :nodoc: - select_values("SELECT name FROM sqlite_master WHERE type = 'view' AND name <> 'sqlite_sequence'", "SCHEMA") - end - - def view_exists?(view_name) # :nodoc: - return false unless view_name.present? - - sql = "SELECT name FROM sqlite_master WHERE type = 'view' AND name <> 'sqlite_sequence'" - sql << " AND name = #{quote(view_name)}" - - 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) - end - end - - # Returns an array of indexes for the given table. - def indexes(table_name, name = nil) #:nodoc: - exec_query("PRAGMA index_list(#{quote_table_name(table_name)})", "SCHEMA").map do |row| - sql = <<-SQL - SELECT sql - FROM sqlite_master - WHERE name=#{quote(row['name'])} AND type='index' - UNION ALL - SELECT sql - FROM sqlite_temp_master - WHERE name=#{quote(row['name'])} AND type='index' - SQL - index_sql = exec_query(sql).first["sql"] - match = /\sWHERE\s+(.+)$/i.match(index_sql) - where = match[1] if match - IndexDefinition.new( - table_name, - row["name"], - row["unique"] != 0, - exec_query("PRAGMA index_info('#{row['name']}')", "SCHEMA").map { |col| - col["name"] - }, nil, nil, where) - end - end - def primary_keys(table_name) # :nodoc: pks = table_structure(table_name).select { |f| f["pk"] > 0 } pks.sort_by { |f| f["pk"] }.map { |f| f["name"] } @@ -403,14 +313,13 @@ module ActiveRecord def change_column(table_name, column_name, type, options = {}) #:nodoc: alter_table(table_name) do |definition| - include_default = options_include_default?(options) definition[column_name].instance_eval do self.type = type self.limit = options[:limit] if options.include?(:limit) - self.default = options[:default] if include_default + self.default = options[:default] if options.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 @@ -422,15 +331,34 @@ module ActiveRecord rename_column_indexes(table_name, column.name, new_column_name) end - protected + def add_reference(table_name, ref_name, **options) # :nodoc: + super(table_name, ref_name, type: :integer, **options) + end + alias :add_belongs_to :add_reference + + def foreign_keys(table_name) + fk_info = select_all("PRAGMA foreign_key_list(#{quote(table_name)})", "SCHEMA") + fk_info.map do |row| + options = { + column: row["from"], + primary_key: row["to"], + on_delete: extract_foreign_key_action(row["on_delete"]), + on_update: extract_foreign_key_action(row["on_update"]) + } + ForeignKeyDefinition.new(table_name, row["table"], options) + end + end + + 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? } @@ -441,12 +369,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 +400,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}" @@ -495,7 +423,7 @@ module ActiveRecord end end - def copy_table_contents(from, to, columns, rename = {}) #:nodoc: + 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) @@ -509,7 +437,7 @@ module ActiveRecord end def sqlite_version - @sqlite_version ||= SQLite3Adapter::Version.new(select_value("select sqlite_version(*)")) + @sqlite_version ||= SQLite3Adapter::Version.new(select_value("SELECT sqlite_version(*)")) end def translate_exception(exception, message) @@ -520,20 +448,25 @@ 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) + when /FOREIGN KEY constraint failed/i + InvalidForeignKey.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 +497,14 @@ module ActiveRecord basic_structure.to_hash end end + + def arel_visitor + Arel::Visitors::SQLite.new(self) + end + + def configure_connection + execute("PRAGMA foreign_keys = ON", "SCHEMA") + 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 2ede92feff..b8fbb489b6 100644 --- a/activerecord/lib/active_record/connection_handling.rb +++ b/activerecord/lib/active_record/connection_handling.rb @@ -1,6 +1,6 @@ module ActiveRecord module ConnectionHandling - RAILS_ENV = -> { (Rails.env if defined?(Rails.env)) || ENV["RAILS_ENV"] || ENV["RACK_ENV"] } + RAILS_ENV = -> { (Rails.env if defined?(Rails.env)) || ENV["RAILS_ENV"].presence || ENV["RACK_ENV"].presence } DEFAULT_ENV = -> { RAILS_ENV.call || "default_env" } # Establishes the connection to the database. Accepts a hash as input where diff --git a/activerecord/lib/active_record/core.rb b/activerecord/lib/active_record/core.rb index 622df0cfc1..8f78330d4a 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,7 +219,7 @@ module ActiveRecord statement.execute(hash.values, self, connection).first rescue TypeError raise ActiveRecord::StatementInvalid - rescue RangeError + rescue ::RangeError nil end end @@ -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,24 +297,24 @@ 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? - relation.where(type_condition).create_with(inheritance_column.to_sym => sti_name) + relation.where(type_condition).create_with(inheritance_column.to_s => sti_name) else relation 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 @@ -452,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.hash ^ self.id.hash + self.class.hash ^ id.hash else super end @@ -474,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 @@ -538,7 +536,7 @@ 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 @@ -561,7 +559,6 @@ module ActiveRecord @marked_for_destruction = false @destroyed_by_association = nil @new_record = true - @txn = nil @_start_transaction_state = {} @transaction_state = nil end diff --git a/activerecord/lib/active_record/counter_cache.rb b/activerecord/lib/active_record/counter_cache.rb index e2da512813..cbd71a3779 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,29 @@ 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 + touch_updates = touch_updates(touch) + updates << sanitize_sql_for_assignment(touch_updates) unless touch_updates.empty? + end + unscoped.where(primary_key => id).update_all updates.join(", ") end @@ -94,13 +123,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 +148,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 9a7a8d25bb..3a9625092e 100644 --- a/activerecord/lib/active_record/dynamic_matchers.rb +++ b/activerecord/lib/active_record/dynamic_matchers.rb @@ -1,17 +1,14 @@ -require "active_support/core_ext/regexp" - module ActiveRecord module DynamicMatchers #:nodoc: - def respond_to_missing?(name, include_private = false) - if self == Base - super - else - match = Method.match(self, name) - match && match.valid? || super - end - end - private + def respond_to_missing?(name, _) + if self == Base + super + else + match = Method.match(self, name) + match && match.valid? || super + end + end def method_missing(name, *arguments, &block) match = Method.match(self, name) diff --git a/activerecord/lib/active_record/enum.rb b/activerecord/lib/active_record/enum.rb index 0a94ab58dd..12ef58a941 100644 --- a/activerecord/lib/active_record/enum.rb +++ b/activerecord/lib/active_record/enum.rb @@ -95,8 +95,7 @@ module ActiveRecord module Enum def self.extended(base) # :nodoc: - base.class_attribute(:defined_enums, instance_writer: false) - base.defined_enums = {} + base.class_attribute(:defined_enums, instance_writer: false, default: {}) end def inherited(base) # :nodoc: @@ -140,6 +139,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 @@ -152,11 +153,12 @@ module ActiveRecord definitions.each do |name, values| # statuses = { } enum_values = ActiveSupport::HashWithIndifferentAccess.new - name = name.to_sym + name = name.to_s # def self.statuses() statuses end - detect_enum_conflict!(name, name.to_s.pluralize, true) - klass.singleton_class.send(:define_method, name.to_s.pluralize) { enum_values } + detect_enum_conflict!(name, name.pluralize, true) + singleton_class.send(:define_method, name.pluralize) { enum_values } + defined_enums[name] = enum_values detect_enum_conflict!(name, name) detect_enum_conflict!(name, "#{name}=") @@ -168,7 +170,7 @@ module ActiveRecord _enum_methods_module.module_eval do pairs = values.respond_to?(:each_pair) ? values.each_pair : values.each_with_index - pairs.each do |value, i| + pairs.each do |label, value| if enum_prefix == true prefix = "#{name}_" elsif enum_prefix @@ -180,23 +182,23 @@ module ActiveRecord suffix = "_#{enum_suffix}" end - value_method_name = "#{prefix}#{value}#{suffix}" - enum_values[value] = i + value_method_name = "#{prefix}#{label}#{suffix}" + enum_values[label] = value + label = label.to_s - # def active?() status == 0 end + # def active?() status == "active" end klass.send(:detect_enum_conflict!, name, "#{value_method_name}?") - define_method("#{value_method_name}?") { self[attr] == value.to_s } + define_method("#{value_method_name}?") { self[attr] == label } - # def active!() update! status: :active end + # def active!() update!(status: 0) end klass.send(:detect_enum_conflict!, name, "#{value_method_name}!") define_method("#{value_method_name}!") { update!(attr => value) } - # scope :active, -> { where status: 0 } + # scope :active, -> { where(status: 0) } klass.send(:detect_enum_conflict!, name, value_method_name, true) klass.scope value_method_name, -> { where(attr => value) } end end - defined_enums[name.to_s] = enum_values end end 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 5ba354d758..6cf2e01179 100644 --- a/activerecord/lib/active_record/fixture_set/file.rb +++ b/activerecord/lib/active_record/fixture_set/file.rb @@ -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 8b47fbdbe4..bad6542be2 100644 --- a/activerecord/lib/active_record/fixtures.rb +++ b/activerecord/lib/active_record/fixtures.rb @@ -70,13 +70,32 @@ module ActiveRecord # test. To ensure consistent data, the environment deletes the fixtures before running the load. # # In addition to being available in the database, the fixture's data may also be accessed by - # using a special dynamic method, which has the same name as the model, and accepts the - # name of the fixture to instantiate: + # using a special dynamic method, which has the same name as the model. # - # test "find" do + # Passing in a fixture name to this dynamic method returns the fixture matching this name: + # + # test "find one" do # assert_equal "Ruby on Rails", web_sites(:rubyonrails).name # end # + # Passing in multiple fixture names returns all fixtures matching these names: + # + # test "find all by name" do + # assert_equal 2, web_sites(:rubyonrails, :google).length + # end + # + # Passing in no arguments returns all fixtures: + # + # test "find all" do + # assert_equal 2, web_sites.length + # end + # + # Passing in any fixture name that does not exist will raise <tt>StandardError</tt>: + # + # test "find by name that does not exist" do + # assert_raise(StandardError) { web_sites(:reddit) } + # end + # # Alternatively, you may enable auto-instantiation of the fixture data. For instance, take the # following tests: # @@ -88,7 +107,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 +122,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 +434,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 +555,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 +616,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 +648,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 @@ -859,32 +878,12 @@ module ActiveRecord included do class_attribute :fixture_path, instance_writer: false - 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 + class_attribute :fixture_table_names, default: [] + class_attribute :fixture_class_names, default: {} + class_attribute :use_transactional_tests, default: true + class_attribute :use_instantiated_fixtures, default: false # true, false, or :no_instances + class_attribute :pre_loaded_fixtures, default: false + class_attribute :config, default: ActiveRecord::Base end module ClassMethods @@ -897,7 +896,7 @@ 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) @@ -921,6 +920,8 @@ module ActiveRecord define_method(accessor_name) do |*fixture_names| force_reload = fixture_names.pop if fixture_names.last == true || fixture_names.last == :reload + return_single_record = fixture_names.size == 1 + fixture_names = @loaded_fixtures[fs_name].fixtures.keys if fixture_names.empty? @fixture_cache[fs_name] ||= {} @@ -935,7 +936,7 @@ module ActiveRecord end end - instances.size == 1 ? instances.first : instances + return_single_record ? instances.first : instances end private accessor_name end @@ -982,6 +983,7 @@ module ActiveRecord @fixture_connections = enlist_fixture_connections @fixture_connections.each do |connection| connection.begin_transaction joinable: false + connection.pool.lock_thread = true end # When connections are established in the future, begin a transaction too @@ -997,6 +999,7 @@ module ActiveRecord if connection && !@fixture_connections.include?(connection) connection.begin_transaction joinable: false + connection.pool.lock_thread = true @fixture_connections << connection end end @@ -1019,6 +1022,7 @@ module ActiveRecord ActiveSupport::Notifications.unsubscribe(@connection_subscriber) if @connection_subscriber @fixture_connections.each do |connection| connection.rollback_transaction if connection.transaction_open? + connection.pool.lock_thread = false end @fixture_connections.clear else diff --git a/activerecord/lib/active_record/gem_version.rb b/activerecord/lib/active_record/gem_version.rb index f33456a744..1a937dbcf7 100644 --- a/activerecord/lib/active_record/gem_version.rb +++ b/activerecord/lib/active_record/gem_version.rb @@ -6,7 +6,7 @@ module ActiveRecord module VERSION MAJOR = 5 - MINOR = 1 + MINOR = 2 TINY = 0 PRE = "alpha" diff --git a/activerecord/lib/active_record/inheritance.rb b/activerecord/lib/active_record/inheritance.rb index a1d4f47372..5776807507 100644 --- a/activerecord/lib/active_record/inheritance.rb +++ b/activerecord/lib/active_record/inheritance.rb @@ -38,8 +38,7 @@ module ActiveRecord included do # Determines whether to store the full constant name including namespace when using STI. # This is true, by default. - class_attribute :store_full_sti_class, instance_writer: false - self.store_full_sti_class = true + class_attribute :store_full_sti_class, instance_writer: false, default: true end module ClassMethods @@ -130,16 +129,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. 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 +156,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) @@ -204,7 +216,7 @@ module ActiveRecord def subclass_from_attributes(attrs) attrs = attrs.to_h if attrs.respond_to?(:permitted?) if attrs.is_a?(Hash) - subclass_name = attrs.with_indifferent_access[inheritance_column] + subclass_name = attrs[inheritance_column] || attrs[inheritance_column.to_sym] if subclass_name.present? find_sti_class(subclass_name) diff --git a/activerecord/lib/active_record/integration.rb b/activerecord/lib/active_record/integration.rb index e4c7a55541..cf954852bc 100644 --- a/activerecord/lib/active_record/integration.rb +++ b/activerecord/lib/active_record/integration.rb @@ -7,17 +7,24 @@ module ActiveRecord included do ## # :singleton-method: - # Indicates the format used to generate the timestamp in the cache key. - # Accepts any of the symbols in <tt>Time::DATE_FORMATS</tt>. + # Indicates the format used to generate the timestamp in the cache key, if + # versioning is off. Accepts any of the symbols in <tt>Time::DATE_FORMATS</tt>. # # This is +:usec+, by default. - class_attribute :cache_timestamp_format, instance_writer: false - self.cache_timestamp_format = :usec + class_attribute :cache_timestamp_format, instance_writer: false, default: :usec + + ## + # :singleton-method: + # Indicates whether to use a stable #cache_key method that is accompanied + # by a changing version in the #cache_version method. + # + # This is +false+, by default until Rails 6.0. + class_attribute :cache_versioning, instance_writer: false, default: false 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 @@ -42,29 +49,62 @@ module ActiveRecord id && id.to_s # Be sure to stringify the id for routes end - # Returns a cache key that can be used to identify this record. + # Returns a stable cache key that can be used to identify this record. # # Product.new.cache_key # => "products/new" - # Product.find(5).cache_key # => "products/5" (updated_at not available) - # Person.find(5).cache_key # => "people/5-20071224150000" (updated_at available) + # Product.find(5).cache_key # => "products/5" # - # You can also pass a list of named timestamps, and the newest in the list will be - # used to generate the key: + # If ActiveRecord::Base.cache_versioning is turned off, as it was in Rails 5.1 and earlier, + # the cache key will also include a version. # - # Person.find(5).cache_key(:updated_at, :last_reviewed_at) + # Product.cache_versioning = false + # Person.find(5).cache_key # => "people/5-20071224150000" (updated_at available) 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}" + if cache_version && timestamp_names.none? + "#{model_name.cache_key}/#{id}" + else + timestamp = if timestamp_names.any? + ActiveSupport::Deprecation.warn(<<-MSG.squish) + Specifying a timestamp name for #cache_key has been deprecated in favor of + the explicit #cache_version method that can be overwritten. + MSG + + 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 + end + + # Returns a cache version that can be used together with the cache key to form + # a recyclable caching scheme. By default, the #updated_at column is used for the + # cache_version, but this method can be overwritten to return something else. + # + # Note, this method will return nil if ActiveRecord::Base.cache_versioning is set to + # +false+ (which it is by default until Rails 6.0). + def cache_version + if cache_versioning && timestamp = try(:updated_at) + timestamp.utc.to_s(:usec) + end + end + + # Returns a cache key along with the version. + def cache_key_with_version + if version = cache_version + "#{cache_key}-#{version}" + else + cache_key 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 8e8a97990a..3c7110369b 100644 --- a/activerecord/lib/active_record/locking/optimistic.rb +++ b/activerecord/lib/active_record/locking/optimistic.rb @@ -51,8 +51,7 @@ module ActiveRecord extend ActiveSupport::Concern included do - class_attribute :lock_optimistically, instance_writer: false - self.lock_optimistically = true + class_attribute :lock_optimistically, instance_writer: false, default: true end def locking_enabled? #:nodoc: @@ -60,13 +59,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 +75,24 @@ 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 + begin + lock_col = self.class.locking_column - attribute_names += [lock_col] - attribute_names.uniq! + previous_lock_value = read_attribute_before_type_cast(lock_col) + + increment_lock + + attribute_names.push(lock_col) - begin 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 +105,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 diff --git a/activerecord/lib/active_record/locking/pessimistic.rb b/activerecord/lib/active_record/locking/pessimistic.rb index e73cb4fc12..263e2a5f7f 100644 --- a/activerecord/lib/active_record/locking/pessimistic.rb +++ b/activerecord/lib/active_record/locking/pessimistic.rb @@ -59,7 +59,16 @@ module ActiveRecord # or pass true for "FOR UPDATE" (the default, an exclusive row lock). Returns # the locked record. def lock!(lock = true) - reload(lock: lock) if persisted? + if persisted? + if changed? + ActiveSupport::Deprecation.warn(<<-MSG.squish) + Locking a record with unpersisted changes is deprecated and will raise an + exception in Rails 5.2. Use `save` to persist the changes, or `reload` to + discard them explicitly. + MSG + end + reload(lock: lock) + end self end diff --git a/activerecord/lib/active_record/log_subscriber.rb b/activerecord/lib/active_record/log_subscriber.rb index f31931316c..2297c77835 100644 --- a/activerecord/lib/active_record/log_subscriber.rb +++ b/activerecord/lib/active_record/log_subscriber.rb @@ -15,31 +15,22 @@ module ActiveRecord rt 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 @@ -52,6 +43,20 @@ module ActiveRecord private + def type_casted_binds(binds, casted_binds) + casted_binds || ActiveRecord::Base.connection.type_casted_binds(binds) + end + + def render_bind(attr, value) + if attr.is_a?(Array) + attr = attr.first + elsif attr.type.binary? && attr.value + value = "<#{attr.value_for_database.to_s.bytesize} bytes of binary data>" + end + + [attr && 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) @@ -76,7 +81,7 @@ module ActiveRecord RED when /transaction\s*\Z/i CYAN - else + else MAGENTA end end diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb index 05568039d8..51c82f4ced 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 #{subclass} < ActiveRecord::Migration[4.2]" end end @@ -543,12 +548,10 @@ module ActiveRecord end def call(env) - if connection.supports_migrations? - mtime = ActiveRecord::Migrator.last_migration.mtime.to_i - if @last_check < mtime - ActiveRecord::Migration.check_pending!(connection) - @last_check = mtime - end + mtime = ActiveRecord::Migrator.last_migration.mtime.to_i + if @last_check < mtime + ActiveRecord::Migration.check_pending!(connection) + @last_check = mtime end @app.call(env) end @@ -687,7 +690,7 @@ module ActiveRecord connection.respond_to?(:reverting) && connection.reverting end - class ReversibleBlockHelper < Struct.new(:reverting) # :nodoc: + ReversibleBlockHelper = Struct.new(:reverting) do # :nodoc: def up yield unless reverting end @@ -767,7 +770,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 +798,7 @@ module ActiveRecord @connection = nil end - def write(text="") + def write(text = "") puts(text) if verbose end @@ -805,7 +808,7 @@ module ActiveRecord write "== %s %s" % [text, "=" * length] end - def say(message, subitem=false) + def say(message, subitem = false) write "#{subitem ? " ->" : "--"} #{message}" end @@ -933,7 +936,7 @@ module ActiveRecord # MigrationProxy is used to defer loading of the actual migration classes # until they are needed - class MigrationProxy < Struct.new(:name, :version, :filename, :scope) + MigrationProxy = Struct.new(:name, :version, :filename, :scope) do def initialize(name, version, filename, scope) super @migration = nil @@ -989,11 +992,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 @@ -1022,14 +1025,13 @@ module ActiveRecord def schema_migrations_table_name SchemaMigration.table_name end + deprecate :schema_migrations_table_name 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 SchemaMigration.table_exists? + SchemaMigration.all_versions.map(&:to_i) + else + [] end end @@ -1055,10 +1057,6 @@ module ActiveRecord Array(@migrations_paths) end - def match_to_migration_filename?(filename) # :nodoc: - Migration::MigrationFilenameRegexp.match?(File.basename(filename)) - end - def parse_migration_filename(filename) # :nodoc: File.basename(filename).scan(Migration::MigrationFilenameRegexp).first end @@ -1066,9 +1064,7 @@ module ActiveRecord def migrations(paths) paths = Array(paths) - files = Dir[*paths.map { |p| "#{p}/**/[0-9]*_*.rb" }] - - migrations = files.map do |file| + migrations = migration_files(paths).map do |file| version, name, scope = parse_migration_filename(file) raise IllegalMigrationNameError.new(file) unless version version = version.to_i @@ -1080,23 +1076,53 @@ module ActiveRecord migrations.sort_by(&:version) end + def migrations_status(paths) + paths = Array(paths) + + db_list = ActiveRecord::SchemaMigration.normalized_versions + + file_list = migration_files(paths).map do |file| + version, name, scope = parse_migration_filename(file) + raise IllegalMigrationNameError.new(file) unless version + version = ActiveRecord::SchemaMigration.normalize_migration_number(version) + status = db_list.delete(version) ? "up" : "down" + [status, version, (name + scope).humanize] + end.compact + + db_list.map! do |version| + ["up", version, "********** NO FILE **********"] + end + + (db_list + file_list).sort_by { |_, version, _| version } + end + + def migration_files(paths) + Dir[*paths.flat_map { |path| "#{path}/**/[0-9]*_*.rb" }] + end + private def move(direction, migrations_paths, steps) migrator = new(direction, migrations(migrations_paths)) - start_index = migrator.migrations.index(migrator.current_migration) - if start_index - finish = migrator.migrations[start_index + steps] - version = finish ? finish.version : 0 - send(direction, migrations_paths, version) + if current_version != 0 && !migrator.current_migration + raise UnknownMigrationVersionError.new(current_version) end + + start_index = + if current_version == 0 + 0 + else + migrator.migrations.index(migrator.current_migration) + end + + finish = migrator.migrations[start_index + steps] + version = finish ? finish.version : 0 + send(direction, migrations_paths, version) end end def initialize(direction, migrations, target_version = nil) - raise StandardError.new("This database does not yet support migrations") unless Base.connection.supports_migrations? - @direction = direction @target_version = target_version @migrated_versions = nil @@ -1104,8 +1130,8 @@ module ActiveRecord validate(@migrations) - Base.connection.initialize_schema_migrations_table - Base.connection.initialize_internal_metadata_table + ActiveRecord::SchemaMigration.create_table + ActiveRecord::InternalMetadata.create_table end def current_version @@ -1167,9 +1193,10 @@ module ActiveRecord 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. @@ -1178,11 +1205,12 @@ module ActiveRecord 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. @@ -1230,10 +1258,10 @@ module ActiveRecord end def validate(migrations) - name ,= migrations.group_by(&:name).find { |_,v| v.length > 1 } + name , = migrations.group_by(&:name).find { |_, v| v.length > 1 } raise DuplicateMigrationNameError.new(name) if name - version ,= migrations.group_by(&:version).find { |_,v| v.length > 1 } + version , = migrations.group_by(&:version).find { |_, v| v.length > 1 } raise DuplicateMigrationVersionError.new(version) if version end diff --git a/activerecord/lib/active_record/migration/command_recorder.rb b/activerecord/lib/active_record/migration/command_recorder.rb index 03103bba98..f9cf59b283 100644 --- a/activerecord/lib/active_record/migration/command_recorder.rb +++ b/activerecord/lib/active_record/migration/command_recorder.rb @@ -92,10 +92,6 @@ module ActiveRecord send(method, args, &block) end - def respond_to_missing?(*args) # :nodoc: - super || delegate.respond_to?(*args) - end - ReversibleAndIrreversibleMethods.each do |method| class_eval <<-EOV, __FILE__, __LINE__ + 1 def #{method}(*args, &block) # def create_table(*args, &block) @@ -225,10 +221,14 @@ module ActiveRecord [:add_foreign_key, reversed_args] end + def respond_to_missing?(method, _) + super || delegate.respond_to?(method) + end + # Forwards any missing method call to the \target. def method_missing(method, *args, &block) - if @delegate.respond_to?(method) - @delegate.send(method, *args, &block) + if delegate.respond_to?(method) + delegate.public_send(method, *args, &block) else super end diff --git a/activerecord/lib/active_record/migration/compatibility.rb b/activerecord/lib/active_record/migration/compatibility.rb index 04e538baa5..188dd0acef 100644 --- a/activerecord/lib/active_record/migration/compatibility.rb +++ b/activerecord/lib/active_record/migration/compatibility.rb @@ -11,9 +11,71 @@ module ActiveRecord const_get(name) end - V5_1 = Current + V5_2 = Current - module FourTwoShared + class V5_1 < V5_2 + end + + class V5_0 < V5_1 + module TableDefinition + def references(*args, **options) + super(*args, type: :integer, **options) + end + alias :belongs_to :references + end + + def create_table(table_name, options = {}) + if adapter_name == "PostgreSQL" + if options[:id] == :uuid && !options.key?(:default) + options[:default] = "uuid_generate_v4()" + end + end + + unless adapter_name == "Mysql2" && options[:id] == :bigint + if [:integer, :bigint].include?(options[:id]) && !options.key?(:default) + options[:default] = nil + 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. + unless options.key?(:id) + options[:id] = :integer + end + + if block_given? + super(table_name, options) do |t| + class << t + prepend TableDefinition + end + yield t + end + else + super + end + end + + def change_table(table_name, options = {}) + if block_given? + super(table_name, options) do |t| + class << t + prepend TableDefinition + end + yield t + end + else + super + end + end + + def add_reference(table_name, ref_name, **options) + super(table_name, ref_name, type: :integer, **options) + end + alias :add_belongs_to :add_reference + end + + class V4_2 < V5_0 module TableDefinition def references(*, **options) options[:index] ||= false @@ -86,13 +148,13 @@ module ActiveRecord def index_name_for_remove(table_name, options = {}) index_name = index_name(table_name, options) - unless index_name_exists?(table_name, index_name, true) + unless index_name_exists?(table_name, index_name) if options.is_a?(Hash) && options.has_key?(:name) options_without_column = options.dup options_without_column.delete :column index_name_without_column = index_name(table_name, options_without_column) - return index_name_without_column if index_name_exists?(table_name, index_name_without_column, false) + return index_name_without_column if index_name_exists?(table_name, index_name_without_column) end raise ArgumentError, "Index name '#{index_name}' on table '#{table_name}' does not exist" @@ -101,29 +163,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 76b3169411..1179a60e9b 100644 --- a/activerecord/lib/active_record/model_schema.rb +++ b/activerecord/lib/active_record/model_schema.rb @@ -1,78 +1,148 @@ +require "monitor" + 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 + class_attribute :table_name_prefix, instance_writer: false, default: "" + class_attribute :table_name_suffix, instance_writer: false, default: "" + class_attribute :schema_migrations_table_name, instance_accessor: false, default: "schema_migrations" + class_attribute :internal_metadata_table_name, instance_accessor: false, default: "ar_internal_metadata" + class_attribute :protected_environments, instance_accessor: false, default: [ "production" ] + class_attribute :pluralize_table_names, instance_writer: false, default: true + class_attribute :ignored_columns, instance_accessor: false, default: [].freeze self.inheritance_column = "type" delegate :type_for_attribute, to: :class + + initialize_load_schema_monitor end # Derives the join table name for +first_table+ and +second_table+. The @@ -213,7 +283,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. # @@ -298,7 +368,7 @@ module ActiveRecord # default values when instantiating the Active Record object for this table. def column_defaults load_schema - _default_attributes.to_hash + @column_defaults ||= _default_attributes.to_hash end def _default_attributes # :nodoc: @@ -353,17 +423,30 @@ module ActiveRecord connection.schema_cache.clear_data_source_cache!(table_name) reload_schema_from_cache + initialize_find_by_cache end + protected + + def initialize_load_schema_monitor + @load_schema_monitor = Monitor.new + end + private + def inherited(child_class) + super + child_class.initialize_load_schema_monitor + end + def schema_loaded? - defined?(@columns_hash) && @columns_hash + defined?(@schema_loaded) && @schema_loaded end def load_schema - unless schema_loaded? - load_schema! + return if schema_loaded? + @load_schema_monitor.synchronize do + load_schema! unless defined?(@columns_hash) && @columns_hash end end @@ -377,6 +460,8 @@ module ActiveRecord user_provided_default: false ) end + + @schema_loaded = true end def reload_schema_from_cache @@ -386,10 +471,12 @@ module ActiveRecord @attribute_types = nil @content_columns = nil @default_attributes = nil + @column_defaults = nil @inheritance_column = nil unless defined?(@explicit_inheritance_column) && @explicit_inheritance_column @attributes_builder = nil @columns = nil @columns_hash = nil + @schema_loaded = false @attribute_names = nil @yaml_encoder = nil direct_descendants.each do |descendant| diff --git a/activerecord/lib/active_record/nested_attributes.rb b/activerecord/lib/active_record/nested_attributes.rb index e983026961..917bc76993 100644 --- a/activerecord/lib/active_record/nested_attributes.rb +++ b/activerecord/lib/active_record/nested_attributes.rb @@ -10,8 +10,7 @@ module ActiveRecord extend ActiveSupport::Concern included do - class_attribute :nested_attributes_options, instance_writer: false - self.nested_attributes_options = {} + class_attribute :nested_attributes_options, instance_writer: false, default: {} end # = Active Record Nested Attributes @@ -393,7 +392,7 @@ module ActiveRecord # 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 @@ -452,13 +451,13 @@ module ActiveRecord # { 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 unless attributes_collection.is_a?(Hash) || attributes_collection.is_a?(Array) - raise ArgumentError, "Hash or Array expected, got #{attributes_collection.class.name} (#{attributes_collection.inspect})" + raise ArgumentError, "Hash or Array expected for attribute `#{association_name}`, got #{attributes_collection.class.name} (#{attributes_collection.inspect})" end check_record_limit!(options[:limit], attributes_collection) @@ -562,7 +561,7 @@ module ActiveRecord 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 diff --git a/activerecord/lib/active_record/null_relation.rb b/activerecord/lib/active_record/null_relation.rb index 254550c378..26966f9433 100644 --- a/activerecord/lib/active_record/null_relation.rb +++ b/activerecord/lib/active_record/null_relation.rb @@ -4,7 +4,7 @@ module ActiveRecord [] end - def delete_all(_conditions = nil) + def delete_all 0 end @@ -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 6933f3f9b8..f652c7c3a1 100644 --- a/activerecord/lib/active_record/persistence.rb +++ b/activerecord/lib/active_record/persistence.rb @@ -100,6 +100,10 @@ module ActiveRecord !(@new_record || @destroyed) end + ## + # :call-seq: + # save(*args) + # # Saves the model. # # If the model is new, a record gets created in the database, otherwise @@ -121,12 +125,16 @@ module ActiveRecord # # Attributes marked as readonly are silently ignored if the record is # being updated. - def save(*args) - create_or_update(*args) + def save(*args, &block) + create_or_update(*args, &block) rescue ActiveRecord::RecordInvalid false end + ## + # :call-seq: + # save!(*args) + # # Saves the model. # # If the model is new, a record gets created in the database, otherwise @@ -148,8 +156,10 @@ module ActiveRecord # # Attributes marked as readonly are silently ignored if the record is # being updated. - def save!(*args) - create_or_update(*args) || raise(RecordNotSaved.new("Failed to save the record", self)) + # + # Unless an error is raised, returns true. + def save!(*args, &block) + create_or_update(*args, &block) || raise(RecordNotSaved.new("Failed to save the record", self)) end # Deletes the record in the database and freezes this instance to @@ -181,7 +191,11 @@ module ActiveRecord _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 @@ -253,7 +267,11 @@ module ActiveRecord verify_readonly_attribute(name) public_send("#{name}=", value) - changed? ? save(validate: false) : true + 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 @@ -330,14 +348,16 @@ module ActiveRecord self end - # Wrapper around #increment that saves the record. This method differs from - # its non-bang version in that it passes through the attribute setter. - # Saving is not subjected to validation checks. Returns +true+ if the - # record could be saved. - def increment!(attribute, by = 1) + # Wrapper around #increment that writes the update to the database. + # Only +attribute+ is updated; the record itself is not saved. + # This means that any other modified attributes will still be dirty. + # Validations and callbacks are skipped. Supports the `touch` option from + # +update_counters+, see that for more. + # Returns +self+. + def increment!(attribute, by = 1, touch: nil) increment(attribute, by) - change = public_send(attribute) - (attribute_was(attribute.to_s) || 0) - self.class.update_counters(id, attribute => change) + change = public_send(attribute) - (attribute_in_database(attribute.to_s) || 0) + self.class.update_counters(id, attribute => change, touch: touch) clear_attribute_change(attribute) # eww self end @@ -349,12 +369,14 @@ module ActiveRecord increment(attribute, -by) end - # Wrapper around #decrement that saves the record. This method differs from - # its non-bang version in the sense that it passes through the attribute setter. - # Saving is not subjected to validation checks. Returns +true+ if the - # record could be saved. - def decrement!(attribute, by = 1) - increment!(attribute, -by) + # Wrapper around #decrement that writes the update to the database. + # Only +attribute+ is updated; the record itself is not saved. + # This means that any other modified attributes will still be dirty. + # Validations and callbacks are skipped. Supports the `touch` option from + # +update_counters+, see that for more. + # Returns +self+. + def decrement!(attribute, by = 1, touch: nil) + increment!(attribute, -by, touch: touch) end # Assigns to +attribute+ the boolean opposite of <tt>attribute?</tt>. So @@ -384,8 +406,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> @@ -515,6 +537,7 @@ module ActiveRecord raise ActiveRecord::StaleObjectError.new(self, "touch") end + @_trigger_update_callback = result result else true @@ -535,9 +558,9 @@ module ActiveRecord self.class.unscoped.where(self.class.primary_key => id) end - def create_or_update(*args) + def create_or_update(*args, &block) _raise_readonly_record_error if readonly? - result = new_record? ? _create_record : _update_record(*args) + result = new_record? ? _create_record(&block) : _update_record(*args, &block) result != false end @@ -546,10 +569,16 @@ 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 + + yield(self) if block_given? + + rows_affected end # Creates a record with values matching those of the instance attributes @@ -561,6 +590,9 @@ module ActiveRecord self.id ||= new_id if self.class.primary_key @new_record = false + + yield(self) if block_given? + id 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 36689f6559..b16e178358 100644 --- a/activerecord/lib/active_record/querying.rb +++ b/activerecord/lib/active_record/querying.rb @@ -8,8 +8,8 @@ module ActiveRecord delegate :destroy, :destroy_all, :delete, :delete_all, :update, :update_all, to: :all delegate :find_each, :find_in_batches, :in_batches, to: :all delegate :select, :group, :order, :except, :reorder, :limit, :offset, :joins, :left_joins, :left_outer_joins, :or, - :where, :rewhere, :preload, :eager_load, :includes, :from, :lock, :readonly, - :having, :create_with, :uniq, :distinct, :references, :none, :unscope, :merge, to: :all + :where, :rewhere, :preload, :eager_load, :includes, :from, :lock, :readonly, :extending, + :having, :create_with, :distinct, :references, :none, :unscope, :merge, to: :all delegate :count, :average, :minimum, :maximum, :sum, :calculate, to: :all delegate :pluck, :ids, to: :all diff --git a/activerecord/lib/active_record/railtie.rb b/activerecord/lib/active_record/railtie.rb index 989d23bc37..73518ca144 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 @@ -166,5 +166,13 @@ end_warning path = app.paths["db"].first config.watchable_files.concat ["#{path}/schema.rb", "#{path}/structure.sql"] end + + initializer "active_record.clear_active_connections" do + config.after_initialize do + ActiveSupport.on_load(:active_record) do + clear_active_connections! + end + end + end end end diff --git a/activerecord/lib/active_record/railties/controller_runtime.rb b/activerecord/lib/active_record/railties/controller_runtime.rb index 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 46235ab922..711099e9e1 100644 --- a/activerecord/lib/active_record/railties/databases.rake +++ b/activerecord/lib/active_record/railties/databases.rake @@ -77,6 +77,8 @@ db_namespace = namespace :db do namespace :migrate do # desc 'Rollbacks the database one migration and re migrate up (options: STEP=x, VERSION=x).' task redo: [:environment, :load_config] do + raise "Empty VERSION provided" if ENV["VERSION"] && ENV["VERSION"].empty? + if ENV["VERSION"] db_namespace["migrate:down"].invoke db_namespace["migrate:up"].invoke @@ -91,16 +93,17 @@ db_namespace = namespace :db do # desc 'Runs the "up" for a given migration VERSION.' task up: [:environment, :load_config] do + raise "VERSION is required" if !ENV["VERSION"] || ENV["VERSION"].empty? + version = ENV["VERSION"] ? ENV["VERSION"].to_i : nil - raise "VERSION is required" unless version ActiveRecord::Migrator.run(:up, ActiveRecord::Tasks::DatabaseTasks.migrations_paths, version) db_namespace["_dump"].invoke end # desc 'Runs the "down" for a given migration VERSION.' task down: [:environment, :load_config] do + raise "VERSION is required - To go down one migration, use db:rollback" if !ENV["VERSION"] || ENV["VERSION"].empty? version = ENV["VERSION"] ? ENV["VERSION"].to_i : nil - raise "VERSION is required - To go down one migration, run db:rollback" unless version ActiveRecord::Migrator.run(:down, ActiveRecord::Tasks::DatabaseTasks.migrations_paths, version) db_namespace["_dump"].invoke end @@ -110,28 +113,13 @@ db_namespace = namespace :db do unless ActiveRecord::SchemaMigration.table_exists? abort "Schema migrations table does not exist yet." end - db_list = ActiveRecord::SchemaMigration.normalized_versions - - file_list = - ActiveRecord::Tasks::DatabaseTasks.migrations_paths.flat_map do |path| - Dir.foreach(path).map do |file| - next unless ActiveRecord::Migrator.match_to_migration_filename?(file) - - version, name, scope = ActiveRecord::Migrator.parse_migration_filename(file) - version = ActiveRecord::SchemaMigration.normalize_migration_number(version) - status = db_list.delete(version) ? "up" : "down" - [status, version, (name + scope).humanize] - end.compact - end - db_list.map! do |version| - ["up", version, "********** NO FILE **********"] - end # output puts "\ndatabase: #{ActiveRecord::Base.connection_config[:database]}\n\n" puts "#{'Status'.center(8)} #{'Migration ID'.ljust(14)} Migration Name" puts "-" * 50 - (db_list + file_list).sort_by { |_, version, _| version }.each do |status, version, name| + paths = ActiveRecord::Tasks::DatabaseTasks.migrations_paths + ActiveRecord::Migrator.migrations_status(paths).each do |status, version, name| puts "#{status.center(8)} #{version.ljust(14)} #{name}" end puts @@ -265,19 +253,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 @@ -291,8 +276,7 @@ db_namespace = namespace :db do current_config = ActiveRecord::Tasks::DatabaseTasks.current_config ActiveRecord::Tasks::DatabaseTasks.structure_dump(current_config, filename) - if ActiveRecord::Base.connection.supports_migrations? && - ActiveRecord::SchemaMigration.table_exists? + if ActiveRecord::SchemaMigration.table_exists? File.open(filename, "a") do |f| f.puts ActiveRecord::Base.connection.dump_schema_information f.print "\n" @@ -312,14 +296,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 +324,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..af6473d250 100644 --- a/activerecord/lib/active_record/readonly_attributes.rb +++ b/activerecord/lib/active_record/readonly_attributes.rb @@ -3,15 +3,14 @@ module ActiveRecord extend ActiveSupport::Concern included do - class_attribute :_attr_readonly, instance_accessor: false - self._attr_readonly = [] + class_attribute :_attr_readonly, instance_accessor: false, default: [] end module ClassMethods # Attributes listed as readonly will be used to create a new record but update operations will # ignore these fields. def attr_readonly(*attributes) - self._attr_readonly = Set.new(attributes.map(&: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 9b692f55d2..e8ee8279fd 100644 --- a/activerecord/lib/active_record/reflection.rb +++ b/activerecord/lib/active_record/reflection.rb @@ -1,5 +1,6 @@ require "thread" require "active_support/core_ext/string/filters" +require "active_support/deprecation" module ActiveRecord # = Active Record Reflection @@ -7,10 +8,8 @@ module ActiveRecord extend ActiveSupport::Concern included do - class_attribute :_reflections, instance_writer: false - class_attribute :aggregate_reflections, instance_writer: false - self._reflections = {} - self.aggregate_reflections = {} + class_attribute :_reflections, instance_writer: false, default: {} + class_attribute :aggregate_reflections, instance_writer: false, default: {} end def self.create(macro, name, scope, options, ar) @@ -136,8 +135,8 @@ module ActiveRecord # BelongsToReflection # HasAndBelongsToManyReflection # ThroughReflection - # PolymorphicReflection - # RuntimeReflection + # PolymorphicReflection + # RuntimeReflection class AbstractReflection # :nodoc: def through_reflection? false @@ -171,12 +170,47 @@ module ActiveRecord JoinKeys = Struct.new(:key, :foreign_key) # :nodoc: - def join_keys(association_klass) - JoinKeys.new(foreign_key, active_record_primary_key) + def join_keys + get_join_keys klass + end + + # Returns a list of scopes that should be applied for this Reflection + # object when querying the database. + def scopes + scope ? [scope] : [] + end + + def scope_chain + chain.map(&:scopes) + end + deprecate :scope_chain + + def join_scopes(table, predicate_builder) # :nodoc: + if scope + [ActiveRecord::Relation.create(klass, table, predicate_builder) + .instance_exec(&scope)] + else + [] + end + end + + def klass_join_scope(table, predicate_builder) # :nodoc: + if klass.current_scope + klass.current_scope.clone.tap { |scope| + scope.joins_values = scope.left_outer_joins_values = [].freeze + } + else + relation = ActiveRecord::Relation.create( + klass, + table, + predicate_builder, + ) + klass.send(:build_default_scope, relation) + end end def constraints - scope_chain.flatten + chain.flat_map(&:scopes) end def counter_cache_column @@ -248,6 +282,25 @@ module ActiveRecord def chain collect_join_chain end + + def get_join_keys(association_klass) + JoinKeys.new(join_pk(association_klass), join_fk) + end + + protected + def actual_source_reflection # FIXME: this is a horrible name + self + end + + private + + def join_pk(_) + foreign_key + end + + def join_fk + active_record_primary_key + end end # Base class for AggregateReflection and AssociationReflection. Objects of @@ -282,7 +335,6 @@ module ActiveRecord end def autosave=(autosave) - @automatic_inverse_of = false @options[:autosave] = autosave parent_reflection = self.parent_reflection if parent_reflection @@ -322,7 +374,7 @@ module ActiveRecord end end - # Holds all the meta-data about an aggregation as it was specified in the + # Holds all the metadata about an aggregation as it was specified in the # Active Record class. class AggregateReflection < MacroReflection #:nodoc: def mapping @@ -331,7 +383,7 @@ module ActiveRecord end end - # Holds all the meta-data about an association as it was specified in the + # Holds all the metadata about an association as it was specified in the # Active Record class. class AssociationReflection < MacroReflection #:nodoc: # Returns the target association's class. @@ -365,6 +417,17 @@ module ActiveRecord @constructable = calculate_constructable(macro, options) @association_scope_cache = {} @scope_lock = Mutex.new + + if options[:class_name] && options[:class_name].class == Class + ActiveSupport::Deprecation.warn(<<-MSG.squish) + Passing a class to the `class_name` is deprecated and will raise + an ArgumentError in Rails 5.2. It eagerloads more classes than + necessary and potentially creates circular dependencies. + + Please pass the class name as a string: + `#{macro} :#{name}, class_name: '#{options[:class_name]}'` + MSG + end end def association_scope_cache(conn, owner) @@ -398,6 +461,10 @@ module ActiveRecord options[:primary_key] || primary_key(klass || self.klass) end + def association_primary_key_type + klass.type_for_attribute(association_primary_key.to_s) + end + def active_record_primary_key @active_record_primary_key ||= options[:primary_key] || primary_key(active_record) end @@ -447,12 +514,6 @@ module ActiveRecord false end - # An array of arrays of scopes. Each item in the outside array corresponds to a reflection - # in the #chain. - def scope_chain - scope ? [[scope]] : [[]] - end - def has_scope? scope end @@ -523,11 +584,9 @@ module ActiveRecord seed + [self] end - protected - - def actual_source_reflection # FIXME: this is a horrible name - self - end + def extensions + Array(options[:extend]) + end private @@ -537,14 +596,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 @@ -671,11 +726,6 @@ module ActiveRecord end end - def join_keys(association_klass) - key = polymorphic? ? association_primary_key(association_klass) : association_primary_key - JoinKeys.new(key, foreign_key) - end - def join_id_for(owner) # :nodoc: owner[foreign_key] end @@ -685,6 +735,14 @@ module ActiveRecord def calculate_constructable(macro, options) !polymorphic? end + + def join_fk + foreign_key + end + + def join_pk(klass) + polymorphic? ? association_primary_key(klass) : association_primary_key + end end class HasAndBelongsToManyReflection < AssociationReflection # :nodoc: @@ -699,16 +757,15 @@ module ActiveRecord end end - # Holds all the meta-data about a :through association as it was specified + # Holds all the metadata about a :through association as it was specified # in the Active Record class. class ThroughReflection < AbstractReflection #:nodoc: - attr_reader :delegate_reflection delegate :foreign_key, :foreign_type, :association_foreign_key, - :active_record_primary_key, :type, to: :source_reflection + :active_record_primary_key, :type, :get_join_keys, to: :source_reflection 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 @@ -786,45 +843,16 @@ module ActiveRecord through_reflection.clear_association_scope_cache end - # Consider the following example: - # - # class Person - # has_many :articles - # has_many :comment_tags, through: :articles - # end - # - # class Article - # has_many :comments - # has_many :comment_tags, through: :comments, source: :tags - # end - # - # class Comment - # has_many :tags - # end - # - # There may be scopes on Person.comment_tags, Article.comment_tags and/or Comment.tags, - # but only Comment.tags will be represented in the #chain. So this method creates an array - # of scopes corresponding to the chain. - def scope_chain - @scope_chain ||= begin - scope_chain = source_reflection.scope_chain.map(&:dup) - - # Add to it the scope from this reflection (if any) - scope_chain.first << scope if scope - - through_scope_chain = through_reflection.scope_chain.map(&:dup) + def scopes + source_reflection.scopes + super + end - if options[:source_type] - type = foreign_type - source_type = options[:source_type] - through_scope_chain.first << lambda { |object| - where(type => source_type) - } - end + def join_scopes(table, predicate_builder) # :nodoc: + source_reflection.join_scopes(table, predicate_builder) + super + end - # Recursively fill out the rest of the array from the through reflection - scope_chain + through_scope_chain - end + def source_type_scope + through_reflection.klass.where(foreign_type => options[:source_type]) end def has_scope? @@ -833,10 +861,6 @@ module ActiveRecord through_reflection.has_scope? end - def join_keys(association_klass) - source_reflection.join_keys(association_klass) - end - # A through association is nested if there would be more than one join table def nested? source_reflection.through_reflection? || through_reflection.through_reflection? @@ -851,6 +875,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.to_s) + end + # Gets an array of possible <tt>:through</tt> source reflection names in both singular and plural form. # # class Post < ActiveRecord::Base @@ -875,15 +903,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 @@ -930,6 +956,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 @@ -951,19 +985,23 @@ module ActiveRecord collect_join_reflections(seed + [self]) end - def collect_join_reflections(seed) - a = source_reflection.add_as_source seed - if options[:source_type] - through_reflection.add_as_polymorphic_through self, a - else - through_reflection.add_as_through a - 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 :delegate_reflection def actual_source_reflection # FIXME: this is a horrible name - source_reflection.send(:actual_source_reflection) + source_reflection.actual_source_reflection + end + + private + def collect_join_reflections(seed) + a = source_reflection.add_as_source seed + if options[:source_type] + through_reflection.add_as_polymorphic_through self, a + else + through_reflection.add_as_through a + end end def primary_key(klass) @@ -972,7 +1010,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 @@ -984,12 +1021,30 @@ 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 end + def scopes + scopes = @previous_reflection.scopes + if @previous_reflection.options[:source_type] + scopes + [@previous_reflection.source_type_scope] + else + scopes + end + end + + def join_scopes(table, predicate_builder) # :nodoc: + scopes = @previous_reflection.join_scopes(table, predicate_builder) + super + if @previous_reflection.options[:source_type] + scopes + [@previous_reflection.source_type_scope] + else + scopes + end + end + def klass @reflection.klass end @@ -1006,10 +1061,6 @@ module ActiveRecord @reflection.plural_name end - def join_keys(association_klass) - @reflection.join_keys(association_klass) - end - def type @reflection.type end @@ -1023,6 +1074,10 @@ module ActiveRecord source_type = @previous_reflection.options[:source_type] lambda { |object| where(type => source_type) } end + + def get_join_keys(association_klass) + @reflection.get_join_keys(association_klass) + end end class RuntimeReflection < PolymorphicReflection # :nodoc: @@ -1054,7 +1109,7 @@ module ActiveRecord end def alias_name - Arel::Table.new(table_name) + Arel::Table.new(table_name, type_caster: klass.type_caster) end def all_includes; yield; end diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb index ef629dcb3b..7a8f9abb36 100644 --- a/activerecord/lib/active_record/relation.rb +++ b/activerecord/lib/active_record/relation.rb @@ -2,7 +2,7 @@ module ActiveRecord # = Active Record \Relation class Relation MULTI_VALUE_METHODS = [:includes, :eager_load, :preload, :select, :group, - :order, :joins, :left_joins, :left_outer_joins, :references, + :order, :joins, :left_outer_joins, :references, :extending, :unscope] SINGLE_VALUE_METHODS = [:limit, :offset, :lock, :readonly, :reordering, @@ -18,6 +18,7 @@ module ActiveRecord attr_reader :table, :klass, :loaded, :predicate_builder alias :model :klass alias :loaded? :loaded + alias :locked? :locked def initialize(klass, table, predicate_builder, values = {}) @klass = klass @@ -261,10 +262,6 @@ module ActiveRecord coder.represent_seq(nil, records) end - def as_json(options = nil) #:nodoc: - records.as_json(options) - end - # Returns size of the records. def size loaded? ? @records.length : count(:all) @@ -273,8 +270,7 @@ module ActiveRecord # Returns true if there are no records. def empty? return @records.empty? if loaded? - - limit_value == 0 || !exists? + !exists? end # Returns true if there are no records. @@ -362,6 +358,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? @@ -370,7 +369,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) @@ -405,9 +404,9 @@ module ActiveRecord # # Note: Updating a large number of records will run an # UPDATE query for each record, which may cause a performance - # issue. So if it is not needed to run callbacks for each update, it is - # preferred to use #update_all for updating all records using - # a single query. + # issue. When running callbacks is not needed for each record update, + # it is preferred to use #update_all for updating all records + # in a single query. def update(id = :all, attributes) if id.is_a?(Array) id.map.with_index { |one_id, idx| update(one_id, attributes[idx]) } @@ -415,8 +414,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 @@ -443,16 +441,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, @@ -500,7 +490,7 @@ module ActiveRecord # # Post.limit(100).delete_all # # => ActiveRecord::ActiveRecordError: delete_all doesn't support limit - def delete_all(conditions = nil) + 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? @@ -509,27 +499,19 @@ module ActiveRecord 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 @@ -627,15 +609,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 @@ -662,7 +635,9 @@ module ActiveRecord end def inspect - entries = records.take([limit_value, 11].compact.min).map!(&:inspect) + subject = loaded? ? records : self + entries = subject.take([limit_value, 11].compact.min).map!(&:inspect) + entries[10] = "..." if entries.size == 11 "#<#{self.class.name} [#{entries.join(', ')}]>" @@ -677,13 +652,18 @@ module ActiveRecord private + 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..13a2c3f511 100644 --- a/activerecord/lib/active_record/relation/batches.rb +++ b/activerecord/lib/active_record/relation/batches.rb @@ -30,14 +30,14 @@ module ActiveRecord # end # # ==== Options - # * <tt>:batch_size</tt> - Specifies the size of the batch. Default to 1000. + # * <tt>:batch_size</tt> - Specifies the size of the batch. Defaults to 1000. # * <tt>:start</tt> - Specifies the primary key value to start from, inclusive of the value. # * <tt>:finish</tt> - Specifies the primary key value to end at, inclusive of the value. # * <tt>:error_on_ignore</tt> - Overrides the application config to specify if an error should be raised when - # an order is present in the relation. + # an order is present in the relation. # # Limits are honored, and if present there is no requirement for the batch - # size, it can be less than, equal, or greater than the limit. + # size: it can be less than, equal to, or greater than the limit. # # The options +start+ and +finish+ are especially useful if you want # multiple workers dealing with the same processing queue. You can make @@ -89,14 +89,14 @@ module ActiveRecord # To be yielded each record one by one, use #find_each instead. # # ==== Options - # * <tt>:batch_size</tt> - Specifies the size of the batch. Default to 1000. + # * <tt>:batch_size</tt> - Specifies the size of the batch. Defaults to 1000. # * <tt>:start</tt> - Specifies the primary key value to start from, inclusive of the value. # * <tt>:finish</tt> - Specifies the primary key value to end at, inclusive of the value. # * <tt>:error_on_ignore</tt> - Overrides the application config to specify if an error should be raised when - # an order is present in the relation. + # an order is present in the relation. # # Limits are honored, and if present there is no requirement for the batch - # size, it can be less than, equal, or greater than the limit. + # size: it can be less than, equal to, or greater than the limit. # # The options +start+ and +finish+ are especially useful if you want # multiple workers dealing with the same processing queue. You can make @@ -140,9 +140,9 @@ module ActiveRecord # If you do not provide a block to #in_batches, it will return a # BatchEnumerator which is enumerable. # - # Person.in_batches.with_index do |relation, batch_index| + # Person.in_batches.each_with_index do |relation, batch_index| # puts "Processing relation ##{batch_index}" - # relation.each { |relation| relation.delete_all } + # relation.delete_all # end # # Examples of calling methods on the returned BatchEnumerator object: @@ -152,12 +152,12 @@ module ActiveRecord # Person.in_batches.each_record(&:party_all_night!) # # ==== Options - # * <tt>:of</tt> - Specifies the size of the batch. Default to 1000. - # * <tt>:load</tt> - Specifies if the relation should be loaded. Default to false. + # * <tt>:of</tt> - Specifies the size of the batch. Defaults to 1000. + # * <tt>:load</tt> - Specifies if the relation should be loaded. Defaults to false. # * <tt>:start</tt> - Specifies the primary key value to start from, inclusive of the value. # * <tt>:finish</tt> - Specifies the primary key value to end at, inclusive of the value. # * <tt>:error_on_ignore</tt> - Overrides the application config to specify if an error should be raised when - # an order is present in the relation. + # an order is present in the relation. # # Limits are honored, and if present there is no requirement for the batch # size, it can be less than, equal, or greater than the limit. @@ -186,7 +186,7 @@ module ActiveRecord # # NOTE: It's not possible to set the order. That is automatically set to # ascending on the primary key ("id ASC") to make the batch ordering - # consistent. Therefore the primary key must be orderable, e.g an integer + # consistent. Therefore the primary key must be orderable, e.g. an integer # or a string. # # NOTE: By its nature, batch processing is subject to race conditions if @@ -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 a887be8a20..c562f214c9 100644 --- a/activerecord/lib/active_record/relation/calculations.rb +++ b/activerecord/lib/active_record/relation/calculations.rb @@ -38,10 +38,16 @@ module ActiveRecord # between databases. In invalid cases, an error from the database is thrown. def count(column_name = nil) if block_given? - to_a.count { |*block_args| yield(*block_args) } - else - calculate(:count, column_name) + unless column_name.nil? + ActiveSupport::Deprecation.warn \ + "When `count' is called with a block, it ignores other arguments. " \ + "This behavior is now deprecated and will result in an ArgumentError in Rails 5.3." + end + + return super() end + + calculate(:count, column_name) end # Calculates the average value on a given column. Returns +nil+ if there's @@ -75,8 +81,17 @@ module ActiveRecord # #calculate for examples with options. # # Person.sum(:age) # => 4562 - def sum(column_name = nil, &block) - return super(&block) if block_given? + def sum(column_name = nil) + if block_given? + unless column_name.nil? + ActiveSupport::Deprecation.warn \ + "When `sum' is called with a block, it ignores other arguments. " \ + "This behavior is now deprecated and will result in an ArgumentError in Rails 5.3." + end + + return super() + end + calculate(:sum, column_name) end @@ -112,10 +127,6 @@ 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) relation = construct_relation_for_association_calculations relation = relation.distinct if operation.to_s.downcase == "count" @@ -197,7 +208,7 @@ 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 @@ -215,8 +226,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 @@ -227,17 +238,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).distinct!(false) + column = aggregate_column(column_name) select_value = operation_over_aggregate_column(column, operation, distinct) @@ -286,7 +297,7 @@ module ActiveRecord operation, distinct).as(aggregate_alias) ] - select_values += select_values unless having_clause.empty? + select_values += self.select_values unless having_clause.empty? select_values.concat group_columns.map { |aliaz, field| if field.respond_to?(:as) @@ -296,7 +307,7 @@ module ActiveRecord end } - relation = except(:group) + relation = except(:group).distinct!(false) relation.group_values = group_fields relation.select_values = select_values @@ -375,9 +386,8 @@ module ActiveRecord relation.select_values = [aliased_column] subquery = relation.arel.as(subquery_alias) - sm = Arel::SelectManager.new relation.engine select_value = operation_over_aggregate_column(column_alias, "count", distinct) - sm.project(select_value).from(subquery) + Arel::SelectManager.new(subquery).project(select_value) end end end diff --git a/activerecord/lib/active_record/relation/delegation.rb b/activerecord/lib/active_record/relation/delegation.rb index d16de4b06c..dada0fddf8 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 @@ -36,13 +36,16 @@ module ActiveRecord # may vary depending on the klass of a relation, so we create a subclass of Relation # for each different klass, and the delegations are compiled into that subclass only. - delegate :to_xml, :encode_with, :length, :collect, :map, :each, :all?, :include?, :to_ary, :join, + delegate :to_xml, :encode_with, :length, :each, :uniq, :to_ary, :join, :[], :&, :|, :+, :-, :sample, :reverse, :compact, :in_groups, :in_groups_of, + :to_sentence, :to_formatted_s, :as_json, :shuffle, :split, :index, to: :records delegate :table_name, :quoted_table_name, :primary_key, :quoted_primary_key, :connection, :columns_hash, to: :klass + delegate :ast, :locked, to: :arel + module ClassSpecificRelation # :nodoc: extend ActiveSupport::Concern @@ -81,7 +84,7 @@ module ActiveRecord end end - protected + private def method_missing(method, *args, &block) if @klass.respond_to?(method) @@ -108,12 +111,10 @@ module ActiveRecord end end - 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 respond_to_missing?(method, _) + super || @klass.respond_to?(method) || arel.respond_to?(method) + end 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 5e580ac865..1d661fa8ed 100644 --- a/activerecord/lib/active_record/relation/finder_methods.rb +++ b/activerecord/lib/active_record/relation/finder_methods.rb @@ -17,8 +17,8 @@ module ActiveRecord # Person.where("administrator = 1").order("created_on DESC").find(1) # # NOTE: The returned records may not be in the same order as the ids you - # provide since database rows are unordered. You'd need to provide an explicit QueryMethods#order - # option if you want the results are sorted. + # provide since database rows are unordered. You will need to provide an explicit QueryMethods#order + # option if you want the results to be sorted. # # ==== Find with lock # @@ -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 @@ -147,19 +147,11 @@ module ActiveRecord def last(limit = nil) return find_last(limit) if loaded? || limit_value - result = limit(limit || 1) + result = limit(limit) result.order!(arel_attribute(primary_key)) if order_values.empty? && primary_key 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 @@ -309,31 +301,23 @@ 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 end - return false if !conditions + return false if !conditions || limit_value == 0 - relation = apply_join_dependency(self, construct_join_dependency(eager_loading: false)) - return false if ActiveRecord::NullRelation === relation + relation = self unless eager_loading? + relation ||= apply_join_dependency(self, construct_join_dependency(eager_loading: false)) - relation = relation.except(:select, :order).select(ONE_AS_ONE).limit(1) + return false if ActiveRecord::NullRelation === relation - case conditions - when Array, Hash - relation = relation.where(conditions) - else - unless conditions == :none - relation = relation.where(primary_key => conditions) - end - end + relation = construct_relation_for_exists(relation, conditions) connection.select_value(relation, "#{name} Exists", relation.bound_attributes) ? true : false - rescue RangeError + rescue ::RangeError false end @@ -345,7 +329,7 @@ module ActiveRecord # of results obtained should be provided in the +result_size+ argument and # the expected number of results should be provided in the +expected_size+ # argument. - def raise_record_not_found_exception!(ids = nil, result_size = nil, expected_size = nil) # :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 name = @klass.name @@ -353,15 +337,15 @@ module ActiveRecord if ids.nil? error = "Couldn't find #{name}" error << " with#{conditions}" if conditions - raise RecordNotFound, error + raise RecordNotFound.new(error, name) elsif Array(ids).size == 1 - error = "Couldn't find #{name} with '#{primary_key}'=#{ids}#{conditions}" - raise RecordNotFound.new(error, name, primary_key, ids) + error = "Couldn't find #{name} with '#{key}'=#{ids}#{conditions}" + raise RecordNotFound.new(error, name, key, ids) else - error = "Couldn't find all #{name.pluralize} with '#{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})" - raise RecordNotFound, error + raise RecordNotFound.new(error, name, primary_key, ids) end end @@ -400,6 +384,19 @@ module ActiveRecord end end + def construct_relation_for_exists(relation, conditions) + relation = relation.except(:select, :distinct, :order)._select!(ONE_AS_ONE).limit!(1) + + case conditions + when Array, Hash + relation.where!(conditions) + else + relation.where!(primary_key => conditions) unless conditions == :none + end + + relation + end + def construct_join_dependency(joins = [], eager_loading: true) including = eager_load_values + includes_values ActiveRecord::Associations::JoinDependency.new(@klass, including, joins, eager_loading: eager_loading) @@ -410,8 +407,7 @@ module ActiveRecord end def apply_join_dependency(relation, join_dependency) - relation = relation.except(:includes, :eager_load, :preload) - relation = relation.joins join_dependency + relation = relation.except(:includes, :eager_load, :preload).joins!(join_dependency) if using_limitable_reflections?(join_dependency.reflections) relation @@ -439,8 +435,6 @@ module ActiveRecord reflections.none?(&:collection?) end - protected - def find_with_ids(*ids) raise UnknownPrimaryKey.new(@klass) if primary_key.nil? @@ -458,14 +452,13 @@ module ActiveRecord else find_some(ids) end - rescue RangeError + 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) + 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 @@ -548,8 +541,12 @@ module ActiveRecord self end - relation = relation.offset(offset_index + index) unless index.zero? - relation.limit(limit).to_a + if limit_value.nil? || index < limit_value + relation = relation.offset(offset_index + index) unless index.zero? + relation.limit(limit).to_a + else + [] + end end end @@ -572,8 +569,6 @@ module ActiveRecord end end - private - def find_last(limit) limit ? records.last(limit) : records.last end diff --git a/activerecord/lib/active_record/relation/predicate_builder.rb b/activerecord/lib/active_record/relation/predicate_builder.rb index 780a1ee422..a6309e0b5c 100644 --- a/activerecord/lib/active_record/relation/predicate_builder.rb +++ b/activerecord/lib/active_record/relation/predicate_builder.rb @@ -1,14 +1,5 @@ module ActiveRecord class PredicateBuilder # :nodoc: - require "active_record/relation/predicate_builder/array_handler" - 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" - delegate :resolve_column_aliases, to: :table def initialize(table) @@ -16,14 +7,11 @@ 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) register_handler(Relation, RelationHandler.new) register_handler(Array, ArrayHandler.new(self)) - register_handler(AssociationQueryValue, AssociationQueryHandler.new(self)) - register_handler(PolymorphicArrayValue, PolymorphicArrayHandler.new(self)) end def build_from_hash(attributes) @@ -66,6 +54,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 @@ -92,9 +82,27 @@ module ActiveRecord attrs, bvs = associated_predicate_builder(column_name).create_binds_for_hash(value) result[column_name] = attrs binds += bvs - next - when value.is_a?(Relation) - binds += value.bound_attributes + when table.associated_with?(column_name) + # Find the foreign key when using queries such as: + # Post.where(author: author) + # + # For polymorphic relationships, find the foreign key and type: + # PriceEstimate.where(estimate_of: treasure) + associated_table = table.associated_table(column_name) + if associated_table.polymorphic_association? + case value.is_a?(Array) ? value.first : value + when Base, Relation + value = [value] unless value.is_a?(Array) + klass = PolymorphicArrayValue + end + end + + klass ||= AssociationQueryValue + result[column_name] = klass.new(associated_table, value).queries.map do |query| + attrs, bvs = create_binds_for_hash(query) + binds.concat(bvs) + attrs + end when value.is_a?(Range) && !table.type(column_name).respond_to?(:subtype) first = value.begin last = value.end @@ -112,17 +120,10 @@ module ActiveRecord if can_be_bound?(column_name, value) result[column_name] = Arel::Nodes::BindParam.new binds << build_bind_param(column_name, value) + elsif value.is_a?(Relation) + binds.concat(value.bound_attributes) 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] @@ -155,7 +156,6 @@ module ActiveRecord end def can_be_bound?(column_name, value) - return if table.associated_with?(column_name) case value when Array, Range table.type(column_name).respond_to?(:subtype) @@ -169,3 +169,12 @@ module ActiveRecord end end end + +require "active_record/relation/predicate_builder/array_handler" +require "active_record/relation/predicate_builder/base_handler" +require "active_record/relation/predicate_builder/basic_object_handler" +require "active_record/relation/predicate_builder/range_handler" +require "active_record/relation/predicate_builder/relation_handler" + +require "active_record/relation/predicate_builder/association_query_value" +require "active_record/relation/predicate_builder/polymorphic_array_value" diff --git a/activerecord/lib/active_record/relation/predicate_builder/array_handler.rb b/activerecord/lib/active_record/relation/predicate_builder/array_handler.rb index 6400caba06..1068e700e2 100644 --- a/activerecord/lib/active_record/relation/predicate_builder/array_handler.rb +++ b/activerecord/lib/active_record/relation/predicate_builder/array_handler.rb @@ -6,11 +6,11 @@ module ActiveRecord end def call(attribute, value) + return attribute.in([]) if value.empty? + return queries_predicates(value) if value.all? { |v| v.is_a?(Hash) } + values = value.map { |x| x.is_a?(Base) ? x.id : x } nils, values = values.partition(&:nil?) - - return attribute.in([]) if values.empty? && nils.empty? - ranges, values = values.partition { |v| v.is_a?(Range) } values_predicate = @@ -26,9 +26,11 @@ module ActiveRecord array_predicates = ranges.map { |range| predicate_builder.build(attribute, range) } array_predicates.unshift(values_predicate) - array_predicates.inject { |composite, predicate| composite.or(predicate) } + array_predicates.inject(&:or) end + # TODO Change this to private once we've dropped Ruby 2.2 support. + # Workaround for Ruby 2.2 "private attribute?" warning. protected attr_reader :predicate_builder @@ -38,6 +40,17 @@ module ActiveRecord other end end + + private + def queries_predicates(queries) + if queries.size > 1 + queries.map do |query| + Arel::Nodes::And.new(predicate_builder.build_from_hash(query)) + end.inject(&:or) + else + predicate_builder.build_from_hash(queries.first) + end + end end end end diff --git a/activerecord/lib/active_record/relation/predicate_builder/association_query_handler.rb b/activerecord/lib/active_record/relation/predicate_builder/association_query_handler.rb deleted file mode 100644 index 7e20cb2c63..0000000000 --- a/activerecord/lib/active_record/relation/predicate_builder/association_query_handler.rb +++ /dev/null @@ -1,89 +0,0 @@ -module ActiveRecord - class PredicateBuilder - class AssociationQueryHandler # :nodoc: - def self.value_for(table, column, value) - associated_table = table.associated_table(column) - klass = if associated_table.polymorphic_association? && ::Array === value && value.first.is_a?(Base) - PolymorphicArrayValue - else - AssociationQueryValue - end - - klass.new(associated_table, value) - end - - def initialize(predicate_builder) - @predicate_builder = predicate_builder - end - - def call(attribute, value) - queries = {} - - table = value.associated_table - if value.base_class - queries[table.association_foreign_type.to_s] = value.base_class.name - end - - queries[table.association_foreign_key.to_s] = value.ids - predicate_builder.build_from_hash(queries) - end - - protected - - attr_reader :predicate_builder - end - - class AssociationQueryValue # :nodoc: - attr_reader :associated_table, :value - - def initialize(associated_table, value) - @associated_table = associated_table - @value = value - end - - def ids - case value - when Relation - value.select(primary_key) - when Array - value.map { |v| convert_to_id(v) } - else - convert_to_id(value) - end - end - - def base_class - if associated_table.polymorphic_association? - @base_class ||= polymorphic_base_class_from_value - end - end - - private - - def primary_key - associated_table.association_primary_key(base_class) - end - - def polymorphic_base_class_from_value - 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 - end - - def convert_to_id(value) - case value - when Base - value._read_attribute(primary_key) - else - value - end - end - end - end -end diff --git a/activerecord/lib/active_record/relation/predicate_builder/association_query_value.rb b/activerecord/lib/active_record/relation/predicate_builder/association_query_value.rb new file mode 100644 index 0000000000..3e19646ae5 --- /dev/null +++ b/activerecord/lib/active_record/relation/predicate_builder/association_query_value.rb @@ -0,0 +1,44 @@ +module ActiveRecord + class PredicateBuilder + class AssociationQueryValue # :nodoc: + def initialize(associated_table, value) + @associated_table = associated_table + @value = value + end + + def queries + [associated_table.association_foreign_key.to_s => ids] + end + + # TODO Change this to private once we've dropped Ruby 2.2 support. + # Workaround for Ruby 2.2 "private attribute?" warning. + protected + attr_reader :associated_table, :value + + private + def ids + case value + when Relation + value.select_values.empty? ? value.select(primary_key) : value + when Array + value.map { |v| convert_to_id(v) } + else + convert_to_id(value) + end + end + + def primary_key + associated_table.association_primary_key + end + + def convert_to_id(value) + case value + when Base + value._read_attribute(primary_key) + else + value + end + end + end + end +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 0a6574fcf1..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 deleted file mode 100644 index 0c7f92b3d0..0000000000 --- a/activerecord/lib/active_record/relation/predicate_builder/polymorphic_array_handler.rb +++ /dev/null @@ -1,57 +0,0 @@ -module ActiveRecord - class PredicateBuilder - class PolymorphicArrayHandler # :nodoc: - def initialize(predicate_builder) - @predicate_builder = predicate_builder - end - - def call(attribute, value) - table = value.associated_table - queries = value.type_to_ids_mapping.map do |type, ids| - { table.association_foreign_type.to_s => type, table.association_foreign_key.to_s => ids } - end - - predicates = queries.map { |query| predicate_builder.build_from_hash(query) } - - if predicates.size > 1 - type_and_ids_predicates = predicates.map { |type_predicate, id_predicate| Arel::Nodes::Grouping.new(type_predicate.and(id_predicate)) } - type_and_ids_predicates.inject(&:or) - else - predicates.first - end - end - - protected - - attr_reader :predicate_builder - end - - class PolymorphicArrayValue # :nodoc: - attr_reader :associated_table, :values - - def initialize(associated_table, values) - @associated_table = associated_table - @values = values - end - - def type_to_ids_mapping - default_hash = Hash.new { |hsh, key| hsh[key] = [] } - values.each_with_object(default_hash) { |value, hash| hash[base_class(value).name] << convert_to_id(value) } - end - - private - - def primary_key(value) - associated_table.association_primary_key(base_class(value)) - end - - def base_class(value) - value.class.base_class - end - - def convert_to_id(value) - value._read_attribute(primary_key(value)) - end - end - end -end diff --git a/activerecord/lib/active_record/relation/predicate_builder/polymorphic_array_value.rb b/activerecord/lib/active_record/relation/predicate_builder/polymorphic_array_value.rb new file mode 100644 index 0000000000..7029ae5f47 --- /dev/null +++ b/activerecord/lib/active_record/relation/predicate_builder/polymorphic_array_value.rb @@ -0,0 +1,52 @@ +module ActiveRecord + class PredicateBuilder + class PolymorphicArrayValue # :nodoc: + def initialize(associated_table, values) + @associated_table = associated_table + @values = values + end + + def queries + type_to_ids_mapping.map do |type, ids| + { + associated_table.association_foreign_type.to_s => type, + associated_table.association_foreign_key.to_s => ids.size > 1 ? ids : ids.first + } + 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 :associated_table, :values + + private + def type_to_ids_mapping + default_hash = Hash.new { |hsh, key| hsh[key] = [] } + values.each_with_object(default_hash) { |value, hash| hash[base_class(value).name] << convert_to_id(value) } + end + + def primary_key(value) + associated_table.association_primary_key(base_class(value)) + end + + def base_class(value) + case value + when Base + value.class.base_class + when Relation + value.klass.base_class + end + end + + def convert_to_id(value) + case value + when Base + value._read_attribute(primary_key(value)) + when Relation + value.select(primary_key(value)) + end + end + end + end +end diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb index 9fbbe32e7f..79e65baae5 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 @@ -77,7 +75,7 @@ module ActiveRecord 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), @@ -242,8 +240,15 @@ module ActiveRecord # Model.select(:field).first.other_field # # => ActiveModel::MissingAttributeError: missing attribute: other_field def select(*fields) - return super if block_given? - raise ArgumentError, "Call this with at least one field" if fields.empty? + if block_given? + if fields.any? + raise ArgumentError, "`select' with block doesn't take arguments." + end + + return super() + end + + raise ArgumentError, "Call `select' with at least one field" if fields.empty? spawn._select!(*fields) end @@ -651,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 @@ -682,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 @@ -755,7 +753,7 @@ module ActiveRecord # end # def none - where("1=0").extending!(NullRelation) + spawn.none! end def none! # :nodoc: @@ -840,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,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? @@ -1112,14 +1100,16 @@ module ActiveRecord end VALID_DIRECTIONS = [:asc, :desc, :ASC, :DESC, - "asc", "desc", "ASC", "DESC"] # :nodoc: + "asc", "desc", "ASC", "DESC"].to_set # :nodoc: def validate_order_args(args) args.each do |arg| next unless arg.is_a?(Hash) arg.each do |_key, value| - raise ArgumentError, "Direction \"#{value}\" is invalid. Valid " \ - "directions are: #{VALID_DIRECTIONS.inspect}" unless VALID_DIRECTIONS.include?(value) + unless VALID_DIRECTIONS.include?(value) + raise ArgumentError, + "Direction \"#{value}\" is invalid. Valid directions are: #{VALID_DIRECTIONS.to_a.inspect}" + end end end end @@ -1142,7 +1132,12 @@ module ActiveRecord arel_attribute(arg).asc when Hash arg.map { |field, dir| - arel_attribute(field).send(dir.downcase) + case field + when Arel::Nodes::SqlLiteral + field.send(dir.downcase) + else + arel_attribute(field).send(dir.downcase) + end } else arg @@ -1184,10 +1179,6 @@ module ActiveRecord end alias having_clause_factory where_clause_factory - def string_containing_comma?(value) - ::String === value && value.include?(",") - end - def default_value_for(name) case name when :create_with 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..119910ee79 100644 --- a/activerecord/lib/active_record/relation/where_clause.rb +++ b/activerecord/lib/active_record/relation/where_clause.rb @@ -25,10 +25,7 @@ module ActiveRecord end def except(*columns) - WhereClause.new( - predicates_except(columns), - binds_except(columns), - ) + WhereClause.new(*except_predicates_and_binds(columns)) end def or(other) @@ -84,6 +81,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 @@ -132,20 +131,35 @@ module ActiveRecord end end - def predicates_except(columns) - predicates.reject do |node| - case node - when Arel::Nodes::Between, Arel::Nodes::In, Arel::Nodes::NotIn, Arel::Nodes::Equality, Arel::Nodes::NotEqual, Arel::Nodes::LessThan, Arel::Nodes::LessThanOrEqual, Arel::Nodes::GreaterThan, Arel::Nodes::GreaterThanOrEqual - subrelation = (node.left.kind_of?(Arel::Attributes::Attribute) ? node.left : node.right) - columns.include?(subrelation.name.to_s) + def except_predicates_and_binds(columns) + except_binds = [] + binds_index = 0 + + predicates = self.predicates.reject do |node| + except = \ + case node + when Arel::Nodes::Between, Arel::Nodes::In, Arel::Nodes::NotIn, Arel::Nodes::Equality, Arel::Nodes::NotEqual, Arel::Nodes::LessThan, Arel::Nodes::LessThanOrEqual, Arel::Nodes::GreaterThan, Arel::Nodes::GreaterThanOrEqual + binds_contains = node.grep(Arel::Nodes::BindParam).size + subrelation = (node.left.kind_of?(Arel::Attributes::Attribute) ? node.left : node.right) + columns.include?(subrelation.name.to_s) + end + + if except && binds_contains > 0 + (binds_index...(binds_index + binds_contains)).each do |i| + except_binds[i] = true + end end + + binds_index += binds_contains if binds_contains + + except end - end - def binds_except(columns) - binds.reject do |attr| - columns.include?(attr.name) + binds = self.binds.reject.with_index do |_, i| + except_binds[i] end + + [predicates, binds] end def predicates_with_wrapped_sql_literals diff --git a/activerecord/lib/active_record/relation/where_clause_factory.rb b/activerecord/lib/active_record/relation/where_clause_factory.rb index 1e7deeffad..04bee73e8f 100644 --- a/activerecord/lib/active_record/relation/where_clause_factory.rb +++ b/activerecord/lib/active_record/relation/where_clause_factory.rb @@ -15,9 +15,12 @@ module ActiveRecord attributes = klass.send(:expand_hash_conditions_for_aggregates, attributes) attributes.stringify_keys! - attributes, binds = predicate_builder.create_binds(attributes) - - parts = predicate_builder.build_from_hash(attributes) + if perform_case_sensitive?(options = other.last) + parts, binds = build_for_case_sensitive(attributes, options) + else + attributes, binds = predicate_builder.create_binds(attributes) + parts = predicate_builder.build_from_hash(attributes) + end when Arel::Nodes::Node parts = [opts] else @@ -27,9 +30,48 @@ module ActiveRecord 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 + + private + + def perform_case_sensitive?(options) + options && options.key?(:case_sensitive) + end + + def build_for_case_sensitive(attributes, options) + parts, binds = [], [] + table = klass.arel_table + + attributes.each do |attribute, value| + if reflection = klass._reflect_on_association(attribute) + attribute = reflection.foreign_key.to_s + value = value[reflection.klass.primary_key] unless value.nil? + end + + if value.nil? + parts << table[attribute].eq(value) + else + column = klass.column_for_attribute(attribute) + + binds << predicate_builder.send(:build_bind_param, attribute, value) + value = Arel::Nodes::BindParam.new + + predicate = if options[:case_sensitive] + klass.connection.case_sensitive_comparison(table, attribute, column, value) + else + klass.connection.case_insensitive_comparison(table, attribute, column, value) + end + + parts << predicate + end + end + + [parts, binds] + end end end end diff --git a/activerecord/lib/active_record/result.rb b/activerecord/lib/active_record/result.rb index 9ed70a9c2b..26b1d48e9e 100644 --- a/activerecord/lib/active_record/result.rb +++ b/activerecord/lib/active_record/result.rb @@ -41,10 +41,15 @@ module ActiveRecord @column_types = column_types end + # Returns the number of elements in the rows array. def length @rows.length end + # Calls the given block once for each element in row collection, passing + # row as parameter. + # + # Returns an +Enumerator+ if no block is given. def each if block_given? hash_rows.each { |row| yield row } @@ -53,6 +58,7 @@ module ActiveRecord end end + # Returns an array of hashes representing each row record. def to_hash hash_rows end @@ -60,11 +66,12 @@ module ActiveRecord alias :map! :map alias :collect! :map - # Returns true if there are no records. + # Returns true if there are no records, otherwise false. def empty? rows.empty? end + # Returns an array of hashes representing each row record. def to_ary hash_rows end @@ -73,11 +80,15 @@ module ActiveRecord hash_rows[idx] end + # Returns the first record from the rows collection. + # If the rows collection is empty, returns +nil+. def first return nil if @rows.empty? Hash[@columns.zip(@rows.first)] end + # Returns the last record from the rows collection. + # If the rows collection is empty, returns +nil+. def last return nil if @rows.empty? Hash[@columns.zip(@rows.last)] diff --git a/activerecord/lib/active_record/sanitization.rb b/activerecord/lib/active_record/sanitization.rb index e7c0936984..64bda1539c 100644 --- a/activerecord/lib/active_record/sanitization.rb +++ b/activerecord/lib/active_record/sanitization.rb @@ -1,11 +1,9 @@ -require "active_support/core_ext/regexp" - module ActiveRecord module Sanitization extend ActiveSupport::Concern module ClassMethods - protected + private # Accepts an array or string of SQL conditions and sanitizes # them into a valid SQL fragment for a WHERE clause. @@ -21,7 +19,7 @@ module ActiveRecord # # sanitize_sql_for_conditions("name='foo''bar' and group_id='4'") # # => "name='foo''bar' and group_id='4'" - def sanitize_sql_for_conditions(condition) + def sanitize_sql_for_conditions(condition) # :doc: return nil if condition.blank? case condition @@ -47,7 +45,7 @@ module ActiveRecord # # 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) + 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) @@ -63,7 +61,7 @@ module ActiveRecord # # sanitize_sql_for_order("id ASC") # # => "id ASC" - def sanitize_sql_for_order(condition) + def sanitize_sql_for_order(condition) # :doc: if condition.is_a?(Array) && condition.first.to_s.include?("?") sanitize_sql_array(condition) else @@ -86,7 +84,7 @@ module ActiveRecord # # { address: Address.new("813 abc st.", "chicago") } # # => { address_street: "813 abc st.", address_city: "chicago" } - def expand_hash_conditions_for_aggregates(attrs) + def expand_hash_conditions_for_aggregates(attrs) # :doc: expanded_attrs = {} attrs.each do |attr, value| if aggregation = reflect_on_aggregation(attr.to_sym) @@ -109,7 +107,7 @@ module ActiveRecord # # 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) + 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) @@ -131,7 +129,7 @@ module ActiveRecord # # sanitize_sql_like("snake_cased_string", "!") # # => "snake!_cased!_string" - def sanitize_sql_like(string, escape_character = "\\") + def sanitize_sql_like(string, escape_character = "\\") # :doc: pattern = Regexp.union(escape_character, "%", "_") string.gsub(pattern) { |x| [escape_character, x].join } end @@ -147,7 +145,7 @@ module ActiveRecord # # sanitize_sql_array(["name='%s' and group_id='%s'", "foo'bar", 4]) # # => "name='foo''bar' and group_id='4'" - def sanitize_sql_array(ary) + 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) @@ -160,7 +158,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 @@ -169,7 +167,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 @@ -177,7 +175,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 @@ -189,7 +187,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) @@ -201,16 +199,16 @@ 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 end end - # TODO: Deprecate this def quoted_id # :nodoc: self.class.connection.quote(@attributes[self.class.primary_key].value_for_database) end + deprecate :quoted_id end end diff --git a/activerecord/lib/active_record/schema.rb b/activerecord/lib/active_record/schema.rb index 784a02d2c3..5104427339 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 @@ -48,7 +48,7 @@ module ActiveRecord instance_eval(&block) if info[:version].present? - initialize_schema_migrations_table + ActiveRecord::SchemaMigration.create_table connection.assume_migrated_upto_version(info[:version], migrations_paths) 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 ab2d64e903..24b81aabc8 100644 --- a/activerecord/lib/active_record/schema_dumper.rb +++ b/activerecord/lib/active_record/schema_dumper.rb @@ -11,13 +11,13 @@ module ActiveRecord ## # :singleton-method: # A list of tables which should not be dumped to the schema. - # Acceptable values are strings as well as regexp. - # This setting is only used if ActiveRecord::Base.schema_format == :ruby + # Acceptable values are strings as well as regexp if ActiveRecord::Base.schema_format == :ruby. + # Only strings are accepted if ActiveRecord::Base.schema_format == :sql. cattr_accessor :ignore_tables @@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 @@ -47,9 +47,18 @@ module ActiveRecord @options = options end - def header(stream) - define_params = @version ? "version: #{@version}" : "" + # turns 20170404131909 into "2017_04_04_131909" + def formatted_version + stringified = @version.to_s + return stringified unless stringified.length == 14 + stringified.insert(4, "_").insert(7, "_").insert(10, "_") + end + + def define_params + @version ? "version: #{formatted_version}" : "" + end + def header(stream) stream.puts <<HEADER # This file is auto-generated from the current state of the database. Instead # of editing this file, please use the migrations feature of Active Record to @@ -85,7 +94,7 @@ HEADER end def tables(stream) - sorted_tables = @connection.data_sources.sort - @connection.views + sorted_tables = @connection.tables.sort sorted_tables.each do |table_name| table(table_name, stream) unless ignored?(table_name) @@ -115,9 +124,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,26 +135,19 @@ 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 - - column_specs.each do |colspec| - values = keys.map { |key| colspec[key] }.compact - tbl.puts " t.#{colspec[:type]} #{values.join(", ")}" + type, colspec = @connection.column_spec(column) + tbl.print " t.#{type} #{column.name.inspect}" + tbl.print ", #{format_colspec(colspec)}" if colspec.present? + tbl.puts end indexes_in_create(table, tbl) @@ -171,7 +171,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") @@ -194,14 +194,10 @@ 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 << "using: #{index.using.inspect}" if !@connection.default_index_type?(index) index_parts << "type: #{index.type.inspect}" if index.type index_parts << "comment: #{index.comment.inspect}" if index.comment index_parts @@ -237,6 +233,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..f59737afb0 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 @@ -39,7 +39,11 @@ module ActiveRecord end def normalized_versions - pluck(:version).map { |v| normalize_migration_number v } + all_versions.map { |v| normalize_migration_number v } + end + + def all_versions + order(:version).pluck(:version) end end 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 9d8253faa3..ba5cc29ac1 100644 --- a/activerecord/lib/active_record/scoping/default.rb +++ b/activerecord/lib/active_record/scoping/default.rb @@ -5,11 +5,8 @@ module ActiveRecord included do # Stores the default scope for the class. - class_attribute :default_scopes, instance_writer: false, instance_predicate: false - class_attribute :default_scope_override, instance_writer: false, instance_predicate: false - - self.default_scopes = [] - self.default_scope_override = nil + class_attribute :default_scopes, instance_writer: false, instance_predicate: false, default: [] + class_attribute :default_scope_override, instance_writer: false, instance_predicate: false, default: nil end module ClassMethods @@ -44,7 +41,7 @@ module ActiveRecord self.current_scope = nil end - protected + private # Use this macro in your model to set a default scope for all operations on # the model. @@ -87,7 +84,7 @@ module ActiveRecord # # Should return a scope, you can call 'super' here etc. # end # end - def default_scope(scope = nil) + def default_scope(scope = nil) # :doc: scope = Proc.new if block_given? if scope.is_a?(Relation) || !scope.respond_to?(:call) @@ -101,7 +98,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 +119,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: + 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 094c0e9c6f..a61fdd6454 100644 --- a/activerecord/lib/active_record/scoping/named.rb +++ b/activerecord/lib/active_record/scoping/named.rb @@ -30,19 +30,22 @@ module ActiveRecord end def default_scoped # :nodoc: - scope = build_default_scope + scope = relation + build_default_scope(scope) || scope + end - if scope - relation.spawn.merge!(scope) + def default_extensions # :nodoc: + if scope = current_scope || build_default_scope + scope.extensions else - relation + [] end end # 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 @@ -156,29 +159,29 @@ module ActiveRecord if body.respond_to?(:to_proc) singleton_class.send(:define_method, name) do |*args| - scope = all.scoping { instance_exec(*args, &body) } + scope = all + scope = scope.scoping { instance_exec(*args, &body) || scope } scope = scope.extending(extension) if extension - - scope || all + scope end else singleton_class.send(:define_method, name) do |*args| - scope = all.scoping { body.call(*args) } + scope = all + scope = scope.scoping { body.call(*args) || scope } scope = scope.extending(extension) if extension - - scope || all + scope end end end - protected + private - def valid_scope_name?(name) - if respond_to?(name, true) && logger - 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 7606961e2e..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/serialization.rb b/activerecord/lib/active_record/serialization.rb index 5a408e7b8e..db2bd0b55e 100644 --- a/activerecord/lib/active_record/serialization.rb +++ b/activerecord/lib/active_record/serialization.rb @@ -9,7 +9,7 @@ module ActiveRecord #:nodoc: end def serializable_hash(options = nil) - options = options.try(:clone) || {} + options = options.try(:dup) || {} options[:except] = Array(options[:except]).map(&:to_s) options[:except] |= Array(self.class.inheritance_column) diff --git a/activerecord/lib/active_record/statement_cache.rb b/activerecord/lib/active_record/statement_cache.rb index 691940ab70..1877489e55 100644 --- a/activerecord/lib/active_record/statement_cache.rb +++ b/activerecord/lib/active_record/statement_cache.rb @@ -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 diff --git a/activerecord/lib/active_record/store.rb b/activerecord/lib/active_record/store.rb index 066573192e..006afe7495 100644 --- a/activerecord/lib/active_record/store.rb +++ b/activerecord/lib/active_record/store.rb @@ -78,7 +78,7 @@ module ActiveRecord module ClassMethods def store(store_attribute, options = {}) - serialize store_attribute, IndifferentCoder.new(options[:coder]) + serialize store_attribute, IndifferentCoder.new(store_attribute, options[:coder]) store_accessor(store_attribute, options[:accessors]) if options.has_key? :accessors end @@ -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 @@ -178,12 +177,12 @@ module ActiveRecord end class IndifferentCoder # :nodoc: - def initialize(coder_or_class_name) + def initialize(attr_name, coder_or_class_name) @coder = if coder_or_class_name.respond_to?(:load) && coder_or_class_name.respond_to?(:dump) coder_or_class_name else - ActiveRecord::Coders::YAMLColumn.new(coder_or_class_name || Object) + ActiveRecord::Coders::YAMLColumn.new(attr_name, coder_or_class_name || Object) end end diff --git a/activerecord/lib/active_record/table_metadata.rb b/activerecord/lib/active_record/table_metadata.rb index 58184f3872..71efc1829a 100644 --- a/activerecord/lib/active_record/table_metadata.rb +++ b/activerecord/lib/active_record/table_metadata.rb @@ -44,7 +44,7 @@ module ActiveRecord 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 @@ -64,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..ba686fc562 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"] @@ -63,9 +71,9 @@ module ActiveRecord @tasks[pattern] = task end - register_task(/mysql/, ActiveRecord::Tasks::MySQLDatabaseTasks) - register_task(/postgresql/, ActiveRecord::Tasks::PostgreSQLDatabaseTasks) - register_task(/sqlite/, ActiveRecord::Tasks::SQLiteDatabaseTasks) + register_task(/mysql/, "ActiveRecord::Tasks::MySQLDatabaseTasks") + register_task(/postgresql/, "ActiveRecord::Tasks::PostgreSQLDatabaseTasks") + register_task(/sqlite/, "ActiveRecord::Tasks::SQLiteDatabaseTasks") def db_dir @db_dir ||= Rails.application.config.paths["db"].first @@ -154,9 +162,11 @@ module ActiveRecord end def migrate - verbose = ENV["VERBOSE"] ? ENV["VERBOSE"] == "true" : true + raise "Empty VERSION provided" if ENV["VERSION"] && ENV["VERSION"].empty? + + verbose = ENV["VERBOSE"] ? ENV["VERBOSE"] != "false" : true version = ENV["VERSION"] ? ENV["VERSION"].to_i : nil - scope = ENV["SCOPE"] + scope = ENV["SCOPE"] verbose_was, Migration.verbose = Migration.verbose, verbose Migrator.migrate(migrations_paths, version) do |migration| scope.blank? || scope == migration.scope @@ -204,13 +214,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 +241,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 @@ -267,20 +269,30 @@ module ActiveRecord if seed_loader seed_loader.load_seed else - raise "You tried to load seed data, but no seed loader is specified. Please specify seed " + - "loader with ActiveRecord::Tasks::DatabaseTasks.seed_loader = your_seed_loader\n" + + raise "You tried to load seed data, but no seed loader is specified. Please specify seed " \ + "loader with ActiveRecord::Tasks::DatabaseTasks.seed_loader = your_seed_loader\n" \ "Seed loader should respond to load_seed method" 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) - key = @tasks.keys.detect { |pattern| adapter[pattern] } - unless key + _key, task = @tasks.each_pair.detect { |pattern, _task| adapter[pattern] } + unless task raise DatabaseNotSupported, "Rake tasks not supported by '#{adapter}' adapter" end - @tasks[key] + task.is_a?(String) ? task.constantize : task end def each_current_configuration(environment) diff --git a/activerecord/lib/active_record/tasks/mysql_database_tasks.rb b/activerecord/lib/active_record/tasks/mysql_database_tasks.rb index 3a5e0b8dfe..541165b3d1 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,29 @@ 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 + + ignore_tables = ActiveRecord::SchemaDumper.ignore_tables + if ignore_tables.any? + args += ignore_tables.map { |table| "--ignore-table=#{configuration['database']}.#{table}" } + end + 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 @@ -102,7 +110,7 @@ module ActiveRecord def grant_statement <<-SQL -GRANT ALL PRIVILEGES ON #{configuration['database']}.* +GRANT ALL PRIVILEGES ON `#{configuration['database']}`.* TO '#{configuration['username']}'@'localhost' IDENTIFIED BY '#{configuration['password']}' WITH GRANT OPTION; SQL diff --git a/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb b/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb index a3a9430c03..7f1a768d8b 100644 --- a/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb +++ b/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb @@ -1,8 +1,11 @@ +require "tempfile" + module ActiveRecord module Tasks # :nodoc: class PostgreSQLDatabaseTasks # :nodoc: DEFAULT_ENCODING = ENV["CHARSET"] || "utf8" ON_ERROR_STOP_1 = "ON_ERROR_STOP=1".freeze + SQL_COMMENT_BEGIN = "--".freeze delegate :connection, :establish_connection, :clear_active_connections!, to: ActiveRecord::Base @@ -17,7 +20,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,7 +46,7 @@ module ActiveRecord create true end - def structure_dump(filename) + def structure_dump(filename, extra_flags) set_psql_env search_path = \ @@ -57,20 +60,30 @@ module ActiveRecord 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}" end end + + ignore_tables = ActiveRecord::SchemaDumper.ignore_tables + if ignore_tables.any? + args += ignore_tables.flat_map { |table| ["-T", table] } + end + args << configuration["database"] run_cmd("pg_dump", args, "dumping") + remove_sql_header_comments(filename) 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 @@ -107,6 +120,22 @@ module ActiveRecord msg << "Please check the output above for any errors and make sure that `#{cmd}` is installed in your PATH and has proper permissions.\n\n" msg end + + def remove_sql_header_comments(filename) + removing_comments = true + tempfile = Tempfile.open("uncommented_structure.sql") + begin + File.foreach(filename) do |line| + unless removing_comments && (line.start_with?(SQL_COMMENT_BEGIN) || line.blank?) + tempfile << line + removing_comments = false + end + end + ensure + tempfile.close + end + FileUtils.mv(tempfile.path, filename) + end end end end diff --git a/activerecord/lib/active_record/tasks/sqlite_database_tasks.rb b/activerecord/lib/active_record/tasks/sqlite_database_tasks.rb index 31f1b7efd4..7043d2f0c8 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,25 @@ module ActiveRecord connection.encoding end - def structure_dump(filename) - dbfile = configuration["database"] - `sqlite3 #{dbfile} .schema > #{filename}` + def structure_dump(filename, extra_flags) + args = [] + args.concat(Array(extra_flags)) if extra_flags + args << configuration["database"] + + ignore_tables = ActiveRecord::SchemaDumper.ignore_tables + if ignore_tables.any? + condition = ignore_tables.map { |table| connection.quote_table_name(table) }.join(", ") + args << "SELECT sql FROM sqlite_master WHERE tbl_name NOT IN (#{condition}) ORDER BY tbl_name, type DESC, name" + else + args << ".schema" + end + run_cmd("sqlite3", args, 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 @@ -54,6 +65,17 @@ module ActiveRecord def root @root end + + def run_cmd(cmd, args, out) + fail run_cmd_error(cmd, args) unless Kernel.system(cmd, *args, out: out) + end + + def run_cmd_error(cmd, args) + msg = "failed to execute:\n" + msg << "#{cmd} #{args.join(' ')}\n\n" + msg << "Please check the output above for any errors and make sure that `#{cmd}` is installed in your PATH and has proper permissions.\n\n" + msg + end end end end diff --git a/activerecord/lib/active_record/timestamp.rb b/activerecord/lib/active_record/timestamp.rb index 6641ab5df1..55f3a194a9 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 # @@ -42,8 +43,7 @@ module ActiveRecord extend ActiveSupport::Concern included do - class_attribute :record_timestamps - self.record_timestamps = true + class_attribute :record_timestamps, default: true end def initialize_dup(other) # :nodoc: @@ -51,15 +51,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 +99,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 +107,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 +134,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 af3fc88282..45795fa287 100644 --- a/activerecord/lib/active_record/transactions.rb +++ b/activerecord/lib/active_record/transactions.rb @@ -11,7 +11,6 @@ module ActiveRecord :before_commit_without_transaction_enrollment, :commit_without_transaction_enrollment, :rollback_without_transaction_enrollment, - terminator: deprecated_false_terminator, scope: [:kind, :name] end @@ -124,7 +123,7 @@ module ActiveRecord # # statement will cause a PostgreSQL error, even though the unique # # constraint is no longer violated: # Number.create(i: 1) - # # => "PGError: ERROR: current transaction is aborted, commands + # # => "PG::Error: ERROR: current transaction is aborted, commands # # ignored until end of transaction block" # end # @@ -274,16 +273,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 = {}) @@ -294,7 +283,7 @@ module ActiveRecord fire_on = Array(options[:on]) assert_valid_transaction_action(fire_on) options[:if] = Array(options[:if]) - options[:if] << "transaction_include_any_action?(#{fire_on})" + options[:if].unshift("transaction_include_any_action?(#{fire_on})") end end @@ -407,10 +396,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: + def remember_transaction_record_state @_start_transaction_state[:id] = id @_start_transaction_state.reverse_merge!( new_record: @new_record, @@ -421,18 +410,18 @@ module ActiveRecord end # Clear the new record state and id of a record. - def clear_transaction_record_state #:nodoc: + 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: + 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: + 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 @@ -450,31 +439,30 @@ module ActiveRecord end # Determine if a record was created or destroyed in a transaction. State should be one of :new_record or :destroyed. - def transaction_record_state(state) #:nodoc: + def transaction_record_state(state) @_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: + 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 diff --git a/activerecord/lib/active_record/type.rb b/activerecord/lib/active_record/type.rb index 0b48d2186a..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" @@ -53,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..53a5e205da --- /dev/null +++ b/activerecord/lib/active_record/type/decimal_without_scale.rb @@ -0,0 +1,13 @@ +module ActiveRecord + module Type + class DecimalWithoutScale < ActiveModel::Type::BigInteger # :nodoc: + def type + :decimal + end + + def type_cast_for_schema(value) + value.to_s.inspect + 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 e19c5a14da..a8d6a63465 100644 --- a/activerecord/lib/active_record/type/internal/abstract_json.rb +++ b/activerecord/lib/active_record/type/internal/abstract_json.rb @@ -24,6 +24,10 @@ module ActiveRecord end end + def changed_in_place?(raw_old_value, new_value) + deserialize(raw_old_value) != new_value + end + def accessor ActiveRecord::Store::StringKeyedHashAccessor end diff --git a/activerecord/lib/active_record/type/serialized.rb b/activerecord/lib/active_record/type/serialized.rb index ac9134bfcb..edbd20a6c1 100644 --- a/activerecord/lib/active_record/type/serialized.rb +++ b/activerecord/lib/active_record/type/serialized.rb @@ -1,6 +1,8 @@ module ActiveRecord module Type class Serialized < DelegateClass(ActiveModel::Type::Value) # :nodoc: + undef to_yaml if method_defined?(:to_yaml) + include ActiveModel::Type::Helpers::Mutable attr_reader :subtype, :coder @@ -43,7 +45,7 @@ module ActiveRecord def assert_valid_value(value) if coder.respond_to?(:assert_valid_value) - coder.assert_valid_value(value) + coder.assert_valid_value(value, action: "serialize") end end diff --git a/activerecord/lib/active_record/type/text.rb b/activerecord/lib/active_record/type/text.rb new file mode 100644 index 0000000000..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/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..7cfd55f516 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>) @@ -57,7 +57,7 @@ module ActiveRecord # or <tt>unless: Proc.new { |user| user.signup_step <= 2 }</tt>). The method, # proc or string should return or evaluate to a +true+ or +false+ value. # * <tt>:strict</tt> - Specifies whether validation should be strict. - # See ActiveModel::Validation#validates! for more information. + # See ActiveModel::Validations#validates! for more information. def validates_presence_of(*attr_names) validates_with PresenceValidator, _merge_attributes(attr_names) end diff --git a/activerecord/lib/active_record/validations/uniqueness.rb b/activerecord/lib/active_record/validations/uniqueness.rb index 8c4930a81d..154cf5f1a4 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,47 +49,16 @@ module ActiveRecord class_hierarchy.detect { |klass| !klass.abstract_class? } end - def build_relation(klass, attribute, value) # :nodoc: - if reflection = klass._reflect_on_association(attribute) - attribute = reflection.foreign_key - value = value.attributes[reflection.klass.primary_key] unless value.nil? - end - - if value.nil? - return klass.unscoped.where!(attribute => value) - end - - # the attribute may be an aliased attribute - if klass.attribute_alias?(attribute) - attribute = klass.attribute_alias(attribute) - end - - attribute_name = attribute.to_s - - table = klass.arel_table - column = klass.columns_hash[attribute_name] - cast_type = klass.type_for_attribute(attribute_name) - - comparison = if !options[:case_sensitive] - # will use SQL LOWER function before comparison, unless it detects a case insensitive collation - klass.connection.case_insensitive_comparison(table, attribute, column, value) - else - klass.connection.case_sensitive_comparison(table, attribute, column, value) - end - klass.unscoped.tap do |scope| - parts = [comparison] - binds = [Relation::QueryAttribute.new(attribute_name, value, cast_type)] - scope.where_clause += Relation::WhereClause.new(parts, binds) - end + def build_relation(klass, attribute, value) + klass.unscoped.where!({ attribute => value }, options) end 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.rb b/activerecord/lib/rails/generators/active_record.rb index 68fca44e3b..a79b8eafea 100644 --- a/activerecord/lib/rails/generators/active_record.rb +++ b/activerecord/lib/rails/generators/active_record.rb @@ -10,7 +10,7 @@ module ActiveRecord # Set the current directory as base for the inherited generators. def self.base_root - File.dirname(__FILE__) + __dir__ end end end diff --git a/activerecord/lib/rails/generators/active_record/migration.rb b/activerecord/lib/rails/generators/active_record/migration.rb index 4263c11ffc..47c0981a49 100644 --- a/activerecord/lib/rails/generators/active_record/migration.rb +++ b/activerecord/lib/rails/generators/active_record/migration.rb @@ -20,6 +20,14 @@ module ActiveRecord key_type = options[:primary_key_type] ", id: :#{key_type}" if key_type end + + def db_migrate_path + if defined?(Rails.application) && Rails.application + Rails.application.config.paths["db/migrate"].to_ary.first + else + "db/migrate" + end + end end end end diff --git a/activerecord/lib/rails/generators/active_record/migration/migration_generator.rb b/activerecord/lib/rails/generators/active_record/migration/migration_generator.rb index 76ed25ea75..1f1c47499b 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: @@ -11,12 +10,16 @@ module ActiveRecord def create_migration_file set_local_assigns! validate_file_name! - migration_template @migration_template, "db/migrate/#{file_name}.rb" + migration_template @migration_template, File.join(db_migrate_path, "#{file_name}.rb") end + # TODO Change this to private once we've dropped Ruby 2.2 support. + # Workaround for Ruby 2.2 "private attribute?" warning. protected attr_reader :migration_action, :join_tables + private + # 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. @@ -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..5cec07d2e3 100644 --- a/activerecord/lib/rails/generators/active_record/model/model_generator.rb +++ b/activerecord/lib/rails/generators/active_record/model/model_generator.rb @@ -17,7 +17,7 @@ module ActiveRecord def create_migration_file return unless options[:migration] && options[:parent].nil? attributes.each { |a| a.attr_options.delete(:index) if a.reference? && !a.has_index? } if options[:indexes] == false - migration_template "../../migration/templates/create_table_migration.rb", "db/migrate/create_#{table_name}.rb" + migration_template "../../migration/templates/create_table_migration.rb", File.join(db_migrate_path, "create_#{table_name}.rb") end def create_model_file @@ -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 8fa0645b0f..a1fb6427f9 100644 --- a/activerecord/test/cases/adapter_test.rb +++ b/activerecord/test/cases/adapter_test.rb @@ -30,9 +30,18 @@ module ActiveRecord assert_nothing_raised { Book.destroy(0) } end + def test_valid_column + @connection.native_database_types.each_key do |type| + assert @connection.valid_type?(type) + end + end + + def test_invalid_column + assert_not @connection.valid_type?(:foobar) + end + def test_tables - tables = nil - ActiveSupport::Deprecation.silence { tables = @connection.tables } + tables = @connection.tables assert_includes tables, "accounts" assert_includes tables, "authors" assert_includes tables, "tasks" @@ -40,15 +49,11 @@ module ActiveRecord 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 @@ -63,26 +68,22 @@ module ActiveRecord 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,35 +185,15 @@ module ActiveRecord 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 - # Oracle adapter uses prefetched primary key values from sequence and passes them to connection adapter insert method - if @connection.prefetch_primary_key? - id_value = @connection.next_sequence_value(@connection.default_sequence_name("fk_test_has_fk", "id")) - @connection.execute "INSERT INTO fk_test_has_fk (id, fk_id) VALUES (#{id_value},0)" - else - @connection.execute "INSERT INTO fk_test_has_fk (fk_id) VALUES (0)" - end - end - - assert_not_nil error.cause + def test_not_null_violations_are_translated_to_specific_exception + error = assert_raises(ActiveRecord::NotNullViolation) do + Post.create end - def test_foreign_key_violations_are_translated_to_specific_exception_with_validate_false - klass_has_fk = Class.new(ActiveRecord::Base) do - self.table_name = "fk_test_has_fk" - end - - error = assert_raises(ActiveRecord::InvalidForeignKey) do - has_fk = klass_has_fk.new - has_fk.fk_id = 1231231231 - has_fk.save(validate: false) - end - - assert_not_nil error.cause - end + assert_not_nil error.cause + end + unless current_adapter?(:SQLite3Adapter) def test_value_limit_violations_are_translated_to_specific_exception error = assert_raises(ActiveRecord::ValueTooLong) do Event.create(title: "abcdefgh") @@ -220,22 +201,13 @@ module ActiveRecord assert_not_nil error.cause end - end - def test_disable_referential_integrity - assert_nothing_raised do - @connection.disable_referential_integrity do - # Oracle adapter uses prefetched primary key values from sequence and passes them to connection adapter insert method - if @connection.prefetch_primary_key? - id_value = @connection.next_sequence_value(@connection.default_sequence_name("fk_test_has_fk", "id")) - @connection.execute "INSERT INTO fk_test_has_fk (id, fk_id) VALUES (#{id_value},0)" - else - @connection.execute "INSERT INTO fk_test_has_fk (fk_id) VALUES (0)" - end - # should delete created record as otherwise disable_referential_integrity will try to enable constraints after executed block - # and will fail (at least on Oracle) - @connection.execute "DELETE FROM fk_test_has_fk" + 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 (9223372036854775808)") end + + assert_not_nil error.cause end end @@ -244,6 +216,15 @@ module ActiveRecord assert result.is_a?(ActiveRecord::Result) end + if ActiveRecord::Base.connection.prepared_statements + def test_select_all_with_legacy_binds + post = Post.create!(title: "foo", body: "bar") + expected = @connection.select_all("SELECT * FROM posts WHERE id = #{post.id}") + result = @connection.select_all("SELECT * FROM posts WHERE id = #{bind_param.to_sql}", nil, [[nil, post.id]]) + assert_equal expected.to_hash, result.to_hash + end + end + def test_select_methods_passing_a_association_relation author = Author.create!(name: "john") Post.create!(author: author, title: "foo", body: "bar") @@ -271,25 +252,68 @@ 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 + assert_not_nil error.message + end + end + end + + class AdapterForeignKeyTest < ActiveRecord::TestCase + self.use_transactional_tests = false + + def setup + @connection = ActiveRecord::Base.connection + end + + def test_foreign_key_violations_are_translated_to_specific_exception_with_validate_false + klass_has_fk = Class.new(ActiveRecord::Base) do + self.table_name = "fk_test_has_fk" + end + + error = assert_raises(ActiveRecord::InvalidForeignKey) do + has_fk = klass_has_fk.new + has_fk.fk_id = 1231231231 + has_fk.save(validate: false) end + + assert_not_nil error.cause end - if current_adapter?(:Mysql2Adapter, :SQLite3Adapter) - def test_tables_returning_both_tables_and_views_is_deprecated - assert_deprecated { @connection.tables } + def test_foreign_key_violations_are_translated_to_specific_exception + error = assert_raises(ActiveRecord::InvalidForeignKey) do + insert_into_fk_test_has_fk end + + assert_not_nil error.cause end - def test_passing_arguments_to_tables_is_deprecated - assert_deprecated { @connection.tables(:books) } + def test_disable_referential_integrity + assert_nothing_raised do + @connection.disable_referential_integrity do + insert_into_fk_test_has_fk + # should delete created record as otherwise disable_referential_integrity will try to enable constraints + # after executed block and will fail (at least on Oracle) + @connection.execute "DELETE FROM fk_test_has_fk" + end + end end + + private + + def insert_into_fk_test_has_fk + # Oracle adapter uses prefetched primary key values from sequence and passes them to connection adapter insert method + if @connection.prefetch_primary_key? + id_value = @connection.next_sequence_value(@connection.default_sequence_name("fk_test_has_fk", "id")) + @connection.execute "INSERT INTO fk_test_has_fk (id,fk_id) VALUES (#{id_value},0)" + else + @connection.execute "INSERT INTO fk_test_has_fk (fk_id) VALUES (0)" + end + end end 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 a70eb5a094..67e1efde27 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 }) 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 }) %w(SPATIAL FULLTEXT UNIQUE).each do |type| expected = "CREATE #{type} INDEX `index_people_on_last_name` ON `people` (`last_name`) " @@ -89,8 +92,8 @@ class Mysql2ActiveSchemaTest < ActiveRecord::Mysql2TestCase assert_equal expected, actual end - expected = "ALTER TABLE `peaple` ADD INDEX `index_peaple_on_last_name` USING btree (`last_name`(10)), ALGORITHM = COPY" - actual = ActiveRecord::Base.connection.change_table(:peaple, bulk: true) do |t| + expected = "ALTER TABLE `people` ADD INDEX `index_people_on_last_name` USING btree (`last_name`(10)), ALGORITHM = COPY" + actual = ActiveRecord::Base.connection.change_table(:people, bulk: true) do |t| t.index :last_name, length: 10, using: :btree, algorithm: :copy end assert_equal expected, actual diff --git a/activerecord/test/cases/adapters/mysql2/boolean_test.rb b/activerecord/test/cases/adapters/mysql2/boolean_test.rb index 2fa39282fb..58698d59db 100644 --- a/activerecord/test/cases/adapters/mysql2/boolean_test.rb +++ b/activerecord/test/cases/adapters/mysql2/boolean_test.rb @@ -38,7 +38,7 @@ class Mysql2BooleanTest < ActiveRecord::Mysql2TestCase assert_equal :string, string_column.type end - test "test type casting with emulated booleans" do + test "type casting with emulated booleans" do emulate_booleans true boolean = BooleanType.create!(archived: true, published: true) @@ -55,7 +55,7 @@ class Mysql2BooleanTest < ActiveRecord::Mysql2TestCase assert_equal 0, @connection.type_cast(false) end - test "test type casting without emulated booleans" do + test "type casting without emulated booleans" do emulate_booleans false boolean = BooleanType.create!(archived: true, published: true) diff --git a/activerecord/test/cases/adapters/mysql2/charset_collation_test.rb b/activerecord/test/cases/adapters/mysql2/charset_collation_test.rb index 8826ad7fd1..e4a6ed5482 100644 --- a/activerecord/test/cases/adapters/mysql2/charset_collation_test.rb +++ b/activerecord/test/cases/adapters/mysql2/charset_collation_test.rb @@ -48,7 +48,7 @@ class Mysql2CharsetCollationTest < ActiveRecord::Mysql2TestCase test "schema dump includes collation" do output = dump_table_schema("charset_collations") - assert_match %r{t.string\s+"string_ascii_bin",\s+collation: "ascii_bin"$}, output - assert_match %r{t.text\s+"text_ucs2_unicode_ci",\s+collation: "ucs2_unicode_ci"$}, output + assert_match %r{t\.string\s+"string_ascii_bin",\s+collation: "ascii_bin"$}, output + assert_match %r{t\.text\s+"text_ucs2_unicode_ci",\s+collation: "ucs2_unicode_ci"$}, output end end diff --git a/activerecord/test/cases/adapters/mysql2/connection_test.rb b/activerecord/test/cases/adapters/mysql2/connection_test.rb index 8d8955e5c9..a2faf43b0d 100644 --- a/activerecord/test/cases/adapters/mysql2/connection_test.rb +++ b/activerecord/test/cases/adapters/mysql2/connection_test.rb @@ -42,7 +42,7 @@ class Mysql2ConnectionTest < ActiveRecord::Mysql2TestCase @connection.update("set @@wait_timeout=1") sleep 2 assert !@connection.active? - + ensure # Repair all fixture connections so other tests won't break. @fixture_connections.each(&:verify!) end @@ -63,20 +63,33 @@ class Mysql2ConnectionTest < ActiveRecord::Mysql2TestCase assert @connection.active? end + def test_verify_with_args_is_deprecated + assert_deprecated do + @connection.verify!(option: true) + end + assert_deprecated do + @connection.verify!([]) + end + assert_deprecated do + @connection.verify!({}) + end + end + def test_execute_after_disconnect @connection.disconnect! + error = assert_raise(ActiveRecord::StatementInvalid) do @connection.execute("SELECT 1") end - assert_match(/closed MySQL connection/, error.message) + assert_kind_of Mysql2::Error, error.cause end def test_quote_after_disconnect @connection.disconnect! - error = assert_raise(Mysql2::Error) do + + assert_raise(Mysql2::Error) do @connection.quote("string") end - assert_match(/closed MySQL connection/, error.message) end def test_active_after_disconnect @@ -84,6 +97,22 @@ class Mysql2ConnectionTest < ActiveRecord::Mysql2TestCase assert_equal false, @connection.active? end + def test_wait_timeout_as_string + run_without_connection do |orig_connection| + ActiveRecord::Base.establish_connection(orig_connection.merge(wait_timeout: "60")) + result = ActiveRecord::Base.connection.select_value("SELECT @@SESSION.wait_timeout") + assert_equal 60, result + end + end + + def test_wait_timeout_as_url + run_without_connection do |orig_connection| + ActiveRecord::Base.establish_connection(orig_connection.merge("url" => "mysql2:///?wait_timeout=60")) + result = ActiveRecord::Base.connection.select_value("SELECT @@SESSION.wait_timeout") + assert_equal 60, result + end + 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") @@ -119,10 +148,10 @@ class Mysql2ConnectionTest < ActiveRecord::Mysql2TestCase end end - def test_passing_arbitary_flags_to_adapter + def test_passing_arbitrary_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 @@ -186,7 +215,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/datetime_precision_quoting_test.rb b/activerecord/test/cases/adapters/mysql2/datetime_precision_quoting_test.rb index 135789a57d..c131a5169c 100644 --- a/activerecord/test/cases/adapters/mysql2/datetime_precision_quoting_test.rb +++ b/activerecord/test/cases/adapters/mysql2/datetime_precision_quoting_test.rb @@ -6,23 +6,27 @@ class Mysql2DatetimePrecisionQuotingTest < ActiveRecord::Mysql2TestCase end test "microsecond precision for MySQL gte 5.6.4" do - stub_version "5.6.4" - assert_microsecond_precision + stub_version "5.6.4" do + assert_microsecond_precision + end end test "no microsecond precision for MySQL lt 5.6.4" do - stub_version "5.6.3" - assert_no_microsecond_precision + stub_version "5.6.3" do + assert_no_microsecond_precision + end end test "microsecond precision for MariaDB gte 5.3.0" do - stub_version "5.5.5-10.1.8-MariaDB-log" - assert_microsecond_precision + stub_version "5.5.5-10.1.8-MariaDB-log" do + assert_microsecond_precision + end end test "no microsecond precision for MariaDB lt 5.3.0" do - stub_version "5.2.9-MariaDB" - assert_no_microsecond_precision + stub_version "5.2.9-MariaDB" do + assert_no_microsecond_precision + end end private @@ -41,5 +45,8 @@ class Mysql2DatetimePrecisionQuotingTest < ActiveRecord::Mysql2TestCase def stub_version(full_version_string) @connection.stubs(:full_version).returns(full_version_string) @connection.remove_instance_variable(:@version) if @connection.instance_variable_defined?(:@version) + yield + ensure + @connection.remove_instance_variable(:@version) if @connection.instance_variable_defined?(:@version) end end diff --git a/activerecord/test/cases/adapters/mysql2/json_test.rb b/activerecord/test/cases/adapters/mysql2/json_test.rb index 630cdb36a4..d311ffb703 100644 --- a/activerecord/test/cases/adapters/mysql2/json_test.rb +++ b/activerecord/test/cases/adapters/mysql2/json_test.rb @@ -1,17 +1,11 @@ require "cases/helper" -require "support/schema_dumping_helper" +require "cases/json_shared_test_cases" if ActiveRecord::Base.connection.supports_json? class Mysql2JSONTest < ActiveRecord::Mysql2TestCase - include SchemaDumpingHelper + include JSONSharedTestCases self.use_transactional_tests = false - class JsonDataType < ActiveRecord::Base - self.table_name = "json_data_type" - - store_accessor :settings, :resolution - end - def setup @connection = ActiveRecord::Base.connection begin @@ -27,169 +21,9 @@ if ActiveRecord::Base.connection.supports_json? JsonDataType.reset_column_information end - def test_column - column = JsonDataType.columns_hash["payload"] - assert_equal :json, column.type - assert_equal "json", column.sql_type - - type = JsonDataType.type_for_attribute("payload") - assert_not type.binary? - end - - def test_change_table_supports_json - @connection.change_table("json_data_type") do |t| - t.json "users" + private + def column_type + :json end - JsonDataType.reset_column_information - column = JsonDataType.columns_hash["users"] - assert_equal :json, column.type - end - - def test_schema_dumping - output = dump_table_schema("json_data_type") - assert_match(/t\.json\s+"settings"/, output) - 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.save - assert_equal({ "string" => "foo", "symbol" => "bar" }, x.reload.payload) - end - - def test_type_cast_json - type = JsonDataType.type_for_attribute("payload") - - 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({}, type.deserialize("{}")) - assert_equal({ "key"=>nil }, type.deserialize('{"key": null}')) - assert_equal({ "c"=>"}",'"a"'=>'b "a b' }, type.deserialize(%q({"c":"}", "\"a\"":"b \"a b"}))) - end - - def test_rewrite - @connection.execute "insert into json_data_type (payload) VALUES ('{\"k\":\"v\"}')" - x = JsonDataType.first - x.payload = { '"a\'' => "b" } - assert x.save! - end - - def test_select - @connection.execute "insert into json_data_type (payload) VALUES ('{\"k\":\"v\"}')" - x = JsonDataType.first - assert_equal({ "k" => "v" }, x.payload) - end - - def test_select_multikey - @connection.execute %q|insert into json_data_type (payload) VALUES ('{"k1":"v1", "k2":"v2", "k3":[1,2,3]}')| - x = JsonDataType.first - assert_equal({ "k1" => "v1", "k2" => "v2", "k3" => [1,2,3] }, x.payload) - end - - def test_null_json - @connection.execute "insert into json_data_type (payload) VALUES(null)" - x = JsonDataType.first - assert_equal(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) - 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_equal(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"] - assert x.save! - end - - def test_with_store_accessors - x = JsonDataType.new(resolution: "320×480") - assert_equal "320×480", x.resolution - - x.save! - x = JsonDataType.first - assert_equal "320×480", x.resolution - - x.resolution = "640×1136" - x.save! - - x = JsonDataType.first - assert_equal "640×1136", x.resolution - end - - def test_duplication_with_store_accessors - x = JsonDataType.new(resolution: "320×480") - assert_equal "320×480", x.resolution - - y = x.dup - assert_equal "320×480", y.resolution - end - - def test_yaml_round_trip_with_store_accessors - x = JsonDataType.new(resolution: "320×480") - assert_equal "320×480", x.resolution - - y = YAML.load(YAML.dump(x)) - assert_equal "320×480", y.resolution - end - - def test_changes_in_place - json = JsonDataType.new - assert_not json.changed? - - json.payload = { "one" => "two" } - assert json.changed? - assert json.payload_changed? - - json.save! - assert_not json.changed? - - json.payload["three"] = "four" - assert json.payload_changed? - - json.save! - json.reload - - assert_equal({ "one" => "two", "three" => "four" }, json.payload) - assert_not json.changed? - end - - def test_assigning_string_literal - json = JsonDataType.create(payload: "foo") - assert_equal "foo", json.payload - end - - def test_assigning_number - json = JsonDataType.create(payload: 1.234) - assert_equal 1.234, json.payload - end - - def test_assigning_boolean - json = JsonDataType.create(payload: true) - assert_equal true, json.payload - end 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..565130c38f 100644 --- a/activerecord/test/cases/adapters/mysql2/mysql2_adapter_test.rb +++ b/activerecord/test/cases/adapters/mysql2/mysql2_adapter_test.rb @@ -17,17 +17,6 @@ class Mysql2AdapterTest < ActiveRecord::Mysql2TestCase end end - def test_valid_column - with_example_table do - column = @conn.columns("ex").find { |col| col.name == "id" } - assert @conn.valid_type?(column.type) - end - end - - def test_invalid_column - assert_not @conn.valid_type?(:foobar) - end - def test_columns_for_distinct_zero_orders assert_equal "posts.id", @conn.columns_for_distinct("posts.id", []) @@ -65,6 +54,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 776549eb7a..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 @@ -143,7 +143,7 @@ class Mysql2ReservedWordTest < ActiveRecord::Mysql2TestCase 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) + 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_migrations_test.rb b/activerecord/test/cases/adapters/mysql2/schema_migrations_test.rb index fa54aac6b3..251a50e41e 100644 --- a/activerecord/test/cases/adapters/mysql2/schema_migrations_test.rb +++ b/activerecord/test/cases/adapters/mysql2/schema_migrations_test.rb @@ -16,9 +16,9 @@ class SchemaMigrationsTest < ActiveRecord::Mysql2TestCase table_name = ActiveRecord::SchemaMigration.table_name connection.drop_table table_name, if_exists: true - connection.initialize_schema_migrations_table + ActiveRecord::SchemaMigration.create_table - assert connection.column_exists?(table_name, :version, :string, collation: "utf8_general_ci") + assert connection.column_exists?(table_name, :version, :string) end end @@ -27,9 +27,9 @@ class SchemaMigrationsTest < ActiveRecord::Mysql2TestCase table_name = ActiveRecord::InternalMetadata.table_name connection.drop_table table_name, if_exists: true - connection.initialize_internal_metadata_table + ActiveRecord::InternalMetadata.create_table - assert connection.column_exists?(table_name, :key, :string, collation: "utf8_general_ci") + assert connection.column_exists?(table_name, :key, :string) end end diff --git a/activerecord/test/cases/adapters/mysql2/schema_test.rb b/activerecord/test/cases/adapters/mysql2/schema_test.rb index aea930cfe6..1fad5585de 100644 --- a/activerecord/test/cases/adapters/mysql2/schema_test.rb +++ b/activerecord/test/cases/adapters/mysql2/schema_test.rb @@ -76,7 +76,7 @@ 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] diff --git a/activerecord/test/cases/adapters/mysql2/sql_types_test.rb b/activerecord/test/cases/adapters/mysql2/sql_types_test.rb index bee42d48f1..d6e7f29a5c 100644 --- a/activerecord/test/cases/adapters/mysql2/sql_types_test.rb +++ b/activerecord/test/cases/adapters/mysql2/sql_types_test.rb @@ -8,7 +8,7 @@ class Mysql2SqlTypesTest < ActiveRecord::Mysql2TestCase assert_equal "blob", type_to_sql(:binary) end - def type_to_sql(*args) - ActiveRecord::Base.connection.type_to_sql(*args) + def type_to_sql(type, limit = nil) + ActiveRecord::Base.connection.type_to_sql(type, limit: limit) 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..71dcfaa241 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,16 +51,16 @@ 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 test "schema dump includes unsigned option" do schema = dump_table_schema "unsigned_types" - assert_match %r{t.integer\s+"unsigned_integer",\s+unsigned: true$}, schema - assert_match %r{t.bigint\s+"unsigned_bigint",\s+unsigned: true$}, schema - assert_match %r{t.float\s+"unsigned_float",\s+limit: 24,\s+unsigned: true$}, schema - assert_match %r{t.decimal\s+"unsigned_decimal",\s+precision: 10,\s+scale: 2,\s+unsigned: true$}, schema + assert_match %r{t\.integer\s+"unsigned_integer",\s+unsigned: true$}, schema + assert_match %r{t\.bigint\s+"unsigned_bigint",\s+unsigned: true$}, schema + assert_match %r{t\.float\s+"unsigned_float",\s+limit: 24,\s+unsigned: true$}, schema + assert_match %r{t\.decimal\s+"unsigned_decimal",\s+precision: 10,\s+scale: 2,\s+unsigned: true$}, schema end end diff --git a/activerecord/test/cases/adapters/mysql2/virtual_column_test.rb b/activerecord/test/cases/adapters/mysql2/virtual_column_test.rb new file mode 100644 index 0000000000..442a4fb7b5 --- /dev/null +++ b/activerecord/test/cases/adapters/mysql2/virtual_column_test.rb @@ -0,0 +1,59 @@ +require "cases/helper" +require "support/schema_dumping_helper" + +if ActiveRecord::Base.connection.supports_virtual_columns? + class Mysql2VirtualColumnTest < ActiveRecord::Mysql2TestCase + include SchemaDumpingHelper + + self.use_transactional_tests = false + + class VirtualColumn < ActiveRecord::Base + end + + def setup + @connection = ActiveRecord::Base.connection + @connection.create_table :virtual_columns, force: true do |t| + t.string :name + t.virtual :upper_name, type: :string, as: "UPPER(`name`)" + t.virtual :name_length, type: :integer, as: "LENGTH(`name`)", stored: true + end + VirtualColumn.create(name: "Rails") + end + + def teardown + @connection.drop_table :virtual_columns, if_exists: true + VirtualColumn.reset_column_information + end + + def test_virtual_column + column = VirtualColumn.columns_hash["upper_name"] + assert_predicate column, :virtual? + assert_match %r{\bVIRTUAL\b}, column.extra + assert_equal "RAILS", VirtualColumn.take.upper_name + end + + def test_stored_column + column = VirtualColumn.columns_hash["name_length"] + assert_predicate column, :virtual? + assert_match %r{\b(?:STORED|PERSISTENT)\b}, column.extra + assert_equal 5, VirtualColumn.take.name_length + end + + def test_change_table + @connection.change_table :virtual_columns do |t| + t.virtual :lower_name, type: :string, as: "LOWER(name)" + end + VirtualColumn.reset_column_information + column = VirtualColumn.columns_hash["lower_name"] + assert_predicate column, :virtual? + assert_match %r{\bVIRTUAL\b}, column.extra + assert_equal "rails", VirtualColumn.take.lower_name + end + + def test_schema_dumping + output = dump_table_schema("virtual_columns") + assert_match(/t\.virtual\s+"upper_name",\s+type: :string,\s+as: "UPPER\(`name`\)"$/i, output) + assert_match(/t\.virtual\s+"name_length",\s+type: :integer,\s+as: "LENGTH\(`name`\)",\s+stored: true$/i, output) + end + end +end diff --git a/activerecord/test/cases/adapters/postgresql/active_schema_test.rb b/activerecord/test/cases/adapters/postgresql/active_schema_test.rb index 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 97960b6c51..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 @@ -312,9 +316,18 @@ class PostgresqlArrayTest < ActiveRecord::PostgreSQLTestCase end def test_encoding_arrays_of_utf8_strings - string_with_utf8 = "nový" - assert_equal [string_with_utf8], @type.deserialize(@type.serialize([string_with_utf8])) - assert_equal [[string_with_utf8]], @type.deserialize(@type.serialize([[string_with_utf8]])) + 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 diff --git a/activerecord/test/cases/adapters/postgresql/bytea_test.rb b/activerecord/test/cases/adapters/postgresql/bytea_test.rb index dc0df8715a..539c90f0bc 100644 --- a/activerecord/test/cases/adapters/postgresql/bytea_test.rb +++ b/activerecord/test/cases/adapters/postgresql/bytea_test.rb @@ -32,9 +32,9 @@ class PostgresqlByteaTest < ActiveRecord::PostgreSQLTestCase end def test_binary_columns_are_limitless_the_upper_limit_is_one_GB - assert_equal "bytea", @connection.type_to_sql(:binary, 100_000) + assert_equal "bytea", @connection.type_to_sql(:binary, limit: 100_000) assert_raise ActiveRecord::ActiveRecordError do - @connection.type_to_sql :binary, 4294967295 + @connection.type_to_sql(:binary, limit: 4294967295) end end @@ -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 @@ -89,13 +89,14 @@ class PostgresqlByteaTest < ActiveRecord::PostgreSQLTestCase Thread.new do other_conn = ActiveRecord::Base.connection other_conn.execute("SET standard_conforming_strings = off") + other_conn.execute("SET escape_string_warning = off") end.join test_via_to_sql end def test_write_binary - data = File.read(File.join(File.dirname(__FILE__), "..", "..", "..", "assets", "example.log")) + data = File.read(File.join(__dir__, "..", "..", "..", "assets", "example.log")) assert(data.size > 1) record = ByteaDataType.create(payload: data) assert_not record.new_record? @@ -106,8 +107,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/collation_test.rb b/activerecord/test/cases/adapters/postgresql/collation_test.rb index b39e298a5d..a603221d8f 100644 --- a/activerecord/test/cases/adapters/postgresql/collation_test.rb +++ b/activerecord/test/cases/adapters/postgresql/collation_test.rb @@ -47,7 +47,7 @@ class PostgresqlCollationTest < ActiveRecord::PostgreSQLTestCase test "schema dump includes collation" do output = dump_table_schema("postgresql_collations") - assert_match %r{t.string\s+"string_c",\s+collation: "C"$}, output - assert_match %r{t.text\s+"text_posix",\s+collation: "POSIX"$}, output + assert_match %r{t\.string\s+"string_c",\s+collation: "C"$}, output + assert_match %r{t\.text\s+"text_posix",\s+collation: "POSIX"$}, output end end diff --git a/activerecord/test/cases/adapters/postgresql/connection_test.rb b/activerecord/test/cases/adapters/postgresql/connection_test.rb index 48c82cb7b9..32afe331fa 100644 --- a/activerecord/test/cases/adapters/postgresql/connection_test.rb +++ b/activerecord/test/cases/adapters/postgresql/connection_test.rb @@ -31,15 +31,21 @@ module ActiveRecord end def test_encoding - assert_not_nil @connection.encoding + assert_queries(1) do + assert_not_nil @connection.encoding + end end def test_collation - assert_not_nil @connection.collation + assert_queries(1) do + assert_not_nil @connection.collation + end end def test_ctype - assert_not_nil @connection.ctype + assert_queries(1) do + assert_not_nil @connection.ctype + end end def test_default_client_min_messages @@ -90,22 +96,22 @@ 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 def test_table_alias_length_logs_name - @connection.instance_variable_set("@table_alias_length", nil) + @connection.instance_variable_set("@max_identifier_length", nil) @connection.table_alias_length assert_equal "SCHEMA", @subscriber.logged[0][1] end @@ -127,8 +133,8 @@ module ActiveRecord if ActiveRecord::Base.connection.prepared_statements def test_statement_key_is_logged - bind = Relation::QueryAttribute.new(nil, 1, Type::Value.new) - @connection.exec_query("SELECT $1::integer", "SQL", [bind], prepare: true) + binds = [bind_attribute(nil, 1)] + @connection.exec_query("SELECT $1::integer", "SQL", binds, prepare: true) name = @subscriber.payloads.last[:statement_name] assert name res = @connection.exec_query("EXPLAIN (FORMAT JSON) EXECUTE #{name}(1)") @@ -156,7 +162,7 @@ module ActiveRecord secondary_connection.query("select pg_terminate_backend(#{original_connection_pid.first.first})") ActiveRecord::Base.connection_pool.checkin(secondary_connection) elsif ARTest.config["with_manual_interventions"] - puts "Kill the connection now (e.g. by restarting the PostgreSQL " + + puts "Kill the connection now (e.g. by restarting the PostgreSQL " \ 'server with the "-m fast" option) and then press enter.' $stdin.gets else @@ -175,9 +181,9 @@ module ActiveRecord new_connection_pid = @connection.query("select pg_backend_pid()") assert_not_equal original_connection_pid, new_connection_pid, - "umm -- looks like you didn't break the connection, because we're still " + + "umm -- looks like you didn't break the connection, because we're still " \ "successfully querying with the same connection pid." - + ensure # Repair all fixture connections so other tests won't break. @fixture_connections.each(&:verify!) end @@ -245,7 +251,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/datatype_test.rb b/activerecord/test/cases/adapters/postgresql/datatype_test.rb index 0ac8b7339b..0725fde5ae 100644 --- a/activerecord/test/cases/adapters/postgresql/datatype_test.rb +++ b/activerecord/test/cases/adapters/postgresql/datatype_test.rb @@ -28,12 +28,12 @@ class PostgresqlDataTypeTest < ActiveRecord::PostgreSQLTestCase end def test_data_type_of_time_types - assert_equal :string, @first_time.column_for_attribute(:time_interval).type - assert_equal :string, @first_time.column_for_attribute(:scaled_time_interval).type + assert_equal :interval, @first_time.column_for_attribute(:time_interval).type + assert_equal :interval, @first_time.column_for_attribute(:scaled_time_interval).type end def test_data_type_of_oid_types - assert_equal :integer, @first_oid.column_for_attribute(:obj_id).type + assert_equal :oid, @first_oid.column_for_attribute(:obj_id).type end def test_time_values @@ -61,9 +61,9 @@ class PostgresqlDataTypeTest < ActiveRecord::PostgreSQLTestCase end def test_text_columns_are_limitless_the_upper_limit_is_one_GB - assert_equal "text", @connection.type_to_sql(:text, 100_000) + assert_equal "text", @connection.type_to_sql(:text, limit: 100_000) assert_raise ActiveRecord::ActiveRecordError do - @connection.type_to_sql :text, 4294967295 + @connection.type_to_sql(:text, limit: 4294967295) end end end diff --git a/activerecord/test/cases/adapters/postgresql/explain_test.rb b/activerecord/test/cases/adapters/postgresql/explain_test.rb index 7493bce4fb..d79fbccf47 100644 --- a/activerecord/test/cases/adapters/postgresql/explain_test.rb +++ b/activerecord/test/cases/adapters/postgresql/explain_test.rb @@ -7,14 +7,14 @@ class PostgreSQLExplainTest < ActiveRecord::PostgreSQLTestCase def test_explain_for_one_query explain = Developer.where(id: 1).explain - assert_match %r(EXPLAIN for: SELECT "developers".* FROM "developers" WHERE "developers"."id" = (?:\$1 \[\["id", 1\]\]|1)), explain + assert_match %r(EXPLAIN for: SELECT "developers"\.\* FROM "developers" WHERE "developers"\."id" = (?:\$1 \[\["id", 1\]\]|1)), explain assert_match %(QUERY PLAN), explain end def test_explain_with_eager_loading explain = Developer.where(id: 1).includes(:audit_logs).explain assert_match %(QUERY PLAN), explain - assert_match %r(EXPLAIN for: SELECT "developers".* FROM "developers" WHERE "developers"."id" = (?:\$1 \[\["id", 1\]\]|1)), explain + assert_match %r(EXPLAIN for: SELECT "developers"\.\* FROM "developers" WHERE "developers"\."id" = (?:\$1 \[\["id", 1\]\]|1)), explain assert_match %(EXPLAIN for: SELECT "audit_logs".* FROM "audit_logs" WHERE "audit_logs"."developer_id" = 1), explain end end diff --git a/activerecord/test/cases/adapters/postgresql/geometric_test.rb b/activerecord/test/cases/adapters/postgresql/geometric_test.rb index a65d4d1ad9..c1f3a4ae2c 100644 --- a/activerecord/test/cases/adapters/postgresql/geometric_test.rb +++ b/activerecord/test/cases/adapters/postgresql/geometric_test.rb @@ -96,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 9236a67b11..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 @@ -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 @@ -113,8 +119,8 @@ if ActiveRecord::Base.connection.supports_extensions? def test_type_cast_hstore assert_equal({ "1" => "2" }, @type.deserialize("\"1\"=>\"2\"")) assert_equal({}, @type.deserialize("")) - assert_equal({ "key"=>nil }, @type.deserialize("key => NULL")) - assert_equal({ "c"=>"}",'"a"'=>'b "a b' }, @type.deserialize(%q(c=>"}", "\"a\""=>"b \"a b"))) + 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,24 +171,43 @@ 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 @@ -194,19 +219,19 @@ if ActiveRecord::Base.connection.supports_extensions? 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 @@ -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/infinity_test.rb b/activerecord/test/cases/adapters/postgresql/infinity_test.rb index 19b00258b6..b9e177e6ec 100644 --- a/activerecord/test/cases/adapters/postgresql/infinity_test.rb +++ b/activerecord/test/cases/adapters/postgresql/infinity_test.rb @@ -30,7 +30,7 @@ class PostgresqlInfinityTest < ActiveRecord::PostgreSQLTestCase record = PostgresqlInfinity.new(float: "-Infinity") assert_equal(-Float::INFINITY, record.float) record = PostgresqlInfinity.new(float: "NaN") - assert_send [record.float, :nan?] + assert record.float.nan?, "Expected #{record.float} to be NaN" end test "update_all with infinity on a float column" do diff --git a/activerecord/test/cases/adapters/postgresql/json_test.rb b/activerecord/test/cases/adapters/postgresql/json_test.rb index 273b2c1c7b..4eeb563781 100644 --- a/activerecord/test/cases/adapters/postgresql/json_test.rb +++ b/activerecord/test/cases/adapters/postgresql/json_test.rb @@ -1,14 +1,8 @@ require "cases/helper" -require "support/schema_dumping_helper" +require "cases/json_shared_test_cases" module PostgresqlJSONSharedTestCases - include SchemaDumpingHelper - - class JsonDataType < ActiveRecord::Base - self.table_name = "json_data_type" - - store_accessor :settings, :resolution - end + include JSONSharedTestCases def setup @connection = ActiveRecord::Base.connection @@ -16,6 +10,7 @@ module PostgresqlJSONSharedTestCases @connection.create_table("json_data_type") do |t| t.public_send column_type, "payload", default: {} # t.json 'payload', default: {} t.public_send column_type, "settings" # t.json 'settings' + t.public_send column_type, "objects", array: true # t.json 'objects', array: true end rescue ActiveRecord::StatementInvalid skip "do not test on PostgreSQL without #{column_type} type." @@ -27,186 +22,23 @@ module PostgresqlJSONSharedTestCases JsonDataType.reset_column_information end - def test_column - column = JsonDataType.columns_hash["payload"] - assert_equal column_type, column.type - assert_equal column_type.to_s, column.sql_type - assert_not column.array? - - type = JsonDataType.type_for_attribute("payload") - assert_not type.binary? - end - def test_default @connection.add_column "json_data_type", "permissions", column_type, default: { "users": "read", "posts": ["read", "write"] } JsonDataType.reset_column_information - assert_equal({ "users"=>"read", "posts"=>["read", "write"] }, JsonDataType.column_defaults["permissions"]) - assert_equal({ "users"=>"read", "posts"=>["read", "write"] }, JsonDataType.new.permissions) - ensure - JsonDataType.reset_column_information - end - - def test_change_table_supports_json - @connection.transaction do - @connection.change_table("json_data_type") do |t| - t.public_send column_type, "users", default: "{}" # t.json 'users', default: '{}' - end - JsonDataType.reset_column_information - column = JsonDataType.columns_hash["users"] - assert_equal column_type, column.type - - raise ActiveRecord::Rollback # reset the schema change - end + assert_equal({ "users" => "read", "posts" => ["read", "write"] }, JsonDataType.column_defaults["permissions"]) + assert_equal({ "users" => "read", "posts" => ["read", "write"] }, JsonDataType.new.permissions) ensure JsonDataType.reset_column_information end - def test_schema_dumping - output = dump_table_schema("json_data_type") - assert_match(/t\.#{column_type.to_s}\s+"payload",\s+default: {}/, output) - 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.save - assert_equal({ "string" => "foo", "symbol" => "bar" }, x.reload.payload) - end - - def test_type_cast_json - type = JsonDataType.type_for_attribute("payload") - - 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({}, type.deserialize("{}")) - assert_equal({ "key"=>nil }, type.deserialize('{"key": null}')) - assert_equal({ "c"=>"}",'"a"'=>'b "a b' }, type.deserialize(%q({"c":"}", "\"a\"":"b \"a b"}))) - end - - def test_rewrite - @connection.execute "insert into json_data_type (payload) VALUES ('{\"k\":\"v\"}')" - x = JsonDataType.first - x.payload = { '"a\'' => "b" } - assert x.save! - end - - def test_select - @connection.execute "insert into json_data_type (payload) VALUES ('{\"k\":\"v\"}')" - x = JsonDataType.first - assert_equal({ "k" => "v" }, x.payload) - end - - def test_select_multikey - @connection.execute %q|insert into json_data_type (payload) VALUES ('{"k1":"v1", "k2":"v2", "k3":[1,2,3]}')| - x = JsonDataType.first - assert_equal({ "k1" => "v1", "k2" => "v2", "k3" => [1,2,3] }, x.payload) - end - - def test_null_json - @connection.execute "insert into json_data_type (payload) VALUES(null)" - x = JsonDataType.first - assert_equal(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_equal(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) - 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"] - assert x.save! - end - - def test_with_store_accessors - x = JsonDataType.new(resolution: "320×480") - assert_equal "320×480", x.resolution - + def test_deserialize_with_array + x = JsonDataType.new(objects: ["foo" => "bar"]) + assert_equal ["foo" => "bar"], x.objects x.save! - x = JsonDataType.first - assert_equal "320×480", x.resolution - - x.resolution = "640×1136" - x.save! - - x = JsonDataType.first - assert_equal "640×1136", x.resolution - end - - def test_duplication_with_store_accessors - x = JsonDataType.new(resolution: "320×480") - assert_equal "320×480", x.resolution - - y = x.dup - assert_equal "320×480", y.resolution - end - - def test_yaml_round_trip_with_store_accessors - x = JsonDataType.new(resolution: "320×480") - assert_equal "320×480", x.resolution - - y = YAML.load(YAML.dump(x)) - assert_equal "320×480", y.resolution - end - - def test_changes_in_place - json = JsonDataType.new - assert_not json.changed? - - json.payload = { "one" => "two" } - assert json.changed? - assert json.payload_changed? - - json.save! - assert_not json.changed? - - json.payload["three"] = "four" - assert json.payload_changed? - - json.save! - json.reload - - assert_equal({ "one" => "two", "three" => "four" }, json.payload) - assert_not json.changed? - end - - def test_assigning_string_literal - json = JsonDataType.create(payload: "foo") - assert_equal "foo", json.payload - end - - def test_assigning_number - json = JsonDataType.create(payload: 1.234) - assert_equal 1.234, json.payload - end - - def test_assigning_boolean - json = JsonDataType.create(payload: true) - assert_equal true, json.payload + assert_equal ["foo" => "bar"], x.objects + x.reload + assert_equal ["foo" => "bar"], x.objects end end diff --git a/activerecord/test/cases/adapters/postgresql/numbers_test.rb b/activerecord/test/cases/adapters/postgresql/numbers_test.rb index 834354dcc9..bfb2b7c27a 100644 --- a/activerecord/test/cases/adapters/postgresql/numbers_test.rb +++ b/activerecord/test/cases/adapters/postgresql/numbers_test.rb @@ -31,7 +31,7 @@ class PostgresqlNumberTest < ActiveRecord::PostgreSQLTestCase assert_equal 123456.789, first.double assert_equal(-::Float::INFINITY, second.single) assert_equal ::Float::INFINITY, second.double - assert_send [third.double, :nan?] + assert third.double.nan?, "Expected #{third.double} to be NaN" end def test_update diff --git a/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb b/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb index e6af93a53e..bfc763e1ef 100644 --- a/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb +++ b/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb @@ -21,17 +21,6 @@ module ActiveRecord end end - def test_valid_column - with_example_table do - column = @connection.columns("ex").find { |col| col.name == "id" } - assert @connection.valid_type?(column.type) - end - end - - def test_invalid_column - assert_not @connection.valid_type?(:foobar) - end - def test_primary_key with_example_table do assert_equal "id", @connection.primary_key("ex") @@ -54,12 +43,6 @@ module ActiveRecord end end - def test_primary_key_raises_error_if_table_not_found - assert_raises(ActiveRecord::StatementInvalid) do - @connection.primary_key("unobtainium") - end - end - def test_exec_insert_with_returning_disabled connection = connection_without_insert_returning result = connection.exec_insert("insert into postgresql_partitioned_table_parent (number) VALUES (1)", nil, [], "id", "postgresql_partitioned_table_parent_id_seq") @@ -219,8 +202,8 @@ module ActiveRecord string = @connection.quote("foo") @connection.exec_query("INSERT INTO ex (id, data) VALUES (1, #{string})") - bind = Relation::QueryAttribute.new("id", 1, Type::Value.new) - result = @connection.exec_query("SELECT id, data FROM ex WHERE id = $1", nil, [bind]) + binds = [bind_attribute("id", 1)] + result = @connection.exec_query("SELECT id, data FROM ex WHERE id = $1", nil, binds) assert_equal 1, result.rows.length assert_equal 2, result.columns.length @@ -234,8 +217,8 @@ module ActiveRecord string = @connection.quote("foo") @connection.exec_query("INSERT INTO ex (id, data) VALUES (1, #{string})") - bind = Relation::QueryAttribute.new("id", "1-fuu", Type::Integer.new) - result = @connection.exec_query("SELECT id, data FROM ex WHERE id = $1", nil, [bind]) + binds = [bind_attribute("id", "1-fuu", Type::Integer.new)] + result = @connection.exec_query("SELECT id, data FROM ex WHERE id = $1", nil, binds) assert_equal 1, result.rows.length assert_equal 2, result.columns.length @@ -263,9 +246,12 @@ module ActiveRecord def test_index_with_opclass with_example_table do - @connection.add_index "ex", "data varchar_pattern_ops", name: "with_opclass" - index = @connection.indexes("ex").find { |idx| idx.name == "with_opclass" } + @connection.add_index "ex", "data varchar_pattern_ops" + index = @connection.indexes("ex").find { |idx| idx.name == "index_ex_on_data_varchar_pattern_ops" } assert_equal "data varchar_pattern_ops", index.columns + + @connection.remove_index "ex", "data varchar_pattern_ops" + assert_not @connection.indexes("ex").find { |idx| idx.name == "index_ex_on_data_varchar_pattern_ops" } end end @@ -360,7 +346,7 @@ module ActiveRecord @connection.select_all "SELECT NULL::anyelement" @connection.select_all "SELECT NULL::anyelement" } - assert_match(/\Aunknown OID \d+: failed to recognize type of 'anyelement'. It will be treated as String.\n\z/, warning) + assert_match(/\Aunknown OID \d+: failed to recognize type of 'anyelement'\. It will be treated as String\.\n\z/, warning) ensure reset_connection end 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..a1e966b915 100644 --- a/activerecord/test/cases/adapters/postgresql/quoting_test.rb +++ b/activerecord/test/cases/adapters/postgresql/quoting_test.rb @@ -1,5 +1,4 @@ require "cases/helper" -require "ipaddr" module ActiveRecord module ConnectionAdapters @@ -18,12 +17,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 +35,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/schema_authorization_test.rb b/activerecord/test/cases/adapters/postgresql/schema_authorization_test.rb index 7193f23880..f86a76e08a 100644 --- a/activerecord/test/cases/adapters/postgresql/schema_authorization_test.rb +++ b/activerecord/test/cases/adapters/postgresql/schema_authorization_test.rb @@ -68,24 +68,13 @@ class SchemaAuthorizationTest < ActiveRecord::PostgreSQLTestCase USERS.each do |u| @connection.clear_cache! set_session_auth u - assert_equal u, @connection.select_value("SELECT name FROM #{TABLE_NAME} WHERE id = $1", "SQL", [bind_param(1)]) + assert_equal u, @connection.select_value("SELECT name FROM #{TABLE_NAME} WHERE id = $1", "SQL", [bind_attribute("id", 1)]) set_session_auth end end end end - def test_schema_uniqueness - assert_nothing_raised do - set_session_auth - USERS.each do |u| - set_session_auth u - assert_equal u, @connection.select_value("SELECT name FROM #{TABLE_NAME} WHERE id = 1") - set_session_auth - end - end - end - def test_sequence_schema_caching assert_nothing_raised do USERS.each do |u| @@ -110,10 +99,6 @@ class SchemaAuthorizationTest < ActiveRecord::PostgreSQLTestCase private def set_session_auth(auth = nil) - @connection.session_auth = auth || "default" - end - - def bind_param(value) - ActiveRecord::Relation::QueryAttribute.new(nil, value, ActiveRecord::Type::Value.new) + @connection.session_auth = auth || "default" end end diff --git a/activerecord/test/cases/adapters/postgresql/schema_test.rb b/activerecord/test/cases/adapters/postgresql/schema_test.rb index 51a2306c59..f6b957476b 100644 --- a/activerecord/test/cases/adapters/postgresql/schema_test.rb +++ b/activerecord/test/cases/adapters/postgresql/schema_test.rb @@ -91,6 +91,7 @@ class SchemaTest < ActiveRecord::PostgreSQLTestCase @connection.execute "CREATE INDEX #{INDEX_E_NAME} ON #{SCHEMA_NAME}.#{TABLE_NAME} USING gin (#{INDEX_E_COLUMN});" @connection.execute "CREATE INDEX #{INDEX_E_NAME} ON #{SCHEMA2_NAME}.#{TABLE_NAME} USING gin (#{INDEX_E_COLUMN});" @connection.execute "CREATE TABLE #{SCHEMA_NAME}.#{PK_TABLE_NAME} (id serial primary key)" + @connection.execute "CREATE TABLE #{SCHEMA2_NAME}.#{PK_TABLE_NAME} (id serial primary key)" @connection.execute "CREATE SEQUENCE #{SCHEMA_NAME}.#{UNMATCHED_SEQUENCE_NAME}" @connection.execute "CREATE TABLE #{SCHEMA_NAME}.#{UNMATCHED_PK_TABLE_NAME} (id integer NOT NULL DEFAULT nextval('#{SCHEMA_NAME}.#{UNMATCHED_SEQUENCE_NAME}'::regclass), CONSTRAINT unmatched_pkey PRIMARY KEY (id))" end @@ -168,17 +169,17 @@ class SchemaTest < ActiveRecord::PostgreSQLTestCase def test_raise_wrapped_exception_on_bad_prepare assert_raises(ActiveRecord::StatementInvalid) do - @connection.exec_query "select * from developers where id = ?", "sql", [bind_param(1)] + @connection.exec_query "select * from developers where id = ?", "sql", [bind_attribute("id", 1)] end end if ActiveRecord::Base.connection.prepared_statements def test_schema_change_with_prepared_stmt altered = false - @connection.exec_query "select * from developers where id = $1", "sql", [bind_param(1)] + @connection.exec_query "select * from developers where id = $1", "sql", [bind_attribute("id", 1)] @connection.exec_query "alter table developers add column zomg int", "sql", [] altered = true - @connection.exec_query "select * from developers where id = $1", "sql", [bind_param(1)] + @connection.exec_query "select * from developers where id = $1", "sql", [bind_attribute("id", 1)] ensure # We are not using DROP COLUMN IF EXISTS because that syntax is only # supported by pg 9.X @@ -301,13 +302,13 @@ class SchemaTest < ActiveRecord::PostgreSQLTestCase def test_index_name_exists with_schema_search_path(SCHEMA_NAME) do - assert @connection.index_name_exists?(TABLE_NAME, INDEX_A_NAME, true) - assert @connection.index_name_exists?(TABLE_NAME, INDEX_B_NAME, true) - assert @connection.index_name_exists?(TABLE_NAME, INDEX_C_NAME, true) - assert @connection.index_name_exists?(TABLE_NAME, INDEX_D_NAME, true) - assert @connection.index_name_exists?(TABLE_NAME, INDEX_E_NAME, true) - assert @connection.index_name_exists?(TABLE_NAME, INDEX_E_NAME, true) - assert_not @connection.index_name_exists?(TABLE_NAME, "missing_index", true) + assert @connection.index_name_exists?(TABLE_NAME, INDEX_A_NAME) + assert @connection.index_name_exists?(TABLE_NAME, INDEX_B_NAME) + assert @connection.index_name_exists?(TABLE_NAME, INDEX_C_NAME) + assert @connection.index_name_exists?(TABLE_NAME, INDEX_D_NAME) + assert @connection.index_name_exists?(TABLE_NAME, INDEX_E_NAME) + assert @connection.index_name_exists?(TABLE_NAME, INDEX_E_NAME) + assert_not @connection.index_name_exists?(TABLE_NAME, "missing_index") end end @@ -361,19 +362,11 @@ class SchemaTest < ActiveRecord::PostgreSQLTestCase end def test_primary_key_assuming_schema_search_path - with_schema_search_path(SCHEMA_NAME) do + with_schema_search_path("#{SCHEMA_NAME}, #{SCHEMA2_NAME}") do assert_equal "id", @connection.primary_key(PK_TABLE_NAME), "primary key should be found" end end - def test_primary_key_raises_error_if_table_not_found_on_schema_search_path - with_schema_search_path(SCHEMA2_NAME) do - assert_raises(ActiveRecord::StatementInvalid) do - @connection.primary_key(PK_TABLE_NAME) - end - end - end - def test_pk_and_sequence_for_with_schema_specified pg_name = ActiveRecord::ConnectionAdapters::PostgreSQL::Name [ @@ -383,7 +376,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 +386,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 +411,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 @@ -474,10 +467,6 @@ class SchemaTest < ActiveRecord::PostgreSQLTestCase assert_equal this_index_column, this_index.columns[0] assert_equal this_index_name, this_index.name end - - def bind_param(value) - ActiveRecord::Relation::QueryAttribute.new(nil, value, ActiveRecord::Type::Value.new) - end end class SchemaForeignKeyTest < ActiveRecord::PostgreSQLTestCase diff --git a/activerecord/test/cases/adapters/postgresql/statement_pool_test.rb b/activerecord/test/cases/adapters/postgresql/statement_pool_test.rb index eb9978a898..146b619a4b 100644 --- a/activerecord/test/cases/adapters/postgresql/statement_pool_test.rb +++ b/activerecord/test/cases/adapters/postgresql/statement_pool_test.rb @@ -3,13 +3,13 @@ require "cases/helper" module ActiveRecord module ConnectionAdapters class PostgreSQLAdapter < AbstractAdapter - class InactivePGconn + class InactivePgConnection def query(*args) - raise PGError + raise PG::Error end def status - PGconn::CONNECTION_BAD + PG::CONNECTION_BAD end end @@ -31,7 +31,7 @@ module ActiveRecord end def test_dealloc_does_not_raise_on_inactive_connection - cache = StatementPool.new InactivePGconn.new, 10 + cache = StatementPool.new InactivePgConnection.new, 10 cache["foo"] = "bar" assert_nothing_raised { cache.clear } 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 01c597beae..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) @@ -56,7 +56,7 @@ class PostgreSQLNameTest < ActiveRecord::PostgreSQLTestCase test "can be used as hash key" do hash = { Name.new("schema", "article_seq") => "success" } assert_equal "success", hash[Name.new("schema", "article_seq")] - assert_equal nil, hash[Name.new("schema", "articles")] - assert_equal nil, hash[Name.new("public", "article_seq")] + 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 9a59691737..6ebe9d82a7 100644 --- a/activerecord/test/cases/adapters/postgresql/uuid_test.rb +++ b/activerecord/test/cases/adapters/postgresql/uuid_test.rb @@ -9,6 +9,14 @@ 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 + + def uuid_default + connection.supports_pgcrypto_uuid? ? {} : { default: uuid_function } + end end class PostgresqlUUIDTest < ActiveRecord::PostgreSQLTestCase @@ -21,6 +29,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 +40,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 @@ -46,6 +63,16 @@ class PostgresqlUUIDTest < ActiveRecord::PostgreSQLTestCase UUIDType.reset_column_information end + def test_add_column_with_null_true_and_default_nil + assert_nothing_raised do + connection.add_column :uuid_data_type, :thingy, :uuid, null: true, default: nil + end + UUIDType.reset_column_information + column = UUIDType.columns_hash["thingy"] + assert column.null + assert_nil column.default + end + def test_data_type_of_uuid_types column = UUIDType.columns_hash["guid"] assert_equal :uuid, column.type @@ -58,12 +85,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 +182,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 +191,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, **uuid_default) 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,7 +224,7 @@ 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 @@ -206,6 +238,34 @@ class PostgresqlUUIDGenerationTest < ActiveRecord::PostgreSQLTestCase assert_match(/\bcreate_table "pg_uuids_2", id: :uuid, default: -> { "my_uuid_generator\(\)" }/, schema) assert_match(/t\.uuid "other_uuid_2", default: -> { "my_uuid_generator\(\)" }/, schema) end + + def test_schema_dumper_for_uuid_primary_key_default + schema = dump_table_schema "pg_uuids_3" + if connection.supports_pgcrypto_uuid? + assert_match(/\bcreate_table "pg_uuids_3", id: :uuid, default: -> { "gen_random_uuid\(\)" }/, schema) + else + assert_match(/\bcreate_table "pg_uuids_3", id: :uuid, default: -> { "uuid_generate_v4\(\)" }/, schema) + end + end + + def test_schema_dumper_for_uuid_primary_key_default_in_legacy_migration + @verbose_was = ActiveRecord::Migration.verbose + ActiveRecord::Migration.verbose = false + + migration = Class.new(ActiveRecord::Migration[5.0]) do + def version; 101 end + def migrate(x) + create_table("pg_uuids_4", id: :uuid) + end + end.new + ActiveRecord::Migrator.new(:up, [migration]).migrate + + schema = dump_table_schema "pg_uuids_4" + assert_match(/\bcreate_table "pg_uuids_4", id: :uuid, default: -> { "uuid_generate_v4\(\)" }/, schema) + ensure + drop_table "pg_uuids_4" + ActiveRecord::Migration.verbose = @verbose_was + end end end @@ -237,6 +297,25 @@ class PostgresqlUUIDTestNilDefault < ActiveRecord::PostgreSQLTestCase schema = dump_table_schema "pg_uuids" assert_match(/\bcreate_table "pg_uuids", id: :uuid, default: nil/, schema) end + + def test_schema_dumper_for_uuid_primary_key_with_default_nil_in_legacy_migration + @verbose_was = ActiveRecord::Migration.verbose + ActiveRecord::Migration.verbose = false + + migration = Class.new(ActiveRecord::Migration[5.0]) do + def version; 101 end + def migrate(x) + create_table("pg_uuids_4", id: :uuid, default: nil) + end + end.new + ActiveRecord::Migrator.new(:up, [migration]).migrate + + schema = dump_table_schema "pg_uuids_4" + assert_match(/\bcreate_table "pg_uuids_4", id: :uuid, default: nil/, schema) + ensure + drop_table "pg_uuids_4" + ActiveRecord::Migration.verbose = @verbose_was + end end end @@ -255,10 +334,10 @@ class PostgresqlUUIDTestInverseOf < ActiveRecord::PostgreSQLTestCase setup do connection.transaction do - connection.create_table("pg_uuid_posts", id: :uuid) do |t| + connection.create_table("pg_uuid_posts", id: :uuid, **uuid_default) do |t| t.string "title" end - connection.create_table("pg_uuid_comments", id: :uuid) do |t| + connection.create_table("pg_uuid_comments", id: :uuid, **uuid_default) do |t| t.references :uuid_post, type: :uuid t.string "content" end @@ -282,7 +361,6 @@ class PostgresqlUUIDTestInverseOf < ActiveRecord::PostgreSQLTestCase assert_raise ActiveRecord::RecordNotFound do UuidPost.find(123456) end - end def test_find_by_with_uuid diff --git a/activerecord/test/cases/adapters/sqlite3/collation_test.rb b/activerecord/test/cases/adapters/sqlite3/collation_test.rb index 28e8f12c18..dd88ed3656 100644 --- a/activerecord/test/cases/adapters/sqlite3/collation_test.rb +++ b/activerecord/test/cases/adapters/sqlite3/collation_test.rb @@ -47,7 +47,7 @@ class SQLite3CollationTest < ActiveRecord::SQLite3TestCase test "schema dump includes collation" do output = dump_table_schema("collation_table_sqlite3") - assert_match %r{t.string\s+"string_nocase",\s+collation: "NOCASE"$}, output - assert_match %r{t.text\s+"text_rtrim",\s+collation: "RTRIM"$}, output + assert_match %r{t\.string\s+"string_nocase",\s+collation: "NOCASE"$}, output + assert_match %r{t\.text\s+"text_rtrim",\s+collation: "RTRIM"$}, output 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 8342b05870..e1cfd703e8 100644 --- a/activerecord/test/cases/adapters/sqlite3/copy_table_test.rb +++ b/activerecord/test/cases/adapters/sqlite3/copy_table_test.rb @@ -41,8 +41,8 @@ class CopyTableTest < ActiveRecord::SQLite3TestCase test_copy_table("comments", "comments_with_index") do @connection.add_index("comments_with_index", ["post_id", "type"]) test_copy_table("comments_with_index", "comments_with_index2") do - assert_equal table_indexes_without_name("comments_with_index"), - table_indexes_without_name("comments_with_index2") + assert_nil table_indexes_without_name("comments_with_index") + assert_nil table_indexes_without_name("comments_with_index2") end end end @@ -59,7 +59,8 @@ class CopyTableTest < ActiveRecord::SQLite3TestCase 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 + assert_nil original_id.limit + assert_nil copied_id.limit end end @@ -75,7 +76,7 @@ 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)) end diff --git a/activerecord/test/cases/adapters/sqlite3/explain_test.rb b/activerecord/test/cases/adapters/sqlite3/explain_test.rb index 128acb79cf..29d97ae78c 100644 --- a/activerecord/test/cases/adapters/sqlite3/explain_test.rb +++ b/activerecord/test/cases/adapters/sqlite3/explain_test.rb @@ -7,13 +7,13 @@ class SQLite3ExplainTest < ActiveRecord::SQLite3TestCase def test_explain_for_one_query explain = Developer.where(id: 1).explain - assert_match %r(EXPLAIN for: SELECT "developers".* FROM "developers" WHERE "developers"."id" = (?:\? \[\["id", 1\]\]|1)), explain + assert_match %r(EXPLAIN for: SELECT "developers"\.\* FROM "developers" WHERE "developers"\."id" = (?:\? \[\["id", 1\]\]|1)), explain assert_match(/(SEARCH )?TABLE developers USING (INTEGER )?PRIMARY KEY/, explain) end def test_explain_with_eager_loading explain = Developer.where(id: 1).includes(:audit_logs).explain - assert_match %r(EXPLAIN for: SELECT "developers".* FROM "developers" WHERE "developers"."id" = (?:\? \[\["id", 1\]\]|1)), explain + assert_match %r(EXPLAIN for: SELECT "developers"\.\* FROM "developers" WHERE "developers"\."id" = (?:\? \[\["id", 1\]\]|1)), explain assert_match(/(SEARCH )?TABLE developers USING (INTEGER )?PRIMARY KEY/, explain) assert_match %(EXPLAIN for: SELECT "audit_logs".* FROM "audit_logs" WHERE "audit_logs"."developer_id" = 1), explain assert_match(/(SCAN )?TABLE audit_logs/, explain) diff --git a/activerecord/test/cases/adapters/sqlite3/quoting_test.rb b/activerecord/test/cases/adapters/sqlite3/quoting_test.rb index 80a37e83ff..aefbb309e6 100644 --- a/activerecord/test/cases/adapters/sqlite3/quoting_test.rb +++ b/activerecord/test/cases/adapters/sqlite3/quoting_test.rb @@ -1,6 +1,5 @@ require "cases/helper" require "bigdecimal" -require "yaml" require "securerandom" class SQLite3QuotingTest < ActiveRecord::SQLite3TestCase @@ -15,31 +14,6 @@ class SQLite3QuotingTest < ActiveRecord::SQLite3TestCase assert_equal expected, @conn.type_cast(binary) end - def test_type_cast_symbol - assert_equal "foo", @conn.type_cast(:foo) - end - - def test_type_cast_date - date = Date.today - expected = @conn.quoted_date(date) - assert_equal expected, @conn.type_cast(date) - end - - def test_type_cast_time - time = Time.now - expected = @conn.quoted_date(time) - assert_equal expected, @conn.type_cast(time) - end - - def test_type_cast_numeric - assert_equal 10, @conn.type_cast(10) - assert_equal 2.2, @conn.type_cast(2.2) - end - - def test_type_cast_nil - assert_equal nil, @conn.type_cast(nil) - end - def test_type_cast_true assert_equal "t", @conn.type_cast(true) end @@ -53,31 +27,6 @@ class SQLite3QuotingTest < ActiveRecord::SQLite3TestCase assert_equal bd.to_f, @conn.type_cast(bd) end - def test_type_cast_unknown_should_raise_error - obj = Class.new.new - assert_raise(TypeError) { @conn.type_cast(obj) } - end - - def test_type_cast_object_which_responds_to_quoted_id - quoted_id_obj = Class.new { - def quoted_id - "'zomg'" - end - - def id - 10 - end - }.new - assert_equal 10, @conn.type_cast(quoted_id_obj) - - quoted_id_obj = Class.new { - def quoted_id - "'zomg'" - end - }.new - assert_raise(TypeError) { @conn.type_cast(quoted_id_obj) } - end - def test_quoting_binary_strings value = "hello".encode("ascii-8bit") type = ActiveRecord::Type::String.new diff --git a/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb b/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb index 66f9349111..9a812e325e 100644 --- a/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb +++ b/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb @@ -49,22 +49,6 @@ module ActiveRecord end end - def test_valid_column - with_example_table do - column = @conn.columns("ex").find { |col| col.name == "id" } - assert @conn.valid_type?(column.type) - end - end - - # sqlite3 databases should be able to support any type and not just the - # ones mentioned in the native_database_types. - # - # Therefore test_invalid column should always return true even if the - # type is not valid. - def test_invalid_column - assert @conn.valid_type?(:foobar) - end - def test_column_types owner = Owner.create!(name: "hello".encode("ascii-8bit")) owner.reload @@ -82,11 +66,11 @@ module ActiveRecord def test_exec_insert with_example_table do - vals = [Relation::QueryAttribute.new("number", 10, Type::Value.new)] - @conn.exec_insert("insert into ex (number) VALUES (?)", "SQL", vals) + binds = [bind_attribute("number", 10)] + @conn.exec_insert("insert into ex (number) VALUES (?)", "SQL", binds) result = @conn.exec_query( - "select number from ex where number = ?", "SQL", vals) + "select number from ex where number = ?", "SQL", binds) assert_equal 1, result.rows.length assert_equal 10, result.rows.first.first @@ -150,7 +134,7 @@ module ActiveRecord with_example_table "id int, data string" do @conn.exec_query('INSERT INTO ex (id, data) VALUES (1, "foo")') result = @conn.exec_query( - "SELECT id, data FROM ex WHERE id = ?", nil, [Relation::QueryAttribute.new(nil, 1, Type::Value.new)]) + "SELECT id, data FROM ex WHERE id = ?", nil, [bind_attribute("id", 1)]) assert_equal 1, result.rows.length assert_equal 2, result.columns.length @@ -164,7 +148,7 @@ module ActiveRecord @conn.exec_query('INSERT INTO ex (id, data) VALUES (1, "foo")') result = @conn.exec_query( - "SELECT id, data FROM ex WHERE id = ?", nil, [Relation::QueryAttribute.new("id", "1-fuu", Type::Integer.new)]) + "SELECT id, data FROM ex WHERE id = ?", nil, [bind_attribute("id", "1-fuu", Type::Integer.new)]) assert_equal 1, result.rows.length assert_equal 2, result.columns.length @@ -190,7 +174,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,29 +251,26 @@ 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 def test_tables_logs_name sql = <<-SQL - SELECT name FROM sqlite_master - WHERE type IN ('table','view') AND name <> 'sqlite_sequence' + SELECT name FROM sqlite_master WHERE name <> 'sqlite_sequence' AND type IN ('table') 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 @@ -297,13 +278,10 @@ module ActiveRecord def test_table_exists_logs_name with_example_table do sql = <<-SQL - SELECT name FROM sqlite_master - WHERE type IN ('table','view') AND name <> 'sqlite_sequence' AND name = 'ex' + SELECT name FROM sqlite_master WHERE name <> 'sqlite_sequence' AND name = 'ex' AND type IN ('table') 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 diff --git a/activerecord/test/cases/adapters/sqlite3/statement_pool_test.rb b/activerecord/test/cases/adapters/sqlite3/statement_pool_test.rb index aebcce3691..37ff973397 100644 --- a/activerecord/test/cases/adapters/sqlite3/statement_pool_test.rb +++ b/activerecord/test/cases/adapters/sqlite3/statement_pool_test.rb @@ -3,7 +3,6 @@ require "cases/helper" class SQLite3StatementPoolTest < ActiveRecord::SQLite3TestCase if Process.respond_to?(:fork) def test_cache_is_per_pid - cache = ActiveRecord::ConnectionAdapters::SQLite3Adapter::StatementPool.new(10) cache["foo"] = "bar" assert_equal "bar", cache["foo"] diff --git a/activerecord/test/cases/ar_schema_test.rb b/activerecord/test/cases/ar_schema_test.rb index e3eccad71f..5b608d8e83 100644 --- a/activerecord/test/cases/ar_schema_test.rb +++ b/activerecord/test/cases/ar_schema_test.rb @@ -1,146 +1,143 @@ require "cases/helper" -if ActiveRecord::Base.connection.supports_migrations? +class ActiveRecordSchemaTest < ActiveRecord::TestCase + self.use_transactional_tests = false + + setup do + @original_verbose = ActiveRecord::Migration.verbose + ActiveRecord::Migration.verbose = false + @connection = ActiveRecord::Base.connection + ActiveRecord::SchemaMigration.drop_table + end - class ActiveRecordSchemaTest < ActiveRecord::TestCase - self.use_transactional_tests = false + teardown do + @connection.drop_table :fruits rescue nil + @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 - setup do - @original_verbose = ActiveRecord::Migration.verbose - ActiveRecord::Migration.verbose = false - @connection = ActiveRecord::Base.connection - ActiveRecord::SchemaMigration.drop_table - end + def test_has_primary_key + old_primary_key_prefix_type = ActiveRecord::Base.primary_key_prefix_type + ActiveRecord::Base.primary_key_prefix_type = :table_name_with_underscore + assert_equal "version", ActiveRecord::SchemaMigration.primary_key - teardown do - @connection.drop_table :fruits rescue nil - @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 + ActiveRecord::SchemaMigration.create_table + assert_difference "ActiveRecord::SchemaMigration.count", 1 do + ActiveRecord::SchemaMigration.create version: 12 end + ensure + ActiveRecord::SchemaMigration.drop_table + ActiveRecord::Base.primary_key_prefix_type = old_primary_key_prefix_type + end - def test_has_primary_key - old_primary_key_prefix_type = ActiveRecord::Base.primary_key_prefix_type - ActiveRecord::Base.primary_key_prefix_type = :table_name_with_underscore - assert_equal "version", ActiveRecord::SchemaMigration.primary_key - - ActiveRecord::SchemaMigration.create_table - assert_difference "ActiveRecord::SchemaMigration.count", 1 do - ActiveRecord::SchemaMigration.create version: 12 + def test_schema_define + ActiveRecord::Schema.define(version: 7) do + create_table :fruits do |t| + t.column :color, :string + t.column :fruit_size, :string # NOTE: "size" is reserved in Oracle + t.column :texture, :string + t.column :flavor, :string end - ensure - ActiveRecord::SchemaMigration.drop_table - ActiveRecord::Base.primary_key_prefix_type = old_primary_key_prefix_type end - def test_schema_define - ActiveRecord::Schema.define(version: 7) do - create_table :fruits do |t| - t.column :color, :string - t.column :fruit_size, :string # NOTE: "size" is reserved in Oracle - t.column :texture, :string - t.column :flavor, :string - end - end - - assert_nothing_raised { @connection.select_all "SELECT * FROM fruits" } - assert_nothing_raised { @connection.select_all "SELECT * FROM schema_migrations" } - assert_equal 7, ActiveRecord::Migrator::current_version - end + assert_nothing_raised { @connection.select_all "SELECT * FROM fruits" } + assert_nothing_raised { @connection.select_all "SELECT * FROM schema_migrations" } + assert_equal 7, ActiveRecord::Migrator::current_version + end - def test_schema_define_w_table_name_prefix - table_name = ActiveRecord::SchemaMigration.table_name - old_table_name_prefix = ActiveRecord::Base.table_name_prefix - ActiveRecord::Base.table_name_prefix = "nep_" - ActiveRecord::SchemaMigration.table_name = "nep_#{table_name}" - ActiveRecord::Schema.define(version: 7) do - create_table :fruits do |t| - t.column :color, :string - t.column :fruit_size, :string # NOTE: "size" is reserved in Oracle - t.column :texture, :string - t.column :flavor, :string - end + def test_schema_define_w_table_name_prefix + table_name = ActiveRecord::SchemaMigration.table_name + old_table_name_prefix = ActiveRecord::Base.table_name_prefix + ActiveRecord::Base.table_name_prefix = "nep_" + ActiveRecord::SchemaMigration.table_name = "nep_#{table_name}" + ActiveRecord::Schema.define(version: 7) do + create_table :fruits do |t| + t.column :color, :string + t.column :fruit_size, :string # NOTE: "size" is reserved in Oracle + t.column :texture, :string + t.column :flavor, :string end - assert_equal 7, ActiveRecord::Migrator::current_version - ensure - ActiveRecord::Base.table_name_prefix = old_table_name_prefix - ActiveRecord::SchemaMigration.table_name = table_name end + assert_equal 7, ActiveRecord::Migrator::current_version + ensure + ActiveRecord::Base.table_name_prefix = old_table_name_prefix + ActiveRecord::SchemaMigration.table_name = table_name + end - def test_schema_raises_an_error_for_invalid_column_type - assert_raise NoMethodError do - ActiveRecord::Schema.define(version: 8) do - create_table :vegetables do |t| - t.unknown :color - end + def test_schema_raises_an_error_for_invalid_column_type + assert_raise NoMethodError do + ActiveRecord::Schema.define(version: 8) do + create_table :vegetables do |t| + t.unknown :color end end end + end - def test_schema_subclass - Class.new(ActiveRecord::Schema).define(version: 9) do - create_table :fruits - end - assert_nothing_raised { @connection.select_all "SELECT * FROM fruits" } + def test_schema_subclass + Class.new(ActiveRecord::Schema).define(version: 9) do + create_table :fruits end + assert_nothing_raised { @connection.select_all "SELECT * FROM fruits" } + end - def test_normalize_version - assert_equal "118", ActiveRecord::SchemaMigration.normalize_migration_number("0000118") - assert_equal "002", ActiveRecord::SchemaMigration.normalize_migration_number("2") - assert_equal "017", ActiveRecord::SchemaMigration.normalize_migration_number("0017") - assert_equal "20131219224947", ActiveRecord::SchemaMigration.normalize_migration_number("20131219224947") - end + def test_normalize_version + assert_equal "118", ActiveRecord::SchemaMigration.normalize_migration_number("0000118") + assert_equal "002", ActiveRecord::SchemaMigration.normalize_migration_number("2") + assert_equal "017", ActiveRecord::SchemaMigration.normalize_migration_number("0017") + assert_equal "20131219224947", ActiveRecord::SchemaMigration.normalize_migration_number("20131219224947") + end - 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 + 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") + 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 + 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| - t.timestamps - end + def test_timestamps_without_null_set_null_to_false_on_create_table + ActiveRecord::Schema.define do + create_table :has_timestamps do |t| + t.timestamps end - - assert !@connection.columns(:has_timestamps).find { |c| c.name == "created_at" }.null - assert !@connection.columns(:has_timestamps).find { |c| c.name == "updated_at" }.null end - def test_timestamps_without_null_set_null_to_false_on_change_table - ActiveRecord::Schema.define do - create_table :has_timestamps + assert !@connection.columns(:has_timestamps).find { |c| c.name == "created_at" }.null + assert !@connection.columns(:has_timestamps).find { |c| c.name == "updated_at" }.null + end - change_table :has_timestamps do |t| - t.timestamps default: Time.now - end - end + def test_timestamps_without_null_set_null_to_false_on_change_table + ActiveRecord::Schema.define do + create_table :has_timestamps - assert !@connection.columns(:has_timestamps).find { |c| c.name == "created_at" }.null - assert !@connection.columns(:has_timestamps).find { |c| c.name == "updated_at" }.null + change_table :has_timestamps do |t| + t.timestamps default: Time.now + end end - def test_timestamps_without_null_set_null_to_false_on_add_timestamps - ActiveRecord::Schema.define do - create_table :has_timestamps - add_timestamps :has_timestamps, default: Time.now - end + assert !@connection.columns(:has_timestamps).find { |c| c.name == "created_at" }.null + assert !@connection.columns(:has_timestamps).find { |c| c.name == "updated_at" }.null + end - assert !@connection.columns(:has_timestamps).find { |c| c.name == "created_at" }.null - assert !@connection.columns(:has_timestamps).find { |c| c.name == "updated_at" }.null + def test_timestamps_without_null_set_null_to_false_on_add_timestamps + ActiveRecord::Schema.define do + create_table :has_timestamps + add_timestamps :has_timestamps, default: Time.now end + + assert !@connection.columns(:has_timestamps).find { |c| c.name == "created_at" }.null + assert !@connection.columns(:has_timestamps).find { |c| c.name == "updated_at" }.null end end diff --git a/activerecord/test/cases/associations/belongs_to_associations_test.rb b/activerecord/test/cases/associations/belongs_to_associations_test.rb index 2418346d1b..c8b26232b6 100644 --- a/activerecord/test/cases/associations/belongs_to_associations_test.rb +++ b/activerecord/test/cases/associations/belongs_to_associations_test.rb @@ -116,6 +116,44 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase ActiveRecord::Base.belongs_to_required_by_default = original_value end + def test_default + david = developers(:david) + jamis = developers(:jamis) + + model = Class.new(ActiveRecord::Base) do + self.table_name = "ships" + def self.name; "Temp"; end + belongs_to :developer, default: -> { david } + end + + ship = model.create! + assert_equal david, ship.developer + + ship = model.create!(developer: jamis) + assert_equal jamis, ship.developer + + ship.update!(developer: nil) + assert_equal david, ship.developer + end + + def test_default_with_lambda + model = Class.new(ActiveRecord::Base) do + self.table_name = "ships" + def self.name; "Temp"; end + belongs_to :developer, default: -> { default_developer } + + def default_developer + Developer.first + end + end + + ship = model.create! + assert_equal developers(:david), ship.developer + + ship = model.create!(developer: developers(:jamis)) + assert_equal developers(:jamis), ship.developer + end + def test_default_scope_on_relations_is_not_cached counter = 0 @@ -285,12 +323,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 +394,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 @@ -1047,7 +1095,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 @@ -1062,6 +1110,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) @@ -1107,12 +1169,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 2f62d0367e..f9d1e44595 100644 --- a/activerecord/test/cases/associations/callbacks_test.rb +++ b/activerecord/test/cases/associations/callbacks_test.rb @@ -7,7 +7,7 @@ require "models/computer" require "models/company" class AssociationCallbacksTest < ActiveRecord::TestCase - fixtures :posts, :authors, :projects, :developers + fixtures :posts, :authors, :author_addresses, :projects, :developers def setup @david = authors(:david) @@ -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? } @@ -128,7 +128,7 @@ class AssociationCallbacksTest < ActiveRecord::TestCase assert ar.developers_log.empty? alice = Developer.new(name: "alice") ar.developers_with_callbacks << alice - assert_equal"after_adding#{alice.id}", ar.developers_log.last + assert_equal "after_adding#{alice.id}", ar.developers_log.last bob = ar.developers_with_callbacks.create(name: "bob") assert_equal "after_adding#{bob.id}", ar.developers_log.last @@ -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 diff --git a/activerecord/test/cases/associations/cascaded_eager_loading_test.rb b/activerecord/test/cases/associations/cascaded_eager_loading_test.rb index e87431bf32..3638c87968 100644 --- a/activerecord/test/cases/associations/cascaded_eager_loading_test.rb +++ b/activerecord/test/cases/associations/cascaded_eager_loading_test.rb @@ -12,7 +12,7 @@ require "models/vertex" require "models/edge" class CascadedEagerLoadingTest < ActiveRecord::TestCase - fixtures :authors, :mixins, :companies, :posts, :topics, :accounts, :comments, + fixtures :authors, :author_addresses, :mixins, :companies, :posts, :topics, :accounts, :comments, :categorizations, :people, :categories, :edges, :vertices def test_eager_association_loading_with_cascaded_two_levels @@ -20,7 +20,7 @@ class CascadedEagerLoadingTest < ActiveRecord::TestCase 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 @@ -28,7 +28,7 @@ class CascadedEagerLoadingTest < ActiveRecord::TestCase 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 @@ -86,7 +86,7 @@ class CascadedEagerLoadingTest < ActiveRecord::TestCase 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 @@ -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 a7a8c6a783..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 diff --git a/activerecord/test/cases/associations/eager_singularization_test.rb b/activerecord/test/cases/associations/eager_singularization_test.rb index 5d1c1c4b9b..16eff15026 100644 --- a/activerecord/test/cases/associations/eager_singularization_test.rb +++ b/activerecord/test/cases/associations/eager_singularization_test.rb @@ -1,147 +1,146 @@ require "cases/helper" -if ActiveRecord::Base.connection.supports_migrations? - class EagerSingularizationTest < ActiveRecord::TestCase - class Virus < ActiveRecord::Base - belongs_to :octopus - end - - class Octopus < ActiveRecord::Base - has_one :virus - end - - class Pass < ActiveRecord::Base - belongs_to :bus - end - - class Bus < ActiveRecord::Base - has_many :passes - end - - class Mess < ActiveRecord::Base - has_and_belongs_to_many :crises - end - - class Crisis < ActiveRecord::Base - has_and_belongs_to_many :messes - has_many :analyses, dependent: :destroy - has_many :successes, through: :analyses - has_many :dresses, dependent: :destroy - has_many :compresses, through: :dresses - end - - class Analysis < ActiveRecord::Base - belongs_to :crisis - belongs_to :success - end - - class Success < ActiveRecord::Base - has_many :analyses, dependent: :destroy - has_many :crises, through: :analyses - end - - class Dress < ActiveRecord::Base - belongs_to :crisis - has_many :compresses - end - - class Compress < ActiveRecord::Base - belongs_to :dress - end - - def setup - connection.create_table :viri do |t| - t.column :octopus_id, :integer - t.column :species, :string - end - connection.create_table :octopi do |t| - t.column :species, :string - end - connection.create_table :passes do |t| - t.column :bus_id, :integer - t.column :rides, :integer - end - connection.create_table :buses do |t| - t.column :name, :string - end - connection.create_table :crises_messes, id: false do |t| - t.column :crisis_id, :integer - t.column :mess_id, :integer - end - connection.create_table :messes do |t| - t.column :name, :string - end - connection.create_table :crises do |t| - t.column :name, :string - end - connection.create_table :successes do |t| - t.column :name, :string - end - connection.create_table :analyses do |t| - t.column :crisis_id, :integer - t.column :success_id, :integer - end - connection.create_table :dresses do |t| - t.column :crisis_id, :integer - end - connection.create_table :compresses do |t| - t.column :dress_id, :integer - end - end - - teardown do - connection.drop_table :viri - connection.drop_table :octopi - connection.drop_table :passes - connection.drop_table :buses - connection.drop_table :crises_messes - connection.drop_table :messes - connection.drop_table :crises - connection.drop_table :successes - connection.drop_table :analyses - connection.drop_table :dresses - connection.drop_table :compresses - end +class EagerSingularizationTest < ActiveRecord::TestCase + class Virus < ActiveRecord::Base + belongs_to :octopus + end - def connection - ActiveRecord::Base.connection + class Octopus < ActiveRecord::Base + has_one :virus + end + + class Pass < ActiveRecord::Base + belongs_to :bus + end + + class Bus < ActiveRecord::Base + has_many :passes + end + + class Mess < ActiveRecord::Base + has_and_belongs_to_many :crises + end + + class Crisis < ActiveRecord::Base + has_and_belongs_to_many :messes + has_many :analyses, dependent: :destroy + has_many :successes, through: :analyses + has_many :dresses, dependent: :destroy + has_many :compresses, through: :dresses + end + + class Analysis < ActiveRecord::Base + belongs_to :crisis + belongs_to :success + end + + class Success < ActiveRecord::Base + has_many :analyses, dependent: :destroy + has_many :crises, through: :analyses + end + + class Dress < ActiveRecord::Base + belongs_to :crisis + has_many :compresses + end + + class Compress < ActiveRecord::Base + belongs_to :dress + end + + def setup + connection.create_table :viri do |t| + t.column :octopus_id, :integer + t.column :species, :string end + connection.create_table :octopi do |t| + t.column :species, :string + end + connection.create_table :passes do |t| + t.column :bus_id, :integer + t.column :rides, :integer + end + connection.create_table :buses do |t| + t.column :name, :string + end + connection.create_table :crises_messes, id: false do |t| + t.column :crisis_id, :integer + t.column :mess_id, :integer + end + connection.create_table :messes do |t| + t.column :name, :string + end + connection.create_table :crises do |t| + t.column :name, :string + end + connection.create_table :successes do |t| + t.column :name, :string + end + connection.create_table :analyses do |t| + t.column :crisis_id, :integer + t.column :success_id, :integer + end + connection.create_table :dresses do |t| + t.column :crisis_id, :integer + end + connection.create_table :compresses do |t| + t.column :dress_id, :integer + end + end - def test_eager_no_extra_singularization_belongs_to - assert_nothing_raised do - Virus.all.merge!(includes: :octopus).to_a - end + teardown do + connection.drop_table :viri + connection.drop_table :octopi + connection.drop_table :passes + connection.drop_table :buses + connection.drop_table :crises_messes + connection.drop_table :messes + connection.drop_table :crises + connection.drop_table :successes + connection.drop_table :analyses + connection.drop_table :dresses + connection.drop_table :compresses + end + + def test_eager_no_extra_singularization_belongs_to + assert_nothing_raised do + Virus.all.merge!(includes: :octopus).to_a end + end - def test_eager_no_extra_singularization_has_one - assert_nothing_raised do - Octopus.all.merge!(includes: :virus).to_a - end + def test_eager_no_extra_singularization_has_one + assert_nothing_raised do + Octopus.all.merge!(includes: :virus).to_a end + end - def test_eager_no_extra_singularization_has_many - assert_nothing_raised do - Bus.all.merge!(includes: :passes).to_a - end + def test_eager_no_extra_singularization_has_many + assert_nothing_raised do + Bus.all.merge!(includes: :passes).to_a end + end - def test_eager_no_extra_singularization_has_and_belongs_to_many - assert_nothing_raised do - Crisis.all.merge!(includes: :messes).to_a - Mess.all.merge!(includes: :crises).to_a - end + def test_eager_no_extra_singularization_has_and_belongs_to_many + assert_nothing_raised do + Crisis.all.merge!(includes: :messes).to_a + Mess.all.merge!(includes: :crises).to_a end + end - def test_eager_no_extra_singularization_has_many_through_belongs_to - assert_nothing_raised do - Crisis.all.merge!(includes: :successes).to_a - end + def test_eager_no_extra_singularization_has_many_through_belongs_to + assert_nothing_raised do + Crisis.all.merge!(includes: :successes).to_a end + end - def test_eager_no_extra_singularization_has_many_through_has_many - assert_nothing_raised do - Crisis.all.merge!(includes: :compresses).to_a - end + def test_eager_no_extra_singularization_has_many_through_has_many + assert_nothing_raised do + Crisis.all.merge!(includes: :compresses).to_a end end + + private + def connection + ActiveRecord::Base.connection + end end diff --git a/activerecord/test/cases/associations/eager_test.rb b/activerecord/test/cases/associations/eager_test.rb index d1c4c1cef8..4271a09c9b 100644 --- a/activerecord/test/cases/associations/eager_test.rb +++ b/activerecord/test/cases/associations/eager_test.rb @@ -241,7 +241,7 @@ class EagerAssociationTest < ActiveRecord::TestCase 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 @@ -356,31 +356,31 @@ class EagerAssociationTest < ActiveRecord::TestCase 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 @@ -395,14 +395,14 @@ class EagerAssociationTest < ActiveRecord::TestCase 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 @@ -452,8 +452,8 @@ class EagerAssociationTest < ActiveRecord::TestCase 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 @@ -464,7 +464,7 @@ class EagerAssociationTest < ActiveRecord::TestCase 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 @@ -739,18 +739,25 @@ class EagerAssociationTest < ActiveRecord::TestCase end 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) + e = assert_raise(ActiveRecord::AssociationNotFoundError) { + Post.all.merge!(includes: :monkeys).find(6) } - assert_raise(ActiveRecord::AssociationNotFoundError, "Association was not found; perhaps you misspelled it? You specified :include => :monkeys") { + assert_equal("Association named 'monkeys' was not found on Post; perhaps you misspelled it?", e.message) + + e = assert_raise(ActiveRecord::AssociationNotFoundError) { Post.all.merge!(includes: [ :monkeys ]).find(6) } - assert_raise(ActiveRecord::AssociationNotFoundError, "Association was not found; perhaps you misspelled it? You specified :include => :monkeys") { + assert_equal("Association named 'monkeys' was not found on Post; perhaps you misspelled it?", e.message) + + e = assert_raise(ActiveRecord::AssociationNotFoundError) { Post.all.merge!(includes: [ "monkeys" ]).find(6) } - assert_raise(ActiveRecord::AssociationNotFoundError, "Association was not found; perhaps you misspelled it? You specified :include => :monkeys, :elephants") { + assert_equal("Association named 'monkeys' was not found on Post; perhaps you misspelled it?", e.message) + + e = assert_raise(ActiveRecord::AssociationNotFoundError) { Post.all.merge!(includes: [ :monkeys, :elephants ]).find(6) } + assert_equal("Association named 'monkeys' was not found on Post; perhaps you misspelled it?", e.message) end def test_eager_has_many_through_with_order @@ -844,7 +851,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 @@ -903,8 +910,8 @@ class EagerAssociationTest < ActiveRecord::TestCase 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 @@ -933,7 +940,11 @@ class EagerAssociationTest < ActiveRecord::TestCase d2 = find_all_ordered(Firm, :account) d1.each_index do |i| assert_equal(d1[i], d2[i]) - assert_equal(d1[i].account, d2[i].account) + if d1[i].account.nil? + assert_nil(d2[i].account) + else + assert_equal(d1[i].account, d2[i].account) + end end end @@ -943,7 +954,13 @@ class EagerAssociationTest < ActiveRecord::TestCase d2 = find_all_ordered(Client, firm_types) d1.each_index do |i| assert_equal(d1[i], d2[i]) - firm_types.each { |type| assert_equal(d1[i].send(type), d2[i].send(type)) } + firm_types.each do |type| + if (expected = d1[i].send(type)).nil? + assert_nil(d2[i].send(type)) + else + assert_equal(expected, d2[i].send(type)) + end + end end end def test_eager_with_valid_association_as_string_not_symbol @@ -1077,12 +1094,6 @@ class EagerAssociationTest < ActiveRecord::TestCase 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 } - - posts = assert_queries(2) do Post.all.merge!(includes: :author, joins: { taggings: :tag }, where: "tags.name = 'General'", order: "posts.id").to_a end assert_equal posts(:welcome, :thinking), posts @@ -1163,7 +1174,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 @@ -1346,6 +1357,7 @@ class EagerAssociationTest < ActiveRecord::TestCase assert_nothing_raised do authors(:david).essays.includes(:writer).any? authors(:david).essays.includes(:writer).exists? + authors(:david).essays.includes(:owner).where("name IS NOT NULL").exists? end end @@ -1360,7 +1372,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 { diff --git a/activerecord/test/cases/associations/extension_test.rb b/activerecord/test/cases/associations/extension_test.rb index cc86e1a16d..f707a170f5 100644 --- a/activerecord/test/cases/associations/extension_test.rb +++ b/activerecord/test/cases/associations/extension_test.rb @@ -36,6 +36,11 @@ class AssociationsExtensionsTest < ActiveRecord::TestCase assert_equal comments(:greetings), posts(:welcome).comments.not_again.find_most_recent end + def test_extension_with_dirty_target + comment = posts(:welcome).comments.build(body: "New comment") + assert_equal comment, posts(:welcome).comments.with_content("New comment") + end + def test_marshalling_extensions david = developers(:david) assert_equal projects(:action_controller), david.projects.find_most_recent @@ -45,7 +50,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 @@ -73,6 +78,12 @@ class AssociationsExtensionsTest < ActiveRecord::TestCase assert_equal post.association(:comments), post.comments.where("1=1").the_association end + def test_association_with_default_scope + assert_raises OopsError do + posts(:welcome).comments.destroy_all + end + end + private def extend!(model) diff --git a/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb b/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb index 06fc7a4388..f73005b3cb 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,12 @@ class DeveloperWithSymbolClassName < Developer has_and_belongs_to_many :projects, class_name: :ProjectWithSymbolsForKeys end +ActiveSupport::Deprecation.silence do + class DeveloperWithConstantClassName < Developer + has_and_belongs_to_many :projects, class_name: ProjectWithSymbolsForKeys + end +end + class DeveloperWithExtendOption < Developer module NamedExtension def category @@ -105,6 +111,21 @@ class ProjectUnscopingDavidDefaultScope < ActiveRecord::Base association_foreign_key: "developer_id" end +class Kitchen < ActiveRecord::Base + has_one :sink +end + +class Sink < ActiveRecord::Base + has_and_belongs_to_many :sources, join_table: :edges + belongs_to :kitchen + accepts_nested_attributes_for :kitchen +end + +class Source < ActiveRecord::Base + self.table_name = "men" + has_and_belongs_to_many :sinks, join_table: :edges +end + class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase fixtures :accounts, :companies, :categories, :posts, :categories_posts, :developers, :projects, :developers_projects, :parrots, :pirates, :parrots_pirates, :treasures, :price_estimates, :tags, :taggings, :computers @@ -249,8 +270,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 @@ -346,19 +367,6 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase assert_equal Developer.find(1).projects.sort_by(&:id).last, proj # prove join table is updated end - def test_create_by_new_record - devel = Developer.new(name: "Marcel", salary: 75000) - devel.projects.build(name: "Make bed") - proj2 = devel.projects.build(name: "Lie in it") - assert_equal devel.projects.last, proj2 - assert !proj2.persisted? - devel.save - assert devel.persisted? - assert proj2.persisted? - assert_equal devel.projects.last, proj2 - assert_equal Developer.find_by_name("Marcel").projects.last, proj2 # prove join table is updated - end - def test_creation_respects_hash_condition # in Oracle '' is saved as null therefore need to save ' ' in not null column post = categories(:general).post_with_conditions.build(body: " ") @@ -379,7 +387,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 @@ -739,8 +747,8 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase end def test_find_scoped_grouped_having - assert_equal 2, projects(:active_record).well_payed_salary_groups.to_a.size - assert projects(:active_record).well_payed_salary_groups.all? { |g| g.salary > 10000 } + assert_equal 2, projects(:active_record).well_paid_salary_groups.to_a.size + assert projects(:active_record).well_paid_salary_groups.all? { |g| g.salary > 10000 } end def test_get_ids @@ -933,20 +941,22 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase assert_not_nil Developer._reflections["shared_computers"] # Checking the fixture for named association is important here, because it's the only way # we've been able to reproduce this bug - assert_not_nil File.read(File.expand_path("../../../fixtures/developers.yml", __FILE__)).index("shared_computers") + assert_not_nil File.read(File.expand_path("../../fixtures/developers.yml", __dir__)).index("shared_computers") assert_equal developers(:david).shared_computers.first, computers(:laptop) end 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 +1010,22 @@ 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 + + def test_has_and_belongs_to_many_with_belongs_to + sink = Sink.create! kitchen: Kitchen.new, sources: [Source.new] + assert_equal 1, sink.sources.count + 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 fed59c2ab3..a794eba691 100644 --- a/activerecord/test/cases/associations/has_many_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_associations_test.rb @@ -40,7 +40,7 @@ require "models/zine" require "models/interest" class HasManyAssociationsTestForReorderWithJoinDependency < ActiveRecord::TestCase - fixtures :authors, :posts, :comments + fixtures :authors, :author_addresses, :posts, :comments def test_should_generate_valid_sql author = authors(:david) @@ -51,7 +51,7 @@ class HasManyAssociationsTestForReorderWithJoinDependency < ActiveRecord::TestCa end class HasManyAssociationsTestPrimaryKeys < ActiveRecord::TestCase - fixtures :authors, :essays, :subscribers, :subscriptions, :people + fixtures :authors, :author_addresses, :essays, :subscribers, :subscriptions, :people def test_custom_primary_key_on_new_record_should_fetch_with_query subscriber = Subscriber.new(nick: "webster132") @@ -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 @@ -100,7 +100,7 @@ end class HasManyAssociationsTest < ActiveRecord::TestCase fixtures :accounts, :categories, :companies, :developers, :projects, - :developers_projects, :topics, :authors, :comments, + :developers_projects, :topics, :authors, :author_addresses, :comments, :posts, :readers, :taggings, :cars, :jobs, :tags, :categorizations, :zines, :interests @@ -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 @@ -528,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 @@ -611,21 +611,16 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_update_all_on_association_accessed_before_save firm = Firm.new(name: "Firm") - clients_proxy_id = firm.clients.object_id firm.clients << Client.first firm.save! assert_equal firm.clients.count, firm.clients.update_all(description: "Great!") - assert_not_equal clients_proxy_id, firm.clients.object_id end def test_update_all_on_association_accessed_before_save_with_explicit_foreign_key - # We can use the same cached proxy object because the id is available for the scope firm = Firm.new(name: "Firm", id: 100) - clients_proxy_id = firm.clients.object_id firm.clients << Client.first firm.save! assert_equal firm.clients.count, firm.clients.update_all(description: "Great!") - assert_equal clients_proxy_id, firm.clients.object_id end def test_belongs_to_sanity @@ -788,6 +783,12 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_equal [1], posts(:welcome).comments.select { |c| c.id == 1 }.map(&:id) end + def test_select_with_block_and_dirty_target + assert_equal 2, posts(:welcome).comments.select { true }.size + posts(:welcome).comments.build + assert_equal 3, posts(:welcome).comments.select { true }.size + end + def test_select_without_foreign_key assert_equal companies(:first_firm).accounts.first.credit_limit, companies(:first_firm).accounts.select(:credit_limit).first.credit_limit end @@ -912,6 +913,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 @@ -983,7 +985,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 @@ -1574,26 +1576,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") @@ -1735,6 +1717,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 @@ -1916,7 +1903,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_calling_many_on_loaded_association_should_not_use_query firm = companies(:first_firm) - firm.clients.collect # force load + firm.clients.load # force load assert_no_queries { assert firm.clients.many? } end @@ -1955,7 +1942,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_calling_none_on_loaded_association_should_not_use_query firm = companies(:first_firm) - firm.clients.collect # force load + firm.clients.load # force load assert_no_queries { assert ! firm.clients.none? } end @@ -1990,7 +1977,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_calling_one_on_loaded_association_should_not_use_query firm = companies(:first_firm) - firm.clients.collect # force load + firm.clients.load # force load assert_no_queries { assert ! firm.clients.one? } end @@ -2050,12 +2037,6 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_equal client_association.new.attributes, client_association.send(:new).attributes end - def test_respond_to_private_class_methods - client_association = companies(:first_firm).clients - assert !client_association.respond_to?(:private_method) - assert client_association.respond_to?(:private_method, true) - end - def test_creating_using_primary_key firm = Firm.all.merge!(order: "id").first client = firm.clients_using_primary_key.create!(name: "test") @@ -2270,7 +2251,15 @@ class HasManyAssociationsTest < ActiveRecord::TestCase test "association with extend option with multiple extensions" do post = posts(:welcome) assert_equal "lifo", post.comments_with_extend_2.author - assert_equal "hello", post.comments_with_extend_2.greeting + assert_equal "hullo", post.comments_with_extend_2.greeting + end + + test "extend option affects per association" do + post = posts(:welcome) + assert_equal "lifo", post.comments_with_extend.author + assert_equal "lifo", post.comments_with_extend_2.author + assert_equal "hello", post.comments_with_extend.greeting + assert_equal "hullo", post.comments_with_extend_2.greeting end test "delete record with complex joins" do @@ -2291,7 +2280,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 @@ -2460,16 +2449,27 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_equal [first_bulb, second_bulb], car.bulbs 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 + test "prevent double insertion of new object when the parent association loaded in the after save callback" do + reset_callbacks(:save, Bulb) do + Bulb.after_save { |record| record.car.bulbs.load } + + car = Car.create! + car.bulbs << Bulb.new + + assert_equal 1, car.bulbs.size + end end - def test_association_force_reload_with_only_true_is_deprecated - company = Company.find(1) + test "prevent double firing the before save callback of new object when the parent association saved in the callback" do + reset_callbacks(:save, Bulb) do + count = 0 + Bulb.before_save { |record| record.car.save && count += 1 } - assert_deprecated { company.clients_of_firm(true) } + car = Car.create! + car.bulbs.create! + + assert_equal 1, count + end end class AuthorWithErrorDestroyingAssociation < ActiveRecord::Base @@ -2510,9 +2510,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 { |record| record.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 9f716d7820..9156f6d57a 100644 --- a/activerecord/test/cases/associations/has_many_through_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_through_associations_test.rb @@ -28,6 +28,9 @@ require "models/member" require "models/membership" require "models/club" require "models/organization" +require "models/user" +require "models/family" +require "models/family_tree" class HasManyThroughAssociationsTest < ActiveRecord::TestCase fixtures :posts, :readers, :people, :comments, :authors, :categories, :taggings, :tags, @@ -61,10 +64,6 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase club1.members.sort_by(&:id) end - def make_model(name) - Class.new(ActiveRecord::Base) { define_singleton_method(:name) { name } } - end - def test_ordered_has_many_through person_prime = Class.new(ActiveRecord::Base) do def self.name; "Person"; end @@ -75,7 +74,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 @@ -149,20 +148,6 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase assert after_destroy_called, "after destroy should be called" end - def make_no_pk_hm_t - lesson = make_model "Lesson" - student = make_model "Student" - - lesson_student = make_model "LessonStudent" - lesson_student.table_name = "lessons_students" - - lesson_student.belongs_to :lesson, anonymous_class: lesson - lesson_student.belongs_to :student, anonymous_class: student - lesson.has_many :lesson_students, anonymous_class: lesson_student - lesson.has_many :students, through: :lesson_students, anonymous_class: student - [lesson, lesson_student, student] - end - def test_pk_is_not_required_for_join post = Post.includes(:scategories).first post2 = Post.includes(:categories).first @@ -402,7 +387,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 +408,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 +470,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 @@ -702,7 +687,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 +701,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"], @@ -880,13 +865,34 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase book.subscriber_ids = [] assert_equal [], book.subscribers.reload end + end + def test_collection_singular_ids_setter_with_changed_primary_key + company = companies(:first_firm) + client = companies(:first_client) + company.clients_using_primary_key_ids = [client.name] + assert_equal [client], company.clients_using_primary_key end def test_collection_singular_ids_setter_raises_exception_when_invalid_ids_set company = companies(:rails_core) - ids = [Developer.first.id, -9999] - 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 @@ -1182,12 +1188,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 +1215,42 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase ensure TenantMembership.current_member = nil end + + def test_has_many_through_with_scope_should_respect_table_alias + family = Family.create! + users = 3.times.map { User.create! } + FamilyTree.create!(member: users[0], family: family) + FamilyTree.create!(member: users[1], family: family) + FamilyTree.create!(member: users[2], family: family, token: "wat") + + assert_equal 2, users[0].family_members.to_a.size + assert_equal 0, users[2].family_members.to_a.size + end + + def test_incorrectly_ordered_through_associations + assert_raises(ActiveRecord::HasManyThroughOrderError) do + DeveloperWithIncorrectlyOrderedHasManyThrough.create( + companies: [Company.create] + ) + end + end + + private + def make_model(name) + Class.new(ActiveRecord::Base) { define_singleton_method(:name) { name } } + end + + def make_no_pk_hm_t + lesson = make_model "Lesson" + student = make_model "Student" + + lesson_student = make_model "LessonStudent" + lesson_student.table_name = "lessons_students" + + lesson_student.belongs_to :lesson, anonymous_class: lesson + lesson_student.belongs_to :student, anonymous_class: student + lesson.has_many :lesson_students, anonymous_class: lesson_student + lesson.has_many :students, through: :lesson_students, anonymous_class: student + [lesson, lesson_student, student] + 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 1a0e6d2f8e..7c11d2e7fc 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) @@ -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 @@ -485,7 +476,7 @@ class HasOneAssociationsTest < ActiveRecord::TestCase assert_equal ships(:black_pearl), pirate.ship assert_equal pirate.id, pirate.ship.pirate_id - assert_equal "Failed to remove the existing associated ship. " + + assert_equal "Failed to remove the existing associated ship. " \ "The record failed to save after its foreign key was set to nil.", error.message 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 - def test_assocation_enum_works_properly + class SpecialSupscription < ActiveRecord::Base + self.table_name = "subscriptions" + belongs_to :book, class_name: "SpecialBook" + end + + def test_association_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_association_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 b2f47d2daf..28b883586d 100644 --- a/activerecord/test/cases/associations/has_one_through_associations_test.rb +++ b/activerecord/test/cases/associations/has_one_through_associations_test.rb @@ -23,7 +23,7 @@ require "models/customer_carrier" class HasOneThroughAssociationsTest < ActiveRecord::TestCase fixtures :member_types, :members, :clubs, :memberships, :sponsors, :organizations, :minivans, - :dashboards, :speedometers, :authors, :posts, :comments, :categories, :essays, :owners + :dashboards, :speedometers, :authors, :author_addresses, :posts, :comments, :categories, :essays, :owners def setup @member = members(:groucho) @@ -82,10 +82,17 @@ 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 + def test_set_record_after_delete_association + @member.club = nil + @member.club = clubs(:moustache_club) + @member.reload + assert_equal clubs(:moustache_club), @member.club + end + def test_has_one_through_polymorphic assert_equal clubs(:moustache_club), @member.sponsor_club end @@ -110,12 +117,12 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase # 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,7 +130,7 @@ 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 } end diff --git a/activerecord/test/cases/associations/inner_join_association_test.rb b/activerecord/test/cases/associations/inner_join_association_test.rb index 7414869c8f..ddf5bc6f0b 100644 --- a/activerecord/test/cases/associations/inner_join_association_test.rb +++ b/activerecord/test/cases/associations/inner_join_association_test.rb @@ -10,7 +10,7 @@ require "models/tagging" require "models/tag" class InnerJoinAssociationTest < ActiveRecord::TestCase - fixtures :authors, :essays, :posts, :comments, :categories, :categories_posts, :categorizations, + fixtures :authors, :author_addresses, :essays, :posts, :comments, :categories, :categories_posts, :categorizations, :taggings, :tags def test_construct_finder_sql_applies_aliases_tables_on_association_conditions diff --git a/activerecord/test/cases/associations/inverse_associations_test.rb b/activerecord/test/cases/associations/inverse_associations_test.rb index 6fe6ee6783..467cc73ecd 100644 --- a/activerecord/test/cases/associations/inverse_associations_test.rb +++ b/activerecord/test/cases/associations/inverse_associations_test.rb @@ -443,7 +443,7 @@ class InverseHasManyTests < ActiveRecord::TestCase assert man.equal?(man.interests.first.man), "Two inverses should lead back to the same object that was originally held" assert man.equal?(man.interests.find(interest.id).man), "Two inversions should lead back to the same object that was originally held" - assert_equal man.name, man.interests.find(interest.id).man.name, "The name of the man should match before the name is changed" + assert_nil man.interests.find(interest.id).man.name, "The name of the man should match before the name is changed" man.name = "Ben Bitdiddle" assert_equal man.name, man.interests.find(interest.id).man.name, "The name of the man should match after the parent name is changed" man.interests.find(interest.id).man.name = "Alyssa P. Hacker" @@ -651,20 +651,6 @@ class InversePolymorphicBelongsToTests < ActiveRecord::TestCase assert_equal face.description, new_man.polymorphic_face.description, "Description of face should be the same after changes to replaced-parent-owned instance" end - def test_child_instance_should_be_shared_with_replaced_via_method_parent - face = faces(:confused) - new_man = Man.new - - assert_not_nil face.polymorphic_man - face.polymorphic_man = new_man - - assert_equal face.description, new_man.polymorphic_face.description, "Description of face should be the same before changes to parent instance" - face.description = "Bongo" - assert_equal face.description, new_man.polymorphic_face.description, "Description of face should be the same after changes to parent instance" - new_man.polymorphic_face.description = "Mungo" - assert_equal face.description, new_man.polymorphic_face.description, "Description of face should be the same after changes to replaced-parent-owned instance" - end - def test_inversed_instance_should_not_be_reloaded_after_stale_state_changed new_man = Man.new face = Face.new diff --git a/activerecord/test/cases/associations/join_model_test.rb b/activerecord/test/cases/associations/join_model_test.rb index 15a7ae941a..c078cef064 100644 --- a/activerecord/test/cases/associations/join_model_test.rb +++ b/activerecord/test/cases/associations/join_model_test.rb @@ -19,7 +19,7 @@ require "models/car" class AssociationsJoinModelTest < ActiveRecord::TestCase self.use_transactional_tests = false unless supports_savepoints? - fixtures :posts, :authors, :categories, :categorizations, :comments, :tags, :taggings, :author_favorites, :vertices, :items, :books, + fixtures :posts, :authors, :author_addresses, :categories, :categorizations, :comments, :tags, :taggings, :author_favorites, :vertices, :items, :books, # Reload edges table from fixtures as otherwise repeated test was failing :edges @@ -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 @@ -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 @@ -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) 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 2cc6468827..6d3757f467 100644 --- a/activerecord/test/cases/associations/left_outer_join_association_test.rb +++ b/activerecord/test/cases/associations/left_outer_join_association_test.rb @@ -5,10 +5,9 @@ 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 + fixtures :authors, :author_addresses, :essays, :posts, :comments, :categorizations, :people def test_construct_finder_sql_applies_aliases_tables_on_association_conditions result = Author.left_outer_joins(:thinking_posts, :welcome_posts).to_a diff --git a/activerecord/test/cases/associations/nested_through_associations_test.rb b/activerecord/test/cases/associations/nested_through_associations_test.rb index dc26f6a383..67ff7355b3 100644 --- a/activerecord/test/cases/associations/nested_through_associations_test.rb +++ b/activerecord/test/cases/associations/nested_through_associations_test.rb @@ -24,7 +24,7 @@ require "models/membership" require "models/essay" class NestedThroughAssociationsTest < ActiveRecord::TestCase - fixtures :authors, :books, :posts, :subscriptions, :subscribers, :tags, :taggings, + fixtures :authors, :author_addresses, :books, :posts, :subscriptions, :subscribers, :tags, :taggings, :people, :readers, :references, :jobs, :ratings, :comments, :members, :member_details, :member_types, :sponsors, :clubs, :organizations, :categories, :categories_posts, :categorizations, :memberships, :essays diff --git a/activerecord/test/cases/associations/required_test.rb b/activerecord/test/cases/associations/required_test.rb index f8b686721e..45e1803858 100644 --- a/activerecord/test/cases/associations/required_test.rb +++ b/activerecord/test/cases/associations/required_test.rb @@ -22,14 +22,21 @@ class RequiredAssociationsTest < ActiveRecord::TestCase @connection.drop_table "children", if_exists: true end - test "belongs_to associations are not required by default" do - model = subclass_of(Child) do - belongs_to :parent, inverse_of: false, - class_name: "RequiredAssociationsTest::Parent" - end + test "belongs_to associations can be optional by default" do + begin + original_value = ActiveRecord::Base.belongs_to_required_by_default + ActiveRecord::Base.belongs_to_required_by_default = false + + model = subclass_of(Child) do + belongs_to :parent, inverse_of: false, + class_name: "RequiredAssociationsTest::Parent" + end - assert model.new.save - assert model.new(parent: Parent.new).save + assert model.new.save + assert model.new(parent: Parent.new).save + ensure + ActiveRecord::Base.belongs_to_required_by_default = original_value + end end test "required belongs_to associations have presence validated" do @@ -46,6 +53,27 @@ class RequiredAssociationsTest < ActiveRecord::TestCase assert record.save end + test "belongs_to associations can be required by default" do + begin + original_value = ActiveRecord::Base.belongs_to_required_by_default + ActiveRecord::Base.belongs_to_required_by_default = true + + model = subclass_of(Child) do + belongs_to :parent, inverse_of: false, + class_name: "RequiredAssociationsTest::Parent" + end + + record = model.new + assert_not record.save + assert_equal ["Parent must exist"], record.errors.full_messages + + record.parent = Parent.new + assert record.save + ensure + ActiveRecord::Base.belongs_to_required_by_default = original_value + end + end + test "has_one associations are not required by default" do model = subclass_of(Parent) do has_one :child, inverse_of: false, diff --git a/activerecord/test/cases/associations_test.rb b/activerecord/test/cases/associations_test.rb index c095b3a91c..2eb31326a5 100644 --- a/activerecord/test/cases/associations_test.rb +++ b/activerecord/test/cases/associations_test.rb @@ -22,7 +22,7 @@ require "models/interest" class AssociationsTest < ActiveRecord::TestCase fixtures :accounts, :companies, :developers, :projects, :developers_projects, - :computers, :people, :readers, :authors, :author_favorites + :computers, :people, :readers, :authors, :author_addresses, :author_favorites def test_eager_loading_should_not_change_count_of_children liquid = Liquid.create(name: "salty") @@ -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" @@ -124,7 +111,7 @@ class AssociationsTest < ActiveRecord::TestCase end class AssociationProxyTest < ActiveRecord::TestCase - fixtures :authors, :posts, :categorizations, :categories, :developers, :projects, :developers_projects + fixtures :authors, :author_addresses, :posts, :categorizations, :categories, :developers, :projects, :developers_projects def test_push_does_not_load_target david = authors(:david) @@ -235,7 +222,14 @@ class AssociationProxyTest < ActiveRecord::TestCase test "proxy object is cached" do david = developers(:david) - assert david.projects.equal?(david.projects) + assert_same david.projects, david.projects + end + + test "proxy object can be stubbed" do + david = developers(:david) + david.projects.define_singleton_method(:extra_method) { 42 } + + assert_equal 42, david.projects.extra_method end test "inverses get set of subsets of the association" do diff --git a/activerecord/test/cases/attribute_methods/read_test.rb b/activerecord/test/cases/attribute_methods/read_test.rb index a8592bd179..1fc63a49d4 100644 --- a/activerecord/test/cases/attribute_methods/read_test.rb +++ b/activerecord/test/cases/attribute_methods/read_test.rb @@ -3,7 +3,7 @@ require "cases/helper" module ActiveRecord module AttributeMethods class ReadTest < ActiveRecord::TestCase - class FakeColumn < Struct.new(:name) + FakeColumn = Struct.new(:name) do def type; :integer; end end @@ -14,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 diff --git a/activerecord/test/cases/attribute_methods_test.rb b/activerecord/test/cases/attribute_methods_test.rb index 4c77ecab7c..4d24a980dc 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 @@ -849,6 +866,13 @@ class AttributeMethodsTest < ActiveRecord::TestCase assert subklass.method_defined?(:id), "subklass is missing id method" end + test "define_attribute_method works with both symbol and string" do + klass = Class.new(ActiveRecord::Base) + + assert_nothing_raised { klass.define_attribute_method(:foo) } + assert_nothing_raised { klass.define_attribute_method("bar") } + end + test "read_attribute with nil should not asplode" do assert_nil Topic.new.read_attribute(nil) 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 f4620ae2da..3705a6be89 100644 --- a/activerecord/test/cases/attributes_test.rb +++ b/activerecord/test/cases/attributes_test.rb @@ -255,5 +255,13 @@ module ActiveRecord 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 c24d7b8835..2203aa1788 100644 --- a/activerecord/test/cases/autosave_association_test.rb +++ b/activerecord/test/cases/autosave_association_test.rb @@ -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" @@ -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 = "" } @@ -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 a2132bb577..dc32e995a4 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 @@ -73,7 +64,7 @@ class LintTest < ActiveRecord::TestCase end class BasicsTest < ActiveRecord::TestCase - fixtures :topics, :companies, :developers, :projects, :computers, :accounts, :minimalistics, "warehouse-things", :authors, :categorizations, :categories, :posts + fixtures :topics, :companies, :developers, :projects, :computers, :accounts, :minimalistics, "warehouse-things", :authors, :author_addresses, :categorizations, :categories, :posts def test_column_names_are_escaped conn = ActiveRecord::Base.connection @@ -107,12 +98,11 @@ 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 + def test_primary_key_and_references_columns_should_be_identical_type + pk = Author.columns_hash["id"] + ref = Post.columns_hash["author_id"] + + assert_equal pk.bigint?, ref.bigint? end def test_many_mutations @@ -144,10 +134,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 @@ -494,12 +482,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 +610,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 @@ -715,6 +703,17 @@ class BasicsTest < ActiveRecord::TestCase assert_nil topic.bonus_time end + def test_attributes + category = Category.new(name: "Ruby") + + expected_attributes = category.attribute_names.map do |attribute_name| + [attribute_name, category.public_send(attribute_name)] + end.to_h + + assert_instance_of Hash, category.attributes + assert_equal expected_attributes, category.attributes + end + def test_boolean b_nil = Boolean.create("value" => nil) nil_id = b_nil.id @@ -898,7 +897,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 @@ -1035,7 +1034,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 @@ -1086,7 +1085,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 @@ -1105,17 +1104,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 @@ -1130,7 +1129,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 @@ -1210,7 +1209,7 @@ class BasicsTest < ActiveRecord::TestCase 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 @@ -1305,12 +1304,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 @@ -1354,6 +1347,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" diff --git a/activerecord/test/cases/batches_test.rb b/activerecord/test/cases/batches_test.rb index f7e21faf0f..fbc3fbb44f 100644 --- a/activerecord/test/cases/batches_test.rb +++ b/activerecord/test/cases/batches_test.rb @@ -35,12 +35,10 @@ class EachTest < ActiveRecord::TestCase end end - if Enumerator.method_defined? :size - def test_each_should_return_a_sized_enumerator - assert_equal 11, Post.find_each(batch_size: 1).size - assert_equal 5, Post.find_each(batch_size: 2, start: 7).size - assert_equal 11, Post.find_each(batch_size: 10_000).size - end + def test_each_should_return_a_sized_enumerator + assert_equal 11, Post.find_each(batch_size: 1).size + assert_equal 5, Post.find_each(batch_size: 2, start: 7).size + assert_equal 11, Post.find_each(batch_size: 10_000).size end def test_each_enumerator_should_execute_one_query_per_batch @@ -145,7 +143,7 @@ class EachTest < ActiveRecord::TestCase def test_find_in_batches_should_quote_batch_order c = Post.connection - assert_sql(/ORDER BY #{c.quote_table_name('posts')}.#{c.quote_column_name('id')}/) do + assert_sql(/ORDER BY #{c.quote_table_name('posts')}\.#{c.quote_column_name('id')}/) do Post.find_in_batches(batch_size: 1) do |batch| assert_kind_of Array, batch assert_kind_of Post, batch.first @@ -410,7 +408,7 @@ class EachTest < ActiveRecord::TestCase def test_in_batches_should_quote_batch_order c = Post.connection - assert_sql(/ORDER BY #{c.quote_table_name('posts')}.#{c.quote_column_name('id')}/) do + assert_sql(/ORDER BY #{c.quote_table_name('posts')}\.#{c.quote_column_name('id')}/) do Post.in_batches(of: 1) do |relation| assert_kind_of ActiveRecord::Relation, relation assert_kind_of Post, relation.first @@ -515,14 +513,12 @@ class EachTest < ActiveRecord::TestCase assert_equal 2, person.reload.author_id # incremented only once end - if Enumerator.method_defined? :size - def test_find_in_batches_should_return_a_sized_enumerator - assert_equal 11, Post.find_in_batches(batch_size: 1).size - assert_equal 6, Post.find_in_batches(batch_size: 2).size - assert_equal 4, Post.find_in_batches(batch_size: 2, start: 4).size - assert_equal 4, Post.find_in_batches(batch_size: 3).size - assert_equal 1, Post.find_in_batches(batch_size: 10_000).size - end + def test_find_in_batches_should_return_a_sized_enumerator + assert_equal 11, Post.find_in_batches(batch_size: 1).size + assert_equal 6, Post.find_in_batches(batch_size: 2).size + assert_equal 4, Post.find_in_batches(batch_size: 2, start: 4).size + assert_equal 4, Post.find_in_batches(batch_size: 3).size + assert_equal 1, Post.find_in_batches(batch_size: 10_000).size end [true, false].each do |load| diff --git a/activerecord/test/cases/bind_parameter_test.rb b/activerecord/test/cases/bind_parameter_test.rb index 98d202dd79..5af44c27eb 100644 --- a/activerecord/test/cases/bind_parameter_test.rb +++ b/activerecord/test/cases/bind_parameter_test.rb @@ -3,36 +3,35 @@ require "models/topic" require "models/author" require "models/post" -module ActiveRecord - class BindParameterTest < ActiveRecord::TestCase - fixtures :topics, :authors, :posts +if ActiveRecord::Base.connection.prepared_statements + module ActiveRecord + class BindParameterTest < ActiveRecord::TestCase + fixtures :topics, :authors, :author_addresses, :posts - class LogListener - attr_accessor :calls + class LogListener + attr_accessor :calls - def initialize - @calls = [] - end + def initialize + @calls = [] + end - def call(*args) - calls << args + def call(*args) + calls << args + end end - end - def setup - super - @connection = ActiveRecord::Base.connection - @subscriber = LogListener.new - @pk = Topic.columns_hash[Topic.primary_key] - @subscription = ActiveSupport::Notifications.subscribe("sql.active_record", @subscriber) - end + def setup + super + @connection = ActiveRecord::Base.connection + @subscriber = LogListener.new + @pk = Topic.columns_hash[Topic.primary_key] + @subscription = ActiveSupport::Notifications.subscribe("sql.active_record", @subscriber) + end - teardown do - ActiveSupport::Notifications.unsubscribe(@subscription) - end + def teardown + ActiveSupport::Notifications.unsubscribe(@subscription) + end - if ActiveRecord::Base.connection.supports_statement_cache? && - ActiveRecord::Base.connection.prepared_statements def test_bind_from_join_in_subquery subquery = Author.joins(:thinking_posts).where(name: "David") scope = Author.from(subquery, "authors").where(id: 1) @@ -40,9 +39,8 @@ module ActiveRecord end def test_binds_are_logged - sub = Arel::Nodes::BindParam.new - binds = [Relation::QueryAttribute.new("id", 1, Type::Value.new)] - sql = "select * from topics where id = #{sub.to_sql}" + binds = [bind_attribute("id", 1)] + sql = "select * from topics where id = #{bind_param.to_sql}" @connection.exec_query(sql, "SQL", binds) @@ -56,43 +54,52 @@ module ActiveRecord assert message, "expected a message with binds" end - def test_logs_bind_vars_after_type_cast - binds = [Relation::QueryAttribute.new("id", "10", Type::Integer.new)] - type_casted_binds = binds.map { |attr| type_cast(attr.value_for_database) } - payload = { - name: "SQL", - sql: "select * from topics where id = ?", - binds: binds, - type_casted_binds: type_casted_binds - } - event = ActiveSupport::Notifications::Event.new( - "foo", - Time.now, - Time.now, - 123, - payload) - - logger = Class.new(ActiveRecord::LogSubscriber) { - attr_reader :debugs - def initialize - super - @debugs = [] - end - - def debug(str) - @debugs << str - end - }.new - - logger.sql event - assert_match([[@pk.name, 10]].inspect, logger.debugs.first) + def test_logs_binds_after_type_cast + binds = [bind_attribute("id", "10", Type::Integer.new)] + assert_logs_binds(binds) end - private + def test_logs_legacy_binds_after_type_cast + binds = [[@pk, "10"]] + assert_logs_binds(binds) + end - def type_cast(value) - ActiveRecord::Base.connection.type_cast(value) + def test_deprecate_supports_statement_cache + assert_deprecated { ActiveRecord::Base.connection.supports_statement_cache? } end + + private + def assert_logs_binds(binds) + payload = { + name: "SQL", + sql: "select * from topics where id = ?", + binds: binds, + type_casted_binds: @connection.type_casted_binds(binds) + } + + event = ActiveSupport::Notifications::Event.new( + "foo", + Time.now, + Time.now, + 123, + payload) + + logger = Class.new(ActiveRecord::LogSubscriber) { + attr_reader :debugs + + def initialize + super + @debugs = [] + end + + def debug(str) + @debugs << str + end + }.new + + logger.sql(event) + assert_match([[@pk.name, 10]].inspect, logger.debugs.first) + end end end end diff --git a/activerecord/test/cases/cache_key_test.rb b/activerecord/test/cases/cache_key_test.rb index bb2829b3c1..7b8264e6e8 100644 --- a/activerecord/test/cases/cache_key_test.rb +++ b/activerecord/test/cases/cache_key_test.rb @@ -4,22 +4,48 @@ module ActiveRecord class CacheKeyTest < ActiveRecord::TestCase self.use_transactional_tests = false - class CacheMe < ActiveRecord::Base; end + class CacheMe < ActiveRecord::Base + self.cache_versioning = false + end + + class CacheMeWithVersion < ActiveRecord::Base + self.cache_versioning = true + end setup do @connection = ActiveRecord::Base.connection - @connection.create_table(:cache_mes) { |t| t.timestamps } + @connection.create_table(:cache_mes, force: true) { |t| t.timestamps } + @connection.create_table(:cache_me_with_versions, force: true) { |t| t.timestamps } end teardown do @connection.drop_table :cache_mes, if_exists: true + @connection.drop_table :cache_me_with_versions, if_exists: true end - test "test_cache_key_format_is_not_too_precise" do + test "cache_key format is not too precise" do record = CacheMe.create key = record.cache_key assert_equal key, record.reload.cache_key end + + test "cache_key has no version when versioning is on" do + record = CacheMeWithVersion.create + assert_equal "active_record/cache_key_test/cache_me_with_versions/#{record.id}", record.cache_key + end + + test "cache_version is only there when versioning is on" do + assert CacheMeWithVersion.create.cache_version.present? + assert_not CacheMe.create.cache_version.present? + end + + test "cache_key_with_version always has both key and version" do + r1 = CacheMeWithVersion.create + assert_equal "active_record/cache_key_test/cache_me_with_versions/#{r1.id}-#{r1.updated_at.to_s(:usec)}", r1.cache_key_with_version + + r2 = CacheMe.create + assert_equal "active_record/cache_key_test/cache_mes/#{r2.id}-#{r2.updated_at.to_s(:usec)}", r2.cache_key_with_version + end end end diff --git a/activerecord/test/cases/calculations_test.rb b/activerecord/test/cases/calculations_test.rb index 7e7076196f..21c5c0efee 100644 --- a/activerecord/test/cases/calculations_test.rb +++ b/activerecord/test/cases/calculations_test.rb @@ -14,9 +14,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 CalculationsTest < ActiveRecord::TestCase fixtures :companies, :accounts, :topics, :speedometers, :minivans, :books @@ -85,14 +85,14 @@ 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| + [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| + [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 @@ -159,14 +159,14 @@ class CalculationsTest < ActiveRecord::TestCase end def test_limit_should_apply_before_count - accounts = Account.limit(3).where("firm_id IS NOT NULL") + accounts = Account.limit(4) assert_equal 3, accounts.count(:firm_id) assert_equal 3, accounts.select(:firm_id).count end def test_limit_should_apply_before_count_arel_attribute - accounts = Account.limit(3).where("firm_id IS NOT NULL") + accounts = Account.limit(4) firm_id_attribute = Account.arel_table[:firm_id] assert_equal 3, accounts.count(firm_id_attribute) @@ -220,6 +220,20 @@ class CalculationsTest < ActiveRecord::TestCase assert_match "credit_limit, firm_name", e.message end + def test_apply_distinct_in_count + queries = assert_sql do + Account.distinct.count + Account.group(:firm_id).distinct.count + end + + queries.each do |query| + # `table_alias_length` in `column_alias_for` would execute + # "SHOW max_identifier_length" statement in PostgreSQL adapter. + next if query == "SHOW max_identifier_length" + assert_match %r{\ASELECT(?! DISTINCT) COUNT\(DISTINCT\b}, query + end + end + def test_should_group_by_summed_field_having_condition c = Account.group(:firm_id).having("sum(credit_limit) > 50").sum(:credit_limit) assert_nil c[1] @@ -228,7 +242,8 @@ class CalculationsTest < ActiveRecord::TestCase end def test_should_group_by_summed_field_having_condition_from_select - c = Account.select("MIN(credit_limit) AS min_credit_limit").group(:firm_id).having("MIN(credit_limit) > 50").sum(:credit_limit) + skip unless current_adapter?(:Mysql2Adapter, :SQLite3Adapter) + c = Account.select("MIN(credit_limit) AS min_credit_limit").group(:firm_id).having("min_credit_limit > 50").sum(:credit_limit) assert_nil c[1] assert_equal 60, c[2] assert_equal 53, c[9] @@ -414,10 +429,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 @@ -446,7 +457,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_includes c.keys, 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 @@ -565,7 +576,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 @@ -601,7 +612,7 @@ class CalculationsTest < ActiveRecord::TestCase 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 @@ -681,7 +692,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 @@ -791,4 +802,16 @@ class CalculationsTest < ActiveRecord::TestCase def test_group_by_attribute_with_custom_type assert_equal({ "proposed" => 2, "published" => 2 }, Book.group(:status).count) end + + def test_deprecate_count_with_block_and_column_name + assert_deprecated do + assert_equal 6, Account.count(:firm_id) { true } + end + end + + def test_deprecate_sum_with_block_and_column_name + assert_deprecated do + assert_equal 6, Account.sum(:firm_id) { 1 } + end + end end diff --git a/activerecord/test/cases/callbacks_test.rb b/activerecord/test/cases/callbacks_test.rb index 4b517e9d70..b3c86586d0 100644 --- a/activerecord/test/cases/callbacks_test.rb +++ b/activerecord/test/cases/callbacks_test.rb @@ -6,10 +6,6 @@ class CallbackDeveloper < ActiveRecord::Base self.table_name = "developers" class << self - def callback_string(callback_method) - "history << [#{callback_method.to_sym.inspect}, :string]" - end - def callback_proc(callback_method) Proc.new { |model| model.history << [callback_method, :proc] } end @@ -33,7 +29,6 @@ class CallbackDeveloper < ActiveRecord::Base ActiveRecord::Callbacks::CALLBACKS.each do |callback_method| next if callback_method.to_s.start_with?("around_") define_callback_method(callback_method) - ActiveSupport::Deprecation.silence { send(callback_method, callback_string(callback_method)) } send(callback_method, callback_proc(callback_method)) send(callback_method, callback_object(callback_method)) send(callback_method) { |model| model.history << [callback_method, :block] } @@ -44,11 +39,6 @@ class CallbackDeveloper < ActiveRecord::Base end end -class CallbackDeveloperWithFalseValidation < CallbackDeveloper - before_validation proc { |model| model.history << [:before_validation, :returning_false]; false } - before_validation proc { |model| model.history << [:before_validation, :should_never_get_here] } -end - class CallbackDeveloperWithHaltedValidation < CallbackDeveloper before_validation proc { |model| model.history << [:before_validation, :throwing_abort]; throw(:abort) } before_validation proc { |model| model.history << [:before_validation, :should_never_get_here] } @@ -125,11 +115,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 @@ -137,23 +127,6 @@ class ContextualCallbacksDeveloper < ActiveRecord::Base end end -class CallbackCancellationDeveloper < ActiveRecord::Base - self.table_name = "developers" - - attr_reader :after_save_called, :after_create_called, :after_update_called, :after_destroy_called - attr_accessor :cancel_before_save, :cancel_before_create, :cancel_before_update, :cancel_before_destroy - - 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 } - - after_save { @after_save_called = true } - after_update { @after_update_called = true } - after_create { @after_create_called = true } - after_destroy { @after_destroy_called = true } -end - class CallbackHaltedDeveloper < ActiveRecord::Base self.table_name = "developers" @@ -178,7 +151,6 @@ class CallbacksTest < ActiveRecord::TestCase david = CallbackDeveloper.new assert_equal [ [ :after_initialize, :method ], - [ :after_initialize, :string ], [ :after_initialize, :proc ], [ :after_initialize, :object ], [ :after_initialize, :block ], @@ -189,12 +161,10 @@ class CallbacksTest < ActiveRecord::TestCase david = CallbackDeveloper.find(1) assert_equal [ [ :after_find, :method ], - [ :after_find, :string ], [ :after_find, :proc ], [ :after_find, :object ], [ :after_find, :block ], [ :after_initialize, :method ], - [ :after_initialize, :string ], [ :after_initialize, :proc ], [ :after_initialize, :object ], [ :after_initialize, :block ], @@ -206,17 +176,14 @@ class CallbacksTest < ActiveRecord::TestCase david.valid? assert_equal [ [ :after_initialize, :method ], - [ :after_initialize, :string ], [ :after_initialize, :proc ], [ :after_initialize, :object ], [ :after_initialize, :block ], [ :before_validation, :method ], - [ :before_validation, :string ], [ :before_validation, :proc ], [ :before_validation, :object ], [ :before_validation, :block ], [ :after_validation, :method ], - [ :after_validation, :string ], [ :after_validation, :proc ], [ :after_validation, :object ], [ :after_validation, :block ], @@ -228,22 +195,18 @@ class CallbacksTest < ActiveRecord::TestCase david.valid? assert_equal [ [ :after_find, :method ], - [ :after_find, :string ], [ :after_find, :proc ], [ :after_find, :object ], [ :after_find, :block ], [ :after_initialize, :method ], - [ :after_initialize, :string ], [ :after_initialize, :proc ], [ :after_initialize, :object ], [ :after_initialize, :block ], [ :before_validation, :method ], - [ :before_validation, :string ], [ :before_validation, :proc ], [ :before_validation, :object ], [ :before_validation, :block ], [ :after_validation, :method ], - [ :after_validation, :string ], [ :after_validation, :proc ], [ :after_validation, :object ], [ :after_validation, :block ], @@ -254,44 +217,36 @@ class CallbacksTest < ActiveRecord::TestCase david = CallbackDeveloper.create("name" => "David", "salary" => 1000000) assert_equal [ [ :after_initialize, :method ], - [ :after_initialize, :string ], [ :after_initialize, :proc ], [ :after_initialize, :object ], [ :after_initialize, :block ], [ :before_validation, :method ], - [ :before_validation, :string ], [ :before_validation, :proc ], [ :before_validation, :object ], [ :before_validation, :block ], [ :after_validation, :method ], - [ :after_validation, :string ], [ :after_validation, :proc ], [ :after_validation, :object ], [ :after_validation, :block ], [ :before_save, :method ], - [ :before_save, :string ], [ :before_save, :proc ], [ :before_save, :object ], [ :before_save, :block ], [ :before_create, :method ], - [ :before_create, :string ], [ :before_create, :proc ], [ :before_create, :object ], [ :before_create, :block ], [ :after_create, :method ], - [ :after_create, :string ], [ :after_create, :proc ], [ :after_create, :object ], [ :after_create, :block ], [ :after_save, :method ], - [ :after_save, :string ], [ :after_save, :proc ], [ :after_save, :object ], [ :after_save, :block ], [ :after_commit, :block ], [ :after_commit, :object ], [ :after_commit, :proc ], - [ :after_commit, :string ], [ :after_commit, :method ] ], david.history end @@ -323,49 +278,40 @@ class CallbacksTest < ActiveRecord::TestCase david.save assert_equal [ [ :after_find, :method ], - [ :after_find, :string ], [ :after_find, :proc ], [ :after_find, :object ], [ :after_find, :block ], [ :after_initialize, :method ], - [ :after_initialize, :string ], [ :after_initialize, :proc ], [ :after_initialize, :object ], [ :after_initialize, :block ], [ :before_validation, :method ], - [ :before_validation, :string ], [ :before_validation, :proc ], [ :before_validation, :object ], [ :before_validation, :block ], [ :after_validation, :method ], - [ :after_validation, :string ], [ :after_validation, :proc ], [ :after_validation, :object ], [ :after_validation, :block ], [ :before_save, :method ], - [ :before_save, :string ], [ :before_save, :proc ], [ :before_save, :object ], [ :before_save, :block ], [ :before_update, :method ], - [ :before_update, :string ], [ :before_update, :proc ], [ :before_update, :object ], [ :before_update, :block ], [ :after_update, :method ], - [ :after_update, :string ], [ :after_update, :proc ], [ :after_update, :object ], [ :after_update, :block ], [ :after_save, :method ], - [ :after_save, :string ], [ :after_save, :proc ], [ :after_save, :object ], [ :after_save, :block ], [ :after_commit, :block ], [ :after_commit, :object ], [ :after_commit, :proc ], - [ :after_commit, :string ], [ :after_commit, :method ] ], david.history end @@ -399,29 +345,24 @@ class CallbacksTest < ActiveRecord::TestCase david.destroy assert_equal [ [ :after_find, :method ], - [ :after_find, :string ], [ :after_find, :proc ], [ :after_find, :object ], [ :after_find, :block ], [ :after_initialize, :method ], - [ :after_initialize, :string ], [ :after_initialize, :proc ], [ :after_initialize, :object ], [ :after_initialize, :block ], [ :before_destroy, :method ], - [ :before_destroy, :string ], [ :before_destroy, :proc ], [ :before_destroy, :object ], [ :before_destroy, :block ], [ :after_destroy, :method ], - [ :after_destroy, :string ], [ :after_destroy, :proc ], [ :after_destroy, :object ], [ :after_destroy, :block ], [ :after_commit, :block ], [ :after_commit, :object ], [ :after_commit, :proc ], - [ :after_commit, :string ], [ :after_commit, :method ] ], david.history end @@ -431,82 +372,16 @@ class CallbacksTest < ActiveRecord::TestCase CallbackDeveloper.delete(david.id) assert_equal [ [ :after_find, :method ], - [ :after_find, :string ], [ :after_find, :proc ], [ :after_find, :object ], [ :after_find, :block ], [ :after_initialize, :method ], - [ :after_initialize, :string ], [ :after_initialize, :proc ], [ :after_initialize, :object ], [ :after_initialize, :block ], ], david.history end - def test_deprecated_before_save_returning_false - david = ImmutableDeveloper.find(1) - assert_deprecated do - assert david.valid? - assert !david.save - exc = assert_raise(ActiveRecord::RecordNotSaved) { david.save! } - assert_equal exc.record, david - assert_equal "Failed to save the record", exc.message - end - - david = ImmutableDeveloper.find(1) - david.salary = 10_000_000 - assert !david.valid? - assert !david.save - assert_raise(ActiveRecord::RecordInvalid) { david.save! } - - someone = CallbackCancellationDeveloper.find(1) - someone.cancel_before_save = true - assert_deprecated do - assert someone.valid? - assert !someone.save - end - assert_save_callbacks_not_called(someone) - end - - def test_deprecated_before_create_returning_false - someone = CallbackCancellationDeveloper.new - someone.cancel_before_create = true - assert_deprecated do - assert someone.valid? - assert !someone.save - end - assert_save_callbacks_not_called(someone) - end - - def test_deprecated_before_update_returning_false - someone = CallbackCancellationDeveloper.find(1) - someone.cancel_before_update = true - assert_deprecated do - assert someone.valid? - assert !someone.save - end - assert_save_callbacks_not_called(someone) - end - - def test_deprecated_before_destroy_returning_false - david = ImmutableDeveloper.find(1) - assert_deprecated do - assert !david.destroy - exc = assert_raise(ActiveRecord::RecordNotDestroyed) { david.destroy! } - assert_equal exc.record, david - assert_equal "Failed to destroy the record", exc.message - end - assert_not_nil ImmutableDeveloper.find_by_id(1) - - someone = CallbackCancellationDeveloper.find(1) - someone.cancel_before_destroy = true - assert_deprecated do - assert !someone.destroy - assert_raise(ActiveRecord::RecordNotDestroyed) { someone.destroy! } - end - assert !someone.after_destroy_called - end - def assert_save_callbacks_not_called(someone) assert !someone.after_save_called assert !someone.after_create_called @@ -527,7 +402,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 +429,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) @@ -564,50 +439,19 @@ class CallbacksTest < ActiveRecord::TestCase assert !someone.after_destroy_called end - def test_callback_returning_false - david = CallbackDeveloperWithFalseValidation.find(1) - assert_deprecated { david.save } - assert_equal [ - [ :after_find, :method ], - [ :after_find, :string ], - [ :after_find, :proc ], - [ :after_find, :object ], - [ :after_find, :block ], - [ :after_initialize, :method ], - [ :after_initialize, :string ], - [ :after_initialize, :proc ], - [ :after_initialize, :object ], - [ :after_initialize, :block ], - [ :before_validation, :method ], - [ :before_validation, :string ], - [ :before_validation, :proc ], - [ :before_validation, :object ], - [ :before_validation, :block ], - [ :before_validation, :returning_false ], - [ :after_rollback, :block ], - [ :after_rollback, :object ], - [ :after_rollback, :proc ], - [ :after_rollback, :string ], - [ :after_rollback, :method ], - ], david.history - end - def test_callback_throwing_abort david = CallbackDeveloperWithHaltedValidation.find(1) david.save assert_equal [ [ :after_find, :method ], - [ :after_find, :string ], [ :after_find, :proc ], [ :after_find, :object ], [ :after_find, :block ], [ :after_initialize, :method ], - [ :after_initialize, :string ], [ :after_initialize, :proc ], [ :after_initialize, :object ], [ :after_initialize, :block ], [ :before_validation, :method ], - [ :before_validation, :string ], [ :before_validation, :proc ], [ :before_validation, :object ], [ :before_validation, :block ], @@ -615,7 +459,6 @@ class CallbacksTest < ActiveRecord::TestCase [ :after_rollback, :block ], [ :after_rollback, :object ], [ :after_rollback, :proc ], - [ :after_rollback, :string ], [ :after_rollback, :method ], ], david.history end diff --git a/activerecord/test/cases/coders/yaml_column_test.rb b/activerecord/test/cases/coders/yaml_column_test.rb index b9c6224425..a26a72712d 100644 --- a/activerecord/test/cases/coders/yaml_column_test.rb +++ b/activerecord/test/cases/coders/yaml_column_test.rb @@ -1,50 +1,51 @@ - require "cases/helper" module ActiveRecord module Coders class YAMLColumnTest < ActiveRecord::TestCase def test_initialize_takes_class - coder = YAMLColumn.new(Object) + coder = YAMLColumn.new("attr_name", Object) assert_equal Object, coder.object_class end def test_type_mismatch_on_different_classes_on_dump - coder = YAMLColumn.new(Array) - assert_raises(SerializationTypeMismatch) do + coder = YAMLColumn.new("tags", Array) + error = assert_raises(SerializationTypeMismatch) do coder.dump("a") end + assert_equal %{can't dump `tags`: was supposed to be a Array, but was a String. -- "a"}, error.to_s end def test_type_mismatch_on_different_classes - coder = YAMLColumn.new(Array) - assert_raises(SerializationTypeMismatch) do + coder = YAMLColumn.new("tags", Array) + error = assert_raises(SerializationTypeMismatch) do coder.load "--- foo" end + assert_equal %{can't load `tags`: was supposed to be a Array, but was a String. -- "foo"}, error.to_s end def test_nil_is_ok - coder = YAMLColumn.new + coder = YAMLColumn.new("attr_name") assert_nil coder.load "--- " end def test_returns_new_with_different_class - coder = YAMLColumn.new SerializationTypeMismatch + coder = YAMLColumn.new("attr_name", SerializationTypeMismatch) assert_equal SerializationTypeMismatch, coder.load("--- ").class end def test_returns_string_unless_starts_with_dash - coder = YAMLColumn.new + coder = YAMLColumn.new("attr_name") assert_equal "foo", coder.load("foo") end def test_load_handles_other_classes - coder = YAMLColumn.new + coder = YAMLColumn.new("attr_name") assert_equal [], coder.load([]) end def test_load_doesnt_swallow_yaml_exceptions - coder = YAMLColumn.new + coder = YAMLColumn.new("attr_name") bad_yaml = "--- {" assert_raises(Psych::SyntaxError) do coder.load(bad_yaml) @@ -52,7 +53,7 @@ module ActiveRecord end def test_load_doesnt_handle_undefined_class_or_module - coder = YAMLColumn.new + coder = YAMLColumn.new("attr_name") missing_class_yaml = '--- !ruby/object:DoesNotExistAndShouldntEver {}\n' assert_raises(ArgumentError) do coder.load(missing_class_yaml) diff --git a/activerecord/test/cases/collection_cache_key_test.rb b/activerecord/test/cases/collection_cache_key_test.rb index a2874438c1..f344c77691 100644 --- a/activerecord/test/cases/collection_cache_key_test.rb +++ b/activerecord/test/cases/collection_cache_key_test.rb @@ -11,16 +11,42 @@ module ActiveRecord fixtures :developers, :projects, :developers_projects, :topics, :comments, :posts test "collection_cache_key on model" do - assert_match(/\Adevelopers\/query-(\h+)-(\d+)-(\d+)\Z/, Developer.collection_cache_key) + assert_match(/\Adevelopers\/query-(\h+)-(\d+)-(\d+)\z/, Developer.collection_cache_key) end test "cache_key for relation" do - developers = Developer.where(name: "David") - last_developer_timestamp = developers.order(updated_at: :desc).first.updated_at + developers = Developer.where(salary: 100000).order(updated_at: :desc) + last_developer_timestamp = developers.first.updated_at + + assert_match(/\Adevelopers\/query-(\h+)-(\d+)-(\d+)\z/, developers.cache_key) + + /\Adevelopers\/query-(\h+)-(\d+)-(\d+)\z/ =~ developers.cache_key + + assert_equal Digest::MD5.hexdigest(developers.to_sql), $1 + assert_equal developers.count.to_s, $2 + assert_equal last_developer_timestamp.to_s(ActiveRecord::Base.cache_timestamp_format), $3 + end + + test "cache_key for relation with limit" do + developers = Developer.where(salary: 100000).order(updated_at: :desc).limit(5) + last_developer_timestamp = developers.first.updated_at - assert_match(/\Adevelopers\/query-(\h+)-(\d+)-(\d+)\Z/, developers.cache_key) + assert_match(/\Adevelopers\/query-(\h+)-(\d+)-(\d+)\z/, developers.cache_key) - /\Adevelopers\/query-(\h+)-(\d+)-(\d+)\Z/ =~ developers.cache_key + /\Adevelopers\/query-(\h+)-(\d+)-(\d+)\z/ =~ developers.cache_key + + assert_equal Digest::MD5.hexdigest(developers.to_sql), $1 + assert_equal developers.count.to_s, $2 + assert_equal last_developer_timestamp.to_s(ActiveRecord::Base.cache_timestamp_format), $3 + end + + test "cache_key for loaded relation" do + developers = Developer.where(salary: 100000).order(updated_at: :desc).limit(5).load + last_developer_timestamp = developers.first.updated_at + + assert_match(/\Adevelopers\/query-(\h+)-(\d+)-(\d+)\z/, developers.cache_key) + + /\Adevelopers\/query-(\h+)-(\d+)-(\d+)\z/ =~ developers.cache_key assert_equal Digest::MD5.hexdigest(developers.to_sql), $1 assert_equal developers.count.to_s, $2 @@ -28,27 +54,27 @@ 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 test "cache_key for empty relation" do developers = Developer.where(name: "Non Existent Developer") - assert_match(/\Adevelopers\/query-(\h+)-0\Z/, developers.cache_key) + assert_match(/\Adevelopers\/query-(\h+)-0\z/, developers.cache_key) end test "cache_key with custom timestamp column" do @@ -64,7 +90,7 @@ module ActiveRecord test "collection proxy provides a cache_key" do developers = projects(:active_record).developers - assert_match(/\Adevelopers\/query-(\h+)-(\d+)-(\d+)\Z/, developers.cache_key) + assert_match(/\Adevelopers\/query-(\h+)-(\d+)-(\d+)\z/, developers.cache_key) end test "cache_key for loaded collection with zero size" do @@ -72,18 +98,18 @@ module ActiveRecord posts = Post.includes(:comments) empty_loaded_collection = posts.first.comments - assert_match(/\Acomments\/query-(\h+)-0\Z/, empty_loaded_collection.cache_key) + assert_match(/\Acomments\/query-(\h+)-0\z/, empty_loaded_collection.cache_key) end test "cache_key for queries with offset which return 0 rows" do developers = Developer.offset(20) - assert_match(/\Adevelopers\/query-(\h+)-0\Z/, developers.cache_key) + assert_match(/\Adevelopers\/query-(\h+)-0\z/, developers.cache_key) end test "cache_key with a relation having selected columns" do developers = Developer.select(:salary) - assert_match(/\Adevelopers\/query-(\h+)-(\d+)-(\d+)\Z/, developers.cache_key) + assert_match(/\Adevelopers\/query-(\h+)-(\d+)-(\d+)\z/, developers.cache_key) end end end diff --git a/activerecord/test/cases/column_definition_test.rb b/activerecord/test/cases/column_definition_test.rb index a65bb89052..90c8d21c43 100644 --- a/activerecord/test/cases/column_definition_test.rb +++ b/activerecord/test/cases/column_definition_test.rb @@ -8,77 +8,25 @@ module ActiveRecord def @adapter.native_database_types { string: "varchar" } end - @viz = @adapter.schema_creation + @viz = @adapter.send(:schema_creation) end # Avoid column definitions in create table statements like: # `title` varchar(255) DEFAULT NULL def test_should_not_include_default_clause_when_default_is_null - column = Column.new("title", nil, SqlTypeMetadata.new(limit: 20)) - column_def = ColumnDefinition.new( - column.name, "string", - column.limit, column.precision, column.scale, column.default, column.null) + column_def = ColumnDefinition.new("title", "string", limit: 20) assert_equal "title varchar(20)", @viz.accept(column_def) end def test_should_include_default_clause_when_default_is_present - column = Column.new("title", "Hello", SqlTypeMetadata.new(limit: 20)) - column_def = ColumnDefinition.new( - column.name, "string", - column.limit, column.precision, column.scale, column.default, column.null) + column_def = ColumnDefinition.new("title", "string", limit: 20, default: "Hello") assert_equal "title varchar(20) DEFAULT 'Hello'", @viz.accept(column_def) end def test_should_specify_not_null_if_null_option_is_false - type_metadata = SqlTypeMetadata.new(limit: 20) - column = Column.new("title", "Hello", type_metadata, false) - column_def = ColumnDefinition.new( - column.name, "string", - column.limit, column.precision, column.scale, column.default, column.null) + column_def = ColumnDefinition.new("title", "string", limit: 20, default: "Hello", null: false) assert_equal "title varchar(20) DEFAULT 'Hello' NOT NULL", @viz.accept(column_def) end - - if current_adapter?(:Mysql2Adapter) - def test_should_set_default_for_mysql_binary_data_types - type = SqlTypeMetadata.new(type: :binary, sql_type: "binary(1)") - binary_column = MySQL::Column.new("title", "a", type) - assert_equal "a", binary_column.default - - type = SqlTypeMetadata.new(type: :binary, sql_type: "varbinary") - varbinary_column = MySQL::Column.new("title", "a", type) - assert_equal "a", varbinary_column.default - end - - def test_should_be_empty_string_default_for_mysql_binary_data_types - type = SqlTypeMetadata.new(type: :binary, sql_type: "binary(1)") - binary_column = MySQL::Column.new("title", "", type, false) - assert_equal "", binary_column.default - - type = SqlTypeMetadata.new(type: :binary, sql_type: "varbinary") - varbinary_column = MySQL::Column.new("title", "", type, false) - assert_equal "", varbinary_column.default - end - - def test_should_not_set_default_for_blob_and_text_data_types - text_type = MySQL::TypeMetadata.new(SqlTypeMetadata.new(type: :text)) - - text_column = MySQL::Column.new("title", nil, text_type) - assert_nil text_column.default - - not_null_text_column = MySQL::Column.new("title", nil, text_type, false) - assert_nil not_null_text_column.default - end - - def test_has_default_should_return_false_for_blob_and_text_data_types - binary_type = SqlTypeMetadata.new(sql_type: "blob") - blob_column = MySQL::Column.new("title", nil, binary_type) - assert !blob_column.has_default? - - text_type = SqlTypeMetadata.new(type: :text) - text_column = MySQL::Column.new("title", nil, text_type) - assert !text_column.has_default? - end - end end end end diff --git a/activerecord/test/cases/comment_test.rb b/activerecord/test/cases/comment_test.rb index 262ad319be..c23be52a6c 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 @@ -73,9 +72,11 @@ if ActiveRecord::Base.connection.supports_comments? end def test_add_index_with_comment_later - @connection.add_index :commenteds, :obvious, name: "idx_obvious", comment: "We need to see obvious comments" - index = @connection.indexes("commenteds").find { |idef| idef.name == "idx_obvious" } - assert_equal "We need to see obvious comments", index.comment + unless current_adapter?(:OracleAdapter) + @connection.add_index :commenteds, :obvious, name: "idx_obvious", comment: "We need to see obvious comments" + index = @connection.indexes("commenteds").find { |idef| idef.name == "idx_obvious" } + assert_equal "We need to see obvious comments", index.comment + end end def test_add_comment_to_column @@ -102,6 +103,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" @@ -112,8 +114,10 @@ if ActiveRecord::Base.connection.supports_comments? assert_match %r[t\.string\s+"obvious"\n], output assert_match %r[t\.string\s+"content",\s+comment: "Whoa, content describes itself!"], output assert_match %r[t\.integer\s+"rating",\s+comment: "I am running out of imagination"], output - assert_match %r[t\.index\s+.+\s+comment: "\\\"Very important\\\" index that powers all the performance.\\nAnd it's fun!"], output - assert_match %r[t\.index\s+.+\s+name: "idx_obvious",.+\s+comment: "We need to see obvious comments"], output + unless current_adapter?(:OracleAdapter) + assert_match %r[t\.index\s+.+\s+comment: "\\\"Very important\\\" index that powers all the performance.\\nAnd it's fun!"], output + assert_match %r[t\.index\s+.+\s+name: "idx_obvious",\s+comment: "We need to see obvious comments"], output + end end def test_schema_dump_omits_blank_comments @@ -135,5 +139,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 d5d16e7568..2a71f08d90 100644 --- a/activerecord/test/cases/connection_adapters/connection_handler_test.rb +++ b/activerecord/test/cases/connection_adapters/connection_handler_test.rb @@ -6,7 +6,18 @@ 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_default_env_fall_back_to_default_env_when_rails_env_or_rack_env_is_empty_string + original_rails_env = ENV["RAILS_ENV"] + original_rack_env = ENV["RACK_ENV"] + ENV["RAILS_ENV"] = ENV["RACK_ENV"] = "" + + assert_equal "default_env", ActiveRecord::ConnectionHandling::DEFAULT_ENV.call + ensure + ENV["RAILS_ENV"] = original_rails_env + ENV["RACK_ENV"] = original_rack_env end def test_establish_connection_uses_spec_name @@ -20,6 +31,66 @@ module ActiveRecord @handler.remove_connection("readonly") end + def test_establish_connection_using_3_levels_config + previous_env, ENV["RAILS_ENV"] = ENV["RAILS_ENV"], "default_env" + + config = { + "default_env" => { + "readonly" => { "adapter" => "sqlite3", "database" => "db/readonly.sqlite3" }, + "primary" => { "adapter" => "sqlite3", "database" => "db/primary.sqlite3" } + }, + "another_env" => { + "readonly" => { "adapter" => "sqlite3", "database" => "db/bad-readonly.sqlite3" }, + "primary" => { "adapter" => "sqlite3", "database" => "db/bad-primary.sqlite3" } + }, + "common" => { "adapter" => "sqlite3", "database" => "db/common.sqlite3" } + } + @prev_configs, ActiveRecord::Base.configurations = ActiveRecord::Base.configurations, config + + @handler.establish_connection(:common) + @handler.establish_connection(:primary) + @handler.establish_connection(:readonly) + + assert_not_nil pool = @handler.retrieve_connection_pool("readonly") + assert_equal "db/readonly.sqlite3", pool.spec.config[:database] + + assert_not_nil pool = @handler.retrieve_connection_pool("primary") + assert_equal "db/primary.sqlite3", pool.spec.config[:database] + + assert_not_nil pool = @handler.retrieve_connection_pool("common") + assert_equal "db/common.sqlite3", pool.spec.config[:database] + ensure + ActiveRecord::Base.configurations = @prev_configs + ENV["RAILS_ENV"] = previous_env + end + + def test_establish_connection_using_two_level_configurations + config = { "development" => { "adapter" => "sqlite3", "database" => "db/primary.sqlite3" } } + @prev_configs, ActiveRecord::Base.configurations = ActiveRecord::Base.configurations, config + + @handler.establish_connection(:development) + + assert_not_nil pool = @handler.retrieve_connection_pool("development") + assert_equal "db/primary.sqlite3", pool.spec.config[:database] + ensure + ActiveRecord::Base.configurations = @prev_configs + end + + def test_establish_connection_using_top_level_key_in_two_level_config + config = { + "development" => { "adapter" => "sqlite3", "database" => "db/primary.sqlite3" }, + "development_readonly" => { "adapter" => "sqlite3", "database" => "db/readonly.sqlite3" } + } + @prev_configs, ActiveRecord::Base.configurations = ActiveRecord::Base.configurations, config + + @handler.establish_connection(:development_readonly) + + assert_not_nil pool = @handler.retrieve_connection_pool("development_readonly") + assert_equal "db/readonly.sqlite3", pool.spec.config[:database] + ensure + ActiveRecord::Base.configurations = @prev_configs + end + def test_retrieve_connection assert @handler.retrieve_connection(@spec_name) end @@ -89,6 +160,41 @@ module ActiveRecord rd.close end + def test_pool_from_any_process_for_uses_most_recent_spec + skip unless current_adapter?(:SQLite3Adapter) + + file = Tempfile.new "lol.sqlite3" + + rd, wr = IO.pipe + rd.binmode + wr.binmode + + pid = fork do + ActiveRecord::Base.configurations["arunit"]["database"] = file.path + ActiveRecord::Base.establish_connection(:arunit) + + pid2 = fork do + wr.write ActiveRecord::Base.connection_config[:database] + wr.close + end + + Process.waitpid pid2 + end + + Process.waitpid pid + + wr.close + + assert_equal file.path, rd.read + + rd.close + ensure + if file + file.close + file.unlink + end + end + def test_a_class_using_custom_pool_and_switching_back_to_primary klass2 = Class.new(Base) { def self.name; "klass2"; end } diff --git a/activerecord/test/cases/connection_adapters/merge_and_resolve_default_url_config_test.rb b/activerecord/test/cases/connection_adapters/merge_and_resolve_default_url_config_test.rb index 4bb5c4f2e2..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" } 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_adapters/type_lookup_test.rb b/activerecord/test/cases/connection_adapters/type_lookup_test.rb index e2e5445a4e..a348c2d783 100644 --- a/activerecord/test/cases/connection_adapters/type_lookup_test.rb +++ b/activerecord/test/cases/connection_adapters/type_lookup_test.rb @@ -89,12 +89,20 @@ unless current_adapter?(:PostgreSQLAdapter) # PostgreSQL does not use type strin end def test_decimal_without_scale - types = %w{decimal(2) decimal(2,0) numeric(2) numeric(2,0) number(2) number(2,0)} - types.each do |type| - cast_type = @connection.type_map.lookup(type) - - assert_equal :decimal, cast_type.type - assert_equal 2, cast_type.cast(2.1) + if current_adapter?(:OracleAdapter) + { + decimal: %w{decimal(2) decimal(2,0) numeric(2) numeric(2,0)}, + integer: %w{number(2) number(2,0)} + } + else + { decimal: %w{decimal(2) decimal(2,0) numeric(2) numeric(2,0) number(2) number(2,0)} } + end.each do |expected_type, types| + types.each do |type| + cast_type = @connection.type_map.lookup(type) + + assert_equal expected_type, cast_type.type + assert_equal 2, cast_type.cast(2.1) + end end end diff --git a/activerecord/test/cases/connection_pool_test.rb b/activerecord/test/cases/connection_pool_test.rb index d7ff9d6880..7e88c9cf7a 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 @@ -307,14 +307,17 @@ module ActiveRecord end end - def test_automatic_reconnect= + def test_automatic_reconnect_restores_after_disconnect pool = ConnectionPool.new ActiveRecord::Base.connection_pool.spec assert pool.automatic_reconnect assert pool.connection pool.disconnect! assert pool.connection + end + def test_automatic_reconnect_can_be_disabled + pool = ConnectionPool.new ActiveRecord::Base.connection_pool.spec pool.disconnect! pool.automatic_reconnect = false @@ -341,14 +344,18 @@ module ActiveRecord end end + class ConnectionTestModel < ActiveRecord::Base + end + def test_connection_notification_is_called payloads = [] subscription = ActiveSupport::Notifications.subscribe("!connection.active_record") do |name, started, finished, unique_id, payload| payloads << payload end - ActiveRecord::Base.establish_connection :arunit + ConnectionTestModel.establish_connection :arunit + assert_equal [:config, :connection_id, :spec_name], payloads[0].keys.sort - assert_equal "primary", payloads[0][:spec_name] + assert_equal "ActiveRecord::ConnectionAdapters::ConnectionPoolTest::ConnectionTestModel", payloads[0][:spec_name] ensure ActiveSupport::Notifications.unsubscribe(subscription) if subscription end @@ -395,7 +402,7 @@ module ActiveRecord all_threads_in_new_connection.wait end rescue Timeout::Error - flunk "pool unable to establish connections concurrently or implementation has " << + flunk "pool unable to establish connections concurrently or implementation has " \ "changed, this test then needs to patch a different :new_connection method" ensure # clean up the threads @@ -437,7 +444,7 @@ module ActiveRecord end end - def test_bang_versions_of_disconnect_and_clear_reloadable_connections_if_unable_to_aquire_all_connections_proceed_anyway + def test_bang_versions_of_disconnect_and_clear_reloadable_connections_if_unable_to_acquire_all_connections_proceed_anyway @pool.checkout_timeout = 0.001 # no need to delay test suite by waiting the whole full default timeout [:disconnect!, :clear_reloadable_connections!].each do |group_action_method| @pool.with_connection do |connection| @@ -455,49 +462,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 - - # 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) + second_thread_done = Concurrent::Event.new - # 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 +547,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 0f62c73f8f..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 diff --git a/activerecord/test/cases/counter_cache_test.rb b/activerecord/test/cases/counter_cache_test.rb index 84f2c3a465..46d7526cc0 100644 --- a/activerecord/test/cases/counter_cache_test.rb +++ b/activerecord/test/cases/counter_cache_test.rb @@ -211,4 +211,155 @@ 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 doesn't touch timestamps with touch: []" do + @topic.update_column :updated_at, 5.minutes.ago + previously_updated_at = @topic.updated_at + + Topic.update_counters(@topic.id, replies_count: -1, touch: []) + + 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 bb16076fd2..66035865be 100644 --- a/activerecord/test/cases/database_statements_test.rb +++ b/activerecord/test/cases/database_statements_test.rb @@ -20,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/date_time_precision_test.rb b/activerecord/test/cases/date_time_precision_test.rb index a1c3c5af9c..e4a2f9ee17 100644 --- a/activerecord/test/cases/date_time_precision_test.rb +++ b/activerecord/test/cases/date_time_precision_test.rb @@ -73,7 +73,7 @@ if subsecond_precision_supported? assert_match %r{t\.datetime\s+"updated_at",\s+precision: 6,\s+null: false$}, output end - if current_adapter?(:PostgreSQLAdapter) + if current_adapter?(:PostgreSQLAdapter, :SQLServerAdapter) def test_datetime_precision_with_zero_should_be_dumped @connection.create_table(:foos, force: true) do |t| t.timestamps precision: 0 diff --git a/activerecord/test/cases/date_time_test.rb b/activerecord/test/cases/date_time_test.rb index 3bc08f80ec..ad7da9de70 100644 --- a/activerecord/test/cases/date_time_test.rb +++ b/activerecord/test/cases/date_time_test.rb @@ -52,7 +52,7 @@ class DateTimeTest < ActiveRecord::TestCase end def test_assign_in_local_timezone - now = DateTime.now + now = DateTime.civil(2017, 3, 1, 12, 0, 0) with_timezone_config default: :local do task = Task.new starting: now assert_equal now, task.starting diff --git a/activerecord/test/cases/defaults_test.rb b/activerecord/test/cases/defaults_test.rb index fcaff38f82..996d298689 100644 --- a/activerecord/test/cases/defaults_test.rb +++ b/activerecord/test/cases/defaults_test.rb @@ -87,9 +87,14 @@ if current_adapter?(:PostgreSQLAdapter) test "schema dump includes default expression" do output = dump_table_schema("defaults") - assert_match %r/t\.date\s+"modified_date",\s+default: -> { "\('now'::text\)::date" }/, output + if ActiveRecord::Base.connection.postgresql_version >= 100000 + assert_match %r/t\.date\s+"modified_date",\s+default: -> { "CURRENT_DATE" }/, output + assert_match %r/t\.datetime\s+"modified_time",\s+default: -> { "CURRENT_TIMESTAMP" }/, output + else + assert_match %r/t\.date\s+"modified_date",\s+default: -> { "\('now'::text\)::date" }/, output + assert_match %r/t\.datetime\s+"modified_time",\s+default: -> { "now\(\)" }/, output + end assert_match %r/t\.date\s+"modified_date_function",\s+default: -> { "now\(\)" }/, output - assert_match %r/t\.datetime\s+"modified_time",\s+default: -> { "now\(\)" }/, output assert_match %r/t\.datetime\s+"modified_time_function",\s+default: -> { "now\(\)" }/, output end end @@ -100,11 +105,21 @@ if current_adapter?(:Mysql2Adapter) include SchemaDumpingHelper if ActiveRecord::Base.connection.version >= "5.6.0" - test "schema dump includes default expression" do + test "schema dump datetime includes default expression" do output = dump_table_schema("datetime_defaults") assert_match %r/t\.datetime\s+"modified_datetime",\s+default: -> { "CURRENT_TIMESTAMP" }/, output end end + + test "schema dump timestamp includes default expression" do + output = dump_table_schema("timestamp_defaults") + assert_match %r/t\.timestamp\s+"modified_timestamp",\s+default: -> { "CURRENT_TIMESTAMP" }/, output + end + + test "schema dump timestamp without default expression" do + output = dump_table_schema("timestamp_defaults") + assert_match %r/t\.timestamp\s+"nullable_timestamp"$/, output + end end class DefaultsTestWithoutTransactionalFixtures < ActiveRecord::TestCase @@ -169,7 +184,7 @@ if current_adapter?(:Mysql2Adapter) assert_nil record.non_null_text assert_nil record.non_null_blob - assert_raises(ActiveRecord::StatementInvalid) { klass.create } + assert_raises(ActiveRecord::NotNullViolation) { klass.create } end end end diff --git a/activerecord/test/cases/dirty_test.rb b/activerecord/test/cases/dirty_test.rb index eee34da664..f72e0d2ead 100644 --- a/activerecord/test/cases/dirty_test.rb +++ b/activerecord/test/cases/dirty_test.rb @@ -298,6 +298,14 @@ class DirtyTest < ActiveRecord::TestCase assert_equal ["arr", "arr matey!"], pirate.catchphrase_change end + def test_virtual_attribute_will_change + assert_deprecated do + parrot = Parrot.create!(name: "Ruby") + parrot.send(:attribute_will_change!, :cancel_save_from_callback) + assert parrot.has_changes_to_save? + end + end + def test_association_assignment_changes_foreign_key pirate = Pirate.create!(catchphrase: "jarl") pirate.parrot = Parrot.create!(name: "Lorre") @@ -338,13 +346,14 @@ class DirtyTest < ActiveRecord::TestCase def test_partial_update_with_optimistic_locking person = Person.new(first_name: "foo") - old_lock_version = 1 with_partial_writes Person, false do assert_queries(2) { 2.times { person.save! } } Person.where(id: person.id).update_all(first_name: "baz") end + old_lock_version = person.lock_version + with_partial_writes Person, true do assert_queries(0) { 2.times { person.save! } } assert_equal old_lock_version, person.reload.lock_version @@ -555,18 +564,17 @@ class DirtyTest < ActiveRecord::TestCase travel_back end - if ActiveRecord::Base.connection.supports_migrations? - class Testings < ActiveRecord::Base; end - def test_field_named_field - ActiveRecord::Base.connection.create_table :testings do |t| - t.string :field - end - assert_nothing_raised do - Testings.new.attributes - end - ensure - ActiveRecord::Base.connection.drop_table :testings rescue nil + class Testings < ActiveRecord::Base; end + def test_field_named_field + ActiveRecord::Base.connection.create_table :testings do |t| + t.string :field end + assert_nothing_raised do + Testings.new.attributes + end + ensure + ActiveRecord::Base.connection.drop_table :testings rescue nil + ActiveRecord::Base.clear_cache! end def test_datetime_attribute_can_be_updated_with_fractional_seconds @@ -660,6 +668,47 @@ class DirtyTest < ActiveRecord::TestCase assert binary.changed? end + test "changes is correct for subclass" do + foo = Class.new(Pirate) do + def catchphrase + super.upcase + end + end + + pirate = foo.create!(catchphrase: "arrrr") + + new_catchphrase = "arrrr matey!" + + pirate.catchphrase = new_catchphrase + assert pirate.catchphrase_changed? + + expected_changes = { + "catchphrase" => ["arrrr", new_catchphrase] + } + + assert_equal new_catchphrase.upcase, pirate.catchphrase + assert_equal expected_changes, pirate.changes + end + + test "changes is correct if override attribute reader" do + pirate = Pirate.create!(catchphrase: "arrrr") + def pirate.catchphrase + super.upcase + end + + new_catchphrase = "arrrr matey!" + + pirate.catchphrase = new_catchphrase + assert pirate.catchphrase_changed? + + expected_changes = { + "catchphrase" => ["arrrr", new_catchphrase] + } + + assert_equal new_catchphrase.upcase, pirate.catchphrase + assert_equal expected_changes, pirate.changes + end + test "attribute_changed? doesn't compute in-place changes for unrelated attributes" do test_type_class = Class.new(ActiveRecord::Type::Value) do define_method(:changed_in_place?) do |*| @@ -723,6 +772,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/dup_test.rb b/activerecord/test/cases/dup_test.rb index 3821e0c949..000ed27efb 100644 --- a/activerecord/test/cases/dup_test.rb +++ b/activerecord/test/cases/dup_test.rb @@ -143,6 +143,8 @@ module ActiveRecord end def test_dup_without_primary_key + skip if current_adapter?(:OracleAdapter) + klass = Class.new(ActiveRecord::Base) do self.table_name = "parrots_pirates" end diff --git a/activerecord/test/cases/enum_test.rb b/activerecord/test/cases/enum_test.rb index b7641fcf32..db3da53487 100644 --- a/activerecord/test/cases/enum_test.rb +++ b/activerecord/test/cases/enum_test.rb @@ -1,8 +1,9 @@ require "cases/helper" +require "models/author" require "models/book" class EnumTest < ActiveRecord::TestCase - fixtures :books + fixtures :books, :authors setup do @book = books(:awdr) @@ -37,6 +38,8 @@ class EnumTest < ActiveRecord::TestCase assert_equal @book, Book.author_visibility_visible.first assert_equal @book, Book.illustrator_visibility_visible.first assert_equal @book, Book.medium_to_read.first + assert_equal books(:ddd), Book.forgotten.first + assert_equal books(:rfr), authors(:david).unpublished_books.first end test "find via where with values" do 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 86fe90ae51..4f6bd9327c 100644 --- a/activerecord/test/cases/explain_test.rb +++ b/activerecord/test/cases/explain_test.rb @@ -47,7 +47,7 @@ if ActiveRecord::Base.connection.supports_explain? def test_exec_explain_with_binds sqls = %w(foo bar) - binds = [[bind_param("wadus", 1)], [bind_param("chaflan", 2)]] + binds = [[bind_attribute("wadus", 1)], [bind_attribute("chaflan", 2)]] queries = sqls.zip(binds) stub_explain_for_query_plans(["query plan foo\n", "query plan bar\n"]) do @@ -79,9 +79,5 @@ if ActiveRecord::Base.connection.supports_explain? yield end end - - def bind_param(name, value) - ActiveRecord::Relation::QueryAttribute.new(name, value, ActiveRecord::Type::Value.new) - end end end diff --git a/activerecord/test/cases/finder_test.rb b/activerecord/test/cases/finder_test.rb index 51563b347c..4837a169fa 100644 --- a/activerecord/test/cases/finder_test.rb +++ b/activerecord/test/cases/finder_test.rb @@ -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 @@ -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 @@ -202,11 +202,29 @@ class FinderTest < ActiveRecord::TestCase assert_equal true, Topic.first.replies.exists? end - # ensures +exists?+ runs valid SQL by excluding order value - def test_exists_with_order + # Ensure +exists?+ runs without an error by excluding distinct value. + # See https://github.com/rails/rails/pull/26981. + def test_exists_with_order_and_distinct assert_equal true, Topic.order(:id).distinct.exists? end + # Ensure +exists?+ runs without an error by excluding order value. + def test_exists_with_order + assert_equal true, Topic.order("invalid sql here").exists? + end + + def test_exists_with_joins + assert_equal true, Topic.joins(:replies).where(replies_topics: { approved: true }).order("replies_topics.created_at DESC").exists? + end + + def test_exists_with_left_joins + assert_equal true, Topic.left_joins(:replies).where(replies_topics: { approved: true }).order("replies_topics.created_at DESC").exists? + end + + def test_exists_with_eager_load + assert_equal true, Topic.eager_load(:replies).where(replies_topics: { approved: true }).order("replies_topics.created_at DESC").exists? + end + def test_exists_with_includes_limit_and_empty_result assert_equal false, Topic.includes(:replies).limit(0).exists? assert_equal false, Topic.includes(:replies).limit(1).where("0 = 1").exists? @@ -236,9 +254,9 @@ class FinderTest < ActiveRecord::TestCase def test_exists_with_aggregate_having_three_mappings_with_one_difference existing_address = customers(:david).address - assert_equal false, Customer.exists?(address: Address.new(existing_address.street, existing_address.city, existing_address.country + "1")) - assert_equal false, Customer.exists?(address: Address.new(existing_address.street, existing_address.city + "1", existing_address.country)) - assert_equal false, Customer.exists?(address: Address.new(existing_address.street + "1", existing_address.city, existing_address.country)) + assert_equal false, Customer.exists?(address: Address.new(existing_address.street, existing_address.city, existing_address.country + "1")) + assert_equal false, Customer.exists?(address: Address.new(existing_address.street, existing_address.city + "1", existing_address.country)) + assert_equal false, Customer.exists?(address: Address.new(existing_address.street + "1", existing_address.city, existing_address.country)) end def test_exists_does_not_instantiate_records @@ -258,8 +276,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 +357,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 +511,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 + assert_nil Topic.limit(1).second_to_last end def test_second_to_last_have_primary_key_order_by_default @@ -516,15 +539,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 + assert_nil Topic.limit(1).third_to_last + assert_nil Topic.limit(2).third + assert_nil Topic.limit(2).third_to_last end def test_third_to_last_have_primary_key_order_by_default @@ -584,7 +607,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 +615,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 @@ -693,34 +716,33 @@ 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 assert Topic.where(author_name: "David", title: "The First Topic", replies_count: 1, approved: false).find(1) assert_raise(ActiveRecord::RecordNotFound) { Topic.where(author_name: "David", title: "The First Topic", replies_count: 1, approved: true).find(1) } assert_raise(ActiveRecord::RecordNotFound) { Topic.where(author_name: "David", title: "HHC", replies_count: 1, approved: false).find(1) } - assert_raise(ActiveRecord::RecordNotFound) { Topic.where(author_name: "David", title: "The First Topic", replies_count: 1, approved: true).find(1) } end def test_condition_interpolation @@ -858,13 +880,13 @@ class FinderTest < ActiveRecord::TestCase end def test_bind_variables_with_quotes - Company.create("name" => "37signals' go'es agains") - assert Company.where(["name = ?", "37signals' go'es agains"]).first + Company.create("name" => "37signals' go'es against") + assert Company.where(["name = ?", "37signals' go'es against"]).first end 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 + Company.create("name" => "37signals' go'es against") + assert Company.where(["name = :name", { name: "37signals' go'es against" }]).first end def test_named_bind_variables @@ -983,7 +1005,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 @@ -1029,7 +1050,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 @@ -1061,8 +1082,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 @@ -1167,12 +1188,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 @@ -1221,7 +1242,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 cf2a73595a..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 diff --git a/activerecord/test/cases/fixtures_test.rb b/activerecord/test/cases/fixtures_test.rb index 3f111447ff..a0a6d3c7ef 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" @@ -93,6 +95,24 @@ class FixturesTest < ActiveRecord::TestCase assert_nil(topics["second"]["author_email_address"]) end + def test_no_args_returns_all + all_topics = topics + assert_equal 5, all_topics.length + assert_equal "The First Topic", all_topics.first["title"] + assert_equal 5, all_topics.last.id + end + + def test_no_args_record_returns_all_without_array + all_binaries = binaries + assert_kind_of(Array, all_binaries) + assert_equal 1, binaries.length + end + + def test_nil_raises + assert_raise(StandardError) { topics(nil) } + assert_raise(StandardError) { topics([nil]) } + end + def test_inserts create_fixtures("topics") first_row = ActiveRecord::Base.connection.select_one("SELECT * FROM topics WHERE author_name = 'David'") @@ -102,64 +122,62 @@ class FixturesTest < ActiveRecord::TestCase assert_nil(second_row["author_email_address"]) end - if ActiveRecord::Base.connection.supports_migrations? - def test_inserts_with_pre_and_suffix - # Reset cache to make finds on the new table work - ActiveRecord::FixtureSet.reset_cache - - ActiveRecord::Base.connection.create_table :prefix_other_topics_suffix do |t| - t.column :title, :string - t.column :author_name, :string - t.column :author_email_address, :string - t.column :written_on, :datetime - t.column :bonus_time, :time - t.column :last_read, :date - t.column :content, :string - t.column :approved, :boolean, default: true - t.column :replies_count, :integer, default: 0 - t.column :parent_id, :integer - t.column :type, :string, limit: 50 - end + def test_inserts_with_pre_and_suffix + # Reset cache to make finds on the new table work + ActiveRecord::FixtureSet.reset_cache - # Store existing prefix/suffix - old_prefix = ActiveRecord::Base.table_name_prefix - old_suffix = ActiveRecord::Base.table_name_suffix + ActiveRecord::Base.connection.create_table :prefix_other_topics_suffix do |t| + t.column :title, :string + t.column :author_name, :string + t.column :author_email_address, :string + t.column :written_on, :datetime + t.column :bonus_time, :time + t.column :last_read, :date + t.column :content, :string + t.column :approved, :boolean, default: true + t.column :replies_count, :integer, default: 0 + t.column :parent_id, :integer + t.column :type, :string, limit: 50 + end - # Set a prefix/suffix we can test against - ActiveRecord::Base.table_name_prefix = "prefix_" - ActiveRecord::Base.table_name_suffix = "_suffix" + # Store existing prefix/suffix + old_prefix = ActiveRecord::Base.table_name_prefix + old_suffix = ActiveRecord::Base.table_name_suffix - other_topic_klass = Class.new(ActiveRecord::Base) do - def self.name - "OtherTopic" - end + # Set a prefix/suffix we can test against + ActiveRecord::Base.table_name_prefix = "prefix_" + ActiveRecord::Base.table_name_suffix = "_suffix" + + other_topic_klass = Class.new(ActiveRecord::Base) do + def self.name + "OtherTopic" end + end - topics = [create_fixtures("other_topics")].flatten.first + topics = [create_fixtures("other_topics")].flatten.first - # This checks for a caching problem which causes a bug in the fixtures - # class-level configuration helper. - assert_not_nil topics, "Fixture data inserted, but fixture objects not returned from create" + # This checks for a caching problem which causes a bug in the fixtures + # class-level configuration helper. + assert_not_nil topics, "Fixture data inserted, but fixture objects not returned from create" - first_row = ActiveRecord::Base.connection.select_one("SELECT * FROM prefix_other_topics_suffix WHERE author_name = 'David'") - assert_not_nil first_row, "The prefix_other_topics_suffix table appears to be empty despite create_fixtures: the row with author_name = 'David' was not found" - assert_equal("The First Topic", first_row["title"]) + first_row = ActiveRecord::Base.connection.select_one("SELECT * FROM prefix_other_topics_suffix WHERE author_name = 'David'") + assert_not_nil first_row, "The prefix_other_topics_suffix table appears to be empty despite create_fixtures: the row with author_name = 'David' was not found" + assert_equal("The First Topic", first_row["title"]) - second_row = ActiveRecord::Base.connection.select_one("SELECT * FROM prefix_other_topics_suffix WHERE author_name = 'Mary'") - assert_nil(second_row["author_email_address"]) + second_row = ActiveRecord::Base.connection.select_one("SELECT * FROM prefix_other_topics_suffix WHERE author_name = 'Mary'") + assert_nil(second_row["author_email_address"]) - assert_equal :prefix_other_topics_suffix, topics.table_name.to_sym - # This assertion should preferably be the last in the list, because calling - # other_topic_klass.table_name sets a class-level instance variable - assert_equal :prefix_other_topics_suffix, other_topic_klass.table_name.to_sym + assert_equal :prefix_other_topics_suffix, topics.table_name.to_sym + # This assertion should preferably be the last in the list, because calling + # other_topic_klass.table_name sets a class-level instance variable + assert_equal :prefix_other_topics_suffix, other_topic_klass.table_name.to_sym - ensure - # Restore prefix/suffix to its previous values - ActiveRecord::Base.table_name_prefix = old_prefix - ActiveRecord::Base.table_name_suffix = old_suffix + ensure + # Restore prefix/suffix to its previous values + ActiveRecord::Base.table_name_prefix = old_prefix + ActiveRecord::Base.table_name_suffix = old_suffix - ActiveRecord::Base.connection.drop_table :prefix_other_topics_suffix rescue nil - end + ActiveRecord::Base.connection.drop_table :prefix_other_topics_suffix rescue nil end def test_insert_with_datetime @@ -192,28 +210,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 @@ -628,6 +656,8 @@ class TransactionalFixturesOnConnectionNotification < ActiveRecord::TestCase def test_transaction_created_on_connection_notification connection = stub(transaction_open?: false) connection.expects(:begin_transaction).with(joinable: false) + pool = connection.stubs(:pool).returns(ActiveRecord::ConnectionAdapters::ConnectionPool.new(ActiveRecord::Base.connection_pool.spec)) + pool.stubs(:lock_thread=).with(false) fire_connection_notification(connection) end @@ -635,12 +665,16 @@ class TransactionalFixturesOnConnectionNotification < ActiveRecord::TestCase # Mocha is not thread-safe so define our own stub to test connection = Class.new do attr_accessor :rollback_transaction_called + attr_accessor :pool def transaction_open?; true; end def begin_transaction(*args); end def rollback_transaction(*args) @rollback_transaction_called = true end end.new + connection.pool = Class.new do + def lock_thread=(lock_thread); false; end + end.new fire_connection_notification(connection) teardown_fixtures assert(connection.rollback_transaction_called, "Expected <mock connection>#rollback_transaction to be called but was not") @@ -750,7 +784,7 @@ end class FasterFixturesTest < ActiveRecord::TestCase self.use_transactional_tests = false - fixtures :categories, :authors + fixtures :categories, :authors, :author_addresses def load_extra_fixture(name) fixture = create_fixtures(name).first @@ -948,7 +982,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 +1034,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 +1045,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 75c3493527..ffa3f63e0d 100644 --- a/activerecord/test/cases/forbidden_attributes_protection_test.rb +++ b/activerecord/test/cases/forbidden_attributes_protection_test.rb @@ -144,7 +144,7 @@ class ForbiddenAttributesProtectionTest < ActiveRecord::TestCase 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 f1d69a215a..5a3b8e3fb5 100644 --- a/activerecord/test/cases/helper.rb +++ b/activerecord/test/cases/helper.rb @@ -6,7 +6,6 @@ require "active_record" require "cases/test_case" require "active_support/dependencies" require "active_support/logger" -require "active_support/core_ext/string/strip" require "support/config" require "support/connection" @@ -27,9 +26,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) && diff --git a/activerecord/test/cases/inheritance_test.rb b/activerecord/test/cases/inheritance_test.rb index 9ad4664567..b4bbdc6dad 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 assert_raises NoMethodError do - ActiveRecord::Base.send :compute_type, "InvalidModel" + Company.send :compute_type, "InvalidModel" end end end @@ -90,7 +90,7 @@ class InheritanceTest < ActiveRecord::TestCase 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 @@ -99,7 +99,7 @@ class InheritanceTest < ActiveRecord::TestCase def test_compute_type_argument_error 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 @@ -316,7 +316,7 @@ class InheritanceTest < ActiveRecord::TestCase end def test_new_with_autoload_paths - path = File.expand_path("../../models/autoloadable", __FILE__) + path = File.expand_path("../models/autoloadable", __dir__) ActiveSupport::Dependencies.autoload_paths << path firm = Company.new(type: "ExtraFirm") @@ -417,7 +417,7 @@ class InheritanceTest < ActiveRecord::TestCase def test_eager_load_belongs_to_primary_key_quoting con = Account.connection - assert_sql(/#{con.quote_table_name('companies')}.#{con.quote_column_name('id')} = 1/) do + assert_sql(/#{con.quote_table_name('companies')}\.#{con.quote_column_name('id')} = 1/) do Account.all.merge!(includes: :firm).find(1) end end diff --git a/activerecord/test/cases/integration_test.rb b/activerecord/test/cases/integration_test.rb index 766917b196..9104976126 100644 --- a/activerecord/test/cases/integration_test.rb +++ b/activerecord/test/cases/integration_test.rb @@ -1,4 +1,3 @@ - require "cases/helper" require "models/company" require "models/developer" @@ -15,7 +14,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 +88,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 @@ -169,7 +168,65 @@ class IntegrationTest < ActiveRecord::TestCase end def test_named_timestamps_for_cache_key - owner = owners(:blackbeard) - assert_equal "owners/#{owner.id}-#{owner.happy_at.utc.to_s(:usec)}", owner.cache_key(:updated_at, :happy_at) + assert_deprecated do + owner = owners(:blackbeard) + assert_equal "owners/#{owner.id}-#{owner.happy_at.utc.to_s(:usec)}", owner.cache_key(:updated_at, :happy_at) + end + end + + def test_cache_key_when_named_timestamp_is_nil + assert_deprecated do + owner = owners(:blackbeard) + owner.happy_at = nil + assert_equal "owners/#{owner.id}", owner.cache_key(:happy_at) + end + end + + def test_cache_key_is_stable_with_versioning_on + Developer.cache_versioning = true + + developer = Developer.first + first_key = developer.cache_key + + developer.touch + second_key = developer.cache_key + + assert_equal first_key, second_key + ensure + Developer.cache_versioning = false + end + + def test_cache_version_changes_with_versioning_on + Developer.cache_versioning = true + + developer = Developer.first + first_version = developer.cache_version + + travel 10.seconds do + developer.touch + end + + second_version = developer.cache_version + + assert_not_equal first_version, second_version + ensure + Developer.cache_versioning = false + end + + def test_cache_key_retains_version_when_custom_timestamp_is_used + Developer.cache_versioning = true + + developer = Developer.first + first_key = developer.cache_key_with_version + + travel 10.seconds do + developer.touch + end + + second_key = developer.cache_key_with_version + + assert_not_equal first_key, second_key + ensure + Developer.cache_versioning = false end end diff --git a/activerecord/test/cases/invertible_migration_test.rb b/activerecord/test/cases/invertible_migration_test.rb index 9d5aace7db..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,11 +212,11 @@ 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 @@ -226,24 +224,18 @@ module ActiveRecord 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 + 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 @@ -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 b06fed4f0d..9b4b61b16e 100644 --- a/activerecord/test/cases/json_serialization_test.rb +++ b/activerecord/test/cases/json_serialization_test.rb @@ -101,8 +101,19 @@ class JsonSerializationTest < ActiveRecord::TestCase assert_match %r{"favorite_quote":"Constraints are liberating"}, methods_json end + def test_uses_serializable_hash_with_frozen_hash + def @contact.serializable_hash(options = nil) + super({ only: %w(name) }.freeze) + end + + json = @contact.to_json + assert_match %r{"name":"Konata Izumi"}, json + assert_no_match %r{awesome}, json + assert_no_match %r{age}, json + 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 +124,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 +148,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 @@ -157,7 +168,7 @@ class JsonSerializationTest < ActiveRecord::TestCase end class DatabaseConnectedJsonEncodingTest < ActiveRecord::TestCase - fixtures :authors, :posts, :comments, :tags, :taggings + fixtures :authors, :author_addresses, :posts, :comments, :tags, :taggings include JsonSerializationHelpers @@ -243,7 +254,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 diff --git a/activerecord/test/cases/json_shared_test_cases.rb b/activerecord/test/cases/json_shared_test_cases.rb new file mode 100644 index 0000000000..ef5ca86874 --- /dev/null +++ b/activerecord/test/cases/json_shared_test_cases.rb @@ -0,0 +1,188 @@ +require "support/schema_dumping_helper" + +module JSONSharedTestCases + include SchemaDumpingHelper + + class JsonDataType < ActiveRecord::Base + self.table_name = "json_data_type" + + store_accessor :settings, :resolution + end + + def test_column + column = JsonDataType.columns_hash["payload"] + assert_equal column_type, column.type + assert_equal column_type.to_s, column.sql_type + + type = JsonDataType.type_for_attribute("payload") + assert_not type.binary? + end + + def test_change_table_supports_json + @connection.change_table("json_data_type") do |t| + t.public_send column_type, "users" + end + JsonDataType.reset_column_information + column = JsonDataType.columns_hash["users"] + assert_equal column_type, column.type + assert_equal column_type.to_s, column.sql_type + end + + def test_schema_dumping + output = dump_table_schema("json_data_type") + assert_match(/t\.#{column_type}\s+"settings"/, output) + 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.save! + assert_equal({ "string" => "foo", "symbol" => "bar" }, x.reload.payload) + end + + def test_type_cast_json + type = JsonDataType.type_for_attribute("payload") + + 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({}, type.deserialize("{}")) + assert_equal({ "key" => nil }, type.deserialize('{"key": null}')) + assert_equal({ "c" => "}", '"a"' => 'b "a b' }, type.deserialize(%q({"c":"}", "\"a\"":"b \"a b"}))) + end + + def test_rewrite + @connection.execute(%q|insert into json_data_type (payload) VALUES ('{"k":"v"}')|) + x = JsonDataType.first + x.payload = { '"a\'' => "b" } + assert x.save! + end + + def test_select + @connection.execute(%q|insert into json_data_type (payload) VALUES ('{"k":"v"}')|) + x = JsonDataType.first + assert_equal({ "k" => "v" }, x.payload) + end + + def test_select_multikey + @connection.execute(%q|insert into json_data_type (payload) VALUES ('{"k1":"v1", "k2":"v2", "k3":[1,2,3]}')|) + x = JsonDataType.first + assert_equal({ "k1" => "v1", "k2" => "v2", "k3" => [1, 2, 3] }, x.payload) + end + + def test_null_json + @connection.execute("insert into json_data_type (payload) VALUES(null)") + x = JsonDataType.first + 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) + 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"] + assert x.save! + end + + def test_with_store_accessors + x = JsonDataType.new(resolution: "320×480") + assert_equal "320×480", x.resolution + + x.save! + x = JsonDataType.first + assert_equal "320×480", x.resolution + + x.resolution = "640×1136" + x.save! + + x = JsonDataType.first + assert_equal "640×1136", x.resolution + end + + def test_duplication_with_store_accessors + x = JsonDataType.new(resolution: "320×480") + assert_equal "320×480", x.resolution + + y = x.dup + assert_equal "320×480", y.resolution + end + + def test_yaml_round_trip_with_store_accessors + x = JsonDataType.new(resolution: "320×480") + assert_equal "320×480", x.resolution + + y = YAML.load(YAML.dump(x)) + assert_equal "320×480", y.resolution + end + + def test_changes_in_place + json = JsonDataType.new + assert_not json.changed? + + json.payload = { "one" => "two" } + assert json.changed? + assert json.payload_changed? + + json.save! + assert_not json.changed? + + json.payload["three"] = "four" + assert json.payload_changed? + + json.save! + json.reload + + assert_equal({ "one" => "two", "three" => "four" }, json.payload) + assert_not json.changed? + end + + def test_changes_in_place_with_ruby_object + time = Time.now.utc + json = JsonDataType.create!(payload: time) + + json.reload + assert_not json.changed? + + json.payload = time + assert_not json.changed? + end + + def test_assigning_string_literal + json = JsonDataType.create!(payload: "foo") + assert_equal "foo", json.payload + end + + def test_assigning_number + json = JsonDataType.create!(payload: 1.234) + assert_equal 1.234, json.payload + end + + def test_assigning_boolean + json = JsonDataType.create!(payload: true) + assert_equal true, json.payload + end +end diff --git a/activerecord/test/cases/locking_test.rb b/activerecord/test/cases/locking_test.rb index 13b6f6daaf..3a3b8e51f9 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! @@ -222,22 +214,115 @@ 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" - t1.save - t1 = LockWithoutDefault.find(t1.id) + 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_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 + + 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_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_queries_count + t1 = LockWithCustomColumnWithoutDefault.create(title: "title1") + + assert_equal "title1", t1.title assert_equal 0, t1.custom_lock_version - assert [0, "0"].include?(t1.custom_lock_version_before_type_cast) + + assert_queries(1) { t1.update(title: "title2") } + + t1.reload + assert_equal "title2", t1.title + assert_equal 1, 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 @@ -351,7 +436,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 @@ -417,7 +502,10 @@ unless in_memory_db? Person.transaction do person = Person.find 1 old, person.first_name = person.first_name, "fooman" - person.lock! + # Locking a dirty record is deprecated + assert_deprecated do + person.lock! + end assert_equal old, person.first_name end end @@ -460,7 +548,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/log_subscriber_test.rb b/activerecord/test/cases/log_subscriber_test.rb index 90ad970e16..b80257962c 100644 --- a/activerecord/test/cases/log_subscriber_test.rb +++ b/activerecord/test/cases/log_subscriber_test.rb @@ -21,6 +21,7 @@ class LogSubscriberTest < ActiveRecord::TestCase TRANSACTION: REGEXP_CYAN, OTHER: REGEXP_MAGENTA } + Event = Struct.new(:duration, :payload) class TestDebugLogSubscriber < ActiveRecord::LogSubscriber attr_reader :debugs @@ -55,25 +56,22 @@ class LogSubscriberTest < ActiveRecord::TestCase end def test_schema_statements_are_ignored - event = Struct.new(:duration, :payload) - logger = TestDebugLogSubscriber.new assert_equal 0, logger.debugs.length - logger.sql(event.new(0, sql: "hi mom!")) + logger.sql(Event.new(0.9, sql: "hi mom!")) assert_equal 1, logger.debugs.length - logger.sql(event.new(0, sql: "hi mom!", name: "foo")) + logger.sql(Event.new(0.9, sql: "hi mom!", name: "foo")) assert_equal 2, logger.debugs.length - logger.sql(event.new(0, sql: "hi mom!", name: "SCHEMA")) + logger.sql(Event.new(0.9, sql: "hi mom!", name: "SCHEMA")) assert_equal 2, logger.debugs.length end def test_sql_statements_are_not_squeezed - event = Struct.new(:duration, :payload) logger = TestDebugLogSubscriber.new - logger.sql(event.new(0, sql: "ruby rails")) + logger.sql(Event.new(0.9, sql: "ruby rails")) assert_match(/ruby rails/, logger.debugs.first) end @@ -86,56 +84,51 @@ class LogSubscriberTest < ActiveRecord::TestCase end def test_basic_query_logging_coloration - event = Struct.new(:duration, :payload) logger = TestDebugLogSubscriber.new logger.colorize_logging = true SQL_COLORINGS.each do |verb, color_regex| - logger.sql(event.new(0, sql: verb.to_s)) + logger.sql(Event.new(0.9, sql: verb.to_s)) assert_match(/#{REGEXP_BOLD}#{color_regex}#{verb}#{REGEXP_CLEAR}/i, logger.debugs.last) end end def test_basic_payload_name_logging_coloration_generic_sql - event = Struct.new(:duration, :payload) logger = TestDebugLogSubscriber.new logger.colorize_logging = true SQL_COLORINGS.each do |verb, _| - logger.sql(event.new(0, sql: verb.to_s)) - assert_match(/#{REGEXP_BOLD}#{REGEXP_MAGENTA} \(0.0ms\)#{REGEXP_CLEAR}/i, logger.debugs.last) + logger.sql(Event.new(0.9, sql: verb.to_s)) + assert_match(/#{REGEXP_BOLD}#{REGEXP_MAGENTA} \(0\.9ms\)#{REGEXP_CLEAR}/i, logger.debugs.last) - logger.sql(event.new(0, sql: verb.to_s, name: "SQL")) - assert_match(/#{REGEXP_BOLD}#{REGEXP_MAGENTA}SQL \(0.0ms\)#{REGEXP_CLEAR}/i, logger.debugs.last) + logger.sql(Event.new(0.9, sql: verb.to_s, name: "SQL")) + assert_match(/#{REGEXP_BOLD}#{REGEXP_MAGENTA}SQL \(0\.9ms\)#{REGEXP_CLEAR}/i, logger.debugs.last) end end def test_basic_payload_name_logging_coloration_named_sql - event = Struct.new(:duration, :payload) logger = TestDebugLogSubscriber.new logger.colorize_logging = true SQL_COLORINGS.each do |verb, _| - logger.sql(event.new(0, sql: verb.to_s, name: "Model Load")) - assert_match(/#{REGEXP_BOLD}#{REGEXP_CYAN}Model Load \(0.0ms\)#{REGEXP_CLEAR}/i, logger.debugs.last) + logger.sql(Event.new(0.9, sql: verb.to_s, name: "Model Load")) + assert_match(/#{REGEXP_BOLD}#{REGEXP_CYAN}Model Load \(0\.9ms\)#{REGEXP_CLEAR}/i, logger.debugs.last) - logger.sql(event.new(0, sql: verb.to_s, name: "Model Exists")) - assert_match(/#{REGEXP_BOLD}#{REGEXP_CYAN}Model Exists \(0.0ms\)#{REGEXP_CLEAR}/i, logger.debugs.last) + logger.sql(Event.new(0.9, sql: verb.to_s, name: "Model Exists")) + assert_match(/#{REGEXP_BOLD}#{REGEXP_CYAN}Model Exists \(0\.9ms\)#{REGEXP_CLEAR}/i, logger.debugs.last) - logger.sql(event.new(0, sql: verb.to_s, name: "ANY SPECIFIC NAME")) - assert_match(/#{REGEXP_BOLD}#{REGEXP_CYAN}ANY SPECIFIC NAME \(0.0ms\)#{REGEXP_CLEAR}/i, logger.debugs.last) + logger.sql(Event.new(0.9, sql: verb.to_s, name: "ANY SPECIFIC NAME")) + assert_match(/#{REGEXP_BOLD}#{REGEXP_CYAN}ANY SPECIFIC NAME \(0\.9ms\)#{REGEXP_CLEAR}/i, logger.debugs.last) end end def test_query_logging_coloration_with_nested_select - event = Struct.new(:duration, :payload) logger = TestDebugLogSubscriber.new logger.colorize_logging = true SQL_COLORINGS.slice(:SELECT, :INSERT, :UPDATE, :DELETE).each do |verb, color_regex| - logger.sql(event.new(0, sql: "#{verb} WHERE ID IN SELECT")) - assert_match(/#{REGEXP_BOLD}#{REGEXP_MAGENTA} \(0.0ms\)#{REGEXP_CLEAR} #{REGEXP_BOLD}#{color_regex}#{verb} WHERE ID IN SELECT#{REGEXP_CLEAR}/i, logger.debugs.last) + logger.sql(Event.new(0.9, sql: "#{verb} WHERE ID IN SELECT")) + assert_match(/#{REGEXP_BOLD}#{REGEXP_MAGENTA} \(0\.9ms\)#{REGEXP_CLEAR} #{REGEXP_BOLD}#{color_regex}#{verb} WHERE ID IN SELECT#{REGEXP_CLEAR}/i, logger.debugs.last) end end def test_query_logging_coloration_with_multi_line_nested_select - event = Struct.new(:duration, :payload) logger = TestDebugLogSubscriber.new logger.colorize_logging = true SQL_COLORINGS.slice(:SELECT, :INSERT, :UPDATE, :DELETE).each do |verb, color_regex| @@ -145,13 +138,12 @@ class LogSubscriberTest < ActiveRecord::TestCase SELECT ID FROM THINGS ) EOS - logger.sql(event.new(0, sql: sql)) - assert_match(/#{REGEXP_BOLD}#{REGEXP_MAGENTA} \(0.0ms\)#{REGEXP_CLEAR} #{REGEXP_BOLD}#{color_regex}.*#{verb}.*#{REGEXP_CLEAR}/mi, logger.debugs.last) + logger.sql(Event.new(0.9, sql: sql)) + assert_match(/#{REGEXP_BOLD}#{REGEXP_MAGENTA} \(0\.9ms\)#{REGEXP_CLEAR} #{REGEXP_BOLD}#{color_regex}.*#{verb}.*#{REGEXP_CLEAR}/mi, logger.debugs.last) end end def test_query_logging_coloration_with_lock - event = Struct.new(:duration, :payload) logger = TestDebugLogSubscriber.new logger.colorize_logging = true sql = <<-EOS @@ -159,14 +151,14 @@ class LogSubscriberTest < ActiveRecord::TestCase (SELECT * FROM mytable FOR UPDATE) ss WHERE col1 = 5; EOS - logger.sql(event.new(0, sql: sql)) - assert_match(/#{REGEXP_BOLD}#{REGEXP_MAGENTA} \(0.0ms\)#{REGEXP_CLEAR} #{REGEXP_BOLD}#{SQL_COLORINGS[:LOCK]}.*FOR UPDATE.*#{REGEXP_CLEAR}/mi, logger.debugs.last) + logger.sql(Event.new(0.9, sql: sql)) + assert_match(/#{REGEXP_BOLD}#{REGEXP_MAGENTA} \(0\.9ms\)#{REGEXP_CLEAR} #{REGEXP_BOLD}#{SQL_COLORINGS[:LOCK]}.*FOR UPDATE.*#{REGEXP_CLEAR}/mi, logger.debugs.last) sql = <<-EOS LOCK TABLE films IN SHARE MODE; EOS - logger.sql(event.new(0, sql: sql)) - assert_match(/#{REGEXP_BOLD}#{REGEXP_MAGENTA} \(0.0ms\)#{REGEXP_CLEAR} #{REGEXP_BOLD}#{SQL_COLORINGS[:LOCK]}.*LOCK TABLE.*#{REGEXP_CLEAR}/mi, logger.debugs.last) + logger.sql(Event.new(0.9, sql: sql)) + assert_match(/#{REGEXP_BOLD}#{REGEXP_MAGENTA} \(0\.9ms\)#{REGEXP_CLEAR} #{REGEXP_BOLD}#{SQL_COLORINGS[:LOCK]}.*LOCK TABLE.*#{REGEXP_CLEAR}/mi, logger.debugs.last) end def test_exists_query_logging diff --git a/activerecord/test/cases/migration/change_schema_test.rb b/activerecord/test/cases/migration/change_schema_test.rb index bdb90eaa74..1d305fa11f 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 @@ -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 @@ -265,6 +269,8 @@ module ActiveRecord if current_adapter?(:PostgreSQLAdapter) assert_equal "timestamp without time zone", klass.columns_hash["foo"].sql_type + elsif current_adapter?(:Mysql2Adapter) + assert_equal "timestamp", klass.columns_hash["foo"].sql_type else assert_equal klass.connection.type_to_sql("datetime"), klass.columns_hash["foo"].sql_type end @@ -405,9 +411,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/column_attributes_test.rb b/activerecord/test/cases/migration/column_attributes_test.rb index 03d781d3d2..48df931543 100644 --- a/activerecord/test/cases/migration/column_attributes_test.rb +++ b/activerecord/test/cases/migration/column_attributes_test.rb @@ -72,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 @@ -165,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 55c06da411..2329888345 100644 --- a/activerecord/test/cases/migration/columns_test.rb +++ b/activerecord/test/cases/migration/columns_test.rb @@ -225,6 +225,16 @@ module ActiveRecord assert_nil TestModel.new.contributor end + def test_change_column_to_drop_default_with_null_false + add_column "test_models", "contributor", :boolean, default: true, null: false + assert TestModel.new.contributor? + + change_column "test_models", "contributor", :boolean, default: nil, null: false + TestModel.reset_column_information + assert_not TestModel.new.contributor? + assert_nil TestModel.new.contributor + end + def test_change_column_with_new_default add_column "test_models", "administrator", :boolean, default: true assert TestModel.new.administrator? diff --git a/activerecord/test/cases/migration/command_recorder_test.rb b/activerecord/test/cases/migration/command_recorder_test.rb index 802a969cb7..007926f1b9 100644 --- a/activerecord/test/cases/migration/command_recorder_test.rb +++ b/activerecord/test/cases/migration/command_recorder_test.rb @@ -211,11 +211,6 @@ module ActiveRecord 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 - end - def test_invert_remove_index add = @recorder.inverse_of :remove_index, [:table, :one] assert_equal [:add_index, [:table, :one]], add diff --git a/activerecord/test/cases/migration/compatibility_test.rb b/activerecord/test/cases/migration/compatibility_test.rb index 0a4b604601..7a80bfb899 100644 --- a/activerecord/test/cases/migration/compatibility_test.rb +++ b/activerecord/test/cases/migration/compatibility_test.rb @@ -1,4 +1,5 @@ require "cases/helper" +require "support/schema_dumping_helper" module ActiveRecord class Migration @@ -55,7 +56,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 +74,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 +91,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,17 +103,119 @@ 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 - } + def test_legacy_migrations_raises_exception_when_inherited + e = assert_raises(StandardError) do + class_eval("class LegacyMigration < ActiveRecord::Migration; end") + end + assert_match(/LegacyMigration < ActiveRecord::Migration\[4\.2\]/, e.message) + end + end + end +end - assert_deprecated do - migration.migrate :up +class LegacyPrimaryKeyTest < ActiveRecord::TestCase + include SchemaDumpingHelper + + self.use_transactional_tests = false + + class LegacyPrimaryKey < ActiveRecord::Base + end + + def setup + @migration = nil + @verbose_was = ActiveRecord::Migration.verbose + ActiveRecord::Migration.verbose = false + end + + def teardown + @migration.migrate(:down) if @migration + ActiveRecord::Migration.verbose = @verbose_was + ActiveRecord::SchemaMigration.delete_all rescue nil + LegacyPrimaryKey.reset_column_information + end + + def test_legacy_primary_key_should_be_auto_incremented + @migration = Class.new(ActiveRecord::Migration[5.0]) { + def change + create_table :legacy_primary_keys do |t| + t.references :legacy_ref end end + }.new + + @migration.migrate(:up) + + legacy_pk = LegacyPrimaryKey.columns_hash["id"] + assert_not legacy_pk.bigint? + assert_not legacy_pk.null + + legacy_ref = LegacyPrimaryKey.columns_hash["legacy_ref_id"] + assert_not legacy_ref.bigint? + + record1 = LegacyPrimaryKey.create! + assert_not_nil record1.id + + record1.destroy + + record2 = LegacyPrimaryKey.create! + assert_not_nil record2.id + assert_operator record2.id, :>, record1.id + end + + def test_legacy_integer_primary_key_should_not_be_auto_incremented + skip if current_adapter?(:SQLite3Adapter) + + @migration = Class.new(ActiveRecord::Migration[5.0]) { + def change + create_table :legacy_primary_keys, id: :integer do |t| + end + end + }.new + + @migration.migrate(:up) + + assert_raises(ActiveRecord::NotNullViolation) do + LegacyPrimaryKey.create! + end + + schema = dump_table_schema "legacy_primary_keys" + assert_match %r{create_table "legacy_primary_keys", id: :integer, default: nil}, schema + end + + if current_adapter?(:Mysql2Adapter) + def test_legacy_bigint_primary_key_should_be_auto_incremented + @migration = Class.new(ActiveRecord::Migration[5.0]) { + def change + create_table :legacy_primary_keys, id: :bigint + end + }.new + + @migration.migrate(:up) + + legacy_pk = LegacyPrimaryKey.columns_hash["id"] + assert legacy_pk.bigint? + assert legacy_pk.auto_increment? + + schema = dump_table_schema "legacy_primary_keys" + assert_match %r{create_table "legacy_primary_keys", (?!id: :bigint, default: nil)}, schema + end + else + def test_legacy_bigint_primary_key_should_not_be_auto_incremented + @migration = Class.new(ActiveRecord::Migration[5.0]) { + def change + create_table :legacy_primary_keys, id: :bigint do |t| + end + end + }.new + + @migration.migrate(:up) + + assert_raises(ActiveRecord::NotNullViolation) do + LegacyPrimaryKey.create! + end + + schema = dump_table_schema "legacy_primary_keys" + assert_match %r{create_table "legacy_primary_keys", id: :bigint, default: nil}, schema 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 f14d68f12b..c4896f3d6e 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_exists: true end end @@ -80,55 +78,66 @@ module ActiveRecord assert_equal [%w(artist_id music_id)], connection.indexes(:artists_musics).map(&:columns) end + def test_create_join_table_respects_reference_key_type + connection.create_join_table :artists, :musics do |t| + t.references :video + end + + artist_id, music_id, video_id = connection.columns(:artists_musics).sort_by(&:name) + + assert_equal video_id.sql_type, artist_id.sql_type + assert_equal video_id.sql_type, music_id.sql_type + end + def test_drop_join_table 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 } - 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..7762d37915 100644 --- a/activerecord/test/cases/migration/foreign_key_test.rb +++ b/activerecord/test/cases/migration/foreign_key_test.rb @@ -1,12 +1,30 @@ require "cases/helper" -require "support/ddl_helper" require "support/schema_dumping_helper" +if ActiveRecord::Base.connection.supports_foreign_keys_in_create? + module ActiveRecord + class Migration + class ForeignKeyInCreateTest < ActiveRecord::TestCase + def test_foreign_keys + foreign_keys = ActiveRecord::Base.connection.foreign_keys("fk_test_has_fk") + assert_equal 1, foreign_keys.size + + fk = foreign_keys.first + assert_equal "fk_test_has_fk", fk.from_table + assert_equal "fk_test_has_pk", fk.to_table + assert_equal "fk_id", fk.column + assert_equal "pk_id", fk.primary_key + assert_equal "fk_name", fk.name unless current_adapter?(:SQLite3Adapter) + end + end + end + end +end + if ActiveRecord::Base.connection.supports_foreign_keys? module ActiveRecord class Migration class ForeignKeyTest < ActiveRecord::TestCase - include DdlHelper include SchemaDumpingHelper include ActiveSupport::Testing::Stream @@ -29,10 +47,8 @@ if ActiveRecord::Base.connection.supports_foreign_keys? end teardown do - if defined?(@connection) - @connection.drop_table "astronauts", if_exists: true - @connection.drop_table "rockets", if_exists: true - end + @connection.drop_table "astronauts", if_exists: true + @connection.drop_table "rockets", if_exists: true end def test_foreign_keys @@ -76,20 +92,23 @@ 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 - @connection.add_foreign_key(:astronauts, :space_shuttles, - column: "rocket_id", primary_key: "pk", name: "custom_pk") + @connection.create_table :space_shuttles, id: false, force: true do |t| + t.bigint :pk, primary_key: true + end - foreign_keys = @connection.foreign_keys("astronauts") - assert_equal 1, foreign_keys.size + @connection.add_foreign_key(:astronauts, :space_shuttles, + column: "rocket_id", primary_key: "pk", name: "custom_pk") - fk = foreign_keys.first - assert_equal "astronauts", fk.from_table - assert_equal "space_shuttles", fk.to_table - assert_equal "pk", fk.primary_key + foreign_keys = @connection.foreign_keys("astronauts") + assert_equal 1, foreign_keys.size - @connection.remove_foreign_key :astronauts, name: "custom_pk" - end + fk = foreign_keys.first + assert_equal "astronauts", fk.from_table + assert_equal "space_shuttles", fk.to_table + assert_equal "pk", fk.primary_key + ensure + @connection.remove_foreign_key :astronauts, name: "custom_pk" + @connection.drop_table :space_shuttles end def test_add_on_delete_restrict_foreign_key @@ -101,7 +120,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 +248,7 @@ if ActiveRecord::Base.connection.supports_foreign_keys? create_table("cities") { |t| } create_table("houses") do |t| - t.column :city_id, :integer + t.references :city end add_foreign_key :houses, :cities, column: "city_id" @@ -261,7 +280,7 @@ if ActiveRecord::Base.connection.supports_foreign_keys? create_table(:schools) create_table(:classes) do |t| - t.column :school_id, :integer + t.references :school end add_foreign_key :classes, :schools end @@ -305,9 +324,11 @@ else @connection.remove_foreign_key :clubs, :categories end - def test_foreign_keys_should_raise_not_implemented - assert_raises NotImplementedError do - @connection.foreign_keys("clubs") + unless current_adapter?(:SQLite3Adapter) + def test_foreign_keys_should_raise_not_implemented + assert_raises NotImplementedError do + @connection.foreign_keys("clubs") + end end end end diff --git a/activerecord/test/cases/migration/index_test.rb b/activerecord/test/cases/migration/index_test.rb index 0f975026b8..f10fcf1398 100644 --- a/activerecord/test/cases/migration/index_test.rb +++ b/activerecord/test/cases/migration/index_test.rb @@ -31,9 +31,10 @@ module ActiveRecord connection.add_index(table_name, [:foo], name: "old_idx") connection.rename_index(table_name, "old_idx", "new_idx") - # if the adapter doesn't support the indexes call, pick defaults that let the test pass - assert_not connection.index_name_exists?(table_name, "old_idx", false) - assert connection.index_name_exists?(table_name, "new_idx", true) + assert_deprecated do + assert_not connection.index_name_exists?(table_name, "old_idx", false) + assert connection.index_name_exists?(table_name, "new_idx", true) + end end def test_rename_index_too_long @@ -45,8 +46,7 @@ module ActiveRecord } assert_match(/too long; the limit is #{connection.allowed_index_name_length} characters/, e.message) - # if the adapter doesn't support the indexes call, pick defaults that let the test pass - assert connection.index_name_exists?(table_name, "old_idx", false) + assert connection.index_name_exists?(table_name, "old_idx") end def test_double_add_index @@ -63,7 +63,7 @@ module ActiveRecord def test_add_index_works_with_long_index_names connection.add_index(table_name, "foo", name: good_index_name) - assert connection.index_name_exists?(table_name, good_index_name, false) + assert connection.index_name_exists?(table_name, good_index_name) connection.remove_index(table_name, name: good_index_name) end @@ -75,7 +75,7 @@ module ActiveRecord } assert_match(/too long; the limit is #{connection.allowed_index_name_length} characters/, e.message) - assert_not connection.index_name_exists?(table_name, too_long_index_name, false) + assert_not connection.index_name_exists?(table_name, too_long_index_name) connection.add_index(table_name, "foo", name: good_index_name) end @@ -83,7 +83,7 @@ module ActiveRecord good_index_name = "x" * connection.index_name_length connection.add_index(table_name, "foo", name: good_index_name, internal: true) - assert connection.index_name_exists?(table_name, good_index_name, false) + assert connection.index_name_exists?(table_name, good_index_name) connection.remove_index(table_name, name: good_index_name) end diff --git a/activerecord/test/cases/migration/pending_migrations_test.rb b/activerecord/test/cases/migration/pending_migrations_test.rb index 61f5a061b0..6970fdcc87 100644 --- a/activerecord/test/cases/migration/pending_migrations_test.rb +++ b/activerecord/test/cases/migration/pending_migrations_test.rb @@ -21,8 +21,6 @@ module ActiveRecord end def test_errors_if_pending - @connection.expect :supports_migrations?, true - ActiveRecord::Migrator.stub :needs_migration?, true do assert_raise ActiveRecord::PendingMigrationError do @pending.call(nil) @@ -31,22 +29,12 @@ module ActiveRecord end def test_checks_if_supported - @connection.expect :supports_migrations?, true @app.expect :call, nil, [:foo] ActiveRecord::Migrator.stub :needs_migration?, false do @pending.call(:foo) end end - - def test_doesnt_check_if_unsupported - @connection.expect :supports_migrations?, false - @app.expect :call, nil, [:foo] - - ActiveRecord::Migrator.stub :needs_migration?, true do - @pending.call(:foo) - end - end end end end diff --git a/activerecord/test/cases/migration/references_foreign_key_test.rb b/activerecord/test/cases/migration/references_foreign_key_test.rb index 528811db49..f1ddac1ee2 100644 --- a/activerecord/test/cases/migration/references_foreign_key_test.rb +++ b/activerecord/test/cases/migration/references_foreign_key_test.rb @@ -1,9 +1,9 @@ require "cases/helper" -if ActiveRecord::Base.connection.supports_foreign_keys? +if ActiveRecord::Base.connection.supports_foreign_keys_in_create? module ActiveRecord class Migration - class ReferencesForeignKeyTest < ActiveRecord::TestCase + class ReferencesForeignKeyInCreateTest < ActiveRecord::TestCase setup do @connection = ActiveRecord::Base.connection @connection.create_table(:testing_parents, force: true) @@ -42,8 +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.index :other_id, unique: true + t.references :other, index: { unique: true } end @connection.create_table :testings do |t| t.references :testing_parent, foreign_key: { primary_key: :other_id } @@ -61,6 +60,24 @@ if ActiveRecord::Base.connection.supports_foreign_keys? assert_equal([["testings", "testing_parents", "parent_id"]], fks.map { |fk| [fk.from_table, fk.to_table, fk.column] }) end + end + end + end +end + +if ActiveRecord::Base.connection.supports_foreign_keys? + module ActiveRecord + class Migration + class ReferencesForeignKeyTest < ActiveRecord::TestCase + setup do + @connection = ActiveRecord::Base.connection + @connection.create_table(:testing_parents, force: true) + end + + teardown do + @connection.drop_table "testings", if_exists: true + @connection.drop_table "testing_parents", if_exists: true + end test "foreign keys cannot be added to polymorphic relations when creating the table" do @connection.create_table :testings do |t| @@ -92,8 +109,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.index :other_id, unique: true + t.references :other, index: { unique: true } end @connection.create_table :testings @connection.change_table :testings do |t| @@ -177,18 +193,31 @@ 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.references :parent1, foreign_key: { to_table: :testing_parents } + t.references :parent2, foreign_key: { to_table: :testing_parents } + end - t.foreign_key :testing_parents, column: :col_1 - t.foreign_key :testing_parents, column: :col_2 + fks = @connection.foreign_keys("testings").sort_by(&:column) + + fk_definitions = fks.map { |fk| [fk.from_table, fk.to_table, fk.column] } + assert_equal([["testings", "testing_parents", "parent1_id"], + ["testings", "testing_parents", "parent2_id"]], fk_definitions) + end + + test "multiple foreign keys can be removed to the selected one" do + @connection.create_table :testings do |t| + t.references :parent1, foreign_key: { to_table: :testing_parents } + t.references :parent2, foreign_key: { to_table: :testing_parents } end - fks = @connection.foreign_keys("testings") + assert_difference "@connection.foreign_keys('testings').size", -1 do + @connection.remove_reference :testings, :parent1, foreign_key: { to_table: :testing_parents } + end + + fks = @connection.foreign_keys("testings").sort_by(&: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) + assert_equal([["testings", "testing_parents", "parent2_id"]], fk_definitions) end end end diff --git a/activerecord/test/cases/migration/references_statements_test.rb b/activerecord/test/cases/migration/references_statements_test.rb index 8fbe60f24e..e9eb9968cb 100644 --- a/activerecord/test/cases/migration/references_statements_test.rb +++ b/activerecord/test/cases/migration/references_statements_test.rb @@ -50,6 +50,21 @@ module ActiveRecord assert column_exists?(table_name, :taggable_type, :string, default: "Photo") end + def test_creates_reference_type_column_with_not_null + connection.create_table table_name, force: true do |t| + t.references :taggable, null: false, polymorphic: true + end + assert column_exists?(table_name, :taggable_id, :integer, null: false) + assert column_exists?(table_name, :taggable_type, :string, null: false) + end + + def test_does_not_share_options_with_reference_type_column + add_reference table_name, :taggable, type: :integer, limit: 2, polymorphic: true + assert column_exists?(table_name, :taggable_id, :integer, limit: 2) + assert column_exists?(table_name, :taggable_type, :string) + assert_not column_exists?(table_name, :taggable_type, :string, limit: 2) + end + def test_creates_named_index add_reference table_name, :tag, index: { name: "index_taggings_on_tag_id" } assert index_exists?(table_name, :tag_id, name: "index_taggings_on_tag_id") @@ -57,7 +72,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 fc4f700916..5da3ad33a3 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 @@ -79,10 +79,33 @@ module ActiveRecord assert_equal ConnectionAdapters::PostgreSQL::Name.new("public", "octopi_#{pk}_seq"), seq end + def test_renaming_table_renames_primary_key + connection.create_table :cats, id: :uuid, default: "uuid_generate_v4()" + rename_table :cats, :felines + + assert connection.table_exists? :felines + refute connection.table_exists? :cats + + primary_key_name = connection.select_values(<<-SQL.strip_heredoc, "SCHEMA")[0] + SELECT c.relname + FROM pg_class c + JOIN pg_index i + ON c.oid = i.indexrelid + WHERE i.indisprimary + AND i.indrelid = 'felines'::regclass + SQL + + assert_equal "felines_pkey", primary_key_name + ensure + connection.drop_table :cats, if_exists: true + connection.drop_table :felines, if_exists: true + end + def test_renaming_table_doesnt_attempt_to_rename_non_existent_sequences - connection.create_table :cats, id: :uuid + connection.create_table :cats, id: :uuid, default: "uuid_generate_v4()" assert_nothing_raised { rename_table :cats, :felines } - ActiveSupport::Deprecation.silence { assert connection.table_exists? :felines } + assert connection.table_exists? :felines + refute connection.table_exists? :cats ensure connection.drop_table :cats, if_exists: true connection.drop_table :felines, if_exists: true diff --git a/activerecord/test/cases/migration_test.rb b/activerecord/test/cases/migration_test.rb index 151f3c8efd..57f94950f9 100644 --- a/activerecord/test/cases/migration_test.rb +++ b/activerecord/test/cases/migration_test.rb @@ -43,10 +43,10 @@ class MigrationTest < ActiveRecord::TestCase ActiveRecord::Base.table_name_prefix = "" ActiveRecord::Base.table_name_suffix = "" - ActiveRecord::Base.connection.initialize_schema_migrations_table - ActiveRecord::Base.connection.execute "DELETE FROM #{ActiveRecord::Migrator.schema_migrations_table_name}" + ActiveRecord::SchemaMigration.create_table + ActiveRecord::SchemaMigration.delete_all - %w(things awesome_things prefix_things_suffix p_awesome_things_s ).each do |table| + %w(things awesome_things prefix_things_suffix p_awesome_things_s).each do |table| Thing.connection.drop_table(table) rescue nil end Thing.reset_column_information @@ -337,20 +337,20 @@ class MigrationTest < ActiveRecord::TestCase end def test_schema_migrations_table_name - original_schema_migrations_table_name = ActiveRecord::Migrator.schema_migrations_table_name + original_schema_migrations_table_name = ActiveRecord::Base.schema_migrations_table_name - assert_equal "schema_migrations", ActiveRecord::Migrator.schema_migrations_table_name + assert_equal "schema_migrations", ActiveRecord::SchemaMigration.table_name ActiveRecord::Base.table_name_prefix = "prefix_" ActiveRecord::Base.table_name_suffix = "_suffix" Reminder.reset_table_name - assert_equal "prefix_schema_migrations_suffix", ActiveRecord::Migrator.schema_migrations_table_name + assert_equal "prefix_schema_migrations_suffix", ActiveRecord::SchemaMigration.table_name ActiveRecord::Base.schema_migrations_table_name = "changed" Reminder.reset_table_name - assert_equal "prefix_changed_suffix", ActiveRecord::Migrator.schema_migrations_table_name + assert_equal "prefix_changed_suffix", ActiveRecord::SchemaMigration.table_name ActiveRecord::Base.table_name_prefix = "" ActiveRecord::Base.table_name_suffix = "" Reminder.reset_table_name - assert_equal "changed", ActiveRecord::Migrator.schema_migrations_table_name + assert_equal "changed", ActiveRecord::SchemaMigration.table_name ensure ActiveRecord::Base.schema_migrations_table_name = original_schema_migrations_table_name Reminder.reset_table_name @@ -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,44 +399,19 @@ class MigrationTest < ActiveRecord::TestCase ActiveRecord::Migrator.migrations_paths = old_path ENV["RAILS_ENV"] = original_rails_env ENV["RACK_ENV"] = original_rack_env - end - - def test_migration_sets_internal_metadata_even_when_fully_migrated - current_env = ActiveRecord::ConnectionHandling::DEFAULT_ENV.call - migrations_path = MIGRATIONS_ROOT + "/valid" - old_path = ActiveRecord::Migrator.migrations_paths - ActiveRecord::Migrator.migrations_paths = migrations_path - ActiveRecord::Migrator.up(migrations_path) - assert_equal current_env, ActiveRecord::InternalMetadata[:environment] - - 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 - - refute_equal current_env, new_env - - sleep 1 # mysql by default does not store fractional seconds in the database - - ActiveRecord::Migrator.up(migrations_path) - assert_equal new_env, ActiveRecord::InternalMetadata[:environment] - ensure - ActiveRecord::Migrator.migrations_paths = old_path - ENV["RAILS_ENV"] = original_rails_env - ENV["RACK_ENV"] = original_rack_env 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] @@ -705,7 +680,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 @@ -887,7 +862,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 @@ -914,7 +889,6 @@ if ActiveRecord::Base.connection.supports_bulk_alter? @indexes ||= Person.connection.indexes("delete_me") end end # AlterTableMigrationsTest - end class CopyMigrationsTest < ActiveRecord::TestCase @@ -1132,4 +1106,21 @@ class CopyMigrationsTest < ActiveRecord::TestCase def test_unknown_migration_version_should_raise_an_argument_error assert_raise(ArgumentError) { ActiveRecord::Migration[1.0] } end + + def test_deprecate_initialize_internal_tables + assert_deprecated { ActiveRecord::Base.connection.initialize_schema_migrations_table } + assert_deprecated { ActiveRecord::Base.connection.initialize_internal_metadata_table } + end + + def test_deprecate_migration_keys + assert_deprecated { ActiveRecord::Base.connection.migration_keys } + end + + def test_deprecate_supports_migrations + assert_deprecated { ActiveRecord::Base.connection.supports_migrations? } + end + + def test_deprecate_schema_migrations_table_name + assert_deprecated { ActiveRecord::Migrator.schema_migrations_table_name } + end end diff --git a/activerecord/test/cases/migrator_test.rb b/activerecord/test/cases/migrator_test.rb index 1ba18bc9c2..2e4b454a86 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 @@ -45,10 +45,11 @@ class MigratorTest < ActiveRecord::TestCase end def test_migrator_with_duplicate_names - assert_raises(ActiveRecord::DuplicateMigrationNameError, "Multiple migrations have the name Chunky") do + e = assert_raises(ActiveRecord::DuplicateMigrationNameError) do list = [ActiveRecord::Migration.new("Chunky"), ActiveRecord::Migration.new("Chunky")] ActiveRecord::Migrator.new(:up, list) end + assert_match(/Multiple migrations have the name Chunky/, e.message) end def test_migrator_with_duplicate_versions @@ -123,6 +124,67 @@ class MigratorTest < ActiveRecord::TestCase assert_equal migration_list.last, migrations.first end + def test_migrations_status + path = MIGRATIONS_ROOT + "/valid" + + ActiveRecord::SchemaMigration.create(version: 2) + ActiveRecord::SchemaMigration.create(version: 10) + + assert_equal [ + ["down", "001", "Valid people have last names"], + ["up", "002", "We need reminders"], + ["down", "003", "Innocent jointable"], + ["up", "010", "********** NO FILE **********"], + ], ActiveRecord::Migrator.migrations_status(path) + end + + def test_migrations_status_in_subdirectories + path = MIGRATIONS_ROOT + "/valid_with_subdirectories" + + ActiveRecord::SchemaMigration.create(version: 2) + ActiveRecord::SchemaMigration.create(version: 10) + + assert_equal [ + ["down", "001", "Valid people have last names"], + ["up", "002", "We need reminders"], + ["down", "003", "Innocent jointable"], + ["up", "010", "********** NO FILE **********"], + ], ActiveRecord::Migrator.migrations_status(path) + end + + def test_migrations_status_with_schema_define_in_subdirectories + path = MIGRATIONS_ROOT + "/valid_with_subdirectories" + prev_paths = ActiveRecord::Migrator.migrations_paths + ActiveRecord::Migrator.migrations_paths = path + + ActiveRecord::Schema.define(version: 3) do + end + + assert_equal [ + ["up", "001", "Valid people have last names"], + ["up", "002", "We need reminders"], + ["up", "003", "Innocent jointable"], + ], ActiveRecord::Migrator.migrations_status(path) + ensure + ActiveRecord::Migrator.migrations_paths = prev_paths + end + + def test_migrations_status_from_two_directories + paths = [MIGRATIONS_ROOT + "/valid_with_timestamps", MIGRATIONS_ROOT + "/to_copy_with_timestamps"] + + ActiveRecord::SchemaMigration.create(version: "20100101010101") + ActiveRecord::SchemaMigration.create(version: "20160528010101") + + assert_equal [ + ["down", "20090101010101", "People have hobbies"], + ["down", "20090101010202", "People have descriptions"], + ["up", "20100101010101", "Valid with timestamps people have last names"], + ["down", "20100201010101", "Valid with timestamps we need reminders"], + ["down", "20100301010101", "Valid with timestamps innocent jointable"], + ["up", "20160528010101", "********** NO FILE **********"], + ], ActiveRecord::Migrator.migrations_status(paths) + end + def test_migrator_interleaved_migrations pass_one = [Sensor.new("One", 1)] @@ -237,6 +299,7 @@ class MigratorTest < ActiveRecord::TestCase def test_migrator_verbosity _, migrations = sensors(3) + ActiveRecord::Migration.verbose = true ActiveRecord::Migrator.new(:up, migrations, 1).migrate assert_not_equal 0, ActiveRecord::Migration.message_count @@ -249,7 +312,6 @@ class MigratorTest < ActiveRecord::TestCase def test_migrator_verbosity_off _, migrations = sensors(3) - ActiveRecord::Migration.message_count = 0 ActiveRecord::Migration.verbose = false ActiveRecord::Migrator.new(:up, migrations, 1).migrate assert_equal 0, ActiveRecord::Migration.message_count @@ -290,6 +352,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 +396,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 +427,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 +451,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/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 a9c3733c20..5a62cbd3a6 100644 --- a/activerecord/test/cases/nested_attributes_test.rb +++ b/activerecord/test/cases/nested_attributes_test.rb @@ -752,7 +752,7 @@ module NestedAttributesOnACollectionAssociationTests exception = assert_raise ArgumentError do @pirate.send(association_setter, "foo") end - assert_equal 'Hash or Array expected, got String ("foo")', exception.message + assert_equal %{Hash or Array expected for attribute `#{@association_name}`, got String ("foo")}, exception.message end def test_should_work_with_update_as_well @@ -971,7 +971,7 @@ 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 } } } @@ -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 diff --git a/activerecord/test/cases/nested_attributes_with_callbacks_test.rb b/activerecord/test/cases/nested_attributes_with_callbacks_test.rb index 8954e8c7e3..b9d2acbed2 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 @@ -120,14 +120,14 @@ class NestedAttributesWithCallbacksTest < ActiveRecord::TestCase assert_assignment_affects_records_in_target(:birds_with_add) end - test("Assignment updates records in target when not loaded" + + test("Assignment updates records in target when not loaded" \ " and callback loads target") do assert_not @pirate.birds_with_add_load.loaded? @pirate.birds_with_add_load_attributes = update_new_and_destroy_bird_attributes assert_assignment_affects_records_in_target(:birds_with_add_load) end - test("Assignment updates records in target when loaded" + + test("Assignment updates records in target when loaded" \ " and callback loads target") do @pirate.birds_with_add_load.load_target @pirate.birds_with_add_load_attributes = update_new_and_destroy_bird_attributes diff --git a/activerecord/test/cases/persistence_test.rb b/activerecord/test/cases/persistence_test.rb index 688c3ed2b1..5895c51714 100644 --- a/activerecord/test/cases/persistence_test.rb +++ b/activerecord/test/cases/persistence_test.rb @@ -49,7 +49,7 @@ class PersistenceTest < ActiveRecord::TestCase assert !test_update_with_order_succeeds.call("id ASC") # test that this wasn't a fluke and using an incorrect order results in an exception else # test that we're failing because the current Arel's engine doesn't support UPDATE ORDER BY queries is using subselects instead - assert_sql(/\AUPDATE .+ \(SELECT .* ORDER BY id DESC\)\Z/i) do + assert_sql(/\AUPDATE .+ \(SELECT .* ORDER BY id DESC\)\z/i) do test_update_with_order_succeeds.call("id DESC") end end @@ -90,6 +90,14 @@ class PersistenceTest < ActiveRecord::TestCase 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 @@ -131,6 +139,14 @@ class PersistenceTest < ActiveRecord::TestCase assert_equal initial_credit + 2, a1.reload.credit_limit end + def test_increment_updates_timestamps + topic = topics(:first) + topic.update_columns(updated_at: 5.minutes.ago) + previous_updated_at = topic.updated_at + topic.increment!(:replies_count, touch: true) + assert_operator previous_updated_at, :<, topic.reload.updated_at + end + def test_destroy_all conditions = "author_name = 'Mary'" topics_by_mary = Topic.all.merge!(where: conditions, order: "id").to_a @@ -222,6 +238,14 @@ class PersistenceTest < ActiveRecord::TestCase assert_equal 41, accounts(:signals37, :reload).credit_limit end + def test_decrement_updates_timestamps + topic = topics(:first) + topic.update_columns(updated_at: 5.minutes.ago) + previous_updated_at = topic.updated_at + topic.decrement!(:replies_count, touch: true) + assert_operator previous_updated_at, :<, topic.reload.updated_at + end + def test_create topic = Topic.new topic.title = "New Topic" @@ -453,6 +477,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 +1005,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..5ded619716 100644 --- a/activerecord/test/cases/primary_keys_test.rb +++ b/activerecord/test/cases/primary_keys_test.rb @@ -7,6 +7,7 @@ require "models/movie" require "models/keyboard" require "models/mixed_case_monkey" require "models/dashboard" +require "models/non_primary_key" class PrimaryKeysTest < ActiveRecord::TestCase fixtures :topics, :subscribers, :movies, :mixed_case_monkeys @@ -89,6 +90,12 @@ class PrimaryKeysTest < ActiveRecord::TestCase assert_equal("John Doe", subscriberReloaded.name) end + def test_id_column_that_is_not_primary_key + NonPrimaryKey.create!(id: 100) + actual = NonPrimaryKey.find_by(id: 100) + assert_match %r{<NonPrimaryKey id: 100}, actual.inspect + end + def test_find_with_more_than_one_string_key assert_equal 2, Subscriber.find(subscribers(:first).nick, subscribers(:second).nick).length end @@ -113,48 +120,49 @@ class PrimaryKeysTest < ActiveRecord::TestCase def test_delete_should_quote_pkey assert_nothing_raised { MixedCaseMonkey.delete(1) } end + def test_update_counters_should_quote_pkey_and_quote_counter_columns assert_nothing_raised { MixedCaseMonkey.update_counters(1, fleaCount: 99) } end + def test_find_with_one_id_should_quote_pkey 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 } end + def test_instance_destroy_should_quote_pkey assert_nothing_raised { MixedCaseMonkey.find(1).destroy } end - def test_supports_primary_key - assert_nothing_raised do - ActiveRecord::Base.connection.supports_primary_key? - end + def test_deprecate_supports_primary_key + assert_deprecated { ActiveRecord::Base.connection.supports_primary_key? } end - if ActiveRecord::Base.connection.supports_primary_key? - def test_primary_key_returns_value_if_it_exists - klass = Class.new(ActiveRecord::Base) do - self.table_name = "developers" - end - - assert_equal "id", klass.primary_key + def test_primary_key_returns_value_if_it_exists + klass = Class.new(ActiveRecord::Base) do + self.table_name = "developers" end - def test_primary_key_returns_nil_if_it_does_not_exist - klass = Class.new(ActiveRecord::Base) do - self.table_name = "developers_projects" - end + assert_equal "id", klass.primary_key + end - assert_nil klass.primary_key + def test_primary_key_returns_nil_if_it_does_not_exist + klass = Class.new(ActiveRecord::Base) do + self.table_name = "developers_projects" end + + assert_nil klass.primary_key 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 @@ -175,6 +183,8 @@ class PrimaryKeysTest < ActiveRecord::TestCase end def test_create_without_primary_key_no_extra_query + skip if current_adapter?(:OracleAdapter) + klass = Class.new(ActiveRecord::Base) do self.table_name = "dashboards" end @@ -216,6 +226,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_integer + @connection.create_table(:auto_increments, id: :integer, force: true) + assert_auto_incremented + end + + def test_primary_key_with_bigint + @connection.create_table(:auto_increments, id: :bigint, 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 @@ -246,6 +293,14 @@ class PrimaryKeyAnyTypeTest < ActiveRecord::TestCase schema = dump_table_schema "barcodes" assert_match %r{create_table "barcodes", primary_key: "code", id: :string, limit: 42}, schema end + + if current_adapter?(:Mysql2Adapter) && subsecond_precision_supported? + test "schema typed primary key column" do + @connection.create_table(:scheduled_logs, id: :timestamp, precision: 6, force: true) + schema = dump_table_schema("scheduled_logs") + assert_match %r/create_table "scheduled_logs", id: :timestamp, precision: 6/, schema + end + end end class CompositePrimaryKeyTest < ActiveRecord::TestCase @@ -260,6 +315,10 @@ class CompositePrimaryKeyTest < ActiveRecord::TestCase t.string :region t.integer :code end + @connection.create_table(:barcodes_reverse, primary_key: ["code", "region"], force: true) do |t| + t.string :region + t.integer :code + end end def teardown @@ -270,6 +329,11 @@ class CompositePrimaryKeyTest < ActiveRecord::TestCase assert_equal ["region", "code"], @connection.primary_keys("barcodes") end + def test_composite_primary_key_out_of_order + skip if current_adapter?(:SQLite3Adapter) + assert_equal ["code", "region"], @connection.primary_keys("barcodes_reverse") + end + def test_primary_key_issues_warning model = Class.new(ActiveRecord::Base) do def self.table_name @@ -282,42 +346,47 @@ class CompositePrimaryKeyTest < ActiveRecord::TestCase assert_match(/WARNING: Active Record does not support composite primary key\./, warning) end - def test_collectly_dump_composite_primary_key + def test_dumping_composite_primary_key schema = dump_table_schema "barcodes" assert_match %r{create_table "barcodes", primary_key: \["region", "code"\]}, schema end + + def test_dumping_composite_primary_key_out_of_order + skip if current_adapter?(:SQLite3Adapter) + schema = dump_table_schema "barcodes_reverse" + assert_match %r{create_table "barcodes_reverse", primary_key: \["code", "region"\]}, schema + end end -if current_adapter?(:Mysql2Adapter) - class PrimaryKeyBigintNilDefaultTest < ActiveRecord::TestCase - include SchemaDumpingHelper +class PrimaryKeyIntegerNilDefaultTest < ActiveRecord::TestCase + include SchemaDumpingHelper - self.use_transactional_tests = false + self.use_transactional_tests = false - def setup - @connection = ActiveRecord::Base.connection - @connection.create_table(:bigint_defaults, id: :bigint, default: nil, force: true) - end + def setup + @connection = ActiveRecord::Base.connection + end - def teardown - @connection.drop_table :bigint_defaults, if_exists: true - end + def teardown + @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? - assert_not column.auto_increment? - end + def test_schema_dump_primary_key_integer_with_default_nil + skip if current_adapter?(:SQLite3Adapter) + @connection.create_table(:int_defaults, id: :integer, default: nil, force: true) + schema = dump_table_schema "int_defaults" + assert_match %r{create_table "int_defaults", id: :integer, default: nil}, schema + 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 - end + def test_schema_dump_primary_key_bigint_with_default_nil + @connection.create_table(:int_defaults, id: :bigint, default: nil, force: true) + schema = dump_table_schema "int_defaults" + assert_match %r{create_table "int_defaults", id: :bigint, default: nil}, schema end end if current_adapter?(:PostgreSQLAdapter, :Mysql2Adapter) - class PrimaryKeyBigSerialTest < ActiveRecord::TestCase + class PrimaryKeyIntegerTest < ActiveRecord::TestCase include SchemaDumpingHelper self.use_transactional_tests = false @@ -327,46 +396,55 @@ if current_adapter?(:PostgreSQLAdapter, :Mysql2Adapter) 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 + @pk_type = current_adapter?(:PostgreSQLAdapter) ? :serial : :integer 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 + test "primary key column type with serial/integer" do + @connection.create_table(:widgets, id: @pk_type, force: true) + column = @connection.columns(:widgets).find { |c| c.name == "id" } + assert_equal :integer, column.type + assert_not column.bigint? end - test "primary key with bigserial are automatically numbered" do + test "primary key with serial/integer are automatically numbered" do + @connection.create_table(:widgets, id: @pk_type, force: true) widget = Widget.create! assert_not_nil widget.id end - test "schema dump primary key with bigserial" do + test "schema dump primary key with serial/integer" do + @connection.create_table(:widgets, id: @pk_type, force: true) 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 + assert_match %r{create_table "widgets", id: :#{@pk_type}, force: :cascade}, schema 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) + @connection.create_table(:widgets, id: :primary_key, limit: 4, unsigned: true, force: true) column = @connection.columns(:widgets).find { |c| c.name == "id" } assert column.auto_increment? assert_equal :integer, column.type - assert_equal 8, column.limit + assert_not column.bigint? assert column.unsigned? + + schema = dump_table_schema "widgets" + assert_match %r{create_table "widgets", id: :integer, unsigned: true, force: :cascade}, schema + end + + test "bigint primary key with unsigned" do + @connection.create_table(:widgets, id: :bigint, unsigned: true, force: true) + column = @connection.columns(:widgets).find { |c| c.name == "id" } + assert column.auto_increment? + assert_equal :integer, column.type + assert column.bigint? + assert column.unsigned? + + schema = dump_table_schema "widgets" + assert_match %r{create_table "widgets", id: :bigint, unsigned: true, force: :cascade}, schema end end end diff --git a/activerecord/test/cases/query_cache_test.rb b/activerecord/test/cases/query_cache_test.rb index 7f7faca70d..494663eb04 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,92 @@ 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! + private def with_temporary_connection_pool + old_pool = ActiveRecord::Base.connection_handler.retrieve_connection_pool(ActiveRecord::Base.connection_specification_name) + new_pool = ActiveRecord::ConnectionAdapters::ConnectionPool.new ActiveRecord::Base.connection_pool.spec + ActiveRecord::Base.connection_handler.send(:owner_to_pool)["primary"] = new_pool - mw = middleware { |env| - raise "lol borked" - } - assert_raises(RuntimeError) { mw.call({}) } + yield + ensure + ActiveRecord::Base.connection_handler.send(:owner_to_pool)["primary"] = old_pool + end + + def test_query_cache_across_threads + with_temporary_connection_pool do + begin + if in_memory_db? + # Separate connections to an in-memory database create an entirely new database, + # with an empty schema etc, so we just stub out this schema on the fly. + ActiveRecord::Base.connection_pool.with_connection do |connection| + connection.create_table :tasks do |t| + t.datetime :starting + t.datetime :ending + end + end + ActiveRecord::FixtureSet.create_fixtures(self.class.fixture_path, ["tasks"], {}, ActiveRecord::Base) + end + + ActiveRecord::Base.connection_pool.connections.each do |conn| + assert_cache :off, conn + end + + 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 - assert ActiveRecord::Base.connection.query_cache_enabled, "cache on" + thread_2_connection = nil + thread = Thread.new { + thread_2_connection = ActiveRecord::Base.connection + + assert_equal thread_2_connection, thread_1_connection + assert_cache :off + + 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 + end end def test_middleware_delegates @@ -62,10 +156,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 +215,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 +259,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 @@ -165,19 +286,51 @@ class QueryCacheTest < ActiveRecord::TestCase end def test_cache_is_not_available_when_using_a_not_connected_connection - spec_name = Task.connection_specification_name - conf = ActiveRecord::Base.configurations["arunit"].merge("name" => "test2") - ActiveRecord::Base.connection_handler.establish_connection(conf) - Task.connection_specification_name = "test2" - refute Task.connected? + with_temporary_connection_pool do + spec_name = Task.connection_specification_name + conf = ActiveRecord::Base.configurations["arunit"].merge("name" => "test2") + ActiveRecord::Base.connection_handler.establish_connection(conf) + Task.connection_specification_name = "test2" + refute Task.connected? - Task.cache do - Task.connection # warmup postgresql connection setup queries - assert_queries(2) { Task.find(1); Task.find(1) } + Task.cache do + begin + if in_memory_db? + Task.connection.create_table :tasks do |t| + t.datetime :starting + t.datetime :ending + end + ActiveRecord::FixtureSet.create_fixtures(self.class.fixture_path, ["tasks"], {}, ActiveRecord::Base) + end + Task.connection # warmup postgresql connection setup queries + assert_queries(2) { Task.find(1); Task.find(1) } + ensure + ActiveRecord::Base.connection_handler.remove_connection(Task.connection_specification_name) + Task.connection_specification_name = spec_name + end + end + end + 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 - ensure - ActiveRecord::Base.connection_handler.remove_connection(Task.connection_specification_name) - Task.connection_specification_name = spec_name end def test_query_cache_doesnt_leak_cached_results_of_rolled_back_queries @@ -232,17 +385,78 @@ class QueryCacheTest < ActiveRecord::TestCase end end + def test_query_cache_does_not_establish_connection_if_unconnected + with_temporary_connection_pool do + ActiveRecord::Base.clear_active_connections! + refute ActiveRecord::Base.connection_handler.active_connections? # sanity check + + 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 + end + + def test_query_cache_is_enabled_on_connections_established_after_middleware_runs + with_temporary_connection_pool do + ActiveRecord::Base.clear_active_connections! + refute ActiveRecord::Base.connection_handler.active_connections? # sanity check + + middleware { + assert ActiveRecord::Base.connection.query_cache_enabled, "QueryCache did not get lazily enabled" + }.call({}) + end + end + + def test_query_caching_is_local_to_the_current_thread + with_temporary_connection_pool do + 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 + 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 fixtures :tasks, :posts, :categories, :categories_posts + def teardown + Task.connection.clear_query_cache + end + def test_cache_gets_cleared_after_migration # warm the cache Post.find(1) @@ -318,4 +532,16 @@ class QueryCacheExpiryTest < ActiveRecord::TestCase end end end + + test "threads use the same connection" do + @connection_1 = ActiveRecord::Base.connection.object_id + + thread_a = Thread.new do + @connection_2 = ActiveRecord::Base.connection.object_id + end + + thread_a.join + + assert_equal @connection_1, @connection_2 + end end diff --git a/activerecord/test/cases/quoting_test.rb b/activerecord/test/cases/quoting_test.rb index 296dafacc2..0819776fbf 100644 --- a/activerecord/test/cases/quoting_test.rb +++ b/activerecord/test/cases/quoting_test.rb @@ -81,53 +81,70 @@ module ActiveRecord end end + class QuotedOne + def quoted_id + 1 + end + end + class SubQuotedOne < QuotedOne + end def test_quote_with_quoted_id - assert_equal 1, @quoter.quote(Struct.new(:quoted_id).new(1), nil) + assert_deprecated(/defined on \S+::QuotedOne at .*quoting_test\.rb:[0-9]/) do + assert_equal 1, @quoter.quote(QuotedOne.new) + end + + assert_deprecated(/defined on \S+::SubQuotedOne\(\S+::QuotedOne\) at .*quoting_test\.rb:[0-9]/) do + assert_equal 1, @quoter.quote(SubQuotedOne.new) + end end def test_quote_nil - assert_equal "NULL", @quoter.quote(nil, 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 @@ -146,6 +163,62 @@ module ActiveRecord end end + class TypeCastingTest < ActiveRecord::TestCase + def setup + @conn = ActiveRecord::Base.connection + end + + def test_type_cast_symbol + assert_equal "foo", @conn.type_cast(:foo) + end + + def test_type_cast_date + date = Date.today + expected = @conn.quoted_date(date) + assert_equal expected, @conn.type_cast(date) + end + + def test_type_cast_time + time = Time.now + expected = @conn.quoted_date(time) + assert_equal expected, @conn.type_cast(time) + end + + def test_type_cast_numeric + assert_equal 10, @conn.type_cast(10) + assert_equal 2.2, @conn.type_cast(2.2) + end + + def test_type_cast_nil + assert_nil @conn.type_cast(nil) + end + + def test_type_cast_unknown_should_raise_error + obj = Class.new.new + assert_raise(TypeError) { @conn.type_cast(obj) } + end + + def test_type_cast_object_which_responds_to_quoted_id + quoted_id_obj = Class.new { + def quoted_id + "'zomg'" + end + + def id + 10 + end + }.new + assert_equal 10, @conn.type_cast(quoted_id_obj) + + quoted_id_obj = Class.new { + def quoted_id + "'zomg'" + end + }.new + assert_raise(TypeError) { @conn.type_cast(quoted_id_obj) } + end + end + class QuoteBooleanTest < ActiveRecord::TestCase def setup @connection = ActiveRecord::Base.connection @@ -161,5 +234,32 @@ module ActiveRecord assert_predicate @connection.type_cast(false), :frozen? end end + + if subsecond_precision_supported? + class QuoteARBaseTest < ActiveRecord::TestCase + class DatetimePrimaryKey < ActiveRecord::Base + end + + def setup + @time = ::Time.utc(2017, 2, 14, 12, 34, 56, 789999) + @connection = ActiveRecord::Base.connection + @connection.create_table :datetime_primary_keys, id: :datetime, precision: 3, force: true + end + + def teardown + @connection.drop_table :datetime_primary_keys, if_exists: true + end + + def test_quote_ar_object + value = DatetimePrimaryKey.new(id: @time) + assert_equal "'2017-02-14 12:34:56.789000'", @connection.quote(value) + end + + def test_type_cast_ar_object + value = DatetimePrimaryKey.new(id: @time) + assert_equal "2017-02-14 12:34:56.789000", @connection.type_cast(value) + end + end + end end end diff --git a/activerecord/test/cases/readonly_test.rb b/activerecord/test/cases/readonly_test.rb index a93061b516..24b678310d 100644 --- a/activerecord/test/cases/readonly_test.rb +++ b/activerecord/test/cases/readonly_test.rb @@ -10,7 +10,7 @@ require "models/person" require "models/ship" class ReadOnlyTest < ActiveRecord::TestCase - fixtures :authors, :posts, :comments, :developers, :projects, :developers_projects, :people, :readers + fixtures :authors, :author_addresses, :posts, :comments, :developers, :projects, :developers_projects, :people, :readers def test_cant_save_readonly_record dev = Developer.find(1) diff --git a/activerecord/test/cases/reflection_test.rb b/activerecord/test/cases/reflection_test.rb index 5dac3d064b..c1c2efb9c8 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 @@ -252,7 +258,9 @@ class ReflectionTest < ActiveRecord::TestCase [Post.reflect_on_association(:first_taggings).scope], [Author.reflect_on_association(:misc_posts).scope] ] - actual = Author.reflect_on_association(:misc_post_first_blue_tags).scope_chain + actual = assert_deprecated do + Author.reflect_on_association(:misc_post_first_blue_tags).scope_chain + end assert_equal expected, actual expected = [ @@ -264,7 +272,9 @@ class ReflectionTest < ActiveRecord::TestCase [], [] ] - actual = Author.reflect_on_association(:misc_post_first_blue_tags_2).scope_chain + actual = assert_deprecated do + Author.reflect_on_association(:misc_post_first_blue_tags_2).scope_chain + end assert_equal expected, actual end @@ -325,6 +335,15 @@ class ReflectionTest < ActiveRecord::TestCase assert_equal "custom_primary_key", Author.reflect_on_association(:tags_with_primary_key).association_primary_key.to_s # nested end + def test_association_primary_key_type + # Normal Association + assert_equal :integer, Author.reflect_on_association(:posts).association_primary_key_type.type + assert_equal :string, Author.reflect_on_association(:essay).association_primary_key_type.type + + # Through Association + assert_equal :string, Author.reflect_on_association(:essay_category).association_primary_key_type.type + end + def test_association_primary_key_raises_when_missing_primary_key reflection = ActiveRecord::Reflection.create(:has_many, :edge, nil, {}, Author) assert_raises(ActiveRecord::UnknownPrimaryKey) { reflection.association_primary_key } @@ -389,15 +408,27 @@ class ReflectionTest < ActiveRecord::TestCase end def test_through_reflection_scope_chain_does_not_modify_other_reflections - orig_conds = Post.reflect_on_association(:first_blue_tags_2).scope_chain.inspect - Author.reflect_on_association(:misc_post_first_blue_tags_2).scope_chain - assert_equal orig_conds, Post.reflect_on_association(:first_blue_tags_2).scope_chain.inspect + orig_conds = assert_deprecated do + Post.reflect_on_association(:first_blue_tags_2).scope_chain + end.inspect + assert_deprecated do + Author.reflect_on_association(:misc_post_first_blue_tags_2).scope_chain + end + assert_equal orig_conds, assert_deprecated { + Post.reflect_on_association(:first_blue_tags_2).scope_chain + }.inspect end def test_symbol_for_class_name assert_equal Client, Firm.reflect_on_association(:unsorted_clients_with_symbol).klass end + def test_class_for_class_name + assert_deprecated do + assert_predicate ActiveRecord::Reflection.create(:has_many, :clients, nil, { class_name: Client }, Firm), :validate? + end + end + def test_join_table category = Struct.new(:table_name, :pluralize_table_names).new("categories", true) product = Struct.new(:table_name, :pluralize_table_names).new("products", true) diff --git a/activerecord/test/cases/relation/delegation_test.rb b/activerecord/test/cases/relation/delegation_test.rb index d2382b9bb2..cb6e4d76d3 100644 --- a/activerecord/test/cases/relation/delegation_test.rb +++ b/activerecord/test/cases/relation/delegation_test.rb @@ -3,36 +3,15 @@ require "models/post" require "models/comment" module ActiveRecord - class DelegationTest < ActiveRecord::TestCase - fixtures :posts - - def call_method(target, method) - method_arity = target.to_a.method(method).arity - - if method_arity.zero? - target.public_send(method) - elsif method_arity < 0 - if method == :shuffle! - target.public_send(method) - else - target.public_send(method, 1) - end - elsif method_arity == 1 - target.public_send(method, 1) - else - raise NotImplementedError - end - end - end - - module DelegationWhitelistBlacklistTests + module DelegationWhitelistTests ARRAY_DELEGATES = [ :+, :-, :|, :&, :[], :shuffle, :all?, :collect, :compact, :detect, :each, :each_cons, :each_with_index, :exclude?, :find_all, :flat_map, :group_by, :include?, :length, :map, :none?, :one?, :partition, :reject, :reverse, :sample, :second, :sort, :sort_by, :third, - :to_ary, :to_set, :to_xml, :to_yaml, :join + :to_ary, :to_set, :to_xml, :to_yaml, :join, + :in_groups, :in_groups_of, :to_sentence, :to_formatted_s, :as_json ] ARRAY_DELEGATES.each do |method| @@ -42,16 +21,18 @@ module ActiveRecord end end - class DelegationAssociationTest < DelegationTest - include DelegationWhitelistBlacklistTests + class DelegationAssociationTest < ActiveRecord::TestCase + include DelegationWhitelistTests + + fixtures :posts def target Post.first.comments end end - class DelegationRelationTest < DelegationTest - include DelegationWhitelistBlacklistTests + class DelegationRelationTest < ActiveRecord::TestCase + include DelegationWhitelistTests fixtures :comments diff --git a/activerecord/test/cases/relation/merging_test.rb b/activerecord/test/cases/relation/merging_test.rb index 278dac8171..c3b39a9295 100644 --- a/activerecord/test/cases/relation/merging_test.rb +++ b/activerecord/test/cases/relation/merging_test.rb @@ -8,7 +8,7 @@ require "models/project" require "models/rating" class RelationMergingTest < ActiveRecord::TestCase - fixtures :developers, :comments, :authors, :posts + fixtures :developers, :comments, :authors, :author_addresses, :posts def test_relation_merging devs = Developer.where("salary >= 80000").merge(Developer.limit(2)).merge(Developer.order("id ASC").where("id < 3")) @@ -21,7 +21,7 @@ class RelationMergingTest < ActiveRecord::TestCase def test_relation_to_sql post = Post.first sql = post.comments.to_sql - assert_match(/.?post_id.? = #{post.id}\Z/i, sql) + assert_match(/.?post_id.? = #{post.id}\z/i, sql) end def test_relation_merging_with_arel_equalities_keeps_last_equality @@ -56,7 +56,7 @@ class RelationMergingTest < ActiveRecord::TestCase def test_relation_merging_with_locks devs = Developer.lock.where("salary >= 80000").order("id DESC").merge(Developer.limit(2)) - assert devs.locked.present? + assert devs.locked? end def test_relation_merging_with_preload @@ -114,7 +114,7 @@ class RelationMergingTest < ActiveRecord::TestCase end class MergingDifferentRelationsTest < ActiveRecord::TestCase - fixtures :posts, :authors, :developers + fixtures :posts, :authors, :author_addresses, :developers test "merging where relations" do hello_by_bob = Post.where(body: "hello").joins(:author). diff --git a/activerecord/test/cases/relation/mutation_test.rb b/activerecord/test/cases/relation/mutation_test.rb index 966ae83a3f..dea787c07f 100644 --- a/activerecord/test/cases/relation/mutation_test.rb +++ b/activerecord/test/cases/relation/mutation_test.rb @@ -3,7 +3,7 @@ require "models/post" module ActiveRecord class RelationMutationTest < ActiveRecord::TestCase - class FakeKlass < Struct.new(:table_name, :name) + FakeKlass = Struct.new(:table_name, :name) do extend ActiveRecord::Delegation::DelegateCache inherited self @@ -36,7 +36,7 @@ module ActiveRecord @relation ||= Relation.new FakeKlass.new("posts"), Post.arel_table, Post.predicate_builder end - (Relation::MULTI_VALUE_METHODS - [:references, :extending, :order, :unscope, :select, :left_joins]).each do |method| + (Relation::MULTI_VALUE_METHODS - [:references, :extending, :order, :unscope, :select]).each do |method| test "##{method}!" do assert relation.public_send("#{method}!", :foo).equal?(relation) assert_equal [:foo], relation.public_send("#{method}_values") @@ -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 @@ -143,7 +143,7 @@ module ActiveRecord assert_equal({ foo: "bar" }, relation.create_with_value) end - test "test_merge!" do + test "merge!" do assert relation.merge!(select: :foo).equal?(relation) assert_equal [:foo], relation.select_values end @@ -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 2796595523..abb7ca72dd 100644 --- a/activerecord/test/cases/relation/or_test.rb +++ b/activerecord/test/cases/relation/or_test.rb @@ -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 a96d1ae5b5..86e150ed79 100644 --- a/activerecord/test/cases/relation/where_chain_test.rb +++ b/activerecord/test/cases/relation/where_chain_test.rb @@ -25,7 +25,7 @@ module ActiveRecord end def test_association_not_eq - expected = Arel::Nodes::Grouping.new(Comment.arel_table[@name].not_eq(Arel::Nodes::BindParam.new)) + expected = Arel::Nodes::Grouping.new(Comment.arel_table[@name].not_eq(bind_param)) relation = Post.joins(:comments).where.not(comments: { title: "hello" }) assert_equal(expected.to_sql, relation.where_clause.ast.to_sql) end diff --git a/activerecord/test/cases/relation/where_clause_test.rb b/activerecord/test/cases/relation/where_clause_test.rb index d8e4c304f0..f8eb0dee91 100644 --- a/activerecord/test/cases/relation/where_clause_test.rb +++ b/activerecord/test/cases/relation/where_clause_test.rb @@ -47,15 +47,15 @@ class ActiveRecord::Relation test "merge removes bind parameters matching overlapping equality clauses" do a = WhereClause.new( [table["id"].eq(bind_param), table["name"].eq(bind_param)], - [attribute("id", 1), attribute("name", "Sean")], + [bind_attribute("id", 1), bind_attribute("name", "Sean")], ) b = WhereClause.new( [table["name"].eq(bind_param)], - [attribute("name", "Jim")] + [bind_attribute("name", "Jim")] ) expected = WhereClause.new( [table["id"].eq(bind_param), table["name"].eq(bind_param)], - [attribute("id", 1), attribute("name", "Jim")], + [bind_attribute("id", 1), bind_attribute("name", "Jim")], ) assert_equal expected, a.merge(b) @@ -103,10 +103,10 @@ class ActiveRecord::Relation table["name"].eq(bind_param), table["age"].gteq(bind_param), ], [ - attribute("name", "Sean"), - attribute("age", 30), + bind_attribute("name", "Sean"), + bind_attribute("age", 30), ]) - expected = WhereClause.new([table["age"].gteq(bind_param)], [attribute("age", 30)]) + expected = WhereClause.new([table["age"].gteq(bind_param)], [bind_attribute("age", 30)]) assert_equal expected, where_clause.except("id", "name") end @@ -146,8 +146,8 @@ class ActiveRecord::Relation end test "or joins the two clauses using OR" do - where_clause = WhereClause.new([table["id"].eq(bind_param)], [attribute("id", 1)]) - other_clause = WhereClause.new([table["name"].eq(bind_param)], [attribute("name", "Sean")]) + where_clause = WhereClause.new([table["id"].eq(bind_param)], [bind_attribute("id", 1)]) + other_clause = WhereClause.new([table["name"].eq(bind_param)], [bind_attribute("name", "Sean")]) expected_ast = Arel::Nodes::Grouping.new( Arel::Nodes::Or.new(table["id"].eq(bind_param), table["name"].eq(bind_param)) @@ -159,7 +159,7 @@ class ActiveRecord::Relation end test "or returns an empty where clause when either side is empty" do - where_clause = WhereClause.new([table["id"].eq(bind_param)], [attribute("id", 1)]) + where_clause = WhereClause.new([table["id"].eq(bind_param)], [bind_attribute("id", 1)]) assert_equal WhereClause.empty, where_clause.or(WhereClause.empty) assert_equal WhereClause.empty, WhereClause.empty.or(where_clause) @@ -170,13 +170,5 @@ class ActiveRecord::Relation def table Arel::Table.new("table") end - - def bind_param - Arel::Nodes::BindParam.new - end - - def attribute(name, value) - ActiveRecord::Attribute.with_cast_value(name, value, ActiveRecord::Type::Value.new) - end end end diff --git a/activerecord/test/cases/relation/where_test.rb b/activerecord/test/cases/relation/where_test.rb index 925af49ffe..cbc466d6b8 100644 --- a/activerecord/test/cases/relation/where_test.rb +++ b/activerecord/test/cases/relation/where_test.rb @@ -15,7 +15,7 @@ require "models/vertex" module ActiveRecord class WhereTest < ActiveRecord::TestCase - fixtures :posts, :edges, :authors, :binaries, :essays, :cars, :treasures, :price_estimates + fixtures :posts, :edges, :authors, :author_addresses, :binaries, :essays, :cars, :treasures, :price_estimates def test_where_copies_bind_params author = authors(:david) @@ -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 @@ -289,6 +289,11 @@ module ActiveRecord assert_equal essays(:david_modest_proposal), essay end + def test_where_on_association_with_select_relation + essay = Essay.where(author: Author.where(name: "David").select(:name)).take + assert_equal essays(:david_modest_proposal), essay + end + def test_where_with_strong_parameters protected_params = Class.new do attr_reader :permitted diff --git a/activerecord/test/cases/relation_test.rb b/activerecord/test/cases/relation_test.rb index 23d27ab90a..5fb32270b7 100644 --- a/activerecord/test/cases/relation_test.rb +++ b/activerecord/test/cases/relation_test.rb @@ -6,9 +6,9 @@ require "models/rating" module ActiveRecord class RelationTest < ActiveRecord::TestCase - fixtures :posts, :comments, :authors + fixtures :posts, :comments, :authors, :author_addresses - class FakeKlass < Struct.new(:table_name, :name) + FakeKlass = Struct.new(:table_name, :name) do extend ActiveRecord::Delegation::DelegateCache inherited self @@ -159,7 +159,7 @@ module ActiveRecord relation = Relation.new(Post, Post.arel_table, Post.predicate_builder) 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 @@ -224,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({ 2=>1, 4=>3, 5=>1 }, authors(:david).posts.merge(posts_with_special_comments_with_ratings).count) + 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 @@ -274,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({ 2=>1, 4=>3, 5=>1 }, authors(:david).posts.merge(posts_with_special_comments_with_ratings).count) + 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 diff --git a/activerecord/test/cases/relations_test.rb b/activerecord/test/cases/relations_test.rb index dcaae5b462..81173945a3 100644 --- a/activerecord/test/cases/relations_test.rb +++ b/activerecord/test/cases/relations_test.rb @@ -22,7 +22,7 @@ require "models/categorization" require "models/edge" class RelationTest < ActiveRecord::TestCase - fixtures :authors, :topics, :entrants, :developers, :companies, :developers_projects, :accounts, :categories, :categorizations, :posts, :comments, + fixtures :authors, :author_addresses, :topics, :entrants, :developers, :companies, :developers_projects, :accounts, :categories, :categorizations, :posts, :comments, :tags, :taggings, :cars, :minivans class TopicWithCallbacks < ActiveRecord::Base @@ -112,7 +112,7 @@ class RelationTest < ActiveRecord::TestCase def test_loaded_first topics = Topic.all.order("id ASC") - topics.to_a # force load + topics.load # force load assert_no_queries do assert_equal "The First Topic", topics.first.title @@ -123,7 +123,7 @@ class RelationTest < ActiveRecord::TestCase def test_loaded_first_with_limit topics = Topic.all.order("id ASC") - topics.to_a # force load + topics.load # force load assert_no_queries do assert_equal ["The First Topic", @@ -136,7 +136,7 @@ class RelationTest < ActiveRecord::TestCase def test_first_get_more_than_available topics = Topic.all.order("id ASC") unloaded_first = topics.first(10) - topics.to_a # force load + topics.load # force load assert_no_queries do loaded_first = topics.first(10) @@ -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 @@ -218,17 +218,34 @@ class RelationTest < ActiveRecord::TestCase assert_equal topics(:fifth).title, topics.first.title end - def test_finding_with_reverted_assoc_order + def test_finding_with_arel_assoc_order + topics = Topic.order(Arel.sql("id") => :desc) + assert_equal 5, topics.to_a.size + assert_equal topics(:fifth).title, topics.first.title + end + + def test_finding_with_reversed_assoc_order topics = Topic.order(id: :asc).reverse_order assert_equal 5, topics.to_a.size assert_equal topics(:fifth).title, topics.first.title end + def test_finding_with_reversed_arel_assoc_order + topics = Topic.order(Arel.sql("id") => :asc).reverse_order + assert_equal 5, topics.to_a.size + assert_equal topics(:fifth).title, topics.first.title + end + def test_reverse_order_with_function topics = Topic.order("length(title)").reverse_order assert_equal topics(:second).title, topics.first.title end + def test_reverse_arel_assoc_order_with_function + topics = Topic.order(Arel.sql("length(title)") => :asc).reverse_order + assert_equal topics(:second).title, topics.first.title + end + def test_reverse_order_with_function_other_predicates topics = Topic.order("author_name, length(title), id").reverse_order assert_equal topics(:second).title, topics.first.title @@ -251,6 +268,12 @@ class RelationTest < ActiveRecord::TestCase end end + def test_reverse_arel_assoc_order_with_multiargument_function + assert_nothing_raised do + Topic.order(Arel.sql("REPLACE(title, '', '')") => :asc).reverse_order + end + end + def test_reverse_order_with_nulls_first_or_last assert_raises(ActiveRecord::IrreversibleOrderError) do Topic.order("title NULLS FIRST").reverse_order @@ -291,7 +314,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 @@ -365,7 +388,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 @@ -442,7 +465,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 @@ -458,55 +481,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 @@ -571,7 +594,7 @@ class RelationTest < ActiveRecord::TestCase end end - def test_respond_to_delegates_to_relation + def test_respond_to_delegates_to_arel relation = Topic.all fake_arel = Struct.new(:responds) { def respond_to?(method, access = false) @@ -584,10 +607,6 @@ class RelationTest < ActiveRecord::TestCase relation.respond_to?(:matching_attributes) assert_equal [:matching_attributes, false], fake_arel.responds.first - - fake_arel.responds = [] - relation.respond_to?(:matching_attributes, true) - assert_equal [:matching_attributes, true], fake_arel.responds.first end def test_respond_to_dynamic_finders @@ -785,7 +804,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 @@ -841,15 +859,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]) @@ -979,7 +988,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 @@ -1014,12 +1023,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") @@ -1027,12 +1030,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") @@ -1154,7 +1151,7 @@ class RelationTest < ActiveRecord::TestCase assert ! posts.loaded? best_posts = posts.where(comments_count: 0) - best_posts.to_a # force load + best_posts.load # force load assert_no_queries { assert_equal 9, best_posts.size } end @@ -1165,7 +1162,7 @@ class RelationTest < ActiveRecord::TestCase assert ! posts.loaded? best_posts = posts.where(comments_count: 0) - best_posts.to_a # force load + best_posts.load # force load assert_no_queries { assert_equal 9, best_posts.size } end @@ -1175,7 +1172,7 @@ class RelationTest < ActiveRecord::TestCase assert_no_queries { assert_equal 0, posts.size } assert ! posts.loaded? - posts.to_a # force load + posts.load # force load assert_no_queries { assert_equal 0, posts.size } end @@ -1204,7 +1201,7 @@ class RelationTest < ActiveRecord::TestCase assert ! no_posts.loaded? best_posts = posts.where(comments_count: 0) - best_posts.to_a # force load + best_posts.load # force load assert_no_queries { assert_equal false, best_posts.empty? } end @@ -1522,7 +1519,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 @@ -1639,9 +1636,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 @@ -1655,17 +1652,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 @@ -1815,7 +1806,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 @@ -1906,6 +1897,12 @@ class RelationTest < ActiveRecord::TestCase assert_equal "#<ActiveRecord::Relation [#{Post.limit(10).map(&:inspect).join(', ')}, ...]>", relation.inspect end + test "relations don't load all records in #inspect" do + assert_sql(/LIMIT|ROWNUM <=|FETCH FIRST/) do + Post.all.inspect + end + end + test "already-loaded relations don't perform a new query in #inspect" do relation = Post.limit(2) relation.to_a @@ -1963,25 +1960,49 @@ class RelationTest < ActiveRecord::TestCase assert !Post.all.respond_to?(:by_lifo) end + def test_unscope_with_subquery + p1 = Post.where(id: 1) + p2 = Post.where(id: 2) + + assert_not_equal p1, p2 + + comments = Comment.where(post: p1).unscope(where: :post_id).where(post: p2) + + assert_not_equal p1.first.comments, comments + assert_equal p2.first.comments, comments + end + + def test_unscope_specific_where_value + posts = Post.where(title: "Welcome to the weblog", body: "Such a lovely day") + + assert_equal 1, posts.count + assert_equal 1, posts.unscope(where: :title).count + assert_equal 1, posts.unscope(where: :body).count + end + def test_unscope_removes_binds - left = Post.where(id: Arel::Nodes::BindParam.new) - column = Post.columns_hash["id"] - left.bind_values += [[column, 20]] + left = Post.where(id: 20) + + binds = [bind_attribute("id", 20, Post.type_for_attribute("id"))] + assert_equal binds, left.bound_attributes relation = left.unscope(where: :id) - assert_equal [], relation.bind_values + assert_equal [], relation.bound_attributes end - def test_merging_removes_rhs_bind_parameters - left = Post.where(id: 20) - right = Post.where(id: [1,2,3,4]) + def test_merging_removes_rhs_binds + left = Post.where(id: 20) + right = Post.where(id: [1, 2, 3, 4]) + + binds = [bind_attribute("id", 20, Post.type_for_attribute("id"))] + assert_equal binds, left.bound_attributes merged = left.merge(right) - assert_equal [], merged.bind_values + assert_equal [], merged.bound_attributes end - def test_merging_keeps_lhs_bind_parameters - binds = [ActiveRecord::Relation::QueryAttribute.new("id", 20, Post.type_for_attribute("id"))] + def test_merging_keeps_lhs_binds + binds = [bind_attribute("id", 20, Post.type_for_attribute("id"))] right = Post.where(id: 20) left = Post.where(id: 10) @@ -1990,15 +2011,6 @@ class RelationTest < ActiveRecord::TestCase assert_equal binds, merged.bound_attributes end - def test_merging_reorders_bind_params - post = Post.first - right = Post.where(id: post.id) - left = Post.where(title: post.title) - - merged = left.merge(right) - assert_equal post, merged.first - end - def test_relation_join_method assert_equal "Thank you for the welcome,Thank you again for the welcome", Post.first.comments.join(",") end diff --git a/activerecord/test/cases/reload_models_test.rb b/activerecord/test/cases/reload_models_test.rb index 5dc9d6d8b7..3f4c0c03e3 100644 --- a/activerecord/test/cases/reload_models_test.rb +++ b/activerecord/test/cases/reload_models_test.rb @@ -13,7 +13,7 @@ class ReloadModelsTest < ActiveRecord::TestCase # development environment. Note that meanwhile the class Pet is not # reloaded, simulating a class that is present in a plugin. Object.class_eval { remove_const :Owner } - Kernel.load(File.expand_path(File.join(File.dirname(__FILE__), "../models/owner.rb"))) + Kernel.load(File.expand_path("../models/owner.rb", __dir__)) pet = Pet.find_by_name("parrot") pet.owner = Owner.find_by_name("ashley") diff --git a/activerecord/test/cases/result_test.rb b/activerecord/test/cases/result_test.rb index 949086fda0..1a0b7c6ca7 100644 --- a/activerecord/test/cases/result_test.rb +++ b/activerecord/test/cases/result_test.rb @@ -45,10 +45,8 @@ module ActiveRecord end end - if Enumerator.method_defined? :size - test "each without block returns a sized enumerator" do - assert_equal 3, result.each.size - end + test "each without block returns a sized enumerator" do + assert_equal 3, result.each.size end test "cast_values returns rows after type casting" do diff --git a/activerecord/test/cases/sanitize_test.rb b/activerecord/test/cases/sanitize_test.rb index 464bb12ccb..72f09186e2 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 @@ -152,11 +152,15 @@ class SanitizeTest < ActiveRecord::TestCase end def test_bind_record - o = Struct.new(:quoted_id).new(1) - assert_equal "1", bind("?", o) + o = Class.new { + def quoted_id + 1 + end + }.new + assert_deprecated { assert_equal "1", bind("?", o) } os = [o] * 3 - assert_equal "1,1,1", bind("?", os) + assert_deprecated { assert_equal "1,1,1", bind("?", os) } end def test_named_bind_with_postgresql_type_casts diff --git a/activerecord/test/cases/schema_dumper_test.rb b/activerecord/test/cases/schema_dumper_test.rb index 57b1bc889a..cb8d449ba9 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 @@ -147,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 @@ -182,22 +178,24 @@ class SchemaDumperTest < ActiveRecord::TestCase end 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 + index_definition = dump_table_schema("companies").split(/\n/).grep(/t\.index.*company_index/).first.strip + if current_adapter?(:PostgreSQLAdapter) + assert_equal 't.index ["firm_id", "type", "rating"], name: "company_index", order: { rating: :desc }', index_definition + elsif current_adapter?(:Mysql2Adapter) + if ActiveRecord::Base.connection.supports_index_sort_order? + assert_equal 't.index ["firm_id", "type", "rating"], name: "company_index", length: { type: 10 }, order: { rating: :desc }', index_definition + else + assert_equal 't.index ["firm_id", "type", "rating"], name: "company_index", length: { type: 10 }', index_definition + end else assert_equal 't.index ["firm_id", "type", "rating"], name: "company_index"', index_definition end end def test_schema_dumps_partial_indices - index_definition = standard_dump.split(/\n/).grep(/t\.index.*company_partial_index/).first.strip - if current_adapter?(:PostgreSQLAdapter) - assert_equal 't.index ["firm_id", "type"], name: "company_partial_index", where: "(rating > 10)", using: :btree', index_definition - elsif current_adapter?(:Mysql2Adapter) - assert_equal 't.index ["firm_id", "type"], name: "company_partial_index", using: :btree', index_definition - elsif current_adapter?(:SQLite3Adapter) && ActiveRecord::Base.connection.supports_partial_index? - assert_equal 't.index ["firm_id", "type"], name: "company_partial_index", where: "rating > 10"', index_definition + index_definition = dump_table_schema("companies").split(/\n/).grep(/t\.index.*company_partial_index/).first.strip + if current_adapter?(:PostgreSQLAdapter, :SQLite3Adapter) && ActiveRecord::Base.connection.supports_partial_index? + assert_equal 't.index ["firm_id", "type"], name: "company_partial_index", where: "(rating > 10)"', index_definition else assert_equal 't.index ["firm_id", "type"], name: "company_partial_index"', index_definition end @@ -250,9 +248,9 @@ class SchemaDumperTest < ActiveRecord::TestCase end def test_schema_dumps_index_type - output = standard_dump - assert_match %r{t\.index \["awesome"\], name: "index_key_tests_on_awesome", type: :fulltext}, output - assert_match %r{t\.index \["pizza"\], name: "index_key_tests_on_pizza", using: :btree}, output + output = dump_table_schema "key_tests" + assert_match %r{t\.index \["awesome"\], name: "index_key_tests_on_awesome", type: :fulltext$}, output + assert_match %r{t\.index \["pizza"\], name: "index_key_tests_on_pizza"$}, output end end @@ -263,23 +261,34 @@ class SchemaDumperTest < ActiveRecord::TestCase if current_adapter?(:PostgreSQLAdapter) def test_schema_dump_includes_bigint_default - output = standard_dump + output = dump_table_schema "defaults" assert_match %r{t\.bigint\s+"bigint_default",\s+default: 0}, output end def test_schema_dump_includes_limit_on_array_type - output = standard_dump + output = dump_table_schema "bigint_array" assert_match %r{t\.bigint\s+"big_int_data_points\",\s+array: true}, output end def test_schema_dump_allows_array_of_decimal_defaults - output = standard_dump + output = dump_table_schema "bigint_array" assert_match %r{t\.decimal\s+"decimal_array_default",\s+default: \["1.23", "3.45"\],\s+array: true}, output end def test_schema_dump_expression_indices - index_definition = standard_dump.split(/\n/).grep(/t\.index.*company_expression_index/).first.strip - assert_equal 't.index "lower((name)::text)", name: "company_expression_index", using: :btree', index_definition + index_definition = dump_table_schema("companies").split(/\n/).grep(/t\.index.*company_expression_index/).first.strip + assert_equal 't.index "lower((name)::text)", name: "company_expression_index"', index_definition + end + + def test_schema_dump_interval_type + output = dump_table_schema "postgresql_times" + assert_match %r{t\.interval\s+"time_interval"$}, output + assert_match %r{t\.interval\s+"scaled_time_interval",\s+precision: 6$}, output + end + + def test_schema_dump_oid_type + output = dump_table_schema "postgresql_oids" + assert_match %r{t\.oid\s+"obj_id"$}, output end if ActiveRecord::Base.connection.supports_extensions? @@ -343,9 +352,9 @@ class SchemaDumperTest < ActiveRecord::TestCase create_table("dogs") do |t| t.column :name, :string - t.column :owner_id, :integer + t.references :owner t.index [:name] - t.foreign_key :dog_owners, column: "owner_id" if supports_foreign_keys? + t.foreign_key :dog_owners, column: "owner_id" end end def down @@ -417,25 +426,40 @@ class SchemaDumperDefaultsTest < ActiveRecord::TestCase setup do @connection = ActiveRecord::Base.connection - @connection.create_table :defaults, force: true do |t| + @connection.create_table :dump_defaults, force: true do |t| t.string :string_with_default, default: "Hello!" t.date :date_with_default, default: "2014-06-05" t.datetime :datetime_with_default, default: "2014-06-05 07:17:04" t.time :time_with_default, default: "07:17:04" + t.decimal :decimal_with_default, default: "1234567890.0123456789", precision: 20, scale: 10 + 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 - return unless @connection - @connection.drop_table "defaults", if_exists: true + @connection.drop_table "dump_defaults", if_exists: true end def test_schema_dump_defaults_with_universally_supported_types - output = dump_table_schema("defaults") + output = dump_table_schema("dump_defaults") assert_match %r{t\.string\s+"string_with_default",.*?default: "Hello!"}, output - assert_match %r{t\.date\s+"date_with_default",\s+default: '2014-06-05'}, output - assert_match %r{t\.datetime\s+"datetime_with_default",\s+default: '2014-06-05 07:17:04'}, output - assert_match %r{t\.time\s+"time_with_default",\s+default: '2000-01-01 07:17:04'}, output + assert_match %r{t\.date\s+"date_with_default",\s+default: "2014-06-05"}, output + assert_match %r{t\.datetime\s+"datetime_with_default",\s+default: "2014-06-05 07:17:04"}, output + assert_match %r{t\.time\s+"time_with_default",\s+default: "2000-01-01 07:17:04"}, output + assert_match %r{t\.decimal\s+"decimal_with_default",\s+precision: 20,\s+scale: 10,\s+default: "1234567890.0123456789"}, 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 61062da3e1..89fb434b27 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 @@ -59,17 +59,6 @@ class DefaultScopingTest < ActiveRecord::TestCase assert_equal "Jamis", DeveloperCalledJamis.create!.name end - unless in_memory_db? - def test_default_scoping_with_threads - 2.times do - Thread.new { - assert_includes DeveloperOrderedBySalary.all.to_sql, "salary DESC" - DeveloperOrderedBySalary.connection.close - }.join - end - end - end - def test_default_scope_with_inheritance wheres = InheritedPoorDeveloperCalledJamis.all.where_values_hash assert_equal "Jamis", wheres["name"] @@ -315,7 +304,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 @@ -433,24 +422,6 @@ class DefaultScopingTest < ActiveRecord::TestCase assert_equal comment, CommentWithDefaultScopeReferencesAssociation.find_by(id: comment.id) end - unless in_memory_db? - def test_default_scope_is_threadsafe - threads = [] - assert_not_equal 1, ThreadsafeDeveloper.unscoped.count - - threads << Thread.new do - Thread.current[:long_default_scope] = true - assert_equal 1, ThreadsafeDeveloper.all.to_a.count - ThreadsafeDeveloper.connection.close - end - threads << Thread.new do - assert_equal 1, ThreadsafeDeveloper.all.to_a.count - ThreadsafeDeveloper.connection.close - end - threads.each(&:join) - end - end - test "additional conditions are ANDed with the default scope" do scope = DeveloperCalledJamis.where(name: "David") assert_equal 2, scope.where_clause.ast.children.length @@ -491,6 +462,8 @@ class DefaultScopingTest < ActiveRecord::TestCase def test_with_abstract_class_scope_should_be_executed_in_correct_context vegetarian_pattern, gender_pattern = if current_adapter?(:Mysql2Adapter) [/`lions`.`is_vegetarian`/, /`lions`.`gender`/] + elsif current_adapter?(:OracleAdapter) + [/"LIONS"."IS_VEGETARIAN"/, /"LIONS"."GENDER"/] else [/"lions"."is_vegetarian"/, /"lions"."gender"/] end @@ -499,3 +472,37 @@ class DefaultScopingTest < ActiveRecord::TestCase assert_match gender_pattern, Lion.female.to_sql end end + +class DefaultScopingWithThreadTest < ActiveRecord::TestCase + self.use_transactional_tests = false + + def test_default_scoping_with_threads + 2.times do + Thread.new { + assert_includes DeveloperOrderedBySalary.all.to_sql, "salary DESC" + DeveloperOrderedBySalary.connection.close + }.join + end + end + + def test_default_scope_is_threadsafe + 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[: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 + threads.each(&:join) + end +end unless in_memory_db? diff --git a/activerecord/test/cases/scoping/named_scoping_test.rb b/activerecord/test/cases/scoping/named_scoping_test.rb index 58e1310ab0..483ea7128d 100644 --- a/activerecord/test/cases/scoping/named_scoping_test.rb +++ b/activerecord/test/cases/scoping/named_scoping_test.rb @@ -23,8 +23,8 @@ class NamedScopingTest < ActiveRecord::TestCase all_posts = Topic.base assert_queries(1) do - all_posts.collect - all_posts.collect + all_posts.collect { true } + all_posts.collect { true } end end @@ -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 @@ -161,13 +161,13 @@ class NamedScopingTest < ActiveRecord::TestCase end def test_first_and_last_should_allow_integers_for_limit - assert_equal Topic.base.first(2), Topic.base.to_a.first(2) + assert_equal Topic.base.first(2), Topic.base.order("id").to_a.first(2) assert_equal Topic.base.last(2), Topic.base.order("id").to_a.last(2) end def test_first_and_last_should_not_use_query_when_results_are_loaded topics = Topic.base - topics.reload # force load + topics.load # force load assert_no_queries do topics.first topics.last @@ -178,7 +178,7 @@ class NamedScopingTest < ActiveRecord::TestCase topics = Topic.base assert_queries(2) do topics.empty? # use count query - topics.collect # force load + topics.load # force load topics.empty? # use loaded (no query) end end @@ -187,7 +187,7 @@ class NamedScopingTest < ActiveRecord::TestCase topics = Topic.base assert_queries(2) do topics.any? # use count query - topics.collect # force load + topics.load # force load topics.any? # use loaded (no query) end end @@ -203,7 +203,7 @@ class NamedScopingTest < ActiveRecord::TestCase def test_any_should_not_fire_query_if_scope_loaded topics = Topic.base - topics.collect # force load + topics.load # force load assert_no_queries { assert topics.any? } end @@ -217,7 +217,7 @@ class NamedScopingTest < ActiveRecord::TestCase topics = Topic.base assert_queries(2) do topics.many? # use count query - topics.collect # force load + topics.load # force load topics.many? # use loaded (no query) end end @@ -233,7 +233,7 @@ class NamedScopingTest < ActiveRecord::TestCase def test_many_should_not_fire_query_if_scope_loaded topics = Topic.base - topics.collect # force load + topics.load # force load assert_no_queries { assert topics.many? } end @@ -384,7 +384,7 @@ class NamedScopingTest < ActiveRecord::TestCase def test_size_should_use_length_when_results_are_loaded topics = Topic.base - topics.reload # force load + topics.load # force load assert_no_queries do topics.size # use loaded (no query) end @@ -551,6 +551,12 @@ class NamedScopingTest < ActiveRecord::TestCase assert_equal 1, SpecialComment.where(body: "go crazy").created.count end + def test_model_class_should_respond_to_extending + assert_raises OopsError do + Comment.unscoped.oops_comments.destroy_all + end + end + def test_model_class_should_respond_to_none assert !Topic.none? Topic.delete_all diff --git a/activerecord/test/cases/scoping/relation_scoping_test.rb b/activerecord/test/cases/scoping/relation_scoping_test.rb index 27b4583457..8535be8402 100644 --- a/activerecord/test/cases/scoping/relation_scoping_test.rb +++ b/activerecord/test/cases/scoping/relation_scoping_test.rb @@ -10,7 +10,7 @@ require "models/person" require "models/reference" class RelationScopingTest < ActiveRecord::TestCase - fixtures :authors, :developers, :projects, :comments, :posts, :developers_projects + fixtures :authors, :author_addresses, :developers, :projects, :comments, :posts, :developers_projects setup do developers(:david) @@ -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 @@ -229,16 +229,23 @@ class RelationScopingTest < ActiveRecord::TestCase end end - def test_circular_joins_with_current_scope_does_not_crash + def test_circular_joins_with_scoping_does_not_crash posts = Post.joins(comments: :post).scoping do - Post.current_scope.first(10) + Post.first(10) end assert_equal posts, Post.joins(comments: :post).first(10) end + + def test_circular_left_joins_with_scoping_does_not_crash + posts = Post.left_joins(comments: :post).scoping do + Post.first(10) + end + assert_equal posts, Post.left_joins(comments: :post).first(10) + end end class NestedRelationScopingTest < ActiveRecord::TestCase - fixtures :authors, :developers, :projects, :comments, :posts + fixtures :authors, :author_addresses, :developers, :projects, :comments, :posts def test_merge_options Developer.where("salary = 80000").scoping do @@ -277,7 +284,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 bebd856faf..e1bdaab5cf 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 @@ -185,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 @@ -211,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 @@ -240,6 +240,20 @@ class SerializedAttributeTest < ActiveRecord::TestCase assert_equal [], light.long_state end + def test_unexpected_serialized_type + Topic.serialize :content, Hash + topic = Topic.create!(content: { zomg: true }) + + Topic.serialize :content, Array + + topic.reload + error = assert_raise(ActiveRecord::SerializationTypeMismatch) do + topic.content + end + expected = "can't load `content`: was supposed to be a Array, but was a Hash. -- {:zomg=>true}" + assert_equal expected, error.to_s + end + def test_serialized_column_should_unserialize_after_update_column t = Topic.create(content: "first") assert_equal("first", t.content) @@ -335,4 +349,32 @@ class SerializedAttributeTest < ActiveRecord::TestCase topic.foo refute topic.changed? end + + def test_serialized_attribute_works_under_concurrent_initial_access + model = Topic.dup + + topic = model.last + topic.update group: "1" + + model.serialize :group, JSON + model.reset_column_information + + # This isn't strictly necessary for the test, but a little bit of + # knowledge of internals allows us to make failures far more likely. + model.define_singleton_method(:define_attribute) do |*args| + Thread.pass + super(*args) + end + + threads = 4.times.map do + Thread.new do + topic.reload.group + end + end + + # All the threads should retrieve the value knowing it is JSON, and + # thus decode it. If this fails, some threads will instead see the + # raw string ("1"), or raise an exception. + assert_equal [1] * threads.size, threads.map(&:value) + end end diff --git a/activerecord/test/cases/statement_cache_test.rb b/activerecord/test/cases/statement_cache_test.rb index f45f63c68e..fab3648564 100644 --- a/activerecord/test/cases/statement_cache_test.rb +++ b/activerecord/test/cases/statement_cache_test.rb @@ -105,5 +105,31 @@ module ActiveRecord refute_equal book, other_book end + + def test_find_by_does_not_use_statement_cache_if_table_name_is_changed + book = Book.create(name: "my book") + + Book.find_by(name: book.name) # warming the statement cache. + + # changing the table name should change the query that is not cached. + Book.table_name = :birds + assert_nil Book.find_by(name: book.name) + ensure + Book.table_name = :books + end + + def test_find_does_not_use_statement_cache_if_table_name_is_changed + book = Book.create(name: "my book") + + Book.find(book.id) # warming the statement cache. + + # changing the table name should change the query that is not cached. + Book.table_name = :birds + assert_raise ActiveRecord::RecordNotFound do + Book.find(book.id) + end + ensure + Book.table_name = :books + end end end diff --git a/activerecord/test/cases/tasks/database_tasks_test.rb b/activerecord/test/cases/tasks/database_tasks_test.rb index d847a02679..c47c97e9d9 100644 --- a/activerecord/test/cases/tasks/database_tasks_test.rb +++ b/activerecord/test/cases/tasks/database_tasks_test.rb @@ -24,7 +24,7 @@ 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) @@ -61,7 +61,7 @@ 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") @@ -85,11 +85,23 @@ 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" } } ActiveRecord::Base.stubs(:configurations).returns(@configurations) + # To refrain from connecting to a newly created empty DB in sqlite3_mem tests + ActiveRecord::Base.connection_handler.stubs(:establish_connection) end def test_ignores_configurations_without_databases @@ -331,13 +343,37 @@ module ActiveRecord ENV["VERBOSE"] = "false" ENV["VERSION"] = "4" - ActiveRecord::Migrator.expects(:migrate).with("custom/path", 4) + ActiveRecord::Migration.expects(:verbose=).with(false) + ActiveRecord::Migration.expects(:verbose=).with(ActiveRecord::Migration.verbose) + ActiveRecord::Tasks::DatabaseTasks.migrate + + ENV.delete("VERBOSE") + ENV.delete("VERSION") + ActiveRecord::Migrator.expects(:migrate).with("custom/path", nil) + ActiveRecord::Migration.expects(:verbose=).with(true) + ActiveRecord::Migration.expects(:verbose=).with(ActiveRecord::Migration.verbose) + ActiveRecord::Tasks::DatabaseTasks.migrate + + ENV["VERBOSE"] = "yes" + ENV["VERSION"] = "unknown" + ActiveRecord::Migrator.expects(:migrate).with("custom/path", 0) + ActiveRecord::Migration.expects(:verbose=).with(true) + ActiveRecord::Migration.expects(:verbose=).with(ActiveRecord::Migration.verbose) ActiveRecord::Tasks::DatabaseTasks.migrate ensure ENV["VERBOSE"], ENV["VERSION"] = verbose, version end + def test_migrate_raise_error_on_empty_version + version = ENV["VERSION"] + ENV["VERSION"] = "" + e = assert_raise(RuntimeError) { ActiveRecord::Tasks::DatabaseTasks.migrate } + assert_equal "Empty VERSION provided", e.message + ensure + ENV["VERSION"] = version + end + def test_migrate_clears_schema_cache_afterward ActiveRecord::Base.expects(:clear_cache!) ActiveRecord::Tasks::DatabaseTasks.migrate @@ -411,7 +447,7 @@ module ActiveRecord ADAPTERS_TASKS.each do |k, v| define_method("test_#{k}_structure_dump") do - eval("@#{v}").expects(:structure_dump).with("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 @@ -422,7 +458,7 @@ module ActiveRecord ADAPTERS_TASKS.each do |k, v| define_method("test_#{k}_structure_load") do - eval("@#{v}").expects(:structure_load).with("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 diff --git a/activerecord/test/cases/tasks/mysql_rake_test.rb b/activerecord/test/cases/tasks/mysql_rake_test.rb index dbe935808e..c22d974536 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 @@ -167,7 +167,7 @@ if current_adapter?(:Mysql2Adapter) def assert_permissions_granted_for(db_user) db_name = @configuration["database"] db_password = @configuration["password"] - @connection.expects(:execute).with("GRANT ALL PRIVILEGES ON #{db_name}.* TO '#{db_user}'@'localhost' IDENTIFIED BY '#{db_password}' WITH GRANT OPTION;") + @connection.expects(:execute).with("GRANT ALL PRIVILEGES ON `#{db_name}`.* TO '#{db_user}'@'localhost' IDENTIFIED BY '#{db_password}' WITH GRANT OPTION;") 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,26 @@ 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_structure_dump_with_ignore_tables + filename = "awesome-file.sql" + ActiveRecord::SchemaDumper.expects(:ignore_tables).returns(["foo", "bar"]) + + Kernel.expects(:system).with("mysqldump", "--result-file", filename, "--no-data", "--routines", "--skip-comments", "--ignore-table=test-db.foo", "--ignore-table=test-db.bar", "test-db").returns(true) + + ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, filename) + end + def test_warn_when_external_structure_dump_command_execution_fails filename = "awesome-file.sql" Kernel.expects(:system) @@ -323,6 +343,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 +364,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..a2e968aedf 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 @@ -217,17 +217,21 @@ if current_adapter?(:PostgreSQLAdapter) class PostgreSQLStructureDumpTest < ActiveRecord::TestCase def setup - @connection = stub(structure_dump: true) + @connection = stub(schema_search_path: nil, structure_dump: true) @configuration = { "adapter" => "postgresql", "database" => "my-app-db" } - @filename = "awesome-file.sql" + @filename = "/tmp/awesome-file.sql" + FileUtils.touch(@filename) ActiveRecord::Base.stubs(:connection).returns(@connection) ActiveRecord::Base.stubs(:establish_connection).returns(true) Kernel.stubs(:system) - File.stubs(:open) + end + + def teardown + FileUtils.rm_f(@filename) end def test_structure_dump @@ -236,6 +240,33 @@ if current_adapter?(:PostgreSQLAdapter) ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, @filename) end + def test_structure_dump_header_comments_removed + Kernel.stubs(:system).returns(true) + File.write(@filename, "-- header comment\n\n-- more header comment\n statement \n-- lower comment\n") + + ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, @filename) + + assert_equal [" statement \n", "-- lower comment\n"], File.readlines(@filename).first(2) + 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_ignore_tables + ActiveRecord::SchemaDumper.expects(:ignore_tables).returns(["foo", "bar"]) + + Kernel.expects(:system).with("pg_dump", "-s", "-x", "-O", "-f", @filename, "-T", "foo", "-T", "bar", "my-app-db").returns(true) + + ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, @filename) + end + def test_structure_dump_with_schema_search_path @configuration["schema_search_path"] = "foo,bar" @@ -263,7 +294,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 +301,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 +331,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..ccb3834fee 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 @@ -180,6 +180,9 @@ if current_adapter?(:SQLite3Adapter) "adapter" => "sqlite3", "database" => @database } + + `sqlite3 #{@database} 'CREATE TABLE bar(id INTEGER)'` + `sqlite3 #{@database} 'CREATE TABLE foo(id INTEGER)'` end def test_structure_dump @@ -189,6 +192,23 @@ if current_adapter?(:SQLite3Adapter) ActiveRecord::Tasks::DatabaseTasks.structure_dump @configuration, filename, "/rails/root" assert File.exist?(dbfile) assert File.exist?(filename) + assert_match(/CREATE TABLE foo/, File.read(filename)) + assert_match(/CREATE TABLE bar/, File.read(filename)) + ensure + FileUtils.rm_f(filename) + FileUtils.rm_f(dbfile) + end + + def test_structure_dump_with_ignore_tables + dbfile = @database + filename = "awesome-file.sql" + ActiveRecord::SchemaDumper.expects(:ignore_tables).returns(["foo"]) + + ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, filename, "/rails/root") + assert File.exist?(dbfile) + assert File.exist?(filename) + assert_match(/bar/, File.read(filename)) + assert_no_match(/foo/, File.read(filename)) ensure FileUtils.rm_f(filename) FileUtils.rm_f(dbfile) diff --git a/activerecord/test/cases/test_case.rb b/activerecord/test/cases/test_case.rb index 60ac3e08a1..9f594fef85 100644 --- a/activerecord/test/cases/test_case.rb +++ b/activerecord/test/cases/test_case.rb @@ -2,7 +2,6 @@ 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" @@ -64,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 @@ -76,6 +75,14 @@ module ActiveRecord model.reset_column_information model.column_names.include?(column_name.to_s) end + + def bind_param + Arel::Nodes::BindParam.new + end + + def bind_attribute(name, value, type = ActiveRecord::Type.default_value) + ActiveRecord::Relation::QueryAttribute.new(name, value, type) + end end class PostgreSQLTestCase < TestCase @@ -125,12 +132,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/time_precision_test.rb b/activerecord/test/cases/time_precision_test.rb index 03f6c234e8..09c585167e 100644 --- a/activerecord/test/cases/time_precision_test.rb +++ b/activerecord/test/cases/time_precision_test.rb @@ -68,7 +68,7 @@ if subsecond_precision_supported? assert_match %r{t\.time\s+"finish",\s+precision: 6$}, output end - if current_adapter?(:PostgreSQLAdapter) + if current_adapter?(:PostgreSQLAdapter, :SQLServerAdapter) def test_time_precision_with_zero_should_be_dumped @connection.create_table(:foos, force: true) do |t| t.time :start, precision: 0 diff --git a/activerecord/test/cases/timestamp_test.rb b/activerecord/test/cases/timestamp_test.rb index cd83518e84..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,34 +430,19 @@ 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 diff --git a/activerecord/test/cases/transaction_callbacks_test.rb b/activerecord/test/cases/transaction_callbacks_test.rb index bd50fe55e9..eaa4dd09a9 100644 --- a/activerecord/test/cases/transaction_callbacks_test.rb +++ b/activerecord/test/cases/transaction_callbacks_test.rb @@ -243,14 +243,14 @@ 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 + 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 + 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) } @@ -269,8 +269,8 @@ 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) } @@ -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 @@ -506,3 +551,43 @@ class TransactionEnrollmentCallbacksTest < ActiveRecord::TestCase assert_equal [:rollback], @topic.history end end + +class CallbacksOnActionAndConditionTest < ActiveRecord::TestCase + self.use_transactional_tests = false + + class TopicWithCallbacksOnActionAndCondition < ActiveRecord::Base + self.table_name = :topics + + after_commit(on: [:create, :update], if: :run_callback?) { |record| record.history << :create_or_update } + + def clear_history + @history = [] + end + + def history + @history ||= [] + end + + def run_callback? + self.history << :run_callback? + true + end + + attr_accessor :save_before_commit_history, :update_title + end + + def test_callback_on_action_with_condition + topic = TopicWithCallbacksOnActionAndCondition.new + topic.save + assert_equal [:run_callback?, :create_or_update], topic.history + + topic.clear_history + topic.approved = true + topic.save + assert_equal [:run_callback?, :create_or_update], topic.history + + topic.clear_history + topic.destroy + assert_equal [], topic.history + end +end diff --git a/activerecord/test/cases/transactions_test.rb b/activerecord/test/cases/transactions_test.rb index 834365660f..5c6d78b574 100644 --- a/activerecord/test/cases/transactions_test.rb +++ b/activerecord/test/cases/transactions_test.rb @@ -10,7 +10,7 @@ require "models/movie" class TransactionTest < ActiveRecord::TestCase self.use_transactional_tests = false - fixtures :topics, :developers, :authors, :posts + fixtures :topics, :developers, :authors, :author_addresses, :posts def setup @first, @second = Topic.find(1, 2).sort_by(&:id) @@ -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" @@ -206,16 +206,6 @@ class TransactionTest < ActiveRecord::TestCase assert_equal posts_count, author.posts.reload.size end - def test_cancellation_from_returning_false_in_before_filter - def @first.before_save_for_transaction - false - end - - assert_deprecated do - @first.save - end - end - def test_cancellation_from_before_destroy_rollbacks_in_destroy add_cancelling_before_destroy_with_db_side_effect_to_topic @first nbooks_before_destroy = Book.count @@ -279,7 +269,11 @@ class TransactionTest < ActiveRecord::TestCase e = assert_raises(RuntimeError) { new_topic.save } assert_equal "Make the transaction rollback", e.message assert_equal new_record_snapshot, !new_topic.persisted?, "The topic should have its old persisted value" - assert_equal id_snapshot, new_topic.id, "The topic should have its old id" + if id_snapshot.nil? + assert_nil new_topic.id, "The topic should have its old id" + else + assert_equal id_snapshot, new_topic.id, "The topic should have its old id" + end assert_equal id_present, new_topic.has_attribute?(Topic.primary_key) end end @@ -443,16 +437,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? 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/uniqueness_validation_test.rb b/activerecord/test/cases/validations/uniqueness_validation_test.rb index 44b4e28777..28605d2f8e 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" @@ -356,7 +372,7 @@ class UniquenessValidationTest < ActiveRecord::TestCase e2 = Event.create(title: "abcdefgh") assert_not e2.valid?, "Created an event whose title is not unique" - elsif current_adapter?(:Mysql2Adapter, :PostgreSQLAdapter, :SQLServerAdapter) + elsif current_adapter?(:Mysql2Adapter, :PostgreSQLAdapter, :OracleAdapter, :SQLServerAdapter) assert_raise(ActiveRecord::ValueTooLong) do Event.create(title: "abcdefgh") end @@ -369,13 +385,13 @@ class UniquenessValidationTest < ActiveRecord::TestCase def test_validate_uniqueness_with_limit_and_utf8 if current_adapter?(:SQLite3Adapter) - # Event.title has limit 5, but does SQLite doesn't truncate. + # Event.title has limit 5, but SQLite doesn't truncate. e1 = Event.create(title: "一二三四五å…七八") assert e1.valid?, "Could not create an event with a unique 8 characters title" e2 = Event.create(title: "一二三四五å…七八") assert_not e2.valid?, "Created an event whose title is not unique" - elsif current_adapter?(:Mysql2Adapter, :PostgreSQLAdapter, :SQLServerAdapter) + elsif current_adapter?(:Mysql2Adapter, :PostgreSQLAdapter, :OracleAdapter, :SQLServerAdapter) assert_raise(ActiveRecord::ValueTooLong) do Event.create(title: "一二三四五å…七八") end diff --git a/activerecord/test/cases/validations_test.rb b/activerecord/test/cases/validations_test.rb index 5d9aa99497..a305aa295a 100644 --- a/activerecord/test/cases/validations_test.rb +++ b/activerecord/test/cases/validations_test.rb @@ -167,6 +167,20 @@ class ValidationsTest < ActiveRecord::TestCase assert topic.valid? end + def test_numericality_validation_checks_against_raw_value + klass = Class.new(Topic) do + def self.model_name + ActiveModel::Name.new(self, nil, "Topic") + end + attribute :wibble, :decimal, scale: 2, precision: 9 + validates_numericality_of :wibble, greater_than_or_equal_to: BigDecimal.new("97.18") + end + + assert_not klass.new(wibble: "97.179").valid? + assert_not klass.new(wibble: 97.179).valid? + assert_not klass.new(wibble: BigDecimal.new("97.179")).valid? + end + def test_acceptance_validator_doesnt_require_db_connection klass = Class.new(ActiveRecord::Base) do self.table_name = "posts" diff --git a/activerecord/test/cases/view_test.rb b/activerecord/test/cases/view_test.rb index 0e38cee334..1d21a2454f 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 @@ -66,15 +66,20 @@ module ViewBehavior 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 @@ -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 @@ -150,7 +154,9 @@ if ActiveRecord::Base.connection.supports_views? end # sqlite dose not support CREATE, INSERT, and DELETE for VIEW - if current_adapter?(:Mysql2Adapter, :PostgreSQLAdapter) + if current_adapter?(:Mysql2Adapter, :SQLServerAdapter) || + current_adapter?(:PostgreSQLAdapter) && ActiveRecord::Base.connection.postgresql_version >= 90300 + class UpdateableViewTest < ActiveRecord::TestCase self.use_transactional_tests = false fixtures :books @@ -196,8 +202,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, :SQLServerAdapter)` +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 +212,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 5192e5050a..bfc13d683d 100644 --- a/activerecord/test/cases/yaml_serialization_test.rb +++ b/activerecord/test/cases/yaml_serialization_test.rb @@ -5,7 +5,7 @@ require "models/post" require "models/author" class YamlSerializationTest < ActiveRecord::TestCase - fixtures :topics, :authors, :posts + fixtures :topics, :authors, :author_addresses, :posts def test_to_yaml_with_time_with_zone_should_not_raise_exception with_timezone_config aware_attributes: true, zone: "Pacific Time (US & Canada)" do @@ -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 @@ -123,8 +123,8 @@ class YamlSerializationTest < ActiveRecord::TestCase def yaml_fixture(file_name) path = File.expand_path( - "../../support/yaml_compatibility_fixtures/#{file_name}.yml", - __FILE__ + "../support/yaml_compatibility_fixtures/#{file_name}.yml", + __dir__ ) File.read(path) 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/config.rb b/activerecord/test/config.rb index 6e2e8b2145..a65e6ff776 100644 --- a/activerecord/test/config.rb +++ b/activerecord/test/config.rb @@ -1,4 +1,4 @@ -TEST_ROOT = File.expand_path(File.dirname(__FILE__)) +TEST_ROOT = __dir__ ASSETS_ROOT = TEST_ROOT + "/assets" FIXTURES_ROOT = TEST_ROOT + "/fixtures" MIGRATIONS_ROOT = TEST_ROOT + "/migrations" diff --git a/activerecord/test/fixtures/books.yml b/activerecord/test/fixtures/books.yml index b3625ee72e..699623a6f9 100644 --- a/activerecord/test/fixtures/books.yml +++ b/activerecord/test/fixtures/books.yml @@ -25,6 +25,7 @@ ddd: name: "Domain-Driven Design" format: "hardcover" status: 2 + read_status: "forgotten" tlg: 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/fixtures/subscribers.yml b/activerecord/test/fixtures/subscribers.yml index c6a8c2fa24..0f6e0cd48e 100644 --- a/activerecord/test/fixtures/subscribers.yml +++ b/activerecord/test/fixtures/subscribers.yml @@ -6,6 +6,6 @@ second: nick: webster132 name: David Heinemeier Hansson -thrid: +third: nick: swistak name: Marcin Raczkowski
\ No newline at end of file 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 fab613afd1..2d9cba77e0 100644 --- a/activerecord/test/models/author.rb +++ b/activerecord/test/models/author.rb @@ -106,6 +106,7 @@ class Author < ActiveRecord::Base has_many :tags_with_primary_key, through: :posts has_many :books + has_many :unpublished_books, -> { where(status: [:proposed, :written]) }, class_name: "Book" has_many :subscriptions, through: :books has_many :subscribers, -> { order("subscribers.nick") }, through: :subscriptions has_many :distinct_subscribers, -> { select("DISTINCT subscribers.*").order("subscribers.nick") }, through: :subscriptions, source: :subscriber diff --git a/activerecord/test/models/book.rb b/activerecord/test/models/book.rb index 17bf3fbcb4..6466e1b341 100644 --- a/activerecord/test/models/book.rb +++ b/activerecord/test/models/book.rb @@ -1,5 +1,5 @@ class Book < ActiveRecord::Base - has_many :authors + belongs_to :author has_many :citations, foreign_key: "book1_id" has_many :references, -> { distinct }, through: :citations, source: :reference_of @@ -8,7 +8,7 @@ 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, forgotten: nil } enum nullable_status: [:single, :married] enum language: [:english, :spanish, :french], _prefix: :in enum author_visibility: [:visible, :invisible], _prefix: true 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/category.rb b/activerecord/test/models/category.rb index e8654dca01..4b2840c653 100644 --- a/activerecord/test/models/category.rb +++ b/activerecord/test/models/category.rb @@ -29,6 +29,15 @@ class Category < ActiveRecord::Base has_many :authors_with_select, -> { select "authors.*, categorizations.post_id" }, through: :categorizations, source: :author scope :general, -> { where(name: "General") } + + # Should be delegated `ast` and `locked` to `arel`. + def self.ast + raise + end + + def self.locked + raise + end end class SpecialCategory < Category diff --git a/activerecord/test/models/comment.rb b/activerecord/test/models/comment.rb index a4b81d56e0..d227f6fe86 100644 --- a/activerecord/test/models/comment.rb +++ b/activerecord/test/models/comment.rb @@ -19,6 +19,23 @@ class Comment < ActiveRecord::Base has_many :children, class_name: "Comment", foreign_key: :parent_id belongs_to :parent, class_name: "Comment", counter_cache: :children_count + class ::OopsError < RuntimeError; end + + module OopsExtension + def destroy_all(*) + raise OopsError + end + end + + default_scope { extending OopsExtension } + + scope :oops_comments, -> { extending OopsExtension } + + # Should not be called if extending modules that having the method exists on an association. + def self.greeting + raise + end + def self.what_are_you "a comment..." end diff --git a/activerecord/test/models/company.rb b/activerecord/test/models/company.rb index 025630087c..d269a95e8c 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| @@ -170,14 +170,6 @@ class Client < Company def overwrite_to_raise end - - class << self - private - - def private_method - "darkness" - end - end end class ExclusivelyDependentFirm < Company @@ -199,7 +191,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 +208,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 60af7c2247..3d40cb1ace 100644 --- a/activerecord/test/models/customer.rb +++ b/activerecord/test/models/customer.rb @@ -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/essay.rb b/activerecord/test/models/essay.rb index 13267fbc21..1f9772870e 100644 --- a/activerecord/test/models/essay.rb +++ b/activerecord/test/models/essay.rb @@ -1,4 +1,5 @@ class Essay < ActiveRecord::Base + belongs_to :author belongs_to :writer, primary_key: :name, polymorphic: true belongs_to :category, primary_key: :name has_one :owner, primary_key: :name 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/family.rb b/activerecord/test/models/family.rb new file mode 100644 index 0000000000..5ae5a78c95 --- /dev/null +++ b/activerecord/test/models/family.rb @@ -0,0 +1,4 @@ +class Family < ActiveRecord::Base + has_many :family_trees, -> { where(token: nil) } + has_many :members, through: :family_trees +end diff --git a/activerecord/test/models/family_tree.rb b/activerecord/test/models/family_tree.rb new file mode 100644 index 0000000000..cd9829fedd --- /dev/null +++ b/activerecord/test/models/family_tree.rb @@ -0,0 +1,4 @@ +class FamilyTree < ActiveRecord::Base + belongs_to :member, class_name: "User", foreign_key: "member_id" + belongs_to :family +end diff --git a/activerecord/test/models/non_primary_key.rb b/activerecord/test/models/non_primary_key.rb new file mode 100644 index 0000000000..1cafb09608 --- /dev/null +++ b/activerecord/test/models/non_primary_key.rb @@ -0,0 +1,2 @@ +class NonPrimaryKey < ActiveRecord::Base +end 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 2dc8f9bd84..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 66a99cbcda..4c913b3b72 100644 --- a/activerecord/test/models/post.rb +++ b/activerecord/test/models/post.rb @@ -13,7 +13,7 @@ class Post < ActiveRecord::Base module NamedExtension2 def greeting - "hello" + "hullo" end end @@ -47,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 @@ -59,6 +59,10 @@ class Post < ActiveRecord::Base def the_association proxy_association end + + def with_content(content) + self.detect { |comment| comment.body == content } + end end has_many :comments_with_extend, extend: NamedExtension, class_name: "Comment", foreign_key: "post_id" do @@ -168,7 +172,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 @@ -231,7 +235,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 5009f8f54b..4fbd986e40 100644 --- a/activerecord/test/models/project.rb +++ b/activerecord/test/models/project.rb @@ -11,7 +11,7 @@ class Project < ActiveRecord::Base 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" + has_and_belongs_to_many :well_paid_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/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..d9381ac9cf 100644 --- a/activerecord/test/models/topic.rb +++ b/activerecord/test/models/topic.rb @@ -14,7 +14,7 @@ class Topic < ActiveRecord::Base scope :replied, -> { where "replies_count > 0" } scope "approved_as_string", -> { where(approved: true) } - scope :anonymous_extension, -> { all } do + scope :anonymous_extension, -> {} do def one 1 end @@ -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/models/user.rb b/activerecord/test/models/user.rb index 47649e0a77..5089a795f4 100644 --- a/activerecord/test/models/user.rb +++ b/activerecord/test/models/user.rb @@ -5,8 +5,12 @@ class User < ActiveRecord::Base has_secure_token :auth_token has_and_belongs_to_many :jobs_pool, - class_name: Job, + class_name: "Job", join_table: "jobs_pool" + + has_one :family_tree, -> { where(token: nil) }, foreign_key: "member_id" + has_one :family, through: :family_tree + has_many :family_members, through: :family, source: :members end class UserWithNotification < User diff --git a/activerecord/test/schema/mysql2_specific_schema.rb b/activerecord/test/schema/mysql2_specific_schema.rb index 9a203a7293..90a314c83c 100644 --- a/activerecord/test/schema/mysql2_specific_schema.rb +++ b/activerecord/test/schema/mysql2_specific_schema.rb @@ -6,6 +6,11 @@ ActiveRecord::Schema.define do end end + create_table :timestamp_defaults, force: true do |t| + t.timestamp :nullable_timestamp + t.timestamp :modified_timestamp, default: -> { "CURRENT_TIMESTAMP" } + end + create_table :binary_fields, force: true do |t| t.binary :var_binary, limit: 255 t.binary :var_binary_large, limit: 4095 diff --git a/activerecord/test/schema/postgresql_specific_schema.rb b/activerecord/test/schema/postgresql_specific_schema.rb index f00b858ea6..e56e8fa36a 100644 --- a/activerecord/test/schema/postgresql_specific_schema.rb +++ b/activerecord/test/schema/postgresql_specific_schema.rb @@ -1,12 +1,15 @@ 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| + uuid_default = connection.supports_pgcrypto_uuid? ? {} : { default: "uuid_generate_v4()" } + + create_table :uuid_parents, id: :uuid, force: true, **uuid_default do |t| t.string :name end - create_table :uuid_children, id: :uuid, force: true do |t| + create_table :uuid_children, id: :uuid, force: true, **uuid_default do |t| t.string :name t.uuid :uuid_parent_id end @@ -22,16 +25,24 @@ ActiveRecord::Schema.define do t.string :char2, limit: 50, default: "a varchar field" t.text :char3, default: "a text field" t.bigint :bigint_default, default: -> { "0::bigint" } - t.text :multiline_default, default: '--- [] + t.text :multiline_default, default: "--- [] + +" + end -' + create_table :postgresql_times, force: true do |t| + t.interval :time_interval + t.interval :scaled_time_interval, precision: 6 end - %w(postgresql_times postgresql_oids postgresql_timestamp_with_zones - postgresql_partitioned_table postgresql_partitioned_table_parent).each do |table_name| - drop_table table_name, if_exists: true + create_table :postgresql_oids, force: true do |t| + t.oid :obj_id end + drop_table "postgresql_timestamp_with_zones", if_exists: true + drop_table "postgresql_partitioned_table", if_exists: true + drop_table "postgresql_partitioned_table_parent", if_exists: true + execute "DROP SEQUENCE IF EXISTS companies_nonstd_seq CASCADE" execute "CREATE SEQUENCE companies_nonstd_seq START 101 OWNED BY companies.id" execute "ALTER TABLE companies ALTER COLUMN id SET DEFAULT nextval('companies_nonstd_seq')" @@ -44,21 +55,6 @@ ActiveRecord::Schema.define do end execute <<_SQL - CREATE TABLE postgresql_times ( - id SERIAL PRIMARY KEY, - time_interval INTERVAL, - scaled_time_interval INTERVAL(6) - ); -_SQL - - execute <<_SQL - CREATE TABLE postgresql_oids ( - id SERIAL PRIMARY KEY, - obj_id OID - ); -_SQL - - execute <<_SQL CREATE TABLE postgresql_timestamp_with_zones ( id SERIAL PRIMARY KEY, time TIMESTAMP WITH TIME ZONE @@ -108,7 +104,7 @@ _SQL end create_table :uuid_items, force: true, id: false do |t| - t.uuid :uuid, primary_key: true + t.uuid :uuid, primary_key: true, **uuid_default t.string :title end end diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb index a4756ec75a..50f1d9bfe7 100644 --- a/activerecord/test/schema/schema.rb +++ b/activerecord/test/schema/schema.rb @@ -54,8 +54,8 @@ ActiveRecord::Schema.define do create_table :authors, force: true do |t| t.string :name, null: false - t.integer :author_address_id - t.integer :author_address_extra_id + t.references :author_address + t.references :author_address_extra t.string :organization_id t.string :owned_essay_id end @@ -88,7 +88,7 @@ ActiveRecord::Schema.define do end create_table :books, force: true do |t| - t.integer :author_id + t.references :author t.string :format t.column :name, :string t.column :status, :integer, default: 0 @@ -126,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| @@ -197,8 +200,8 @@ 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], name: "company_partial_index", where: "rating > 10" + t.index [:firm_id, :type, :rating], name: "company_index", length: { type: 10 }, order: { rating: :desc } + t.index [:firm_id, :type], name: "company_partial_index", where: "(rating > 10)" t.index :name, name: "company_name_index", using: :btree t.index "lower(name)", name: "company_expression_index" if supports_expression_index? end @@ -303,7 +306,7 @@ ActiveRecord::Schema.define do end create_table :engines, force: true do |t| - t.integer :car_id + t.references :car, index: false end create_table :entrants, force: true do |t| @@ -326,6 +329,15 @@ ActiveRecord::Schema.define do create_table :eyes, force: true do |t| end + create_table :families, force: true do |t| + end + + create_table :family_trees, force: true do |t| + t.references :family + t.references :member + t.string :token + end + create_table :funny_jokes, force: true do |t| t.string :name end @@ -401,6 +413,9 @@ ActiveRecord::Schema.define do t.string :name end + create_table :kitchens, force: true do |t| + end + create_table :legacy_things, force: true do |t| t.integer :tps_report_number t.integer :version, null: false, default: 0 @@ -436,10 +451,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 @@ -569,6 +586,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 @@ -649,8 +667,8 @@ ActiveRecord::Schema.define do end create_table :posts, force: true do |t| - t.integer :author_id - t.string :title, null: false + t.references :author + t.string :title, null: false # use VARCHAR2(4000) instead of CLOB datatype as CLOB data type has many limitations in # Oracle SELECT WHERE clause which causes many unit test failures if current_adapter?(:OracleAdapter) @@ -768,6 +786,10 @@ ActiveRecord::Schema.define do t.belongs_to :ship end + create_table :sinks, force: true do |t| + t.references :kitchen + end + create_table :shop_accounts, force: true do |t| t.references :customer t.references :customer_carrier @@ -903,7 +925,6 @@ ActiveRecord::Schema.define do 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 @@ -927,14 +948,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 @@ -1000,16 +1021,14 @@ ActiveRecord::Schema.define do create_table :records, force: true do |t| end - 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 + disable_referential_integrity do + create_table :fk_test_has_pk, primary_key: "pk_id", force: :cascade do |t| end - create_table :fk_test_has_pk, force: true, primary_key: "pk_id" do |t| + create_table :fk_test_has_fk, force: true do |t| + t.references :fk, null: false + t.foreign_key :fk_test_has_pk, column: "fk_id", name: "fk_name", primary_key: "pk_id" end - - add_foreign_key :fk_test_has_fk, :fk_test_has_pk, column: "fk_id", name: "fk_name", primary_key: "pk_id" end create_table :overloaded_types, force: true do |t| @@ -1027,6 +1046,10 @@ ActiveRecord::Schema.define do create_table :test_with_keyword_column_name, force: true do |t| t.string :desc end + + create_table :non_primary_keys, force: true, id: false do |t| + t.integer :id + end end Course.connection.create_table :courses, force: true do |t| @@ -1046,3 +1069,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/schema/sqlite_specific_schema.rb b/activerecord/test/schema/sqlite_specific_schema.rb deleted file mode 100644 index cc7c36fe2b..0000000000 --- a/activerecord/test/schema/sqlite_specific_schema.rb +++ /dev/null @@ -1,18 +0,0 @@ -ActiveRecord::Schema.define do - execute "DROP TABLE fk_test_has_fk" rescue nil - execute "DROP TABLE fk_test_has_pk" rescue nil - execute <<_SQL - CREATE TABLE 'fk_test_has_pk' ( - 'pk_id' INTEGER NOT NULL PRIMARY KEY - ); -_SQL - - execute <<_SQL - CREATE TABLE 'fk_test_has_fk' ( - 'id' INTEGER NOT NULL PRIMARY KEY, - 'fk_id' INTEGER NOT NULL, - - FOREIGN KEY ('fk_id') REFERENCES 'fk_test_has_pk'('pk_id') - ); -_SQL -end 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..1a609e13c3 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 @@ -9,7 +10,10 @@ module ARTest end def self.connection_config - config["connections"][connection_name] + config.fetch("connections").fetch(connection_name) do + puts "Connection #{connection_name.inspect} not found. Available connections: #{config['connections'].keys.join(', ')}" + exit 1 + end end def self.connect |