diff options
Diffstat (limited to 'activerecord')
182 files changed, 2158 insertions, 2428 deletions
diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index 96094a285f..1f8163db12 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,755 +1,64 @@ -* Deprecate `Migrator.schema_migrations_table_name`. +* Deprecate passing arguments and block at the same time to `count` and `sum` in `ActiveRecord::Calculations`. *Ryuta Kamizono* -* Fix select with block doesn't return newly built records in has_many association. +* Loading model schema from database is now thread-safe. - Fixes #28348. + Fixes #28589. - *Ryuta Kamizono* - -* Check whether `Rails.application` defined before calling it - - In #27674 we changed the migration generator to generate migrations at the - path defined in `Rails.application.config.paths` however the code checked - for the presence of the `Rails` constant but not the `Rails.application` - method which caused problems when using Active Record and generators outside - of the context of a Rails application. - - Fixes #28325. - - *Andrew White* - -* Fix `deserialize` with JSON array. - - Fixes #28285. - - *Ryuta Kamizono* - -* Fix `rake db:schema:load` with subdirectories. - - *Ryuta Kamizono* - -* Fix `rake db:migrate:status` with subdirectories. - - *Ryuta Kamizono* - -* Don't share options between reference id and type columns - - When using a polymorphic reference column in a migration, sharing options - between the two columns doesn't make sense since they are different types. - The `reference_id` column is usually an integer and the `reference_type` - column a string so options like `unsigned: true` will result in an invalid - table definition. - - *Ryuta Kamizono* - -* Use `max_identifier_length` for `index_name_length` in PostgreSQL adapter. - - *Ryuta Kamizono* - -* Deprecate `supports_migrations?` on connection adapters. - - *Ryuta Kamizono* - -* Fix regression of #1969 with SELECT aliases in HAVING clause. - - *Eugene Kenny* - -* Deprecate using `#quoted_id` in quoting. - - *Ryuta Kamizono* - -* Fix `wait_timeout` to configurable for mysql2 adapter. - - Fixes #26556. - - *Ryuta Kamizono* - - -## Rails 5.1.0.beta1 (February 23, 2017) ## - -* Correctly dump native timestamp types for MySQL. - - The native timestamp type in MySQL is different from datetime type. - Internal representation of the timestamp type is UNIX time, This means - that timestamp columns are affected by time zone. - - > SET time_zone = '+00:00'; - Query OK, 0 rows affected (0.00 sec) - - > INSERT INTO time_with_zone(ts,dt) VALUES (NOW(),NOW()); - Query OK, 1 row affected (0.02 sec) - - > SELECT * FROM time_with_zone; - +---------------------+---------------------+ - | ts | dt | - +---------------------+---------------------+ - | 2016-02-07 22:11:44 | 2016-02-07 22:11:44 | - +---------------------+---------------------+ - 1 row in set (0.00 sec) - - > SET time_zone = '-08:00'; - Query OK, 0 rows affected (0.00 sec) - - > SELECT * FROM time_with_zone; - +---------------------+---------------------+ - | ts | dt | - +---------------------+---------------------+ - | 2016-02-07 14:11:44 | 2016-02-07 22:11:44 | - +---------------------+---------------------+ - 1 row in set (0.00 sec) - - *Ryuta Kamizono* - -* All integer-like PKs are autoincrement unless they have an explicit default. - - *Matthew Draper* - -* Omit redundant `using: :btree` for schema dumping. - - *Ryuta Kamizono* - -* Deprecate passing `default` to `index_name_exists?`. - - *Ryuta Kamizono* - -* PostgreSQL: schema dumping support for interval and OID columns. - - *Ryuta Kamizono* - -* Deprecate `supports_primary_key?` on connection adapters since it's - been long unused and unsupported. - - *Ryuta Kamizono* - -* Make `table_name=` reset current statement cache, - so queries are not run against the previous table name. - - *namusyaka* - -* Allow `ActiveRecord::Base#as_json` to be passed a frozen Hash. - - *Isaac Betesh* - -* Fix inspection behavior when the :id column is not primary key. - - *namusyaka* - -* Deprecate locking records with unpersisted changes. - - *Marc Schütz* - -* Remove deprecated behavior that halts callbacks when the return is false. - - *Rafael Mendonça França* - -* Deprecate `ColumnDumper#migration_keys`. - - *Ryuta Kamizono* - -* Fix `association_primary_key_type` for reflections with symbol primary key. - - Fixes #27864. - - *Daniel Colson* - -* Virtual/generated column support for MySQL 5.7.5+ and MariaDB 5.2.0+. - - MySQL generated columns: https://dev.mysql.com/doc/refman/5.7/en/create-table-generated-columns.html - MariaDB virtual columns: https://mariadb.com/kb/en/mariadb/virtual-computed-columns/ - - Declare virtual columns with `t.virtual name, type: …, as: "expression"`. - Pass `stored: true` to persist the generated value (false by default). - - Example: - - create_table :generated_columns 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 - t.index :name_length # May be indexed, too! - end - - *Ryuta Kamizono* - -* Deprecate `initialize_schema_migrations_table` and `initialize_internal_metadata_table`. - - *Ryuta Kamizono* - -* Support foreign key creation for SQLite3. - - *Ryuta Kamizono* - -* Place generated migrations into the path set by `config.paths["db/migrate"]`. - - *Kevin Glowacz* - -* Raise `ActiveRecord::InvalidForeignKey` when a foreign key constraint fails on SQLite3. - - *Ryuta Kamizono* - -* Add the touch option to `#increment!` and `#decrement!`. - - *Hiroaki Izu* - -* Deprecate passing a class to the `class_name` because it eagerloads more classes than - necessary and potentially creates circular dependencies. - - *Kir Shatrov* - -* Raise error when has_many through is defined before through association. - - Fixes #26834. - - *Chris Holmes* - -* Deprecate passing `name` to `indexes`. - - *Ryuta Kamizono* - -* Remove deprecated tasks: `db:test:clone`, `db:test:clone_schema`, `db:test:clone_structure`. - - *Rafel Mendonça França* - -* Compare deserialized values for `PostgreSQL::OID::Hstore` types when - calling `ActiveRecord::Dirty#changed_in_place?`. - - Fixes #27502. - - *Jon Moss* - -* Raise `ArgumentError` when passing an `ActiveRecord::Base` instance to `.find`, - `.exists?` and `.update`. - - *Rafael Mendonça França* - -* Respect precision option for arrays of timestamps. - - Fixes #27514. - - *Sean Griffin* - -* Optimize slow model instantiation when using STI and `store_full_sti_class = false` option. - - *Konstantin Lazarev* - -* Add `touch` option to counter cache modifying methods. - - Works when updating, resetting, incrementing and decrementing counters: - - # Touches `updated_at`/`updated_on`. - Topic.increment_counter(:messages_count, 1, touch: true) - Topic.decrement_counter(:messages_count, 1, touch: true) - - # Touches `last_discussed_at`. - Topic.reset_counters(18, :messages, touch: :last_discussed_at) - - # Touches `updated_at` and `last_discussed_at`. - Topic.update_counters(18, messages_count: 5, touch: %i( updated_at last_discussed_at )) - - Fixes #26724. - - *Jarred Trost* - -* Remove deprecated `#uniq`, `#uniq!`, and `#uniq_value`. - - *Ryuta Kamizono* - -* Remove deprecated `#insert_sql`, `#update_sql`, and `#delete_sql`. - - *Ryuta Kamizono* - -* Remove deprecated `#use_transactional_fixtures` configuration. - - *Rafael Mendonça França* - -* Remove deprecated `#raise_in_transactional_callbacks` configuration. - - *Rafael Mendonça França* - -* Remove deprecated `#load_schema_for`. - - *Rafael Mendonça França* - -* Remove deprecated conditions parameter from `#destroy_all` and `#delete_all`. - - *Rafael Mendonça França* - -* Remove deprecated support to passing arguments to `#select` when a block is provided. - - *Rafael Mendonça França* - -* Remove deprecated support to query using commas on LIMIT. - - *Rafael Mendonça França* - -* Remove deprecated support to passing a class as a value in a query. - - *Rafael Mendonça França* - -* Raise `ActiveRecord::IrreversibleOrderError` when using `last` with an irreversible - order. - - *Rafael Mendonça França* - -* Raise when a `has_many :through` association has an ambiguous reflection name. - - *Rafael Mendonça França* - -* Raise when `ActiveRecord::Migration` is inherited from directly. - - *Rafael Mendonça França* - -* Remove deprecated `original_exception` argument in `ActiveRecord::StatementInvalid#initialize` - and `ActiveRecord::StatementInvalid#original_exception`. - - *Rafael Mendonça França* - -* `#tables` and `#table_exists?` return only tables and not views. - - All the deprecations on those methods were removed. - - *Rafael Mendonça França* - -* Remove deprecated `name` argument from `#tables`. - - *Rafael Mendonça França* - -* Remove deprecated support to passing a column to `#quote`. - - *Rafael Mendonça França* + *Vikrant Chaudhary*, *David Abdemoulaie* -* Set `:time` as a timezone aware type and remove deprecation when - `config.active_record.time_zone_aware_types` is not explicitly set. +* 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. - *Rafael Mendonça França* + 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+ -* Remove deprecated force reload argument in singular and collection association readers. + *DHH* - *Rafael Mendonça França* +* Respect `SchemaDumper.ignore_tables` in rake tasks for databases structure dump -* Remove deprecated `activerecord.errors.messages.restrict_dependent_destroy.one` and - `activerecord.errors.messages.restrict_dependent_destroy.many` i18n scopes. + *Rusty Geldmacher*, *Guillermo Iguaran* - *Rafael Mendonça França* +* Add type caster to `RuntimeReflection#alias_name` -* Allow passing extra flags to `db:structure:load` and `db:structure:dump` - - Introduces `ActiveRecord::Tasks::DatabaseTasks.structure_(load|dump)_flags` to customize the - eventual commands run against the database, e.g. mysqldump/pg_dump. - - *Kir Shatrov* - -* Notifications see frozen SQL string. - - Fixes #23774. - - *Richard Monette* - -* RuntimeErrors are no longer translated to `ActiveRecord::StatementInvalid`. - - *Richard Monette* - -* Change the schema cache format to use YAML instead of Marshal. - - *Kir Shatrov* - -* Support index length and order options using both string and symbol - column names. - - Fixes #27243. - - *Ryuta Kamizono* - -* Raise `ActiveRecord::RangeError` when values that executed are out of range. - - *Ryuta Kamizono* - -* Raise `ActiveRecord::NotNullViolation` when a record cannot be inserted - or updated because it would violate a not null constraint. - - *Ryuta Kamizono* - -* Emulate db trigger behaviour for after_commit :destroy, :update. - - Race conditions can occur when an ActiveRecord is destroyed - twice or destroyed and updated. The callbacks should only be - triggered once, similar to a SQL database trigger. - - *Stefan Budeanu* - -* Moved `DecimalWithoutScale`, `Text`, and `UnsignedInteger` from Active Model to Active Record. - - *Iain Beeston* - -* Fix `write_attribute` method to check whether an attribute is aliased or not, and - use the aliased attribute name if needed. - - *Prathamesh Sonpatki* - -* Fix `read_attribute` method to check whether an attribute is aliased or not, and - use the aliased attribute name if needed. - - Fixes #26417. - - *Prathamesh Sonpatki* - -* PostgreSQL & MySQL: Use big integer as primary key type for new tables. - - *Jon McCartie*, *Pavel Pravosud* - -* Change the type argument of `ActiveRecord::Base#attribute` to be optional. - The default is now `ActiveRecord::Type::Value.new`, which provides no type - casting behavior. - - *Sean Griffin* - -* Don't treat unsigned integers with zerofill as signed. - - Fixes #27125. - - *Ryuta Kamizono* - -* Fix the uniqueness validation scope with a polymorphic association. - - *Sergey Alekseev* - -* Raise `ActiveRecord::RecordNotFound` from collection `*_ids` setters - for unknown IDs with a better error message. - - Changes the collection `*_ids` setters to cast provided IDs the data - type of the primary key set in the association, not the model - primary key. - - *Dominic Cleal* - -* For PostgreSQL >= 9.4 use `pgcrypto`'s `gen_random_uuid()` instead of - `uuid-ossp`'s UUID generation function. - - *Yuji Yaginuma*, *Yaw Boakye* - -* Introduce `Model#reload_<association>` to bring back the behavior - of `Article.category(true)` where `category` is a singular - association. - - The force reloading of the association reader was deprecated - in #20888. Unfortunately the suggested alternative of - `article.reload.category` does not expose the same behavior. - - This patch adds a reader method with the prefix `reload_` for - singular associations. This method has the same semantics as - passing true to the association reader used to have. - - *Yves Senn* - -* Make sure eager loading `ActiveRecord::Associations` also loads - constants defined in `ActiveRecord::Associations::Preloader`. - - *Yves Senn* - -* Allow `ActionController::Parameters`-like objects to be passed as - values for Postgres HStore columns. - - Fixes #26904. + Fixes #28959. *Jon Moss* -* Added `stat` method to `ActiveRecord::ConnectionAdapters::ConnectionPool`. - - Example: - - ActiveRecord::Base.connection_pool.stat # => - { size: 15, connections: 1, busy: 1, dead: 0, idle: 0, waiting: 0, checkout_timeout: 5 } - - *Pavel Evstigneev* - -* Avoid `unscope(:order)` when `limit_value` is presented for `count` - and `exists?`. - - If `limit_value` is presented, records fetching order is very important - for performance. We should not unscope the order in the case. +* Deprecate `supports_statement_cache?`. *Ryuta Kamizono* -* Fix an Active Record `DateTime` field `NoMethodError` caused by incomplete - datetime. - - Fixes #24195. - - *Sen Zhang* - -* Allow `slice` to take an array of methods(without the need for splatting). +* Quote database name in `db:create` grant statement (when database user does not have access to create the database). - *Cohen Carlisle* + *Rune Philosof* -* Improved partial writes with HABTM and has many through associations - to fire database query only if relation has been changed. - - Fixes #19663. - - *Mehmet Emin İNAÇ* - -* Deprecate passing arguments and block at the same time to - `ActiveRecord::QueryMethods#select`. - - *Prathamesh Sonpatki* - -* Optimistic locking: Added ability to update `locking_column` value. - Ignore optimistic locking if trying to update with new `locking_column` value. +* Raise error `UnknownMigrationVersionError` on the movement of migrations + when the current migration does not exist. *bogdanvlviv* -* Fixed: Optimistic locking does not work well with `null` in the database. - - Fixes #26024. +* Fix `bin/rails db:forward` first migration. *bogdanvlviv* -* Fixed support for case insensitive comparisons of `text` columns in - PostgreSQL. - - *Edho Arief* - -* Serialize JSON attribute value `nil` as SQL `NULL`, not JSON `null`. - - *Trung Duc Tran* - -* Return `true` from `update_attribute` when the value of the attribute - to be updated is unchanged. - - Fixes #26593. - - *Prathamesh Sonpatki* - -* Always store errors details information with symbols. - - When the association is autosaved we were storing the details with - string keys. This was creating inconsistency with other details that are - added using the `Errors#add` method. It was also inconsistent with the - `Errors#messages` storage. - - To fix this inconsistency we are always storing with symbols. This will - cause a small breaking change because in those cases the details could - be accessed as strings keys but now it can not. - - Fix #26499. - - *Rafael Mendonça França*, *Marcus Vieira* - -* Calling `touch` on a model using optimistic locking will now leave the model - in a non-dirty state with no attribute changes. - - Fixes #26496. +* Support Descending Indexes for MySQL. - *Jakob Skjerning* - -* Using a mysql2 connection after it fails to reconnect will now have an error message - saying the connection is closed rather than an undefined method error message. - - *Dylan Thacker-Smith* - -* PostgreSQL array columns will now respect the encoding of strings contained - in the array. - - Fixes #26326. - - *Sean Griffin* - -* Inverse association instances will now be set before `after_find` or - `after_initialize` callbacks are run. - - Fixes #26320. - - *Sean Griffin* - -* Remove unnecessarily association load when a `belongs_to` association has already been - loaded then the foreign key is changed directly and the record saved. - - *James Coleman* - -* Remove standardized column types/arguments spaces in schema dump. - - *Tim Petricola* - -* Avoid loading records from database when they are already loaded using - the `pluck` method on a collection. - - Fixes #25921. - - *Ryuta Kamizono* - -* Remove text default treated as an empty string in non-strict mode for - consistency with other types. - - Strict mode controls how MySQL handles invalid or missing values in - data-change statements such as INSERT or UPDATE. If strict mode is not - in effect, MySQL inserts adjusted values for invalid or missing values - and produces warnings. - - def test_mysql_not_null_defaults_non_strict - using_strict(false) do - with_mysql_not_null_table do |klass| - record = klass.new - assert_nil record.non_null_integer - assert_nil record.non_null_string - assert_nil record.non_null_text - assert_nil record.non_null_blob - - record.save! - record.reload - - assert_equal 0, record.non_null_integer - assert_equal "", record.non_null_string - assert_equal "", record.non_null_text - assert_equal "", record.non_null_blob - end - end - end - - https://dev.mysql.com/doc/refman/5.7/en/sql-mode.html#sql-mode-strict - - *Ryuta Kamizono* - -* SQLite3 migrations to add a column to an existing table can now be - successfully rolled back when the column was given and invalid column - type. - - Fixes #26087. - - *Travis O'Neill* - -* Deprecate `sanitize_conditions`. Use `sanitize_sql` instead. + 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* +* Fix inconsistency with changed attributes when overriding AR attribute reader. -* 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. - - Fixes #25504. - - *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* + *bogdanvlviv* -* 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/Rakefile b/activerecord/Rakefile index 7be3d851f1..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 = [] @@ -134,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 0b37e9076c..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 diff --git a/activerecord/lib/active_record.rb b/activerecord/lib/active_record.rb index 96b8545dfc..29f49c6195 100644 --- a/activerecord/lib/active_record.rb +++ b/activerecord/lib/active_record.rb @@ -177,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/associations.rb b/activerecord/lib/active_record/associations.rb index 4606c91ffd..7c37132d3a 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -1276,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 # @@ -1443,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 # @@ -1573,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 # @@ -1647,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" @@ -1660,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 @@ -1765,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 # diff --git a/activerecord/lib/active_record/associations/association.rb b/activerecord/lib/active_record/associations/association.rb index 1cb2b2d7c6..6b13e6936f 100644 --- a/activerecord/lib/active_record/associations/association.rb +++ b/activerecord/lib/active_record/associations/association.rb @@ -152,14 +152,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)] } 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 a1609ab0fb..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) @@ -118,6 +119,12 @@ module ActiveRecord::Associations::Builder # :nodoc: model.after_destroy callback.(:changes_to_save) end + 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) model.after_destroy lambda { |o| o.association(reflection.name).handle_dependency } end diff --git a/activerecord/lib/active_record/associations/collection_association.rb b/activerecord/lib/active_record/associations/collection_association.rb index 77282e6463..62c944fce3 100644 --- a/activerecord/lib/active_record/associations/collection_association.rb +++ b/activerecord/lib/active_record/associations/collection_association.rb @@ -280,35 +280,6 @@ 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 - - begin - if index - record_was = target[index] - target[index] = record - else - target << record - end - - set_inverse_instance(record) - - yield(record) if block_given? - rescue - if index - target[index] = record_was - else - target.delete(record) - end - - raise - end - - callback(:after_add, record) unless skip_callbacks - - record - end - def scope scope = super scope.none! if null_scope? @@ -385,15 +356,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 @@ -448,19 +423,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 bc2f359c65..74a4d515c2 100644 --- a/activerecord/lib/active_record/associations/collection_proxy.rb +++ b/activerecord/lib/active_record/associations/collection_proxy.rb @@ -743,10 +743,6 @@ module ActiveRecord # # ] #-- - def uniq - load_target.uniq - end - def calculate(operation, column_name) null_scope? ? scope.calculate(operation, column_name) : super end @@ -1125,6 +1121,19 @@ module ActiveRecord delegate(*delegate_methods, to: :scope) + module DelegateExtending # :nodoc: + private + def method_missing(method, *args, &block) + extending_values = association_scope.extending_values + if extending_values.any? && (extending_values - self.class.included_modules).any? + self.class.include(*extending_values) + public_send(method, *args, &block) + else + super + end + end + end + private def find_nth_with_limit(index, limit) @@ -1145,20 +1154,16 @@ module ActiveRecord @association.find_from_target? end + def association_scope + @association.association_scope + end + def exec_queries load_target end def respond_to_missing?(method, _) - scope.respond_to?(method) || super - end - - def method_missing(method, *args, &block) - if scope.respond_to?(method) - scope.public_send(method, *args, &block) - else - super - end + association_scope.respond_to?(method) || super end end end diff --git a/activerecord/lib/active_record/associations/has_many_association.rb b/activerecord/lib/active_record/associations/has_many_association.rb index b413eb3f9c..10ca0e47ff 100644 --- a/activerecord/lib/active_record/associations/has_many_association.rb +++ b/activerecord/lib/active_record/associations/has_many_association.rb @@ -31,12 +31,7 @@ module ActiveRecord def insert_record(record, validate = true, raise = false) set_owner_attributes(record) - - if raise - record.save!(validate: validate) - else - record.save(validate: validate) - end + super end def empty? 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 c4a7fe4432..53ffb3b68d 100644 --- a/activerecord/lib/active_record/associations/has_many_through_association.rb +++ b/activerecord/lib/active_record/associations/has_many_through_association.rb @@ -39,11 +39,7 @@ module ActiveRecord ensure_not_nested if record.new_record? || record.has_changes_to_save? - if raise - record.save!(validate: validate) - else - return unless record.save(validate: validate) - end + return unless super end save_through_record(record) diff --git a/activerecord/lib/active_record/associations/join_dependency.rb b/activerecord/lib/active_record/associations/join_dependency.rb index 8995b1e352..643226267c 100644 --- a/activerecord/lib/active_record/associations/join_dependency.rb +++ b/activerecord/lib/active_record/associations/join_dependency.rb @@ -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| @@ -175,27 +170,15 @@ module ActiveRecord 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) 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/through_association.rb b/activerecord/lib/active_record/associations/preloader/through_association.rb index 9d44a02021..d53d3f777b 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, @@ -43,9 +41,7 @@ module ActiveRecord 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. @@ -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/attribute_methods.rb b/activerecord/lib/active_record/attribute_methods.rb index ebe06566cc..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: diff --git a/activerecord/lib/active_record/attribute_methods/dirty.rb b/activerecord/lib/active_record/attribute_methods/dirty.rb index 6aa414ba6b..bd5003d63a 100644 --- a/activerecord/lib/active_record/attribute_methods/dirty.rb +++ b/activerecord/lib/active_record/attribute_methods/dirty.rb @@ -282,7 +282,7 @@ module ActiveRecord #{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. + by Active Record, add `attribute :#{attr_name}` to your class. EOW mutations_from_database.deprecated_force_change(attr_name) end diff --git a/activerecord/lib/active_record/attribute_set.rb b/activerecord/lib/active_record/attribute_set.rb index 66b278219a..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 diff --git a/activerecord/lib/active_record/autosave_association.rb b/activerecord/lib/active_record/autosave_association.rb index 6bccbc06cd..829a4f6e86 100644 --- a/activerecord/lib/active_record/autosave_association.rb +++ b/activerecord/lib/active_record/autosave_association.rb @@ -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 @@ -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 @@ -384,7 +382,7 @@ module ActiveRecord autosave = reflection.options[:autosave] # reconstruct the scope now that we know the owner's id - association.reset_scope if association.respond_to?(:reset_scope) + association.reset_scope if records = associated_records_to_validate_or_save(association, @new_record_before_save, autosave) if autosave 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 3f2e86a98d..61bf5477aa 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb @@ -506,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 @@ -825,7 +827,7 @@ module ActiveRecord # end # # class Book < ActiveRecord::Base - # establish_connection "library_db" + # establish_connection :library_db # end # # class ScaryBook < Book @@ -857,9 +859,9 @@ 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). 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 769f488469..c6811a4802 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb @@ -137,9 +137,10 @@ module ActiveRecord # 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. diff --git a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb index e5a24b2aca..61233dcc51 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb @@ -10,8 +10,15 @@ module ActiveRecord value = id_value_for_database(value) if value.is_a?(Base) if value.respond_to?(:quoted_id) + at = value.method(:quoted_id).source_location + at &&= " at %s:%d" % at + + owner = value.method(:quoted_id).owner.to_s + klass = value.class.to_s + klass += "(#{owner})" unless owner == klass + ActiveSupport::Deprecation.warn \ - "Using #quoted_id is deprecated and will be removed in Rails 5.2." + "Defining #quoted_id is deprecated and will be ignored in Rails 5.2. (defined on #{klass}#{at})" return value.quoted_id end @@ -61,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) @@ -152,11 +148,18 @@ module ActiveRecord "'#{quote_string(value.to_s)}'" end - private - - 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 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 c48a4acff8..a4fecc4a8e 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb @@ -133,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 4682afc188..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,8 +2,33 @@ 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 - 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 @@ -121,7 +146,7 @@ module ActiveRecord end def polymorphic_options - as_options(polymorphic) + as_options(polymorphic).merge(null: options[:null]) end def index_options 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 1e826ff5ad..16a398f631 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb @@ -105,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. @@ -974,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 @@ -1003,19 +995,6 @@ module ActiveRecord insert_versions_sql(versions) end - def insert_versions_sql(versions) # :nodoc: - 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 initialize_schema_migrations_table # :nodoc: ActiveRecord::SchemaMigration.create_table end @@ -1261,6 +1240,10 @@ module ActiveRecord end end + def schema_creation + SchemaCreation.new(self) + end + def create_table_definition(*args) TableDefinition.new(*args) end @@ -1269,6 +1252,17 @@ module ActiveRecord AlterTable.new create_table_definition(name) 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 + def index_column_names(column_names) if column_names.is_a?(String) && /\W/.match?(column_names) column_names @@ -1286,13 +1280,32 @@ module ActiveRecord end def foreign_key_name(table_name, options) - identifier = "#{table_name}_#{options.fetch(:column)}_fk" - hashed_identifier = Digest::SHA256.hexdigest(identifier).first(10) options.fetch(:name) do + identifier = "#{table_name}_#{options.fetch(:column)}_fk" + hashed_identifier = Digest::SHA256.hexdigest(identifier).first(10) + "fk_rails_#{hashed_identifier}" end end + 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 @@ -1313,6 +1326,19 @@ module ActiveRecord 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 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 ef1d9f81a9..85d6fbe8b3 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb @@ -74,7 +74,7 @@ module ActiveRecord SIMPLE_INT = /\A\d+\z/ 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() @@ -142,34 +140,10 @@ 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) # :nodoc: !native_database_types[type].nil? end - def schema_creation - SchemaCreation.new self - end - - # Returns an array of +Column+ objects for the table specified by +table_name+. - def columns(table_name) # :nodoc: - table_name = table_name.to_s - column_definitions(table_name).map do |field| - new_column_from_field(table_name, field) - end - end - # this method must only be called while holding connection pool's mutex def lease if in_use? @@ -447,7 +421,7 @@ module ActiveRecord # 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. @@ -483,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 @@ -637,6 +603,18 @@ module ActiveRecord 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 55ec112c17..abc15f595f 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb @@ -22,14 +22,6 @@ module ActiveRecord 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> @@ -56,9 +48,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 @@ -75,14 +64,6 @@ module ActiveRecord 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 @@ -95,16 +76,8 @@ module ActiveRecord 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? @@ -171,10 +144,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: @@ -316,46 +285,6 @@ module ActiveRecord execute "TRUNCATE TABLE #{quote_table_name(table_name)}", name end - # Returns an array of indexes for the given table. - def indexes(table_name, name = nil) #:nodoc: - if name - ActiveSupport::Deprecation.warn(<<-MSG.squish) - Passing name to #indexes is deprecated without replacement. - MSG - end - - indexes = [] - current_index = nil - execute_and_free("SHOW KEYS FROM #{quote_table_name(table_name)}", "SCHEMA") do |result| - 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.merge!(row[:Column_name] => row[:Sub_part].to_i) if row[:Sub_part] - end - end - - indexes - end - - def new_column_from_field(table_name, field) # :nodoc: - type_metadata = fetch_type_metadata(field[:Type], field[:Extra]) - if type_metadata.type == :datetime && field[:Default] == "CURRENT_TIMESTAMP" - default, default_function = nil, field[:Default] - else - default, default_function = field[:Default], nil - end - new_column(field[:Field], default, type_metadata, field[:Null] == "YES", table_name, default_function, field[:Collation], comment: field[:Comment].presence) - end - def table_comment(table_name) # :nodoc: scope = quoted_scope(table_name) @@ -658,10 +587,6 @@ module ActiveRecord 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 @@ -789,16 +714,14 @@ module ActiveRecord # 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? @@ -864,19 +787,12 @@ 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 mismatched_foreign_key(message) 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 3e0afd9761..e2ba0ba1a0 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql/schema_dumper.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql/schema_dumper.rb @@ -55,13 +55,14 @@ module ActiveRecord 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)} AS \((?<expression>.+?)\) #{column.extra}/m =~ create_table_info + 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 = #{quote(@config[:database])}" \ - " AND table_name = #{quote(column.table_name)}" \ + " WHERE table_schema = #{scope[:schema]}" \ + " AND table_name = #{scope[:name]}" \ " AND column_name = #{quote(column.name)}" select_value(sql, "SCHEMA").inspect end diff --git a/activerecord/lib/active_record/connection_adapters/mysql/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/mysql/schema_statements.rb index 10c8bd179a..f9e1e046ea 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql/schema_statements.rb @@ -2,7 +2,96 @@ 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) diff --git a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb index 45e400b75b..af55cfe2f6 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb @@ -10,8 +10,6 @@ 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 config[:flags].kind_of? Array 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/quoting.rb b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb index 6663448a99..da8d0c6992 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. @@ -77,6 +77,9 @@ 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 @@ -105,7 +108,7 @@ 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 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 a332375b78..3eff9e2f83 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb @@ -140,27 +140,17 @@ module ActiveRecord ] end - IndexDefinition.new(table_name, index_name, unique, columns, [], orders, where, nil, using.to_sym, comment.presence) - end.compact - end - - def new_column_from_field(table_name, field) # :nondoc: - column_name, type, default, notnull, oid, fmod, collation, comment = field - oid = oid.to_i - fmod = fmod.to_i - type_metadata = fetch_type_metadata(column_name, type, oid, fmod) - default_value = extract_value_from_default(default) - default_function = extract_default_function(default_value, default) - PostgreSQLColumn.new( - column_name, - default_value, - type_metadata, - !notnull, - table_name, - default_function, - collation, - comment: comment.presence - ) + IndexDefinition.new( + table_name, + index_name, + unique, + columns, + orders: orders, + where: where, + using: using.to_sym, + comment: comment.presence + ) + end end def table_options(table_name) # :nodoc: @@ -196,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. @@ -362,16 +352,18 @@ module ActiveRecord end def primary_keys(table_name) # :nodoc: - scope = quoted_scope(table_name) select_values(<<-SQL.strip_heredoc, "SCHEMA") - SELECT column_name - FROM information_schema.key_column_usage kcu - JOIN information_schema.table_constraints tc - USING (table_schema, table_name, constraint_name) - WHERE constraint_type = 'PRIMARY KEY' - AND kcu.table_name = #{scope[:name]} - AND kcu.table_schema = #{scope[:schema]} - ORDER BY kcu.ordinal_position + 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 @@ -538,14 +530,6 @@ module ActiveRecord end end - def extract_foreign_key_action(specifier) # :nodoc: - case specifier - when "c"; :cascade - when "n"; :nullify - when "r"; :restrict - end - end - # Maps logical Rails types to PostgreSQL-specific data types. def type_to_sql(type, limit: nil, precision: nil, scale: nil, array: nil, **) # :nodoc: sql = \ @@ -593,19 +577,53 @@ 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 diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/utils.rb b/activerecord/lib/active_record/connection_adapters/postgresql/utils.rb index a3f9ce6d64..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 diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index 22c37abb78..f74f966fd9 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -29,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 @@ -121,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 @@ -201,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 @@ -247,34 +233,42 @@ 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: @@ -301,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 @@ -376,11 +370,6 @@ module ActiveRecord 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 @@ -414,7 +403,7 @@ 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 @@ -560,7 +549,7 @@ module ActiveRecord end def has_default_function?(default_value, default) - !default_value && (%r{\w+\(.*\)|\(.*\)::\w+} === default) + !default_value && %r{\w+\(.*\)|\(.*\)::\w+|CURRENT_DATE|CURRENT_TIMESTAMP}.match?(default) end def load_additional_types(type_map, oids = nil) @@ -651,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 @@ -690,7 +679,7 @@ module ActiveRecord # 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") @@ -777,8 +766,8 @@ module ActiveRecord $1.strip if $1 end - def create_table_definition(*args) - PostgreSQL::TableDefinition.new(*args) + def arel_visitor + Arel::Visitors::PostgreSQL.new(self) end def can_perform_case_insensitive_comparison_for?(column) diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/sqlite3/schema_statements.rb index 4ba245a0d8..e02491edb6 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3/schema_statements.rb @@ -2,7 +2,67 @@ 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'" diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb index d24bfc0c93..7233325d5a 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb @@ -84,14 +84,6 @@ module ActiveRecord SQLite3::Table.new(table_name, base) end - def schema_creation # :nodoc: - SQLite3::SchemaCreation.new self - end - - def arel_visitor # :nodoc: - Arel::Visitors::SQLite.new(self) - end - def initialize(connection, logger, connection_options, config) super(connection, logger, config) @@ -113,12 +105,6 @@ module ActiveRecord sqlite_version >= "3.8.0" end - # Returns true, since this connection adapter supports prepared statement - # caching. - def supports_statement_cache? - true - end - def requires_reloading? true end @@ -267,53 +253,6 @@ module ActiveRecord # SCHEMA STATEMENTS ======================================== - def new_column_from_field(table_name, field) # :nondoc: - case field["dflt_value"] - when /^null$/i - field["dflt_value"] = nil - when /^'(.*)'$/m - field["dflt_value"] = $1.gsub("''", "'") - when /^"(.*)"$/m - field["dflt_value"] = $1.gsub('""', '"') - end - - collation = field["collation"] - sql_type = field["type"] - type_metadata = fetch_type_metadata(sql_type) - new_column(field["name"], field["dflt_value"], type_metadata, field["notnull"].to_i == 0, table_name, nil, collation) - end - - # Returns an array of indexes for the given table. - def indexes(table_name, name = nil) #:nodoc: - if name - ActiveSupport::Deprecation.warn(<<-MSG.squish) - Passing name to #indexes is deprecated without replacement. - MSG - end - - exec_query("PRAGMA index_list(#{quote_table_name(table_name)})", "SCHEMA").map do |row| - sql = <<-SQL - SELECT sql - 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"] } @@ -559,16 +498,8 @@ module ActiveRecord end end - def create_table_definition(*args) - SQLite3::TableDefinition.new(*args) - end - - def extract_foreign_key_action(specifier) - case specifier - when "CASCADE"; :cascade - when "SET NULL"; :nullify - when "RESTRICT"; :restrict - end + def arel_visitor + Arel::Visitors::SQLite.new(self) end def configure_connection 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/dynamic_matchers.rb b/activerecord/lib/active_record/dynamic_matchers.rb index 08d42f3dd4..3a9625092e 100644 --- a/activerecord/lib/active_record/dynamic_matchers.rb +++ b/activerecord/lib/active_record/dynamic_matchers.rb @@ -1,16 +1,14 @@ - 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 0ab03b2ab3..496abfc5d9 100644 --- a/activerecord/lib/active_record/enum.rb +++ b/activerecord/lib/active_record/enum.rb @@ -154,11 +154,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}=") @@ -170,7 +171,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 @@ -182,23 +183,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/fixtures.rb b/activerecord/lib/active_record/fixtures.rb index e79167d568..c19216702c 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: # @@ -909,6 +928,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] ||= {} @@ -923,7 +944,7 @@ module ActiveRecord end end - instances.size == 1 ? instances.first : instances + return_single_record ? instances.first : instances end private accessor_name end diff --git a/activerecord/lib/active_record/gem_version.rb b/activerecord/lib/active_record/gem_version.rb index 174f716152..1a937dbcf7 100644 --- a/activerecord/lib/active_record/gem_version.rb +++ b/activerecord/lib/active_record/gem_version.rb @@ -6,9 +6,9 @@ module ActiveRecord module VERSION MAJOR = 5 - MINOR = 1 + MINOR = 2 TINY = 0 - PRE = "beta1" + PRE = "alpha" STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".") end diff --git a/activerecord/lib/active_record/inheritance.rb b/activerecord/lib/active_record/inheritance.rb index fbdaeaae51..236a65eba7 100644 --- a/activerecord/lib/active_record/inheritance.rb +++ b/activerecord/lib/active_record/inheritance.rb @@ -217,7 +217,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 8e71b60b29..32362fa86f 100644 --- a/activerecord/lib/active_record/integration.rb +++ b/activerecord/lib/active_record/integration.rb @@ -7,12 +7,21 @@ 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 + + ## + # :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 + self.cache_versioning = false end # Returns a +String+, which Action Pack uses for constructing a URL to this @@ -42,35 +51,65 @@ 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) if new_record? "#{model_name.cache_key}/new" else - timestamp = if timestamp_names.any? - max_updated_column_timestamp(timestamp_names) + if cache_version && timestamp_names.none? + "#{model_name.cache_key}/#{id}" else - max_updated_column_timestamp - end + 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 - if timestamp - timestamp = timestamp.utc.to_s(cache_timestamp_format) - "#{model_name.cache_key}/#{id}-#{timestamp}" - else - "#{model_name.cache_key}/#{id}" + 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 + module ClassMethods # Defines your model's +to_param+ method to generate "pretty" URLs # using +method_name+, which can be any attribute or method that diff --git a/activerecord/lib/active_record/locking/optimistic.rb b/activerecord/lib/active_record/locking/optimistic.rb index 2659c60f1f..78ce9f8291 100644 --- a/activerecord/lib/active_record/locking/optimistic.rb +++ b/activerecord/lib/active_record/locking/optimistic.rb @@ -47,8 +47,6 @@ module ActiveRecord # self.locking_column = :lock_person # end # - # Please note that the optimistic locking will be ignored if you update the - # locking column's value. module Optimistic extend ActiveSupport::Concern @@ -80,13 +78,11 @@ module ActiveRecord def _update_record(attribute_names = self.attribute_names) return super unless locking_enabled? - - lock_col = self.class.locking_column - - return super if attribute_names.include?(lock_col) return 0 if attribute_names.empty? begin + lock_col = self.class.locking_column + previous_lock_value = read_attribute_before_type_cast(lock_col) increment_lock diff --git a/activerecord/lib/active_record/log_subscriber.rb b/activerecord/lib/active_record/log_subscriber.rb index ea101946f4..2297c77835 100644 --- a/activerecord/lib/active_record/log_subscriber.rb +++ b/activerecord/lib/active_record/log_subscriber.rb @@ -44,17 +44,17 @@ module ActiveRecord private def type_casted_binds(binds, casted_binds) - casted_binds || binds.map { |attr| type_cast attr.value_for_database } + casted_binds || ActiveRecord::Base.connection.type_casted_binds(binds) 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 + 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.name, value] + [attr && attr.name, value] end def colorize_payload_name(name, payload_name) @@ -89,10 +89,6 @@ module ActiveRecord def logger ActiveRecord::Base.logger end - - def type_cast(value) - ActiveRecord::Base.connection.type_cast(value) - end end end diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb index 4e1df1432c..51c82f4ced 100644 --- a/activerecord/lib/active_record/migration.rb +++ b/activerecord/lib/active_record/migration.rb @@ -1104,13 +1104,21 @@ module ActiveRecord 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 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 85032ce470..188dd0acef 100644 --- a/activerecord/lib/active_record/migration/compatibility.rb +++ b/activerecord/lib/active_record/migration/compatibility.rb @@ -11,7 +11,10 @@ module ActiveRecord const_get(name) end - V5_1 = Current + V5_2 = Current + + class V5_1 < V5_2 + end class V5_0 < V5_1 module TableDefinition diff --git a/activerecord/lib/active_record/model_schema.rb b/activerecord/lib/active_record/model_schema.rb index 54216caaaf..013562708c 100644 --- a/activerecord/lib/active_record/model_schema.rb +++ b/activerecord/lib/active_record/model_schema.rb @@ -1,3 +1,5 @@ +require "monitor" + module ActiveRecord module ModelSchema extend ActiveSupport::Concern @@ -152,6 +154,8 @@ module ActiveRecord 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 @@ -377,7 +381,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: @@ -435,15 +439,27 @@ module ActiveRecord 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 @@ -457,6 +473,8 @@ module ActiveRecord user_provided_default: false ) end + + @schema_loaded = true end def reload_schema_from_cache @@ -466,10 +484,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 01ecd79b8f..3f39fb84e8 100644 --- a/activerecord/lib/active_record/nested_attributes.rb +++ b/activerecord/lib/active_record/nested_attributes.rb @@ -458,7 +458,7 @@ module ActiveRecord 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) diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb index 7ceb7d1a55..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 @@ -150,8 +158,8 @@ module ActiveRecord # being updated. # # Unless an error is raised, returns true. - def save!(*args) - create_or_update(*args) || raise(RecordNotSaved.new("Failed to save the record", self)) + 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 @@ -550,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 @@ -567,6 +575,9 @@ module ActiveRecord 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 @@ -579,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/querying.rb b/activerecord/lib/active_record/querying.rb index 36689f6559..c4a22398f0 100644 --- a/activerecord/lib/active_record/querying.rb +++ b/activerecord/lib/active_record/querying.rb @@ -9,7 +9,7 @@ module ActiveRecord 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 + :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 0276d41494..73518ca144 100644 --- a/activerecord/lib/active_record/railtie.rb +++ b/activerecord/lib/active_record/railtie.rb @@ -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/databases.rake b/activerecord/lib/active_record/railties/databases.rake index 1c7206aca4..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 diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb index 24ca8b0be4..65fdbc2fe4 100644 --- a/activerecord/lib/active_record/reflection.rb +++ b/activerecord/lib/active_record/reflection.rb @@ -199,7 +199,7 @@ module ActiveRecord def klass_join_scope(table, predicate_builder) # :nodoc: if klass.current_scope klass.current_scope.clone.tap { |scope| - scope.joins_values = [] + scope.joins_values = scope.left_outer_joins_values = [].freeze } else relation = ActiveRecord::Relation.create( @@ -212,7 +212,7 @@ module ActiveRecord end def constraints - chain.map(&:scopes).flatten + chain.flat_map(&:scopes) end def counter_cache_column @@ -1105,7 +1105,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 2d6b21bec5..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 @@ -269,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. @@ -404,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]) } @@ -635,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(', ')}]>" diff --git a/activerecord/lib/active_record/relation/batches.rb b/activerecord/lib/active_record/relation/batches.rb index 76031515fd..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 diff --git a/activerecord/lib/active_record/relation/calculations.rb b/activerecord/lib/active_record/relation/calculations.rb index 9cabd1af13..c562f214c9 100644 --- a/activerecord/lib/active_record/relation/calculations.rb +++ b/activerecord/lib/active_record/relation/calculations.rb @@ -37,7 +37,16 @@ module ActiveRecord # Note: not all valid {Relation#select}[rdoc-ref:QueryMethods#select] expressions are valid #count expressions. The specifics differ # between databases. In invalid cases, an error from the database is thrown. def count(column_name = nil) - return super() if block_given? + if block_given? + 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 @@ -73,7 +82,16 @@ module ActiveRecord # # Person.sum(:age) # => 4562 def sum(column_name = nil) - return super() if block_given? + 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 @@ -368,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 0612151584..4f739a0415 100644 --- a/activerecord/lib/active_record/relation/delegation.rb +++ b/activerecord/lib/active_record/relation/delegation.rb @@ -25,6 +25,8 @@ module ActiveRecord def inherited(child_class) child_class.initialize_relation_delegate_cache + delegate = child_class.relation_delegate_class(ActiveRecord::Associations::CollectionProxy) + delegate.include ActiveRecord::Associations::CollectionProxy::DelegateExtending super end end @@ -36,7 +38,7 @@ module ActiveRecord # may vary depending on the klass of a relation, so we create a subclass of Relation # for each different klass, and the delegations are compiled into that subclass only. - delegate :to_xml, :encode_with, :length, :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 @@ -44,6 +46,8 @@ module ActiveRecord 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 @@ -109,12 +113,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 - 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 5d24f5f5ca..1d661fa8ed 100644 --- a/activerecord/lib/active_record/relation/finder_methods.rb +++ b/activerecord/lib/active_record/relation/finder_methods.rb @@ -307,21 +307,14 @@ module ActiveRecord 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, :distinct).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 @@ -391,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) @@ -401,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 diff --git a/activerecord/lib/active_record/relation/predicate_builder.rb b/activerecord/lib/active_record/relation/predicate_builder.rb index 18ae10a652..a6309e0b5c 100644 --- a/activerecord/lib/active_record/relation/predicate_builder.rb +++ b/activerecord/lib/active_record/relation/predicate_builder.rb @@ -1,13 +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/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) @@ -20,8 +12,6 @@ module ActiveRecord 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) @@ -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 88b6c37d43..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,7 +26,7 @@ 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. @@ -40,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 29860ec677..0000000000 --- a/activerecord/lib/active_record/relation/predicate_builder/association_query_handler.rb +++ /dev/null @@ -1,88 +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 - - # 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 - 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 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..2fe0f81cab --- /dev/null +++ b/activerecord/lib/active_record/relation/predicate_builder/association_query_value.rb @@ -0,0 +1,41 @@ +module ActiveRecord + class PredicateBuilder + class AssociationQueryValue # :nodoc: + attr_reader :associated_table, :value + + def initialize(associated_table, value) + @associated_table = associated_table + @value = value + end + + def queries + [associated_table.association_foreign_key.to_s => ids] + end + + 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/polymorphic_array_handler.rb b/activerecord/lib/active_record/relation/predicate_builder/polymorphic_array_handler.rb deleted file mode 100644 index 335124c952..0000000000 --- a/activerecord/lib/active_record/relation/predicate_builder/polymorphic_array_handler.rb +++ /dev/null @@ -1,59 +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 - - # 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 - 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..9bb2f8c8dc --- /dev/null +++ b/activerecord/lib/active_record/relation/predicate_builder/polymorphic_array_value.rb @@ -0,0 +1,49 @@ +module ActiveRecord + class PredicateBuilder + class PolymorphicArrayValue # :nodoc: + attr_reader :associated_table, :values + + 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 + + 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 4ee413c805..79e65baae5 100644 --- a/activerecord/lib/active_record/relation/query_methods.rb +++ b/activerecord/lib/active_record/relation/query_methods.rb @@ -248,7 +248,7 @@ module ActiveRecord return super() end - raise ArgumentError, "Call this with at least one field" if fields.empty? + raise ArgumentError, "Call `select' with at least one field" if fields.empty? spawn._select!(*fields) end @@ -1100,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 @@ -1130,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 diff --git a/activerecord/lib/active_record/relation/where_clause.rb b/activerecord/lib/active_record/relation/where_clause.rb index 417b24c7bb..119910ee79 100644 --- a/activerecord/lib/active_record/relation/where_clause.rb +++ b/activerecord/lib/active_record/relation/where_clause.rb @@ -148,10 +148,10 @@ module ActiveRecord (binds_index...(binds_index + binds_contains)).each do |i| except_binds[i] = true end - - binds_index += binds_contains end + binds_index += binds_contains if binds_contains + except end diff --git a/activerecord/lib/active_record/schema_dumper.rb b/activerecord/lib/active_record/schema_dumper.rb index 2bbfd01698..24b81aabc8 100644 --- a/activerecord/lib/active_record/schema_dumper.rb +++ b/activerecord/lib/active_record/schema_dumper.rb @@ -11,8 +11,8 @@ 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 = [] @@ -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 diff --git a/activerecord/lib/active_record/scoping/named.rb b/activerecord/lib/active_record/scoping/named.rb index 27cdf8cb7e..029156189d 100644 --- a/activerecord/lib/active_record/scoping/named.rb +++ b/activerecord/lib/active_record/scoping/named.rb @@ -156,17 +156,17 @@ 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 diff --git a/activerecord/lib/active_record/tasks/database_tasks.rb b/activerecord/lib/active_record/tasks/database_tasks.rb index 82604a915f..ba686fc562 100644 --- a/activerecord/lib/active_record/tasks/database_tasks.rb +++ b/activerecord/lib/active_record/tasks/database_tasks.rb @@ -71,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 @@ -162,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 @@ -286,11 +288,11 @@ module ActiveRecord 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 920830b9cf..541165b3d1 100644 --- a/activerecord/lib/active_record/tasks/mysql_database_tasks.rb +++ b/activerecord/lib/active_record/tasks/mysql_database_tasks.rb @@ -60,6 +60,12 @@ module ActiveRecord 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") @@ -104,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 5155ced0e2..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 @@ -63,8 +66,15 @@ module ActiveRecord "--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 @@ -110,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 1f756c2979..7043d2f0c8 100644 --- a/activerecord/lib/active_record/tasks/sqlite_database_tasks.rb +++ b/activerecord/lib/active_record/tasks/sqlite_database_tasks.rb @@ -36,9 +36,18 @@ module ActiveRecord end def structure_dump(filename, extra_flags) - dbfile = configuration["database"] - flags = extra_flags.join(" ") if extra_flags - `sqlite3 #{flags} #{dbfile} .schema > #{filename}` + 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, extra_flags) @@ -56,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/transactions.rb b/activerecord/lib/active_record/transactions.rb index 690deee508..45795fa287 100644 --- a/activerecord/lib/active_record/transactions.rb +++ b/activerecord/lib/active_record/transactions.rb @@ -123,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 # 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/test/cases/adapter_test.rb b/activerecord/test/cases/adapter_test.rb index 070fca240f..a1fb6427f9 100644 --- a/activerecord/test/cases/adapter_test.rb +++ b/activerecord/test/cases/adapter_test.rb @@ -216,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") 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/json_test.rb b/activerecord/test/cases/adapters/mysql2/json_test.rb index 6954006003..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_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_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/schema_migrations_test.rb b/activerecord/test/cases/adapters/mysql2/schema_migrations_test.rb index 605baa9905..251a50e41e 100644 --- a/activerecord/test/cases/adapters/mysql2/schema_migrations_test.rb +++ b/activerecord/test/cases/adapters/mysql2/schema_migrations_test.rb @@ -18,7 +18,7 @@ class SchemaMigrationsTest < ActiveRecord::Mysql2TestCase 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 @@ -29,7 +29,7 @@ class SchemaMigrationsTest < ActiveRecord::Mysql2TestCase 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/unsigned_type_test.rb b/activerecord/test/cases/adapters/mysql2/unsigned_type_test.rb index a0823be143..71dcfaa241 100644 --- a/activerecord/test/cases/adapters/mysql2/unsigned_type_test.rb +++ b/activerecord/test/cases/adapters/mysql2/unsigned_type_test.rb @@ -58,9 +58,9 @@ class Mysql2UnsignedTypeTest < ActiveRecord::Mysql2TestCase 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/postgresql/bytea_test.rb b/activerecord/test/cases/adapters/postgresql/bytea_test.rb index 505c297cd4..539c90f0bc 100644 --- a/activerecord/test/cases/adapters/postgresql/bytea_test.rb +++ b/activerecord/test/cases/adapters/postgresql/bytea_test.rb @@ -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? 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 c52d9e37cc..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 @@ -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)") 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/json_test.rb b/activerecord/test/cases/adapters/postgresql/json_test.rb index d4e627001c..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 @@ -28,16 +22,6 @@ 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 @@ -48,34 +32,6 @@ module PostgresqlJSONSharedTestCases 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 - 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_deserialize_with_array x = JsonDataType.new(objects: ["foo" => "bar"]) assert_equal ["foo" => "bar"], x.objects @@ -84,140 +40,6 @@ module PostgresqlJSONSharedTestCases x.reload assert_equal ["foo" => "bar"], x.objects 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_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_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 class PostgresqlJSONTest < ActiveRecord::PostgreSQLTestCase diff --git a/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb b/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb index 003e6e62e7..bfc763e1ef 100644 --- a/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb +++ b/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb @@ -202,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 @@ -217,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 @@ -346,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/schema_authorization_test.rb b/activerecord/test/cases/adapters/postgresql/schema_authorization_test.rb index f6a07da85f..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| @@ -112,8 +101,4 @@ class SchemaAuthorizationTest < ActiveRecord::PostgreSQLTestCase def set_session_auth(auth = nil) @connection.session_auth = auth || "default" end - - def bind_param(value) - ActiveRecord::Relation::QueryAttribute.new(nil, value, ActiveRecord::Type::Value.new) - end end diff --git a/activerecord/test/cases/adapters/postgresql/schema_test.rb b/activerecord/test/cases/adapters/postgresql/schema_test.rb index 7b065ff320..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 @@ -361,7 +362,7 @@ 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 @@ -466,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/uuid_test.rb b/activerecord/test/cases/adapters/postgresql/uuid_test.rb index 6aa6a79705..52e4a38cae 100644 --- a/activerecord/test/cases/adapters/postgresql/uuid_test.rb +++ b/activerecord/test/cases/adapters/postgresql/uuid_test.rb @@ -13,6 +13,10 @@ module PostgresqlUUIDHelper 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 @@ -178,7 +182,7 @@ class PostgresqlUUIDGenerationTest < ActiveRecord::PostgreSQLTestCase t.uuid "other_uuid_2", default: "my_uuid_generator()" end - connection.create_table("pg_uuids_3", id: :uuid) do |t| + connection.create_table("pg_uuids_3", id: :uuid, **uuid_default) do |t| t.string "name" end end @@ -320,10 +324,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 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/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/sqlite3_adapter_test.rb b/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb index 2179d1294c..9a812e325e 100644 --- a/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb +++ b/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb @@ -66,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 @@ -134,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 @@ -148,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 diff --git a/activerecord/test/cases/associations/belongs_to_associations_test.rb b/activerecord/test/cases/associations/belongs_to_associations_test.rb index 5875a1871f..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 diff --git a/activerecord/test/cases/associations/callbacks_test.rb b/activerecord/test/cases/associations/callbacks_test.rb index 5fd2411f6f..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) @@ -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 diff --git a/activerecord/test/cases/associations/cascaded_eager_loading_test.rb b/activerecord/test/cases/associations/cascaded_eager_loading_test.rb index ddb5c7a4aa..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 diff --git a/activerecord/test/cases/associations/eager_test.rb b/activerecord/test/cases/associations/eager_test.rb index 11f4aae5b3..4271a09c9b 100644 --- a/activerecord/test/cases/associations/eager_test.rb +++ b/activerecord/test/cases/associations/eager_test.rb @@ -1094,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 @@ -1363,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 diff --git a/activerecord/test/cases/associations/extension_test.rb b/activerecord/test/cases/associations/extension_test.rb index 974a3080d4..87d842f21d 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 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 d6b595d7e7..4bf1b5bcd5 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 @@ -111,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 @@ -939,7 +954,7 @@ 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 @@ -1021,4 +1036,9 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase 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 14f515fa1c..6a479a344c 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") @@ -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 @@ -1903,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 @@ -1942,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 @@ -1977,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 @@ -2037,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") @@ -2447,15 +2441,29 @@ 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 + 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 + 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 } + + car = Car.create! + car.bulbs.create! + + assert_equal 1, count + end + end + class AuthorWithErrorDestroyingAssociation < ActiveRecord::Base self.table_name = "authors" has_many :posts_with_error_destroying, @@ -2496,7 +2504,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_loading_association_in_validate_callback_doesnt_affect_persistence reset_callbacks(:validation, Bulb) do - Bulb.after_validation { |m| m.car.bulbs.load } + Bulb.after_validation { |record| record.car.bulbs.load } car = Car.create!(name: "Car") bulb = car.bulbs.create! 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 ea52fb5a67..9156f6d57a 100644 --- a/activerecord/test/cases/associations/has_many_through_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_through_associations_test.rb @@ -64,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 @@ -152,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 @@ -1252,4 +1234,23 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase ) 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_through_associations_test.rb b/activerecord/test/cases/associations/has_one_through_associations_test.rb index 38a729d2d4..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) 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 287b3e9ebc..467cc73ecd 100644 --- a/activerecord/test/cases/associations/inverse_associations_test.rb +++ b/activerecord/test/cases/associations/inverse_associations_test.rb @@ -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 a4345f3857..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 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 42dbbad1c8..6d3757f467 100644 --- a/activerecord/test/cases/associations/left_outer_join_association_test.rb +++ b/activerecord/test/cases/associations/left_outer_join_association_test.rb @@ -7,7 +7,7 @@ require "models/categorization" require "models/person" 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_test.rb b/activerecord/test/cases/associations_test.rb index 26056f6f63..4ab690bfc6 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") @@ -111,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) diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb index 979a59f566..15c253890b 100644 --- a/activerecord/test/cases/base_test.rb +++ b/activerecord/test/cases/base_test.rb @@ -64,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 @@ -703,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 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 3214d778d4..93f8ab18c2 100644 --- a/activerecord/test/cases/calculations_test.rb +++ b/activerecord/test/cases/calculations_test.rb @@ -809,4 +809,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/coders/yaml_column_test.rb b/activerecord/test/cases/coders/yaml_column_test.rb index 59ef389326..a26a72712d 100644 --- a/activerecord/test/cases/coders/yaml_column_test.rb +++ b/activerecord/test/cases/coders/yaml_column_test.rb @@ -1,4 +1,3 @@ - require "cases/helper" module ActiveRecord diff --git a/activerecord/test/cases/collection_cache_key_test.rb b/activerecord/test/cases/collection_cache_key_test.rb index 381a78a8e2..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) + + /\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) + 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 @@ -48,7 +74,7 @@ module ActiveRecord 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 d230700119..90c8d21c43 100644 --- a/activerecord/test/cases/column_definition_test.rb +++ b/activerecord/test/cases/column_definition_test.rb @@ -8,7 +8,7 @@ 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: diff --git a/activerecord/test/cases/comment_test.rb b/activerecord/test/cases/comment_test.rb index 63f67a9a16..c23be52a6c 100644 --- a/activerecord/test/cases/comment_test.rb +++ b/activerecord/test/cases/comment_test.rb @@ -72,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 @@ -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 diff --git a/activerecord/test/cases/connection_adapters/connection_handler_test.rb b/activerecord/test/cases/connection_adapters/connection_handler_test.rb index 681399c8bb..2a71f08d90 100644 --- a/activerecord/test/cases/connection_adapters/connection_handler_test.rb +++ b/activerecord/test/cases/connection_adapters/connection_handler_test.rb @@ -9,6 +9,17 @@ module ActiveRecord @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 config = { "readonly" => { "adapter" => "sqlite3" } } resolver = ConnectionAdapters::ConnectionSpecification::Resolver.new(config) diff --git a/activerecord/test/cases/defaults_test.rb b/activerecord/test/cases/defaults_test.rb index a6297673c9..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 diff --git a/activerecord/test/cases/dirty_test.rb b/activerecord/test/cases/dirty_test.rb index c13a962e3e..721861975a 100644 --- a/activerecord/test/cases/dirty_test.rb +++ b/activerecord/test/cases/dirty_test.rb @@ -349,13 +349,14 @@ class DirtyTest < ActiveRecord::TestCase def test_partial_update_with_optimistic_locking person = Person.new(first_name: "foo") - old_lock_version = person.lock_version 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 @@ -670,6 +671,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 |*| 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/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 89d8a8bdca..4837a169fa 100644 --- a/activerecord/test/cases/finder_test.rb +++ b/activerecord/test/cases/finder_test.rb @@ -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 @@ -725,7 +743,6 @@ class FinderTest < ActiveRecord::TestCase 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 diff --git a/activerecord/test/cases/fixtures_test.rb b/activerecord/test/cases/fixtures_test.rb index 51133e9495..a0a6d3c7ef 100644 --- a/activerecord/test/cases/fixtures_test.rb +++ b/activerecord/test/cases/fixtures_test.rb @@ -95,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'") @@ -766,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 diff --git a/activerecord/test/cases/inheritance_test.rb b/activerecord/test/cases/inheritance_test.rb index e570e9ac1d..b4bbdc6dad 100644 --- a/activerecord/test/cases/inheritance_test.rb +++ b/activerecord/test/cases/inheritance_test.rb @@ -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 d7aa091623..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" @@ -169,13 +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 - owner = owners(:blackbeard) - owner.happy_at = nil - assert_equal "owners/#{owner.id}", owner.cache_key(:happy_at) + 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/json_serialization_test.rb b/activerecord/test/cases/json_serialization_test.rb index 5a1d066aef..9b4b61b16e 100644 --- a/activerecord/test/cases/json_serialization_test.rb +++ b/activerecord/test/cases/json_serialization_test.rb @@ -168,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 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..d190b027bf --- /dev/null +++ b/activerecord/test/cases/json_shared_test_cases.rb @@ -0,0 +1,177 @@ +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_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 23095618a4..3a3b8e51f9 100644 --- a/activerecord/test/cases/locking_test.rb +++ b/activerecord/test/cases/locking_test.rb @@ -247,17 +247,6 @@ class OptimisticLockingTest < ActiveRecord::TestCase assert_equal "new title2", t2.title end - def test_lock_without_default_should_update_with_lock_col - t1 = LockWithoutDefault.create(title: "title1", lock_version: 6) - - assert_equal 6, t1.lock_version - - t1.update(lock_version: 0) - t1.reload - - assert_equal 0, t1.lock_version - end - def test_lock_without_default_queries_count t1 = LockWithoutDefault.create(title: "title1") @@ -270,12 +259,6 @@ class OptimisticLockingTest < ActiveRecord::TestCase assert_equal "title2", t1.title assert_equal 1, t1.lock_version - assert_queries(1) { t1.update(title: "title3", lock_version: 6) } - - t1.reload - assert_equal "title3", t1.title - assert_equal 6, t1.lock_version - t2 = LockWithoutDefault.new(title: "title1") assert_queries(1) { t2.save! } @@ -321,17 +304,6 @@ class OptimisticLockingTest < ActiveRecord::TestCase assert_equal "new title2", t2.title end - def test_lock_with_custom_column_without_default_should_update_with_lock_col - t1 = LockWithCustomColumnWithoutDefault.create(title: "title1", custom_lock_version: 6) - - assert_equal 6, t1.custom_lock_version - - t1.update(custom_lock_version: 0) - t1.reload - - assert_equal 0, t1.custom_lock_version - end - def test_lock_with_custom_column_without_default_queries_count t1 = LockWithCustomColumnWithoutDefault.create(title: "title1") @@ -344,12 +316,6 @@ class OptimisticLockingTest < ActiveRecord::TestCase assert_equal "title2", t1.title assert_equal 1, t1.custom_lock_version - assert_queries(1) { t1.update(title: "title3", custom_lock_version: 6) } - - t1.reload - assert_equal "title3", t1.title - assert_equal 6, t1.custom_lock_version - t2 = LockWithCustomColumnWithoutDefault.new(title: "title1") assert_queries(1) { t2.save! } 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/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/references_statements_test.rb b/activerecord/test/cases/migration/references_statements_test.rb index 06c44c8c52..e9eb9968cb 100644 --- a/activerecord/test/cases/migration/references_statements_test.rb +++ b/activerecord/test/cases/migration/references_statements_test.rb @@ -50,6 +50,14 @@ 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) diff --git a/activerecord/test/cases/migration/rename_table_test.rb b/activerecord/test/cases/migration/rename_table_test.rb index 19588d28a2..7bcabd0cc6 100644 --- a/activerecord/test/cases/migration/rename_table_test.rb +++ b/activerecord/test/cases/migration/rename_table_test.rb @@ -80,7 +80,7 @@ module ActiveRecord 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 } assert connection.table_exists? :felines ensure diff --git a/activerecord/test/cases/migration_test.rb b/activerecord/test/cases/migration_test.rb index da7875187a..57f94950f9 100644 --- a/activerecord/test/cases/migration_test.rb +++ b/activerecord/test/cases/migration_test.rb @@ -402,33 +402,6 @@ class MigrationTest < ActiveRecord::TestCase ActiveRecord::Migrator.up(migrations_path) 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 - ActiveRecord::Migrator.up(migrations_path) - end - def test_internal_metadata_stores_environment_when_other_data_exists ActiveRecord::InternalMetadata.delete_all ActiveRecord::InternalMetadata[:foo] = "bar" diff --git a/activerecord/test/cases/migrator_test.rb b/activerecord/test/cases/migrator_test.rb index aadbc375af..2e4b454a86 100644 --- a/activerecord/test/cases/migrator_test.rb +++ b/activerecord/test/cases/migrator_test.rb @@ -299,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 @@ -311,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 diff --git a/activerecord/test/cases/nested_attributes_test.rb b/activerecord/test/cases/nested_attributes_test.rb index b87419d203..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 diff --git a/activerecord/test/cases/persistence_test.rb b/activerecord/test/cases/persistence_test.rb index 5b7e2fd008..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 diff --git a/activerecord/test/cases/primary_keys_test.rb b/activerecord/test/cases/primary_keys_test.rb index 12386635f6..5ded619716 100644 --- a/activerecord/test/cases/primary_keys_test.rb +++ b/activerecord/test/cases/primary_keys_test.rb @@ -183,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 diff --git a/activerecord/test/cases/quoting_test.rb b/activerecord/test/cases/quoting_test.rb index f260d043e4..0819776fbf 100644 --- a/activerecord/test/cases/quoting_test.rb +++ b/activerecord/test/cases/quoting_test.rb @@ -81,8 +81,21 @@ module ActiveRecord end end + class QuotedOne + def quoted_id + 1 + end + end + class SubQuotedOne < QuotedOne + end def test_quote_with_quoted_id - assert_deprecated { assert_equal 1, @quoter.quote(Struct.new(:quoted_id).new(1)) } + 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 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/relation/delegation_test.rb b/activerecord/test/cases/relation/delegation_test.rb index 8cb7b82015..cb6e4d76d3 100644 --- a/activerecord/test/cases/relation/delegation_test.rb +++ b/activerecord/test/cases/relation/delegation_test.rb @@ -3,29 +3,7 @@ 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, @@ -43,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 4f92f71a09..dea787c07f 100644 --- a/activerecord/test/cases/relation/mutation_test.rb +++ b/activerecord/test/cases/relation/mutation_test.rb @@ -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") @@ -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 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 ed8ffddcf5..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) @@ -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 1241bb54a4..5fb32270b7 100644 --- a/activerecord/test/cases/relation_test.rb +++ b/activerecord/test/cases/relation_test.rb @@ -6,7 +6,7 @@ require "models/rating" module ActiveRecord class RelationTest < ActiveRecord::TestCase - fixtures :posts, :comments, :authors + fixtures :posts, :comments, :authors, :author_addresses FakeKlass = Struct.new(:table_name, :name) do extend ActiveRecord::Delegation::DelegateCache diff --git a/activerecord/test/cases/relations_test.rb b/activerecord/test/cases/relations_test.rb index 0c94e891eb..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) @@ -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 @@ -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 @@ -1132,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 @@ -1143,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 @@ -1153,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 @@ -1182,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 @@ -1878,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 @@ -1947,25 +1972,37 @@ class RelationTest < ActiveRecord::TestCase 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 + 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) @@ -1974,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/schema_dumper_test.rb b/activerecord/test/cases/schema_dumper_test.rb index fccba4738f..cb8d449ba9 100644 --- a/activerecord/test/cases/schema_dumper_test.rb +++ b/activerecord/test/cases/schema_dumper_test.rb @@ -182,7 +182,11 @@ class SchemaDumperTest < ActiveRecord::TestCase if current_adapter?(:PostgreSQLAdapter) assert_equal 't.index ["firm_id", "type", "rating"], name: "company_index", order: { rating: :desc }', index_definition elsif current_adapter?(:Mysql2Adapter) - assert_equal 't.index ["firm_id", "type", "rating"], name: "company_index", length: { type: 10 }', index_definition + 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 diff --git a/activerecord/test/cases/scoping/default_scoping_test.rb b/activerecord/test/cases/scoping/default_scoping_test.rb index a6c22ac672..89fb434b27 100644 --- a/activerecord/test/cases/scoping/default_scoping_test.rb +++ b/activerecord/test/cases/scoping/default_scoping_test.rb @@ -462,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 diff --git a/activerecord/test/cases/scoping/named_scoping_test.rb b/activerecord/test/cases/scoping/named_scoping_test.rb index d261fd5321..0c2cffe0d3 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 @@ -167,7 +167,7 @@ class NamedScopingTest < ActiveRecord::TestCase 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 diff --git a/activerecord/test/cases/scoping/relation_scoping_test.rb b/activerecord/test/cases/scoping/relation_scoping_test.rb index a1ae57fdbb..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) @@ -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 diff --git a/activerecord/test/cases/serialized_attribute_test.rb b/activerecord/test/cases/serialized_attribute_test.rb index 673392b4c4..e1bdaab5cf 100644 --- a/activerecord/test/cases/serialized_attribute_test.rb +++ b/activerecord/test/cases/serialized_attribute_test.rb @@ -349,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/tasks/database_tasks_test.rb b/activerecord/test/cases/tasks/database_tasks_test.rb index 5653fd83fd..c47c97e9d9 100644 --- a/activerecord/test/cases/tasks/database_tasks_test.rb +++ b/activerecord/test/cases/tasks/database_tasks_test.rb @@ -343,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 diff --git a/activerecord/test/cases/tasks/mysql_rake_test.rb b/activerecord/test/cases/tasks/mysql_rake_test.rb index f30e0958c3..c22d974536 100644 --- a/activerecord/test/cases/tasks/mysql_rake_test.rb +++ b/activerecord/test/cases/tasks/mysql_rake_test.rb @@ -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 @@ -305,6 +305,15 @@ if current_adapter?(:Mysql2Adapter) 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) diff --git a/activerecord/test/cases/tasks/postgresql_rake_test.rb b/activerecord/test/cases/tasks/postgresql_rake_test.rb index a23100c32a..a2e968aedf 100644 --- a/activerecord/test/cases/tasks/postgresql_rake_test.rb +++ b/activerecord/test/cases/tasks/postgresql_rake_test.rb @@ -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,15 @@ 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"] @@ -246,6 +259,14 @@ if current_adapter?(:PostgreSQLAdapter) 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" diff --git a/activerecord/test/cases/tasks/sqlite_rake_test.rb b/activerecord/test/cases/tasks/sqlite_rake_test.rb index 0d917f3f6c..ccb3834fee 100644 --- a/activerecord/test/cases/tasks/sqlite_rake_test.rb +++ b/activerecord/test/cases/tasks/sqlite_rake_test.rb @@ -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 31b11c19f7..9f594fef85 100644 --- a/activerecord/test/cases/test_case.rb +++ b/activerecord/test/cases/test_case.rb @@ -75,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 diff --git a/activerecord/test/cases/transactions_test.rb b/activerecord/test/cases/transactions_test.rb index 111495c481..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) 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 07288568e8..1d21a2454f 100644 --- a/activerecord/test/cases/view_test.rb +++ b/activerecord/test/cases/view_test.rb @@ -154,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, :SQLServerAdapter) + 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 diff --git a/activerecord/test/cases/yaml_serialization_test.rb b/activerecord/test/cases/yaml_serialization_test.rb index 1571b31329..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 @@ -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.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/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/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..76b484e616 100644 --- a/activerecord/test/models/comment.rb +++ b/activerecord/test/models/comment.rb @@ -19,6 +19,11 @@ class Comment < ActiveRecord::Base has_many :children, class_name: "Comment", foreign_key: :parent_id belongs_to :parent, class_name: "Comment", counter_cache: :children_count + # 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 20e37710e7..d269a95e8c 100644 --- a/activerecord/test/models/company.rb +++ b/activerecord/test/models/company.rb @@ -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 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/post.rb b/activerecord/test/models/post.rb index e74aedb814..a2028b3eb9 100644 --- a/activerecord/test/models/post.rb +++ b/activerecord/test/models/post.rb @@ -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 diff --git a/activerecord/test/models/topic.rb b/activerecord/test/models/topic.rb index 0420e2d15c..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 diff --git a/activerecord/test/schema/postgresql_specific_schema.rb b/activerecord/test/schema/postgresql_specific_schema.rb index 860c63b27c..e56e8fa36a 100644 --- a/activerecord/test/schema/postgresql_specific_schema.rb +++ b/activerecord/test/schema/postgresql_specific_schema.rb @@ -3,11 +3,13 @@ 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 @@ -102,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 08bef08abc..50f1d9bfe7 100644 --- a/activerecord/test/schema/schema.rb +++ b/activerecord/test/schema/schema.rb @@ -413,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 @@ -783,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 |