diff options
Diffstat (limited to 'activerecord')
312 files changed, 4825 insertions, 4002 deletions
diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index 3576f5c905..cd4b297c8c 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,3 +1,345 @@ +* Honour the order of the joining model in a `has_many :through` association when eager loading. + + Example: + + The below will now follow the order of `by_lines` when eager loading `authors`. + + class Article < ActiveRecord::Base + has_many :by_lines, -> { order(:position) } + has_many :authors, through: :by_lines + end + + Fixes #17864. + + *Yasyf Mohamedali*, *Joel Turkel* + +* Ensure that the Suppressor runs before validations. + + This moves the suppressor up to be run before validations rather than after + validations. There's no reason to validate a record you aren't planning on saving. + + *Eileen M. Uchitelle* + +## Rails 5.0.0.beta3 (February 24, 2016) ## + +* Ensure that mutations of the array returned from `ActiveRecord::Relation#to_a` + do not affect the original relation, by returning a duplicate array each time. + + This brings the behavior in line with `CollectionProxy#to_a`, which was + already more careful. + + *Matthew Draper* + +* Fixed `where` for polymorphic associations when passed an array containing different types. + + Fixes #17011. + + Example: + + PriceEstimate.where(estimate_of: [Treasure.find(1), Car.find(2)]) + # => SELECT "price_estimates".* FROM "price_estimates" + WHERE (("price_estimates"."estimate_of_type" = 'Treasure' AND "price_estimates"."estimate_of_id" = 1) + OR ("price_estimates"."estimate_of_type" = 'Car' AND "price_estimates"."estimate_of_id" = 2)) + + *Philippe Huibonhoa* + +* Fix a bug where using `t.foreign_key` twice with the same `to_table` within + the same table definition would only create one foreign key. + + *George Millo* + +* Fix a regression on has many association, where calling a child from parent in child's callback + results in same child records getting added repeatedly to target. + + Fixes #13387. + + *Bogdan Gusiev*, *Jon Hinson* + +* Rework `ActiveRecord::Relation#last`. + + 1. Never perform additional SQL on loaded relation + 2. Use SQL reverse order instead of loading relation if relation doesn't have limit + 3. Deprecated relation loading when SQL order can not be automatically reversed + + Topic.order("title").load.last(3) + # before: SELECT ... + # after: No SQL + + Topic.order("title").last + # before: SELECT * FROM `topics` + # after: SELECT * FROM `topics` ORDER BY `topics`.`title` DESC LIMIT 1 + + Topic.order("coalesce(author, title)").last + # before: SELECT * FROM `topics` + # after: Deprecation Warning for irreversible order + + *Bogdan Gusiev* + + +* Allow `joins` to be unscoped. + + Fixes #13775. + + *Takashi Kokubun* + +* Add ActiveRecord `#second_to_last` and `#third_to_last` methods. + + *Brian Christian* + +* Added `numeric` helper into migrations. + + Example: + + create_table(:numeric_types) do |t| + t.numeric :numeric_type, precision: 10, scale: 2 + end + + *Mehmet Emin İNAÇ* + +* Bumped the minimum supported version of PostgreSQL to >= 9.1. + Both PG 9.0 and 8.4 are past their end of life date: + http://www.postgresql.org/support/versioning/ + + *Remo Mueller* + +## Rails 5.0.0.beta2 (February 01, 2016) ## + +* `ActiveRecord::Relation#reverse_order` throws `ActiveRecord::IrreversibleOrderError` + when the order can not be reversed using current trivial algorithm. + Also raises the same error when `#reverse_order` is called on + relation without any order and table has no primary key: + + Topic.order("concat(author_name, title)").reverse_order + # Before: SELECT `topics`.* FROM `topics` ORDER BY concat(author_name DESC, title) DESC + # After: raises ActiveRecord::IrreversibleOrderError + Edge.all.reverse_order + # Before: SELECT `edges`.* FROM `edges` ORDER BY `edges`.`` DESC + # After: raises ActiveRecord::IrreversibleOrderError + + *Bogdan Gusiev* + +* Improve schema_migrations insertion performance by inserting all versions + in one INSERT SQL. + + *Akira Matsuda*, *Naoto Koshikawa* + +* Using `references` or `belongs_to` in migrations will always add index + for the referenced column by default, without adding `index: true` option + to generated migration file. Users can opt out of this by passing + `index: false`. + + Fixes #18146. + + *Matthew Draper*, *Prathamesh Sonpatki* + +* Run `type` attributes through attributes API type-casting before + instantiating the corresponding subclass. This makes it possible to define + custom STI mappings. + + Fixes #21986. + + *Yves Senn* + +* Don't try to quote functions or expressions passed to `:default` option if + they are passed as procs. + + This will generate proper query with the passed function or expression for + the default option, instead of trying to quote it in incorrect fashion. + + Example: + + create_table :posts do |t| + t.datetime :published_at, default: -> { 'NOW()' } + end + + *Ryuta Kamizono* + +* Fix regression when loading fixture files with symbol keys. + + Fixes #22584. + + *Yves Senn* + +* Use `version` column as primary key for schema_migrations table because + `schema_migrations` versions are guaranteed to be unique. + + This makes it possible to use `update_attributes` on models that do + not have a primary key. + + *Richard Schneeman* + +* Add short-hand methods for text and blob types in MySQL. + + In Pg and Sqlite3, `:text` and `:binary` have variable unlimited length. + But in MySQL, these have limited length for each types (ref #21591, #21619). + This change adds short-hand methods for each text and blob types. + + Example: + + create_table :foos do |t| + t.tinyblob :tiny_blob + t.mediumblob :medium_blob + t.longblob :long_blob + t.tinytext :tiny_text + t.mediumtext :medium_text + t.longtext :long_text + end + + *Ryuta Kamizono* + +* Take into account UTC offset when assigning string representation of + timestamp with offset specified to attribute of time type. + + *Andrey Novikov* + +* When calling `first` with a `limit` argument, return directly from the + `loaded?` records if available. + + *Ben Woosley* + +* Deprecate sending the `offset` argument to `find_nth`. Please use the + `offset` method on relation instead. + + *Ben Woosley* + +## Rails 5.0.0.beta1 (December 18, 2015) ## + +* Order the result of `find(ids)` to match the passed array, if the relation + has no explicit order defined. + + Fixes #20338. + + *Miguel Grazziotin*, *Matthew Draper* + +* Omit default limit values in dumped schema. It's tidier, and if the defaults + change in the future, we can address that via Migration API Versioning. + + *Jean Boussier* + +* Support passing the schema name as a prefix to table name in + `ConnectionAdapters::SchemaStatements#indexes`. Previously the prefix would + be considered a full part of the index name, and only the schema in the + current search path would be considered. + + *Grey Baker* + +* Ignore index name in `index_exists?` and `remove_index` when not passed a + name to check for. + + *Grey Baker* + +* Extract support for the legacy `mysql` database adapter from core. It will + live on in a separate gem for now, but most users should just use `mysql2`. + + *Abdelkader Boudih* + +* ApplicationRecord is a new superclass for all app models, analogous to app + controllers subclassing ApplicationController instead of + ActionController::Base. This gives apps a single spot to configure app-wide + model behavior. + + Newly generated applications have `app/models/application_record.rb` + present by default. + + *Genadi Samokovarov* + +* Version the API presented to migration classes, so we can change parameter + defaults without breaking existing migrations, or forcing them to be + rewritten through a deprecation cycle. + + New migrations specify the Rails version they were written for: + + class AddStatusToOrders < ActiveRecord::Migration[5.0] + def change + # ... + end + end + + *Matthew Draper*, *Ravil Bayramgalin* + +* Use bind params for `limit` and `offset`. This will generate significantly + fewer prepared statements for common tasks like pagination. To support this + change, passing a string containing a comma to `limit` has been deprecated, + and passing an Arel node to `limit` is no longer supported. + + Fixes #22250. + + *Sean Griffin* + +* Introduce after_{create,update,delete}_commit callbacks. + + Before: + + after_commit :add_to_index_later, on: :create + after_commit :update_in_index_later, on: :update + after_commit :remove_from_index_later, on: :destroy + + After: + + after_create_commit :add_to_index_later + after_update_commit :update_in_index_later + after_destroy_commit :remove_from_index_later + + Fixes #22515. + + *Genadi Samokovarov* + +* Respect the column default values for `inheritance_column` when + instantiating records through the base class. + + Fixes #17121. + + Example: + + # The schema of BaseModel has `t.string :type, default: 'SubType'` + subtype = BaseModel.new + assert_equals SubType, subtype.class + + *Kuldeep Aggarwal* + +* Fix `rake db:structure:dump` on Postgres when multiple schemas are used. + + Fixes #22346. + + *Nick Muerdter*, *ckoenig* + +* Add schema dumping support for PostgreSQL geometric data types. + + *Ryuta Kamizono* + +* Except keys of `build_record`'s argument from `create_scope` in `initialize_attributes`. + + Fixes #21893. + + *Yuichiro Kaneko* + +* Deprecate `connection.tables` on the SQLite3 and MySQL adapters. + Also deprecate passing arguments to `#tables`. + And deprecate `table_exists?`. + + The `#tables` method of some adapters (mysql, mysql2, sqlite3) would return + both tables and views while others (postgresql) just return tables. To make + their behavior consistent, `#tables` will return only tables in the future. + + The `#table_exists?` method would check both tables and views. To make + their behavior consistent with `#tables`, `#table_exists?` will check only + tables in the future. + + *Yuichiro Kaneko* + +* Improve support for non Active Record objects on `validates_associated` + + Skipping `marked_for_destruction?` when the associated object does not responds + to it make easier to validate virtual associations built on top of Active Model + objects and/or serialized objects that implement a `valid?` instance method. + + *Kassio Borges*, *Lucas Mazza* + +* Change connection management middleware to return a new response with + a body proxy, rather than mutating the original. + + *Kevin Buchanan* + * Make `db:migrate:status` to render `1_some.rb` format migrate files. These files are in `db/migrate`: @@ -60,7 +402,7 @@ *Takashi Kokubun* -* Use advisory locking to raise a ConcurrentMigrationError instead of +* Use advisory locking to raise a `ConcurrentMigrationError` instead of attempting to migrate when another migration is currently running. *Sam Davies* @@ -80,7 +422,7 @@ *Aaron Suggs* * Avoid disabling errors on the PostgreSQL connection when enabling the - standard_conforming_strings setting. Errors were previously disabled because + `standard_conforming_strings` setting. Errors were previously disabled because the setting wasn't writable in Postgres 8.1 and didn't exist in earlier versions. Now Rails only supports Postgres 8.2+ we're fine to assume the setting exists. Disabling errors caused problems when using a connection @@ -92,7 +434,7 @@ *Harry Marr* -* Set `scope.reordering_value` to `true` if :reordering values are specified. +* Set `scope.reordering_value` to `true` if `:reordering`-values are specified. Fixes #21886. @@ -151,13 +493,13 @@ validates_numericality_of :pitch end - - Old style - - `guitar.errors["tuning_pegs.pitch"] = ["is not a number"]` + # Old style + guitar.errors["tuning_pegs.pitch"] = ["is not a number"] - - New style (if defined globally, or set in has_many_relationship) - - `guitar.errors["tuning_pegs[1].pitch"] = ["is not a number"]` + # New style (if defined globally, or set in has_many_relationship) + guitar.errors["tuning_pegs[1].pitch"] = ["is not a number"] - *Michael Probber and Terence Sun* + *Michael Probber*, *Terence Sun* * Exit with non-zero status for failed database rake tasks. @@ -170,21 +512,23 @@ *Rafael Sales* -* Add ability to default to `uuid` as primary key when generating database migrations +* Add ability to default to `uuid` as primary key when generating database migrations. - config.generators do |g| - g.orm :active_record, primary_key_type: :uuid - end + Example: + + config.generators do |g| + g.orm :active_record, primary_key_type: :uuid + end *Jon McCartie* -* Don't cache arguments in #find_by if they are an ActiveRecord::Relation +* Don't cache arguments in `#find_by` if they are an `ActiveRecord::Relation`. - Fixes #20817 + Fixes #20817. *Hiroaki Izu* -* Qualify column name inserted by `group` in calculation +* Qualify column name inserted by `group` in calculation. Giving `group` an unqualified column name now works, even if the relation has `JOIN` with another table which also has a column of the name. @@ -209,11 +553,11 @@ *Jake Worth* * Add an immutable string type to help reduce memory usage for apps which do - not need mutation detection on Strings. + not need mutation detection on strings. *Sean Griffin* -* Give `AcriveRecord::Relation#update` its own deprecation warning when +* Give `ActiveRecord::Relation#update` its own deprecation warning when passed an `ActiveRecord::Base` instance. Fixes #21945. @@ -227,7 +571,7 @@ *Yves Senn* -* No longer pass depreacted option `-i` to `pg_dump`. +* No longer pass deprecated option `-i` to `pg_dump`. *Paul Sadauskas* @@ -242,7 +586,7 @@ *Matthew Draper*, *Jean Boussier* -* Remove unused `pk_and_sequence_for` in AbstractMysqlAdapter. +* Remove unused `pk_and_sequence_for` in `AbstractMysqlAdapter`. *Ryuta Kamizono* @@ -250,10 +594,10 @@ To load the fixtures file `accounts.yml` as the `User` model, use: - _fixture: - model_class: User - david: - name: David + _fixture: + model_class: User + david: + name: David Fixes #9516. @@ -268,7 +612,7 @@ *Jimmy Bourassa* -* Fixed taking precision into count when assigning a value to timestamp attribute +* Fixed taking precision into count when assigning a value to timestamp attribute. Timestamp column can have less precision than ruby timestamp In result in how big a fraction of a second can be stored in the @@ -298,7 +642,7 @@ *Yves Senn*, *Matthew Draper* * Add `ActiveRecord::Base.ignored_columns` to make some columns - invisible from ActiveRecord. + invisible from Active Record. *Jean Boussier* @@ -309,9 +653,9 @@ * Ensure `select` quotes aliased attributes, even when using `from`. - Fixes #21488 + Fixes #21488. - *Sean Griffin & @johanlunds* + *Sean Griffin*, *@johanlunds* * MySQL: support `unsigned` numeric data types. @@ -356,7 +700,7 @@ *Ben Murphy*, *Matthew Draper* -* `bin/rake db:migrate` uses +* `bin/rails db:migrate` uses `ActiveRecord::Tasks::DatabaseTasks.migrations_paths` instead of `Migrator.migrations_paths`. @@ -374,6 +718,13 @@ *Wojciech Wnętrzak* +* Instantiating an AR model with `ActionController::Parameters` now raises + an `ActiveModel::ForbiddenAttributesError` if the parameters include a + `type` field that has not been explicitly permitted. Previously, the + `type` field was simply ignored in the same situation. + + *Prem Sichanugrist* + * PostgreSQL, `create_schema`, `drop_schema` and `rename_table` now quote schema names. @@ -429,7 +780,7 @@ * Add `ActiveRecord::Relation#in_batches` to work with records and relations in batches. - Available options are `of` (batch size), `load`, `begin_at`, and `end_at`. + Available options are `of` (batch size), `load`, `start`, and `finish`. Examples: @@ -630,7 +981,7 @@ * Include the `Enumerable` module in `ActiveRecord::Relation` - *Sean Griffin & bogdan* + *Sean Griffin*, *bogdan* * Use `Enumerable#sum` in `ActiveRecord::Relation` if a block is given. @@ -666,7 +1017,7 @@ Fixes #20515. - *Sean Griffin & jmondo* + *Sean Griffin*, *jmondo* * Deprecate the PostgreSQL `:point` type in favor of a new one which will return `Point` objects instead of an `Array` @@ -751,13 +1102,6 @@ *Alex Coomans* -* Dump indexes in `create_table` instead of `add_index`. - - If the adapter supports indexes in `create_table`, generated SQL is - slightly more efficient. - - *Ryuta Kamizono* - * Correctly dump `:options` on `create_table` for MySQL. *Ryuta Kamizono* @@ -1056,13 +1400,16 @@ *Sean Griffin* * `scoping` no longer pollutes the current scope of sibling classes when using - STI. e.x. + STI. + + Fixes #18806. + + Example: StiOne.none.scoping do StiTwo.all end - Fixes #18806. *Sean Griffin* @@ -1074,7 +1421,7 @@ *Yves Senn* -* `find_in_batches` now accepts an `:end_at` parameter that complements the `:start` +* `find_in_batches` now accepts an `:finish` parameter that complements the `:start` parameter to specify where to stop batch processing. *Vipul A M* @@ -1103,7 +1450,7 @@ * Use `SCHEMA` instead of `DB_STRUCTURE` for specifying a structure file. - This makes the db:structure tasks consistent with test:load_structure. + This makes the `db:structure` tasks consistent with `test:load_structure`. *Dieter Komendera* @@ -1129,18 +1476,6 @@ *Chris Sinjakli* -* Validation errors would be raised for parent records when an association - was saved when the parent had `validate: false`. It should not be the - responsibility of the model to validate an associated object unless the - object was created or modified by the parent. - - This fixes the issue by skipping validations if the parent record is - persisted, not changed, and not marked for destruction. - - Fixes #17621. - - *Eileen M. Uchitelle, Aaron Patterson* - * Fix n+1 query problem when eager loading nil associations (fixes #18312) *Sammy Larbi* diff --git a/activerecord/MIT-LICENSE b/activerecord/MIT-LICENSE index 7c2197229d..1f496cf280 100644 --- a/activerecord/MIT-LICENSE +++ b/activerecord/MIT-LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2004-2015 David Heinemeier Hansson +Copyright (c) 2004-2016 David Heinemeier Hansson Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the @@ -17,4 +17,4 @@ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
\ No newline at end of file +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/activerecord/README.rdoc b/activerecord/README.rdoc index 3eac8cc422..cfbee4d6f7 100644 --- a/activerecord/README.rdoc +++ b/activerecord/README.rdoc @@ -125,7 +125,7 @@ This would also define the following accessors: <tt>Product#name</tt> and ) {Learn more}[link:classes/ActiveRecord/Base.html] and read about the built-in support for - MySQL[link:classes/ActiveRecord/ConnectionAdapters/MysqlAdapter.html], + MySQL[link:classes/ActiveRecord/ConnectionAdapters/Mysql2Adapter.html], PostgreSQL[link:classes/ActiveRecord/ConnectionAdapters/PostgreSQLAdapter.html], and SQLite3[link:classes/ActiveRecord/ConnectionAdapters/SQLite3Adapter.html]. @@ -138,7 +138,7 @@ This would also define the following accessors: <tt>Product#name</tt> and * Database agnostic schema management with Migrations. - class AddSystemSettings < ActiveRecord::Migration + class AddSystemSettings < ActiveRecord::Migration[5.0] def up create_table :system_settings do |t| t.string :name @@ -188,7 +188,7 @@ Admit the Database: The latest version of Active Record can be installed with RubyGems: - % gem install activerecord + $ gem install activerecord Source code can be downloaded as part of the Rails project on GitHub: @@ -215,4 +215,3 @@ Bug reports can be filed for the Ruby on Rails project here: Feature requests should be discussed on the rails-core mailing list here: * https://groups.google.com/forum/?fromgroups#!forum/rubyonrails-core - diff --git a/activerecord/RUNNING_UNIT_TESTS.rdoc b/activerecord/RUNNING_UNIT_TESTS.rdoc index bae40604b1..a74fcf2df7 100644 --- a/activerecord/RUNNING_UNIT_TESTS.rdoc +++ b/activerecord/RUNNING_UNIT_TESTS.rdoc @@ -20,7 +20,6 @@ example: Simply executing <tt>bundle exec rake test</tt> is equivalent to the following: - $ bundle exec rake test:mysql $ bundle exec rake test:mysql2 $ bundle exec rake test:postgresql $ bundle exec rake test:sqlite3 diff --git a/activerecord/Rakefile b/activerecord/Rakefile index c93099a921..46df733cfe 100644 --- a/activerecord/Rakefile +++ b/activerecord/Rakefile @@ -17,14 +17,17 @@ def run_without_aborting(*tasks) abort "Errors running #{errors.join(', ')}" if errors.any? end -desc 'Run mysql, mysql2, sqlite, and postgresql tests by default' +desc 'Run mysql2, sqlite, and postgresql tests by default' task :default => :test -desc 'Run mysql, mysql2, sqlite, and postgresql tests' +task :package +task "package:clean" + +desc 'Run mysql2, sqlite, and postgresql tests' task :test do tasks = defined?(JRUBY_VERSION) ? %w(test_jdbcmysql test_jdbcsqlite3 test_jdbcpostgresql) : - %w(test_mysql test_mysql2 test_sqlite3 test_postgresql) + %w(test_mysql2 test_sqlite3 test_postgresql) run_without_aborting(*tasks) end @@ -32,7 +35,7 @@ namespace :test do task :isolated do tasks = defined?(JRUBY_VERSION) ? %w(isolated_test_jdbcmysql isolated_test_jdbcsqlite3 isolated_test_jdbcpostgresql) : - %w(isolated_test_mysql isolated_test_mysql2 isolated_test_sqlite3 isolated_test_postgresql) + %w(isolated_test_mysql2 isolated_test_sqlite3 isolated_test_postgresql) run_without_aborting(*tasks) end end @@ -43,7 +46,7 @@ namespace :db do task :drop => ['db:mysql:drop', 'db:postgresql:drop'] end -%w( mysql mysql2 postgresql sqlite3 sqlite3_mem db2 oracle jdbcmysql jdbcpostgresql jdbcsqlite3 jdbcderby jdbch2 jdbchsqldb ).each do |adapter| +%w( mysql2 postgresql sqlite3 sqlite3_mem db2 oracle jdbcmysql jdbcpostgresql jdbcsqlite3 jdbcderby jdbch2 jdbchsqldb ).each do |adapter| namespace :test do Rake::TestTask.new(adapter => "#{adapter}:env") { |t| adapter_short = adapter == 'db2' ? adapter : adapter[/^[a-z0-9]+/] @@ -87,14 +90,14 @@ namespace :db do namespace :mysql do desc 'Build the MySQL test databases' task :build do - config = ARTest.config['connections']['mysql'] + config = ARTest.config['connections']['mysql2'] %x( mysql --user=#{config['arunit']['username']} --password=#{config['arunit']['password']} -e "create DATABASE #{config['arunit']['database']} DEFAULT CHARACTER SET utf8 DEFAULT COLLATE utf8_unicode_ci ") %x( mysql --user=#{config['arunit2']['username']} --password=#{config['arunit2']['password']} -e "create DATABASE #{config['arunit2']['database']} DEFAULT CHARACTER SET utf8 DEFAULT COLLATE utf8_unicode_ci ") end desc 'Drop the MySQL test databases' task :drop do - config = ARTest.config['connections']['mysql'] + config = ARTest.config['connections']['mysql2'] %x( mysqladmin --user=#{config['arunit']['username']} --password=#{config['arunit']['password']} -f drop #{config['arunit']['database']} ) %x( mysqladmin --user=#{config['arunit2']['username']} --password=#{config['arunit2']['password']} -f drop #{config['arunit2']['database']} ) end diff --git a/activerecord/activerecord.gemspec b/activerecord/activerecord.gemspec index bd95b57303..4405da2812 100644 --- a/activerecord/activerecord.gemspec +++ b/activerecord/activerecord.gemspec @@ -24,5 +24,5 @@ Gem::Specification.new do |s| s.add_dependency 'activesupport', version s.add_dependency 'activemodel', version - s.add_dependency 'arel', '7.0.0.alpha' + s.add_dependency 'arel', '~> 7.0' end diff --git a/activerecord/bin/test b/activerecord/bin/test index f8adf2aabc..7417b068bf 100755 --- a/activerecord/bin/test +++ b/activerecord/bin/test @@ -6,7 +6,7 @@ module Minitest opts.separator "" opts.separator "Active Record options:" opts.on("-a", "--adapter [ADAPTER]", - "Run tests using a specific adapter (sqlite3, sqlite3_mem, mysql, mysql2, postgresql)") do |adapter| + "Run tests using a specific adapter (sqlite3, sqlite3_mem, mysql2, postgresql)") do |adapter| ENV["ARCONN"] = adapter.strip end diff --git a/activerecord/lib/active_record.rb b/activerecord/lib/active_record.rb index 264f869c68..ab3846ae65 100644 --- a/activerecord/lib/active_record.rb +++ b/activerecord/lib/active_record.rb @@ -1,5 +1,5 @@ #-- -# Copyright (c) 2004-2015 David Heinemeier Hansson +# Copyright (c) 2004-2016 David Heinemeier Hansson # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -40,6 +40,7 @@ module ActiveRecord autoload :CounterCache autoload :DynamicMatchers autoload :Enum + autoload :InternalMetadata autoload :Explain autoload :Inheritance autoload :Integration diff --git a/activerecord/lib/active_record/aggregations.rb b/activerecord/lib/active_record/aggregations.rb index be88c7c9e8..3ff41ed81b 100644 --- a/activerecord/lib/active_record/aggregations.rb +++ b/activerecord/lib/active_record/aggregations.rb @@ -33,7 +33,7 @@ module ActiveRecord # the database). # # class Customer < ActiveRecord::Base - # composed_of :balance, class_name: "Money", mapping: %w(balance amount) + # composed_of :balance, class_name: "Money", mapping: %w(amount currency) # composed_of :address, mapping: [ %w(address_street street), %w(address_city city) ] # end # diff --git a/activerecord/lib/active_record/association_relation.rb b/activerecord/lib/active_record/association_relation.rb index ee0bb8fafe..c18e88e4cf 100644 --- a/activerecord/lib/active_record/association_relation.rb +++ b/activerecord/lib/active_record/association_relation.rb @@ -10,7 +10,7 @@ module ActiveRecord end def ==(other) - other == to_a + other == records end def build(*args, &block) diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index b806a2f832..e13fe33b85 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -1164,6 +1164,7 @@ module ActiveRecord # Adds one or more objects to the collection by setting their foreign keys to the collection's primary key. # Note that this operation instantly fires update SQL without waiting for the save or update call on the # parent object, unless the parent object is a new record. + # This will also run validations and callbacks of associated object(s). # [collection.delete(object, ...)] # Removes one or more objects from the collection by setting their foreign keys to +NULL+. # Objects will be in addition destroyed if they're associated with <tt>dependent: :destroy</tt>, @@ -1181,7 +1182,8 @@ module ActiveRecord # [collection=objects] # Replaces the collections content by deleting and adding objects as appropriate. If the <tt>:through</tt> # option is true callbacks in the join models are triggered except destroy callbacks, since deletion is - # direct. + # direct by default. You can specify <tt>dependent: :destroy</tt> or + # <tt>dependent: :nullify</tt> to override this. # [collection_singular_ids] # Returns an array of the associated objects' ids # [collection_singular_ids=ids] @@ -1591,6 +1593,8 @@ module ActiveRecord # If true, the associated object will be touched (the updated_at/on attributes set to current time) # when this record is either saved or destroyed. If you specify a symbol, that attribute # will be updated with the current time in addition to the updated_at/on attribute. + # Please note that with touching no validation is performed and only the +after_touch+, + # +after_commit+ and +after_rollback+ callbacks are executed. # [:inverse_of] # Specifies the name of the #has_one or #has_many association on the associated # object that is the inverse of this #belongs_to association. Does not work in @@ -1639,7 +1643,7 @@ module ActiveRecord # The join table should not have a primary key or a model associated with it. You must manually generate the # join table with a migration such as this: # - # class CreateDevelopersProjectsJoinTable < ActiveRecord::Migration + # class CreateDevelopersProjectsJoinTable < ActiveRecord::Migration[5.0] # def change # create_join_table :developers, :projects # end diff --git a/activerecord/lib/active_record/associations/association.rb b/activerecord/lib/active_record/associations/association.rb index c7b396f3d4..d64ab64c99 100644 --- a/activerecord/lib/active_record/associations/association.rb +++ b/activerecord/lib/active_record/associations/association.rb @@ -163,9 +163,12 @@ module ActiveRecord @reflection = @owner.class._reflect_on_association(reflection_name) end - def initialize_attributes(record) #:nodoc: + def initialize_attributes(record, except_from_scope_attributes = nil) #:nodoc: + except_from_scope_attributes ||= {} skip_assign = [reflection.foreign_key, reflection.type].compact - attributes = create_scope.except(*(record.changed - skip_assign)) + assigned_keys = record.changed + assigned_keys += except_from_scope_attributes.keys.map(&:to_s) + attributes = create_scope.except(*(assigned_keys - skip_assign)) record.assign_attributes(attributes) set_inverse_instance(record) end @@ -248,7 +251,7 @@ module ActiveRecord def build_record(attributes) reflection.build_association(attributes) do |record| - initialize_attributes(record) + initialize_attributes(record, attributes) end end diff --git a/activerecord/lib/active_record/associations/builder/belongs_to.rb b/activerecord/lib/active_record/associations/builder/belongs_to.rb index dae468ba54..346329c610 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 + [:foreign_type, :polymorphic, :touch, :counter_cache, :optional] + super + [:polymorphic, :touch, :counter_cache, :optional] end def self.valid_dependent_options @@ -106,8 +106,7 @@ module ActiveRecord::Associations::Builder # :nodoc: touch = reflection.options[:touch] callback = lambda { |record| - touch_method = touching_delayed_records? ? :touch : :touch_later - BelongsTo.touch_record(record, foreign_key, n, touch, touch_method) + BelongsTo.touch_record(record, foreign_key, n, touch, belongs_to_touch_method) } model.after_save callback, if: :changed? @@ -116,8 +115,7 @@ module ActiveRecord::Associations::Builder # :nodoc: end def self.add_destroy_callbacks(model, reflection) - name = reflection.name - model.after_destroy lambda { |o| o.association(name).handle_dependency } + model.after_destroy lambda { |o| o.association(reflection.name).handle_dependency } end def self.define_validations(model, reflection) diff --git a/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb b/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb index b888148841..5fbd79d118 100644 --- a/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb +++ b/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb @@ -76,6 +76,9 @@ module ActiveRecord::Associations::Builder # :nodoc: left_model.retrieve_connection end + def self.primary_key + false + end } join_model.name = "HABTM_#{association_name.to_s.camelize}" diff --git a/activerecord/lib/active_record/associations/builder/has_one.rb b/activerecord/lib/active_record/associations/builder/has_one.rb index 9d64ae877b..4de846d12b 100644 --- a/activerecord/lib/active_record/associations/builder/has_one.rb +++ b/activerecord/lib/active_record/associations/builder/has_one.rb @@ -5,7 +5,7 @@ module ActiveRecord::Associations::Builder # :nodoc: end def self.valid_options(options) - valid = super + [:as, :foreign_type] + valid = super + [:as] valid += [:through, :source, :source_type] if options[:through] valid end diff --git a/activerecord/lib/active_record/associations/builder/singular_association.rb b/activerecord/lib/active_record/associations/builder/singular_association.rb index 58a9c8ff24..bb96202a22 100644 --- a/activerecord/lib/active_record/associations/builder/singular_association.rb +++ b/activerecord/lib/active_record/associations/builder/singular_association.rb @@ -3,7 +3,7 @@ module ActiveRecord::Associations::Builder # :nodoc: class SingularAssociation < Association #:nodoc: def self.valid_options(options) - super + [:dependent, :primary_key, :inverse_of, :required] + super + [:foreign_type, :dependent, :primary_key, :inverse_of, :required] end def self.define_accessors(model, reflection) diff --git a/activerecord/lib/active_record/associations/collection_association.rb b/activerecord/lib/active_record/associations/collection_association.rb index f32dddb8f0..2dca6b612e 100644 --- a/activerecord/lib/active_record/associations/collection_association.rb +++ b/activerecord/lib/active_record/associations/collection_association.rb @@ -72,7 +72,10 @@ module ActiveRecord pk_type = reflection.primary_key_type ids = Array(ids).reject(&:blank?) ids.map! { |i| pk_type.cast(i) } - replace(klass.find(ids).index_by(&:id).values_at(*ids)) + records = klass.where(reflection.association_primary_key => ids).index_by do |r| + r.send(reflection.association_primary_key) + end.values_at(*ids) + replace(records) end def reset @@ -133,6 +136,14 @@ module ActiveRecord first_nth_or_last(:forty_two, *args) end + def third_to_last(*args) + first_nth_or_last(:third_to_last, *args) + end + + def second_to_last(*args) + first_nth_or_last(:second_to_last, *args) + end + def last(*args) first_nth_or_last(:last, *args) end @@ -414,12 +425,16 @@ module ActiveRecord def replace_on_target(record, index, skip_callbacks) callback(:before_add, record) unless skip_callbacks + + was_loaded = loaded? yield(record) if block_given? - if index - @target[index] = record - else - @target << record + unless !was_loaded && loaded? + if index + @target[index] = record + else + @target << record + end end callback(:after_add, record) unless skip_callbacks diff --git a/activerecord/lib/active_record/associations/collection_proxy.rb b/activerecord/lib/active_record/associations/collection_proxy.rb index fe693cfbb6..b9aed05135 100644 --- a/activerecord/lib/active_record/associations/collection_proxy.rb +++ b/activerecord/lib/active_record/associations/collection_proxy.rb @@ -197,6 +197,16 @@ module ActiveRecord @association.forty_two(*args) end + # Same as #first except returns only the third-to-last record. + def third_to_last(*args) + @association.third_to_last(*args) + end + + # Same as #first except returns only the second-to-last record. + def second_to_last(*args) + @association.second_to_last(*args) + end + # Returns the last record, or the last +n+ records, from the collection. # If the collection is empty, the first form returns +nil+, and the second # form returns an empty array. @@ -969,6 +979,10 @@ module ActiveRecord end alias_method :to_a, :to_ary + def records # :nodoc: + load_target + end + # Adds one or more +records+ to the collection by setting their foreign keys # to the association's primary key. Returns +self+, so several appends may be # chained together. 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 deb0f8c9f5..36fc381343 100644 --- a/activerecord/lib/active_record/associations/has_many_through_association.rb +++ b/activerecord/lib/active_record/associations/has_many_through_association.rb @@ -66,6 +66,11 @@ module ActiveRecord through_record = through_association.build(*options_for_through_record) through_record.send("#{source_reflection.name}=", record) + + if options[:source_type] + through_record.send("#{source_reflection.foreign_type}=", options[:source_type]) + end + through_record end end diff --git a/activerecord/lib/active_record/associations/join_dependency.rb b/activerecord/lib/active_record/associations/join_dependency.rb index 9f183c3e7e..0e4e951269 100644 --- a/activerecord/lib/active_record/associations/join_dependency.rb +++ b/activerecord/lib/active_record/associations/join_dependency.rb @@ -32,7 +32,7 @@ module ActiveRecord @alias_cache[node][column] end - class Table < Struct.new(:node, :columns) + class Table < Struct.new(:node, :columns) # :nodoc: def table Arel::Nodes::TableAlias.new node.table, node.aliased_table_name end diff --git a/activerecord/lib/active_record/associations/join_dependency/join_association.rb b/activerecord/lib/active_record/associations/join_dependency/join_association.rb index a6ad09a38a..c5fbe0d1d1 100644 --- a/activerecord/lib/active_record/associations/join_dependency/join_association.rb +++ b/activerecord/lib/active_record/associations/join_dependency/join_association.rb @@ -54,12 +54,18 @@ module ActiveRecord end scope_chain_index += 1 - relation = ActiveRecord::Relation.create( - klass, - table, - predicate_builder, - ) - scope_chain_items.concat [klass.send(:build_default_scope, relation)].compact + klass_scope = + if klass.current_scope + klass.current_scope.clone + else + relation = ActiveRecord::Relation.create( + klass, + table, + predicate_builder, + ) + klass.send(:build_default_scope, relation) + end + scope_chain_items.concat [klass_scope].compact rel = scope_chain_items.inject(scope_chain_items.shift) do |left, right| left.merge right @@ -74,9 +80,8 @@ module ActiveRecord value = foreign_klass.base_class.name column = klass.columns_hash[reflection.type.to_s] - substitute = klass.connection.substitute_at(column) binds << Relation::QueryAttribute.new(column.name, value, klass.type_for_attribute(column.name)) - constraint = constraint.and table[reflection.type].eq substitute + constraint = constraint.and klass.arel_attribute(reflection.type, table).eq(Arel::Nodes::BindParam.new) end joins << table.create_join(table, table.create_on(constraint), join_type) diff --git a/activerecord/lib/active_record/associations/preloader/association.rb b/activerecord/lib/active_record/associations/preloader/association.rb index e11a5cfb8a..3032bc786e 100644 --- a/activerecord/lib/active_record/associations/preloader/association.rb +++ b/activerecord/lib/active_record/associations/preloader/association.rb @@ -47,7 +47,7 @@ module ActiveRecord # This is overridden by HABTM as the condition should be on the foreign_key column in # the join table def association_key - table[association_key_name] + klass.arel_attribute(association_key_name, table) end # The name of the key on the model which declares the association diff --git a/activerecord/lib/active_record/associations/preloader/through_association.rb b/activerecord/lib/active_record/associations/preloader/through_association.rb index 24aa47bb51..b0203909ce 100644 --- a/activerecord/lib/active_record/associations/preloader/through_association.rb +++ b/activerecord/lib/active_record/associations/preloader/through_association.rb @@ -18,7 +18,8 @@ module ActiveRecord through_records = owners.map do |owner| association = owner.association through_reflection.name - [owner, Array(association.reader)] + center = target_records_from_association(association) + [owner, Array(center)] end reset_association owners, through_reflection.name @@ -37,28 +38,35 @@ module ActiveRecord } end - record_offset = {} - @preloaded_records.each_with_index do |record,i| - record_offset[record] = i - end - - through_records.each_with_object({}) { |(lhs,center),records_by_owner| + through_records.each_with_object({}) do |(lhs,center), records_by_owner| pl_to_middle = center.group_by { |record| middle_to_pl[record] } records_by_owner[lhs] = pl_to_middle.flat_map do |pl, middles| rhs_records = middles.flat_map { |r| association = r.association source_reflection.name - association.reader + target_records_from_association(association) }.compact - rhs_records.sort_by { |rhs| record_offset[rhs] } + # Respect the order on `reflection_scope` if it exists, else use the natural order. + if reflection_scope.values[:order].present? + @id_map ||= id_to_index_map @preloaded_records + rhs_records.sort_by { |rhs| @id_map[rhs] } + else + rhs_records + end end - } + end end private + def id_to_index_map(ids) + id_map = {} + ids.each_with_index { |id, index| id_map[id] = index } + id_map + end + def reset_association(owners, association_name) should_reset = (through_scope != through_reflection.klass.unscoped) || (reflection.options[:source_type] && through_reflection.collection?) @@ -91,6 +99,10 @@ module ActiveRecord scope end + + def target_records_from_association(association) + association.loaded? ? association.target : association.reader + end end end end diff --git a/activerecord/lib/active_record/associations/singular_association.rb b/activerecord/lib/active_record/associations/singular_association.rb index c7cc48ba16..f913f0852a 100644 --- a/activerecord/lib/active_record/associations/singular_association.rb +++ b/activerecord/lib/active_record/associations/singular_association.rb @@ -45,7 +45,7 @@ module ActiveRecord end def get_records - return scope.limit(1).to_a if skip_statement_cache? + return scope.limit(1).records if skip_statement_cache? conn = klass.connection sc = reflection.association_scope_cache(conn, owner) do diff --git a/activerecord/lib/active_record/attribute_assignment.rb b/activerecord/lib/active_record/attribute_assignment.rb index a6d81c82b4..4c22be8235 100644 --- a/activerecord/lib/active_record/attribute_assignment.rb +++ b/activerecord/lib/active_record/attribute_assignment.rb @@ -29,14 +29,6 @@ module ActiveRecord assign_multiparameter_attributes(multi_parameter_attributes) unless multi_parameter_attributes.empty? end - # Tries to assign given value to given attribute. - # In case of an error, re-raises with the ActiveRecord constant. - def _assign_attribute(k, v) # :nodoc: - super - rescue ActiveModel::UnknownAttributeError - raise UnknownAttributeError.new(self, k) - end - # Assign any deferred nested attributes after the base attributes have been set. def assign_nested_parameter_attributes(pairs) pairs.each { |k, v| _assign_attribute(k, v) } diff --git a/activerecord/lib/active_record/attribute_methods.rb b/activerecord/lib/active_record/attribute_methods.rb index cbdd4950a6..e902eb7531 100644 --- a/activerecord/lib/active_record/attribute_methods.rb +++ b/activerecord/lib/active_record/attribute_methods.rb @@ -1,7 +1,7 @@ require 'active_support/core_ext/enumerable' require 'active_support/core_ext/string/filters' require 'mutex_m' -require 'concurrent' +require 'concurrent/map' module ActiveRecord # = Active Record Attribute Methods @@ -34,30 +34,6 @@ module ActiveRecord BLACKLISTED_CLASS_METHODS = %w(private public protected allocate new name parent superclass) - class AttributeMethodCache - def initialize - @module = Module.new - @method_cache = Concurrent::Map.new - end - - def [](name) - @method_cache.compute_if_absent(name) do - safe_name = name.unpack('h*'.freeze).first - temp_method = "__temp__#{safe_name}" - ActiveRecord::AttributeMethods::AttrNames.set_name_cache safe_name, name - @module.module_eval method_body(temp_method, safe_name), __FILE__, __LINE__ - @module.instance_method temp_method - end - end - - private - - # Override this method in the subclasses for method body. - def method_body(method_name, const_name) - raise NotImplementedError, "Subclasses must implement a method_body(method_name, const_name) method." - end - end - class GeneratedAttributeMethods < Module; end # :nodoc: module ClassMethods @@ -191,6 +167,18 @@ module ActiveRecord end end + # Returns true if the given attribute exists, otherwise false. + # + # class Person < ActiveRecord::Base + # end + # + # Person.has_attribute?('name') # => true + # Person.has_attribute?(:age) # => true + # Person.has_attribute?(:nothing) # => false + def has_attribute?(attr_name) + attribute_types.key?(attr_name.to_s) + end + # Returns the column object for the named attribute. # Returns a +ActiveRecord::ConnectionAdapters::NullColumn+ if the # named attribute does not exist. diff --git a/activerecord/lib/active_record/attribute_methods/read.rb b/activerecord/lib/active_record/attribute_methods/read.rb index 5197e21fa4..ab2ecaa7c5 100644 --- a/activerecord/lib/active_record/attribute_methods/read.rb +++ b/activerecord/lib/active_record/attribute_methods/read.rb @@ -1,8 +1,11 @@ module ActiveRecord module AttributeMethods module Read - ReaderMethodCache = Class.new(AttributeMethodCache) { - private + extend ActiveSupport::Concern + + module ClassMethods + protected + # We want to generate the methods via module_eval rather than # define_method, because define_method is slower on dispatch. # Evaluating many similar methods may use more memory as the instruction @@ -21,21 +24,6 @@ module ActiveRecord # to allocate an object on each call to the attribute method. # Making it frozen means that it doesn't get duped when used to # key the @attributes in read_attribute. - def method_body(method_name, const_name) - <<-EOMETHOD - def #{method_name} - name = ::ActiveRecord::AttributeMethods::AttrNames::ATTR_#{const_name} - _read_attribute(name) { |n| missing_attribute(n, caller) } - end - EOMETHOD - end - }.new - - extend ActiveSupport::Concern - - module ClassMethods - protected - def define_method_attribute(name) safe_name = name.unpack('h*'.freeze).first temp_method = "__temp__#{safe_name}" diff --git a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb index 9e693b6aee..ebaaa54b2b 100644 --- a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb +++ b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb @@ -1,3 +1,5 @@ +require 'active_support/core_ext/string/strip' + module ActiveRecord module AttributeMethods module TimeZoneConversion @@ -7,9 +9,9 @@ module ActiveRecord end def cast(value) - if value.is_a?(Array) - value.map { |v| cast(v) } - elsif value.is_a?(Hash) + return if value.nil? + + if value.is_a?(Hash) set_time_zone_without_conversion(super) elsif value.respond_to?(:in_time_zone) begin @@ -17,24 +19,38 @@ module ActiveRecord rescue ArgumentError nil end + else + map_avoiding_infinite_recursion(super) { |v| cast(v) } end end private def convert_time_to_time_zone(value) - if value.is_a?(Array) - value.map { |v| convert_time_to_time_zone(v) } - elsif value.acts_like?(:time) + return if value.nil? + + if value.acts_like?(:time) value.in_time_zone - else + elsif value.is_a?(::Float) value + else + map_avoiding_infinite_recursion(value) { |v| convert_time_to_time_zone(v) } end end def set_time_zone_without_conversion(value) ::Time.zone.local_to_utc(value).in_time_zone end + + def map_avoiding_infinite_recursion(value) + map(value) do |v| + if value.equal?(v) + nil + else + yield(v) + end + end + end end extend ActiveSupport::Concern @@ -77,7 +93,7 @@ module ActiveRecord !result && cast_type.type == :time && time_zone_aware_types.include?(:not_explicitly_configured) - ActiveSupport::Deprecation.warn(<<-MESSAGE) + ActiveSupport::Deprecation.warn(<<-MESSAGE.strip_heredoc) Time columns will become time zone aware in Rails 5.1. This still causes `String`s to be parsed as if they were in `Time.zone`, and `Time`s to be converted to `Time.zone`. diff --git a/activerecord/lib/active_record/attribute_methods/write.rb b/activerecord/lib/active_record/attribute_methods/write.rb index bbf2a51a0e..5599b590ca 100644 --- a/activerecord/lib/active_record/attribute_methods/write.rb +++ b/activerecord/lib/active_record/attribute_methods/write.rb @@ -1,19 +1,6 @@ module ActiveRecord module AttributeMethods module Write - WriterMethodCache = Class.new(AttributeMethodCache) { - private - - def method_body(method_name, const_name) - <<-EOMETHOD - def #{method_name}(value) - name = ::ActiveRecord::AttributeMethods::AttrNames::ATTR_#{const_name} - write_attribute(name, value) - end - EOMETHOD - end - }.new - extend ActiveSupport::Concern included do diff --git a/activerecord/lib/active_record/autosave_association.rb b/activerecord/lib/active_record/autosave_association.rb index fc12c3f45a..bac5a38a5d 100644 --- a/activerecord/lib/active_record/autosave_association.rb +++ b/activerecord/lib/active_record/autosave_association.rb @@ -331,15 +331,30 @@ module ActiveRecord validation_context = self.validation_context unless [:create, :update].include?(self.validation_context) unless valid = record.valid?(validation_context) if reflection.options[:autosave] + indexed_attribute = !index.nil? && (reflection.options[:index_errors] || ActiveRecord::Base.index_nested_attribute_errors) + record.errors.each do |attribute, message| - if index.nil? || (!reflection.options[:index_errors] && !ActiveRecord::Base.index_nested_attribute_errors) - attribute = "#{reflection.name}.#{attribute}" - else + if indexed_attribute attribute = "#{reflection.name}[#{index}].#{attribute}" + else + attribute = "#{reflection.name}.#{attribute}" end errors[attribute] << message errors[attribute].uniq! end + + record.errors.details.each_key do |attribute| + if indexed_attribute + reflection_attribute = "#{reflection.name}[#{index}].#{attribute}" + else + reflection_attribute = "#{reflection.name}.#{attribute}" + end + + record.errors.details[attribute].each do |error| + errors.details[reflection_attribute] << error + errors.details[reflection_attribute].uniq! + end + end else errors.add(reflection.name) end diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index 9782e58299..7ed2fe48be 100644 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -13,7 +13,6 @@ require 'active_support/core_ext/kernel/singleton_class' require 'active_support/core_ext/module/introspection' require 'active_support/core_ext/object/duplicable' require 'active_support/core_ext/class/subclasses' -require 'arel' require 'active_record/attribute_decorators' require 'active_record/errors' require 'active_record/log_subscriber' @@ -132,9 +131,6 @@ module ActiveRecord #:nodoc: # end # end # - # You can alternatively use <tt>self[:attribute]=(value)</tt> and <tt>self[:attribute]</tt> - # or <tt>write_attribute(:attribute, value)</tt> and <tt>read_attribute(:attribute)</tt>. - # # == Attribute query methods # # In addition to the basic accessors, query methods are also automatically available on the Active Record object. @@ -261,7 +257,7 @@ module ActiveRecord #:nodoc: # The +errors+ property of this exception contains an array of # AttributeAssignmentError # objects that should be inspected to determine which attributes triggered the errors. - # * RecordInvalid - raised by {ActiveRecord::Base#save}[rdoc-ref:Persistence#save] and + # * RecordInvalid - raised by {ActiveRecord::Base#save!}[rdoc-ref:Persistence#save!] and # {ActiveRecord::Base.create!}[rdoc-ref:Persistence::ClassMethods#create!] # when the record is invalid. # * RecordNotFound - No record responded to the {ActiveRecord::Base.find}[rdoc-ref:FinderMethods#find] method. diff --git a/activerecord/lib/active_record/callbacks.rb b/activerecord/lib/active_record/callbacks.rb index cfd8cbda67..1f1b11eb68 100644 --- a/activerecord/lib/active_record/callbacks.rb +++ b/activerecord/lib/active_record/callbacks.rb @@ -175,26 +175,11 @@ module ActiveRecord # end # end # - # The callback macros usually accept a symbol for the method they're supposed to run, but you can also - # pass a "method string", which will then be evaluated within the binding of the callback. Example: - # - # class Topic < ActiveRecord::Base - # before_destroy 'self.class.delete_all "parent_id = #{id}"' - # end - # - # Notice that single quotes (') are used so the <tt>#{id}</tt> part isn't evaluated until the callback - # is triggered. Also note that these inline callbacks can be stacked just like the regular ones: - # - # class Topic < ActiveRecord::Base - # before_destroy 'self.class.delete_all "parent_id = #{id}"', - # 'puts "Evaluated after parents are destroyed"' - # end - # # == <tt>before_validation*</tt> returning statements # # If the +before_validation+ callback throws +:abort+, the process will be # aborted and {ActiveRecord::Base#save}[rdoc-ref:Persistence#save] will return +false+. - # If {ActiveRecord::Base#save!}[rdoc-ref:Persistence#save!] is called it will raise a ActiveRecord::RecordInvalid exception. + # If {ActiveRecord::Base#save!}[rdoc-ref:Persistence#save!] is called it will raise an ActiveRecord::RecordInvalid exception. # Nothing will be appended to the errors object. # # == Canceling callbacks @@ -208,12 +193,12 @@ module ActiveRecord # # Sometimes the code needs that the callbacks execute in a specific order. For example, a +before_destroy+ # callback (+log_children+ in this case) should be executed before the children get destroyed by the - # <tt>dependent: destroy</tt> option. + # <tt>dependent: :destroy</tt> option. # # Let's look at the code below: # # class Topic < ActiveRecord::Base - # has_many :children, dependent: destroy + # has_many :children, dependent: :destroy # # before_destroy :log_children # @@ -228,7 +213,7 @@ module ActiveRecord # You can use the +prepend+ option on the +before_destroy+ callback to avoid this. # # class Topic < ActiveRecord::Base - # has_many :children, dependent: destroy + # has_many :children, dependent: :destroy # # before_destroy :log_children, prepend: true # @@ -238,7 +223,7 @@ module ActiveRecord # end # end # - # This way, the +before_destroy+ gets executed before the <tt>dependent: destroy</tt> is called, and the data is still available. + # This way, the +before_destroy+ gets executed before the <tt>dependent: :destroy</tt> is called, and the data is still available. # # == \Transactions # diff --git a/activerecord/lib/active_record/collection_cache_key.rb b/activerecord/lib/active_record/collection_cache_key.rb index 3c4ca3d116..5dcc98424a 100644 --- a/activerecord/lib/active_record/collection_cache_key.rb +++ b/activerecord/lib/active_record/collection_cache_key.rb @@ -7,18 +7,27 @@ module ActiveRecord if collection.loaded? size = collection.size - timestamp = collection.max_by(×tamp_column).public_send(timestamp_column) + if size > 0 + timestamp = collection.max_by(×tamp_column).public_send(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)}" query = collection + .unscope(:select) .select("COUNT(*) AS size", "MAX(#{column}) AS timestamp") .unscope(:order) result = connection.select_one(query) - size = result["size"] - timestamp = column_type.deserialize(result["timestamp"]) + if result.blank? + size = 0 + timestamp = nil + else + size = result["size"] + timestamp = column_type.deserialize(result["timestamp"]) + end + end if timestamp diff --git a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb index 0d850c7625..ccd2899489 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb @@ -1,5 +1,5 @@ require 'thread' -require 'concurrent' +require 'concurrent/map' require 'monitor' module ActiveRecord @@ -197,7 +197,7 @@ module ActiveRecord elapsed = Time.now - t0 if elapsed >= timeout - msg = 'could not obtain a database connection within %0.3f seconds (waited %0.3f seconds)' % + msg = 'could not obtain a connection from the pool within %0.3f seconds (waited %0.3f seconds); all pooled connections were in use' % [timeout, elapsed] raise ConnectionTimeoutError, msg end @@ -960,12 +960,11 @@ module ActiveRecord def call(env) testing = env['rack.test'] - response = @app.call(env) - response[2] = ::Rack::BodyProxy.new(response[2]) do + status, headers, body = @app.call(env) + proxy = ::Rack::BodyProxy.new(body) do ActiveRecord::Base.clear_active_connections! unless testing end - - response + [status, headers, proxy] rescue Exception ActiveRecord::Base.clear_active_connections! unless testing raise 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 848aeb821c..aa5ae15285 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb @@ -27,10 +27,10 @@ module ActiveRecord end # Returns an ActiveRecord::Result instance. - def select_all(arel, name = nil, binds = []) + def select_all(arel, name = nil, binds = [], preparable: nil) arel, binds = binds_from_relation arel, binds sql = to_sql(arel, binds) - if arel.is_a?(String) + if !prepared_statements || (arel.is_a?(String) && preparable.nil?) preparable = false else preparable = visitor.preparable @@ -58,8 +58,8 @@ module ActiveRecord # Returns an array of the values of the first column in a select: # select_values("SELECT id FROM companies LIMIT 3") => [1,2,3] - def select_values(arel, name = nil) - arel, binds = binds_from_relation arel, [] + def select_values(arel, name = nil, binds = []) + arel, binds = binds_from_relation arel, binds select_rows(to_sql(arel, binds), name, binds).map(&:first) end @@ -69,7 +69,11 @@ module ActiveRecord end undef_method :select_rows - # Executes the SQL statement in the context of this connection. + # Executes the SQL statement in the context of this connection and returns + # the raw result from the connection adapter. + # Note: depending on your database connector, the result returned by this + # method may be manually memory managed. Consider using the exec_query + # wrapper instead. def execute(sql, name = nil) end undef_method :execute @@ -106,7 +110,7 @@ module ActiveRecord exec_query(sql, name, binds) end - # Returns the last auto-generated ID from the affected table. + # Executes an INSERT query and returns the new record's ID # # +id_value+ will be returned unless the value is nil, in # which case the database will attempt to calculate the last inserted @@ -115,20 +119,24 @@ module ActiveRecord # If the next id was calculated in advance (as in Oracle), it should be # passed in as +id_value+. def insert(arel, name = nil, pk = nil, id_value = nil, sequence_name = nil, binds = []) - sql, binds = sql_for_insert(to_sql(arel, binds), pk, id_value, sequence_name, binds) - value = exec_insert(sql, name, binds, pk, sequence_name) + sql, binds, pk, sequence_name = sql_for_insert(to_sql(arel, binds), pk, id_value, sequence_name, binds) + value = exec_insert(sql, name, binds, pk, sequence_name) id_value || last_inserted_id(value) end + alias create insert + alias insert_sql insert # Executes the update statement and returns the number of rows affected. def update(arel, name = nil, binds = []) exec_update(to_sql(arel, binds), name, binds) end + alias update_sql update # Executes the delete statement and returns the number of rows affected. def delete(arel, name = nil, binds = []) exec_delete(to_sql(arel, binds), name, binds) end + alias delete_sql delete # Returns +true+ when the connection adapter supports prepared statement # caching, otherwise returns +false+ @@ -208,7 +216,7 @@ module ActiveRecord # * You are joining an existing open transaction # * You are creating a nested (savepoint) transaction # - # The mysql, mysql2 and postgresql adapters support setting the transaction + # The mysql2 and postgresql adapters support setting the transaction # isolation level. However, support is disabled for MySQL versions below 5, # because they are affected by a bug[http://bugs.mysql.com/bug.php?id=39170] # which means the isolation level gets persisted outside the transaction. @@ -296,14 +304,15 @@ module ActiveRecord # Inserts the given fixture into the table. Overridden in adapters that require # something beyond a simple insert (eg. Oracle). def insert_fixture(fixture, table_name) - columns = schema_cache.columns_hash(table_name) + fixture = fixture.stringify_keys + columns = schema_cache.columns_hash(table_name) binds = fixture.map do |name, value| if column = columns[name] type = lookup_cast_type_from_column(column) Relation::QueryAttribute.new(name, value, type) else - raise Fixture::FixtureError, %(table "#{table_name}" has no column named "#{name}".) + raise Fixture::FixtureError, %(table "#{table_name}" has no column named #{name.inspect}.) end end key_list = fixture.keys.map { |name| quote_column_name(name) } @@ -344,18 +353,12 @@ module ActiveRecord # The default strategy for an UPDATE with joins is to use a subquery. This doesn't work # on MySQL (even when aliasing the tables), but MySQL allows using JOIN directly in # an UPDATE statement, so in the MySQL adapters we redefine this to do that. - def join_to_update(update, select) #:nodoc: - key = update.key + def join_to_update(update, select, key) # :nodoc: subselect = subquery_for(key, select) update.where key.in(subselect) end - - def join_to_delete(delete, select, key) #:nodoc: - subselect = subquery_for(key, select) - - delete.where key.in(subselect) - end + alias join_to_delete join_to_update protected @@ -375,24 +378,8 @@ module ActiveRecord exec_query(sql, name, binds, prepare: true) end - # Returns the last auto-generated ID from the affected table. - def insert_sql(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) - execute(sql, name) - id_value - end - - # Executes the update statement and returns the number of rows affected. - def update_sql(sql, name = nil) - execute(sql, name) - end - - # Executes the delete statement and returns the number of rows affected. - def delete_sql(sql, name = nil) - update_sql(sql, name) - end - def sql_for_insert(sql, pk, id_value, sequence_name, binds) - [sql, binds] + [sql, binds, pk, sequence_name] end def last_inserted_id(result) diff --git a/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb b/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb index 5e27cfe507..33dbab41cb 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb @@ -61,11 +61,11 @@ module ActiveRecord @query_cache.clear end - def select_all(arel, name = nil, binds = []) + def select_all(arel, name = nil, binds = [], preparable: nil) if @query_cache_enabled && !locked?(arel) arel, binds = binds_from_relation arel, binds sql = to_sql(arel, binds) - cache_sql(sql, binds) { super(sql, name, binds) } + cache_sql(sql, binds) { super(sql, name, binds, preparable: visitor.preparable) } else super end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb index 9ec0a67c8f..7e3760d34b 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb @@ -93,7 +93,7 @@ module ActiveRecord # Override to return the quoted table name for assignment. Defaults to # table quoting. # - # This works for mysql and mysql2 where table.column can be used to + # This works for mysql2 where table.column can be used to # resolve ambiguity. # # We override this in the sqlite3 and postgresql adapters to use only @@ -102,9 +102,13 @@ module ActiveRecord quote_table_name("#{table}.#{attr}") end - def quote_default_expression(value, column) #:nodoc: - value = lookup_cast_type(column.sql_type).serialize(value) - quote(value) + def quote_default_expression(value, column) # :nodoc: + if value.is_a?(Proc) + value.call + else + value = lookup_cast_type(column.sql_type).serialize(value) + quote(value) + end end def quoted_true 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 e2ef56798b..4f97c7c065 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb @@ -69,7 +69,7 @@ module ActiveRecord def initialize( name, polymorphic: false, - index: false, + index: true, foreign_key: false, type: :integer, **options @@ -182,6 +182,7 @@ module ActiveRecord end CODE end + alias_method :numeric, :decimal end # Represents the schema of an SQL table in an abstract way. This class @@ -190,7 +191,7 @@ module ActiveRecord # Inside migration files, the +t+ object in {create_table}[rdoc-ref:SchemaStatements#create_table] # is actually of this type: # - # class SomeMigration < ActiveRecord::Migration + # class SomeMigration < ActiveRecord::Migration[5.0] # def up # create_table :foo do |t| # puts t.class # => "ActiveRecord::ConnectionAdapters::TableDefinition" @@ -202,22 +203,17 @@ module ActiveRecord # end # end # - # The table definitions - # The Columns are stored as a ColumnDefinition in the #columns attribute. class TableDefinition include ColumnMethods - # An array of ColumnDefinition objects, representing the column changes - # that have been defined. attr_accessor :indexes - attr_reader :name, :temporary, :options, :as, :foreign_keys, :native + attr_reader :name, :temporary, :options, :as, :foreign_keys - def initialize(types, name, temporary, options, as = nil) + def initialize(name, temporary, options, as = nil) @columns_hash = {} @indexes = {} - @foreign_keys = {} + @foreign_keys = [] @primary_keys = nil - @native = types @temporary = temporary @options = options @as = as @@ -334,7 +330,7 @@ module ActiveRecord end def foreign_key(table_name, options = {}) # :nodoc: - foreign_keys[table_name] = options + foreign_keys.push([table_name, options]) end # Appends <tt>:datetime</tt> columns <tt>:created_at</tt> and @@ -366,11 +362,8 @@ module ActiveRecord def new_column_definition(name, type, options) # :nodoc: type = aliased_types(type.to_s, type) column = create_column_definition name, type - limit = options.fetch(:limit) do - native[type][:limit] if native[type].is_a?(Hash) - end - column.limit = limit + column.limit = options[:limit] column.precision = options[:precision] column.scale = options[:scale] column.default = options[:default] @@ -444,6 +437,7 @@ module ActiveRecord # t.bigint # t.float # t.decimal + # t.numeric # t.datetime # t.timestamp # t.time @@ -631,11 +625,6 @@ module ActiveRecord def foreign_key_exists?(*args) # :nodoc: @base.foreign_key_exists?(name, *args) end - - private - def native - @base.native_database_types - end end end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb index e252ddb4cf..b1b6044e72 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb @@ -14,7 +14,7 @@ module ActiveRecord def column_spec_for_primary_key(column) return if column.type == :integer - spec = { id: column.type.inspect } + spec = { id: schema_type(column).inspect } spec.merge!(prepare_column_options(column).delete_if { |key, _| [:name, :type].include?(key) }) end @@ -24,7 +24,7 @@ module ActiveRecord def prepare_column_options(column) spec = {} spec[:name] = column.name.inspect - spec[:type] = schema_type(column) + spec[:type] = schema_type(column).to_s spec[:null] = 'false' unless column.null if limit = schema_limit(column) @@ -57,12 +57,12 @@ module ActiveRecord private def schema_type(column) - column.type.to_s + column.type end def schema_limit(column) - limit = column.limit || native_database_types[column.type][:limit] - limit.inspect if limit + limit = column.limit + limit.inspect if limit && limit != native_database_types[column.type][:limit] end def schema_precision(column) @@ -76,11 +76,17 @@ module ActiveRecord def schema_default(column) type = lookup_cast_type_from_column(column) default = type.deserialize(column.default) - unless default.nil? + if default.nil? + schema_expression(column) + else type.type_cast_for_schema(default) end end + def schema_expression(column) + "-> { #{column.default_function.inspect} }" if column.default_function + end + def schema_collation(column) column.collation.inspect if column.collation end 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 d5f8dbc8fc..f0f855963a 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb @@ -82,18 +82,19 @@ module ActiveRecord # def index_exists?(table_name, column_name, options = {}) column_names = Array(column_name).map(&:to_s) - index_name = options.key?(:name) ? options[:name].to_s : index_name(table_name, column: column_names) checks = [] - checks << lambda { |i| i.name == index_name } checks << lambda { |i| i.columns == column_names } checks << lambda { |i| i.unique } if options[:unique] + checks << lambda { |i| i.name == options[:name].to_s } if options[:name] 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. - def columns(table_name) end + def columns(table_name) + raise NotImplementedError, "#columns is not implemented" + end # Checks to see if a column exists in a given table. # @@ -111,18 +112,25 @@ module ActiveRecord # def column_exists?(table_name, column_name, type = nil, options = {}) column_name = column_name.to_s - columns(table_name).any?{ |c| c.name == column_name && - (!type || c.type == type) && - (!options.key?(:limit) || c.limit == options[:limit]) && - (!options.key?(:precision) || c.precision == options[:precision]) && - (!options.key?(:scale) || c.scale == options[:scale]) && - (!options.key?(:default) || c.default == options[:default]) && - (!options.key?(:null) || c.null == options[:null]) } + checks = [] + checks << lambda { |c| c.name == column_name } + checks << lambda { |c| c.type == type } if type + (migration_keys - [:name]).each do |attr| + checks << lambda { |c| c.send(attr) == options[attr] } if options.key?(attr) + end + + columns(table_name).any? { |c| checks.all? { |check| check[c] } } end # Returns just a table's primary key def primary_key(table_name) pks = primary_keys(table_name) + warn <<-WARNING.strip_heredoc if pks.count > 1 + WARNING: Rails does not support composite primary key. + + #{table_name} has composite primary key. Composite primary key is ignored. + WARNING + pks.first if pks.one? end @@ -263,7 +271,7 @@ module ActiveRecord yield td if block_given? - if options[:force] && table_exists?(table_name) + if options[:force] && data_source_exists?(table_name) drop_table(table_name, options) end @@ -451,7 +459,7 @@ module ActiveRecord # The +type+ parameter is normally one of the migrations native types, # which is one of the following: # <tt>:primary_key</tt>, <tt>:string</tt>, <tt>:text</tt>, - # <tt>:integer</tt>, <tt>:bigint</tt>, <tt>:float</tt>, <tt>:decimal</tt>, + # <tt>:integer</tt>, <tt>:bigint</tt>, <tt>:float</tt>, <tt>:decimal</tt>, <tt>:numeric</tt>, # <tt>:datetime</tt>, <tt>:time</tt>, <tt>:date</tt>, # <tt>:binary</tt>, <tt>:boolean</tt>. # @@ -469,9 +477,9 @@ module ActiveRecord # Allows or disallows +NULL+ values in the column. This option could # have been named <tt>:null_allowed</tt>. # * <tt>:precision</tt> - - # Specifies the precision for a <tt>:decimal</tt> column. + # Specifies the precision for the <tt>:decimal</tt> and <tt>:numeric</tt> columns. # * <tt>:scale</tt> - - # Specifies the scale for a <tt>:decimal</tt> column. + # Specifies the scale for the <tt>:decimal</tt> and <tt>:numeric</tt> columns. # # Note: The precision is the total number of significant digits # and the scale is the number of digits that can be stored following @@ -488,8 +496,6 @@ module ActiveRecord # Default is (10,0). # * PostgreSQL: <tt>:precision</tt> [1..infinity], # <tt>:scale</tt> [0..infinity]. No default. - # * SQLite2: Any <tt>:precision</tt> and <tt>:scale</tt> may be used. - # Internal storage as strings. No default. # * SQLite3: No restrictions on <tt>:precision</tt> and <tt>:scale</tt>, # but the maximum supported <tt>:precision</tt> is 16. No default. # * Oracle: <tt>:precision</tt> [1..38], <tt>:scale</tt> [-84..127]. @@ -692,7 +698,7 @@ module ActiveRecord # # CREATE FULLTEXT INDEX index_developers_on_name ON developers (name) -- MySQL # - # Note: only supported by MySQL. Supported: <tt>:fulltext</tt> and <tt>:spatial</tt> on MyISAM tables. + # Note: only supported by MySQL. def add_index(table_name, column_name, options = {}) index_name, index_type, index_columns, index_options = add_index_options(table_name, column_name, options) execute "CREATE #{index_type} INDEX #{quote_column_name(index_name)} ON #{quote_table_name(table_name)} (#{index_columns})#{index_options}" @@ -700,15 +706,15 @@ module ActiveRecord # Removes the given index from the table. # - # Removes the +index_accounts_on_column+ in the +accounts+ table. + # Removes the index on +branch_id+ in the +accounts+ table if exactly one such index exists. # # remove_index :accounts, :branch_id # - # Removes the index named +index_accounts_on_branch_id+ in the +accounts+ table. + # Removes the index on +branch_id+ in the +accounts+ table if exactly one such index exists. # # remove_index :accounts, column: :branch_id # - # Removes the index named +index_accounts_on_branch_id_and_party_id+ in the +accounts+ table. + # Removes the index on +branch_id+ and +party_id+ in the +accounts+ table if exactly one such index exists. # # remove_index :accounts, column: [:branch_id, :party_id] # @@ -958,9 +964,9 @@ module ActiveRecord def dump_schema_information #:nodoc: sm_table = ActiveRecord::Migrator.schema_migrations_table_name - ActiveRecord::SchemaMigration.order('version').map { |sm| - "INSERT INTO #{sm_table} (version) VALUES ('#{sm.version}');" - }.join "\n\n" + sql = "INSERT INTO #{sm_table} (version) VALUES " + sql << ActiveRecord::SchemaMigration.order('version').pluck(:version).map {|v| "('#{v}')" }.join(', ') + sql << ";\n\n" end # Should not be called normally, but this operation is non-destructive. @@ -969,6 +975,14 @@ module ActiveRecord ActiveRecord::SchemaMigration.create_table end + def initialize_internal_metadata_table + ActiveRecord::InternalMetadata.create_table + end + + def internal_string_options_for_primary_key # :nodoc: + { primary_key: true } + end + def assume_migrated_upto_version(version, migrations_paths) migrations_paths = Array(migrations_paths) version = version.to_i @@ -984,14 +998,12 @@ module ActiveRecord execute "INSERT INTO #{sm_table} (version) VALUES ('#{version}')" end - inserted = Set.new - (versions - migrated).each do |v| - if inserted.include?(v) - raise "Duplicate migration #{v}. Please renumber your migrations to resolve the conflict." - elsif v < version - execute "INSERT INTO #{sm_table} (version) VALUES ('#{v}')" - inserted << v + inserting = (versions - migrated).select {|v| v < version} + if inserting.any? + if (duplicate = inserting.detect {|v| inserting.count(v) > 1}) + raise "Duplicate migration #{duplicate}. Please renumber your migrations to resolve the conflict." end + execute "INSERT INTO #{sm_table} (version) VALUES #{inserting.map {|v| "('#{v}')"}.join(', ') }" end end @@ -1029,11 +1041,12 @@ module ActiveRecord end # Given a set of columns and an ORDER BY clause, returns the columns for a SELECT DISTINCT. - # Both PostgreSQL and Oracle overrides this for custom DISTINCT syntax - they + # PostgreSQL, MySQL, and Oracle overrides this for custom DISTINCT syntax - they # require the order columns appear in the SELECT. # # columns_for_distinct("posts.id", ["posts.created_at desc"]) - def columns_for_distinct(columns, orders) #:nodoc: + # + def columns_for_distinct(columns, orders) # :nodoc: columns end @@ -1088,7 +1101,7 @@ module ActiveRecord if index_name.length > max_index_length raise ArgumentError, "Index name '#{index_name}' on table '#{table_name}' is too long; the limit is #{max_index_length} characters" end - if table_exists?(table_name) && index_name_exists?(table_name, index_name, false) + if data_source_exists?(table_name) && index_name_exists?(table_name, index_name, false) raise ArgumentError, "Index name '#{index_name}' on table '#{table_name}' already exists" end index_columns = quoted_columns_for_index(column_names, options).join(", ") @@ -1127,21 +1140,35 @@ module ActiveRecord end def index_name_for_remove(table_name, options = {}) - index_name = index_name(table_name, options) + # if the adapter doesn't support the indexes call the best we can do + # is return the default index name for the options provided + return index_name(table_name, options) unless respond_to?(:indexes) - unless index_name_exists?(table_name, index_name, true) - if options.is_a?(Hash) && options.has_key?(:name) - options_without_column = options.dup - options_without_column.delete :column - index_name_without_column = index_name(table_name, options_without_column) + checks = [] - return index_name_without_column if index_name_exists?(table_name, index_name_without_column, false) - end + if options.is_a?(Hash) + checks << lambda { |i| i.name == options[:name].to_s } if options.has_key?(:name) + column_names = Array(options[:column]).map(&:to_s) + else + column_names = Array(options).map(&:to_s) + end - raise ArgumentError, "Index name '#{index_name}' on table '#{table_name}' does not exist" + if column_names.any? + checks << lambda { |i| i.columns.join('_and_') == column_names.join('_and_') } end - index_name + raise ArgumentError "No name or columns specified" if checks.none? + + matching_indexes = indexes(table_name).select { |i| checks.all? { |check| check[i] } } + + if matching_indexes.count > 1 + raise ArgumentError, "Multiple indexes found on #{table_name} columns #{column_names}. " \ + "Specify an index name from #{matching_indexes.map(&:name).join(', ')}" + elsif matching_indexes.none? + raise ArgumentError, "No indexes found on #{table_name} with the options provided." + else + matching_indexes.first.name + end end def rename_table_indexes(table_name, new_name) @@ -1168,7 +1195,7 @@ module ActiveRecord private def create_table_definition(name, temporary = false, options = nil, as = nil) - TableDefinition.new native_database_types, name, temporary, options, as + TableDefinition.new(name, temporary, options, as) end def create_alter_table(name) diff --git a/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb b/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb index 447c809e85..ca795cb1ad 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb @@ -33,6 +33,7 @@ module ActiveRecord class NullTransaction #:nodoc: def initialize; end + def state; end def closed?; true; end def open?; false; end def joinable?; false; end @@ -166,8 +167,13 @@ module ActiveRecord def commit_transaction transaction = @stack.last - transaction.before_commit_records - @stack.pop + + begin + transaction.before_commit_records + ensure + @stack.pop + end + transaction.commit transaction.commit_records end diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb index 55910865e5..5ef434734a 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb @@ -1,5 +1,4 @@ require 'active_record/type' -require 'active_support/core_ext/benchmark' require 'active_record/connection_adapters/determine_if_preparable_visitor' require 'active_record/connection_adapters/schema_cache' require 'active_record/connection_adapters/sql_type_metadata' @@ -23,6 +22,7 @@ module ActiveRecord autoload :TableDefinition autoload :Table autoload :AlterTable + autoload :ReferenceDefinition end autoload_at 'active_record/connection_adapters/abstract/connection_pool' do @@ -95,14 +95,15 @@ module ActiveRecord attr_reader :prepared_statements - def initialize(connection, logger = nil, pool = nil) #:nodoc: + def initialize(connection, logger = nil, config = {}) # :nodoc: super() @connection = connection @owner = nil @instrumenter = ActiveSupport::Notifications.instrumenter @logger = logger - @pool = pool + @config = config + @pool = nil @schema_cache = SchemaCache.new self @visitor = nil @prepared_statements = false @@ -289,14 +290,14 @@ module ActiveRecord # locks # # Return true if we got the lock, otherwise false - def get_advisory_lock(key) # :nodoc: + def get_advisory_lock(lock_id) # :nodoc: end # This is meant to be implemented by the adapters that support advisory # locks. # # Return true if we released the lock, otherwise false - def release_advisory_lock(key) # :nodoc: + def release_advisory_lock(lock_id) # :nodoc: end # A list of extensions, to be filled in by adapters that support them. @@ -309,12 +310,6 @@ module ActiveRecord {} end - # Returns a bind substitution value given a bind +column+ - # NOTE: The column param is currently being used by the sqlserver-adapter - def substitute_at(column, _unused = 0) - Arel::Nodes::BindParam.new - end - # REFERENTIAL INTEGRITY ==================================== # Override to turn off referential integrity while executing <tt>&block</tt>. @@ -375,7 +370,7 @@ module ActiveRecord end # Provides access to the underlying database driver for this adapter. For - # example, this method returns a Mysql object in case of MysqlAdapter, + # example, this method returns a Mysql2::Client object in case of Mysql2Adapter, # and a PGconn object in case of PostgreSQLAdapter. # # This is useful for when you need to call a proprietary method such as @@ -390,21 +385,19 @@ module ActiveRecord def release_savepoint(name = nil) end - def case_sensitive_modifier(node, table_attribute) - node - end - def case_sensitive_comparison(table, attribute, column, value) - table_attr = table[attribute] - value = case_sensitive_modifier(value, table_attr) unless value.nil? - table_attr.eq(value) + if value.nil? + table[attribute].eq(value) + else + table[attribute].eq(Arel::Nodes::BindParam.new) + end end def case_insensitive_comparison(table, attribute, column, value) if can_perform_case_insensitive_comparison_for?(column) - table[attribute].lower.eq(table.lower(value)) + table[attribute].lower.eq(table.lower(Arel::Nodes::BindParam.new)) else - case_sensitive_comparison(table, attribute, column, value) + table[attribute].eq(Arel::Nodes::BindParam.new) end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb index 3e3bbc267b..b12bac2737 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb @@ -1,7 +1,10 @@ require 'active_record/connection_adapters/abstract_adapter' +require 'active_record/connection_adapters/mysql/column' +require 'active_record/connection_adapters/mysql/explain_pretty_printer' require 'active_record/connection_adapters/mysql/schema_creation' require 'active_record/connection_adapters/mysql/schema_definitions' require 'active_record/connection_adapters/mysql/schema_dumper' +require 'active_record/connection_adapters/mysql/type_metadata' require 'active_support/core_ext/string/strip' @@ -19,108 +22,16 @@ module ActiveRecord MySQL::SchemaCreation.new(self) end - class Column < ConnectionAdapters::Column # :nodoc: - delegate :strict, :extra, to: :sql_type_metadata, allow_nil: true - - def initialize(*) - super - assert_valid_default(default) - extract_default - end - - def extract_default - if blob_or_text_column? - @default = null || strict ? nil : '' - elsif missing_default_forged_as_empty_string?(default) - @default = nil - end - end - - def has_default? - return false if blob_or_text_column? # MySQL forbids defaults on blob and text columns - super - end - - def blob_or_text_column? - sql_type =~ /blob/i || type == :text - end - - def unsigned? - /unsigned/ === sql_type - end - - def case_sensitive? - collation && !collation.match(/_ci$/) - end - - def auto_increment? - extra == 'auto_increment' - end - - private - - # MySQL misreports NOT NULL column default when none is given. - # We can't detect this for columns which may have a legitimate '' - # default (string) but we can for others (integer, datetime, boolean, - # and the rest). - # - # Test whether the column has default '', is not null, and is not - # a type allowing default ''. - def missing_default_forged_as_empty_string?(default) - type != :string && !null && default == '' - end - - def assert_valid_default(default) - if blob_or_text_column? && default.present? - raise ArgumentError, "#{type} columns cannot have a default value: #{default.inspect}" - end - end - end - - class MysqlTypeMetadata < DelegateClass(SqlTypeMetadata) # :nodoc: - attr_reader :extra, :strict - - def initialize(type_metadata, extra: "", strict: false) - super(type_metadata) - @type_metadata = type_metadata - @extra = extra - @strict = strict - end - - def ==(other) - other.is_a?(MysqlTypeMetadata) && - attributes_for_hash == other.attributes_for_hash - end - alias eql? == - - def hash - attributes_for_hash.hash - end - - protected - - def attributes_for_hash - [self.class, @type_metadata, extra, strict] - end - end - ## # :singleton-method: - # By default, the MysqlAdapter will consider all columns of type <tt>tinyint(1)</tt> - # as boolean. If you wish to disable this emulation (which was the default - # behavior in versions 0.13.1 and earlier) you can add the following line + # By default, the Mysql2Adapter will consider all columns of type <tt>tinyint(1)</tt> + # as boolean. If you wish to disable this emulation you can add the following line # to your application.rb file: # - # ActiveRecord::ConnectionAdapters::Mysql[2]Adapter.emulate_booleans = false + # ActiveRecord::ConnectionAdapters::Mysql2Adapter.emulate_booleans = false class_attribute :emulate_booleans self.emulate_booleans = true - LOST_CONNECTION_ERROR_MESSAGES = [ - "Server shutdown in progress", - "Broken pipe", - "Lost connection to MySQL server during query", - "MySQL server has gone away" ] - QUOTED_TRUE, QUOTED_FALSE = '1', '0' NATIVE_DATABASE_TYPES = { @@ -134,19 +45,15 @@ module ActiveRecord time: { name: "time" }, date: { name: "date" }, binary: { name: "blob" }, - blob: { name: "blob" }, boolean: { name: "tinyint", limit: 1 }, - bigint: { name: "bigint" }, json: { name: "json" }, } INDEX_TYPES = [:fulltext, :spatial] INDEX_USINGS = [:btree, :hash] - # FIXME: Make the first parameter more similar for the two adapters def initialize(connection, logger, connection_options, config) - super(connection, logger) - @connection_options, @config = connection_options, config + super(connection, logger, config) @quoted_column_names, @quoted_table_names = {}, {} @visitor = Arel::Visitors::MySQL.new self @@ -157,16 +64,18 @@ module ActiveRecord else @prepared_statements = false end + + if version < '5.0.0' + raise "Your version of MySQL (#{full_version.match(/^\d+\.\d+\.\d+/)[0]}) is too old. Active Record supports MySQL >= 5.0." + end end - MAX_INDEX_LENGTH_FOR_CHARSETS_OF_4BYTES_MAXLEN = 191 CHARSETS_OF_4BYTES_MAXLEN = ['utf8mb4', 'utf16', 'utf16le', 'utf32'] - def initialize_schema_migrations_table - if CHARSETS_OF_4BYTES_MAXLEN.include?(charset) - ActiveRecord::SchemaMigration.create_table(MAX_INDEX_LENGTH_FOR_CHARSETS_OF_4BYTES_MAXLEN) - else - ActiveRecord::SchemaMigration.create_table - end + + 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 @@ -192,12 +101,8 @@ module ActiveRecord true end - # MySQL 4 technically support transaction isolation, but it is affected by a bug - # where the transaction level gets persisted for the whole session: - # - # http://bugs.mysql.com/bug.php?id=39170 def supports_transaction_isolation? - version >= '5.0.0' + true end def supports_explain? @@ -213,25 +118,23 @@ module ActiveRecord end def supports_views? - version >= '5.0.0' + true end def supports_datetime_with_precision? version >= '5.6.4' end - # 5.0.0 definitely supports it, possibly supported by earlier versions but - # not sure def supports_advisory_locks? - version >= '5.0.0' + true end - def get_advisory_lock(key, timeout = 0) # :nodoc: - select_value("SELECT GET_LOCK('#{key}', #{timeout});").to_s == '1' + def get_advisory_lock(lock_name, timeout = 0) # :nodoc: + select_value("SELECT GET_LOCK('#{lock_name}', #{timeout});").to_s == '1' end - def release_advisory_lock(key) # :nodoc: - select_value("SELECT RELEASE_LOCK('#{key}')").to_s == '1' + def release_advisory_lock(lock_name) # :nodoc: + select_value("SELECT RELEASE_LOCK('#{lock_name}')").to_s == '1' end def native_database_types @@ -251,7 +154,7 @@ module ActiveRecord end def new_column(field, default, sql_type_metadata = nil, null = true, default_function = nil, collation = nil) # :nodoc: - Column.new(field, default, sql_type_metadata, null, default_function, collation) + MySQL::Column.new(field, default, sql_type_metadata, null, default_function, collation) end # Must return the MySQL error number from the exception, if the exception has an @@ -325,72 +228,7 @@ module ActiveRecord result = exec_query(sql, 'EXPLAIN', binds) elapsed = Time.now - start - ExplainPrettyPrinter.new.pp(result, elapsed) - end - - class ExplainPrettyPrinter # :nodoc: - # Pretty prints the result of an EXPLAIN in a way that resembles the output of the - # MySQL shell: - # - # +----+-------------+-------+-------+---------------+---------+---------+-------+------+-------------+ - # | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | - # +----+-------------+-------+-------+---------------+---------+---------+-------+------+-------------+ - # | 1 | SIMPLE | users | const | PRIMARY | PRIMARY | 4 | const | 1 | | - # | 1 | SIMPLE | posts | ALL | NULL | NULL | NULL | NULL | 1 | Using where | - # +----+-------------+-------+-------+---------------+---------+---------+-------+------+-------------+ - # 2 rows in set (0.00 sec) - # - # This is an exercise in Ruby hyperrealism :). - def pp(result, elapsed) - widths = compute_column_widths(result) - separator = build_separator(widths) - - pp = [] - - pp << separator - pp << build_cells(result.columns, widths) - pp << separator - - result.rows.each do |row| - pp << build_cells(row, widths) - end - - pp << separator - pp << build_footer(result.rows.length, elapsed) - - pp.join("\n") + "\n" - end - - private - - def compute_column_widths(result) - [].tap do |widths| - result.columns.each_with_index do |column, i| - cells_in_column = [column] + result.rows.map {|r| r[i].nil? ? 'NULL' : r[i].to_s} - widths << cells_in_column.map(&:length).max - end - end - end - - def build_separator(widths) - padding = 1 - '+' + widths.map {|w| '-' * (w + (padding*2))}.join('+') + '+' - end - - def build_cells(items, widths) - cells = [] - items.each_with_index do |item, i| - item = 'NULL' if item.nil? - justifier = item.is_a?(Numeric) ? 'rjust' : 'ljust' - cells << item.to_s.send(justifier, widths[i]) - end - '| ' + cells.join(' | ') + ' |' - end - - def build_footer(nrows, elapsed) - rows_label = nrows == 1 ? 'row' : 'rows' - "#{nrows} #{rows_label} in set (%.2f sec)" % elapsed - end + MySQL::ExplainPrettyPrinter.new.pp(result, elapsed) end def clear_cache! @@ -403,18 +241,13 @@ module ActiveRecord log(sql, name) { @connection.query(sql) } end - # MysqlAdapter has to free a result after using it, so we use this method to write - # stuff in an abstract way without concerning ourselves about whether it needs to be - # explicitly freed or not. - def execute_and_free(sql, name = nil) #:nodoc: + # Mysql2Adapter doesn't have to free a result after using it, but we use this method + # to write stuff in an abstract way without concerning ourselves about whether it + # needs to be explicitly freed or not. + def execute_and_free(sql, name = nil) # :nodoc: yield execute(sql, name) end - def update_sql(sql, name = nil) #:nodoc: - super - @connection.affected_rows - end - def begin_db_transaction execute "BEGIN" end @@ -435,7 +268,7 @@ module ActiveRecord # In the simple case, MySQL allows us to place JOINs directly into the UPDATE # query. However, this does not allow for LIMIT, OFFSET and ORDER. To support # these, we must use a subquery. - def join_to_update(update, select) #:nodoc: + def join_to_update(update, select, key) # :nodoc: if select.limit || select.offset || select.orders.any? super else @@ -497,18 +330,44 @@ module ActiveRecord end def tables(name = nil) # :nodoc: + ActiveSupport::Deprecation.warn(<<-MSG.squish) + #tables currently returns both tables and views. + This behavior is deprecated and will be changed with Rails 5.1 to only return tables. + Use #data_sources instead. + MSG + + if name + ActiveSupport::Deprecation.warn(<<-MSG.squish) + Passing arguments to #tables is deprecated without replacement. + MSG + end + + data_sources + end + + def data_sources sql = "SELECT table_name FROM information_schema.tables " sql << "WHERE table_schema = #{quote(@config[:database])}" select_values(sql, 'SCHEMA') end - alias data_sources tables def truncate(table_name, name = nil) execute "TRUNCATE TABLE #{quote_table_name(table_name)}", name end def table_exists?(table_name) + # Update lib/active_record/internal_metadata.rb when this gets removed + ActiveSupport::Deprecation.warn(<<-MSG.squish) + #table_exists? currently checks both tables and views. + This behavior is deprecated and will be changed with Rails 5.1 to only check tables. + Use #data_source_exists? instead. + MSG + + data_source_exists?(table_name) + end + + def data_source_exists?(table_name) return false unless table_name.present? schema, name = table_name.to_s.split('.', 2) @@ -519,7 +378,6 @@ module ActiveRecord select_values(sql, 'SCHEMA').any? end - alias data_source_exists? table_exists? def views # :nodoc: select_values("SHOW FULL TABLES WHERE table_type = 'VIEW'", 'SCHEMA') @@ -562,14 +420,16 @@ module ActiveRecord end # Returns an array of +Column+ objects for the table specified by +table_name+. - def columns(table_name)#:nodoc: + def columns(table_name) # :nodoc: sql = "SHOW FULL FIELDS FROM #{quote_table_name(table_name)}" execute_and_free(sql, 'SCHEMA') do |result| each_hash(result).map do |field| - field_name = set_field_encoding(field[:Field]) - sql_type = field[:Type] - type_metadata = fetch_type_metadata(sql_type, field[:Extra]) - new_column(field_name, field[:Default], type_metadata, field[:Null] == "YES", nil, field[:Collation]) + type_metadata = fetch_type_metadata(field[:Type], field[:Extra]) + if type_metadata.type == :datetime && field[:Default] == "CURRENT_TIMESTAMP" + new_column(field[:Field], nil, type_metadata, field[:Null] == "YES", field[:Default], field[:Collation]) + else + new_column(field[:Field], field[:Default], type_metadata, field[:Null] == "YES", nil, field[:Collation]) + end end end end @@ -618,6 +478,7 @@ module ActiveRecord # it can be helpful to provide these in a migration's +change+ method so it can be reverted. # In that case, +options+ and the block will be used by create_table. def drop_table(table_name, options = {}) + create_table_info_cache.delete(table_name) if create_table_info_cache.key?(table_name) execute "DROP#{' TEMPORARY' if options[:temporary]} TABLE#{' IF EXISTS' if options[:if_exists]} #{quote_table_name(table_name)}#{' CASCADE' if options[:force] == :cascade}" end @@ -746,25 +607,32 @@ module ActiveRecord SQL end - def case_sensitive_modifier(node, table_attribute) - node = Arel::Nodes.build_quoted node, table_attribute - Arel::Nodes::Bin.new(node) - end - def case_sensitive_comparison(table, attribute, column, value) - if column.case_sensitive? - table[attribute].eq(value) - else + if value.nil? || column.case_sensitive? super + else + table[attribute].eq(Arel::Nodes::Bin.new(Arel::Nodes::BindParam.new)) end end - def case_insensitive_comparison(table, attribute, column, value) - if column.case_sensitive? - super - else - table[attribute].eq(value) - end + def can_perform_case_insensitive_comparison_for?(column) + column.case_sensitive? + end + private :can_perform_case_insensitive_comparison_for? + + # In MySQL 5.7.5 and up, ONLY_FULL_GROUP_BY affects handling of queries that use + # DISTINCT and ORDER BY. It requires the ORDER BY columns in the select list for + # distinct queries, and requires that the ORDER BY include the distinct column. + # See https://dev.mysql.com/doc/refman/5.7/en/group-by-handling.html + def columns_for_distinct(columns, orders) # :nodoc: + order_columns = orders.reject(&:blank?).map { |s| + # Convert Arel node to string + s = s.to_sql unless s.is_a?(String) + # Remove any ASC/DESC modifiers + s.gsub(/\s+(?:ASC|DESC)\b/i, '') + }.reject(&:blank?).map.with_index { |column, i| "#{column} AS alias_#{i}" } + + [super, *order_columns].join(', ') end def strict_mode? @@ -819,7 +687,7 @@ module ActiveRecord def register_integer_type(mapping, key, options) # :nodoc: mapping.register_type(key) do |sql_type| - if /unsigned/i =~ sql_type + if /\bunsigned\z/ === sql_type Type::UnsignedInteger.new(options) else Type::Integer.new(options) @@ -836,7 +704,7 @@ module ActiveRecord end def fetch_type_metadata(sql_type, extra = "") - MysqlTypeMetadata.new(super(sql_type), extra: extra, strict: strict_mode?) + MySQL::TypeMetadata.new(super(sql_type), extra: extra, strict: strict_mode?) end def add_index_length(option_strings, column_names, options = {}) @@ -946,11 +814,13 @@ module ActiveRecord subsubselect = select.clone subsubselect.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 = Arel::SelectManager.new(select.engine) subselect.project Arel.sql(key.name) - # Materialized subquery by adding distinct - # to work with MySQL 5.7.6 which sets optimizer_switch='derived_merge=on' - subselect.from subsubselect.distinct.as('__active_record_temp') + subselect.from subsubselect.as('__active_record_temp') end def mariadb? @@ -1013,13 +883,16 @@ module ActiveRecord end end + def create_table_info_cache # :nodoc: + @create_table_info_cache ||= {} + end + def create_table_info(table_name) # :nodoc: - @create_table_info_cache = {} - @create_table_info_cache[table_name] ||= select_one("SHOW CREATE TABLE #{quote_table_name(table_name)}")["Create Table"] + create_table_info_cache[table_name] ||= select_one("SHOW CREATE TABLE #{quote_table_name(table_name)}")["Create Table"] end def create_table_definition(name, temporary = false, options = nil, as = nil) # :nodoc: - MySQL::TableDefinition.new(native_database_types, name, temporary, options, as) + MySQL::TableDefinition.new(name, temporary, options, as) end def integer_to_sql(limit) # :nodoc: @@ -1029,7 +902,6 @@ module ActiveRecord when 3; 'mediumint' when nil, 4; 'int' when 5..8; 'bigint' - when 11; 'int(11)' # backward compatibility with Rails 2.0 else raise(ActiveRecordError, "No integer type has byte size #{limit}") end end @@ -1082,11 +954,8 @@ module ActiveRecord end end - ActiveRecord::Type.register(:json, MysqlJson, adapter: :mysql) ActiveRecord::Type.register(:json, MysqlJson, adapter: :mysql2) - ActiveRecord::Type.register(:string, MysqlString, adapter: :mysql) ActiveRecord::Type.register(:string, MysqlString, adapter: :mysql2) - ActiveRecord::Type.register(:unsigned_integer, Type::UnsignedInteger, adapter: :mysql) ActiveRecord::Type.register(:unsigned_integer, Type::UnsignedInteger, adapter: :mysql2) end end diff --git a/activerecord/lib/active_record/connection_adapters/column.rb b/activerecord/lib/active_record/connection_adapters/column.rb index 81de7c03fb..10f908538f 100644 --- a/activerecord/lib/active_record/connection_adapters/column.rb +++ b/activerecord/lib/active_record/connection_adapters/column.rb @@ -30,7 +30,7 @@ module ActiveRecord end def bigint? - /bigint/ === sql_type + /\Abigint\b/ === sql_type end # Returns the human name of the column name. diff --git a/activerecord/lib/active_record/connection_adapters/connection_specification.rb b/activerecord/lib/active_record/connection_adapters/connection_specification.rb index 08d46fca96..4bc6447368 100644 --- a/activerecord/lib/active_record/connection_adapters/connection_specification.rb +++ b/activerecord/lib/active_record/connection_adapters/connection_specification.rb @@ -33,7 +33,7 @@ module ActiveRecord def initialize(url) raise "Database URL cannot be empty" if url.blank? @uri = uri_parser.parse(url) - @adapter = @uri.scheme.tr('-', '_') + @adapter = @uri.scheme && @uri.scheme.tr('-', '_') @adapter = "postgresql" if @adapter == "postgres" if @uri.opaque @@ -175,7 +175,7 @@ module ActiveRecord rescue Gem::LoadError => e raise Gem::LoadError, "Specified '#{spec[:adapter]}' for database adapter, but the gem is not loaded. Add `gem '#{e.name}'` to your Gemfile (and ensure its version is at the minimum required by ActiveRecord)." rescue LoadError => e - raise LoadError, "Could not load '#{path_to_adapter}'. Make sure that the adapter in config/database.yml is valid. If you use an adapter other than 'mysql', 'mysql2', 'postgresql' or 'sqlite3' add the necessary adapter gem to the Gemfile.", e.backtrace + raise LoadError, "Could not load '#{path_to_adapter}'. Make sure that the adapter in config/database.yml is valid. If you use an adapter other than 'mysql2', 'postgresql' or 'sqlite3' add the necessary adapter gem to the Gemfile.", e.backtrace end adapter_method = "#{spec[:adapter]}_connection" diff --git a/activerecord/lib/active_record/connection_adapters/mysql/column.rb b/activerecord/lib/active_record/connection_adapters/mysql/column.rb new file mode 100644 index 0000000000..9c45fdd44a --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/mysql/column.rb @@ -0,0 +1,50 @@ +module ActiveRecord + module ConnectionAdapters + module MySQL + class Column < ConnectionAdapters::Column # :nodoc: + delegate :strict, :extra, to: :sql_type_metadata, allow_nil: true + + def initialize(*) + super + assert_valid_default + extract_default + end + + def has_default? + return false if blob_or_text_column? # MySQL forbids defaults on blob and text columns + super + end + + def blob_or_text_column? + /\A(?:tiny|medium|long)?blob\b/ === sql_type || type == :text + end + + def unsigned? + /\bunsigned\z/ === sql_type + end + + def case_sensitive? + collation && collation !~ /_ci\z/ + end + + def auto_increment? + extra == 'auto_increment' + end + + private + + def extract_default + if blob_or_text_column? + @default = null || strict ? nil : '' + end + end + + def assert_valid_default + if blob_or_text_column? && default.present? + raise ArgumentError, "#{type} columns cannot have a default value: #{default.inspect}" + end + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/mysql/explain_pretty_printer.rb b/activerecord/lib/active_record/connection_adapters/mysql/explain_pretty_printer.rb new file mode 100644 index 0000000000..1820853196 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/mysql/explain_pretty_printer.rb @@ -0,0 +1,70 @@ +module ActiveRecord + module ConnectionAdapters + module MySQL + class ExplainPrettyPrinter # :nodoc: + # Pretty prints the result of an EXPLAIN in a way that resembles the output of the + # MySQL shell: + # + # +----+-------------+-------+-------+---------------+---------+---------+-------+------+-------------+ + # | id | select_type | table | type | possible_keys | key | key_len | ref | rows | Extra | + # +----+-------------+-------+-------+---------------+---------+---------+-------+------+-------------+ + # | 1 | SIMPLE | users | const | PRIMARY | PRIMARY | 4 | const | 1 | | + # | 1 | SIMPLE | posts | ALL | NULL | NULL | NULL | NULL | 1 | Using where | + # +----+-------------+-------+-------+---------------+---------+---------+-------+------+-------------+ + # 2 rows in set (0.00 sec) + # + # This is an exercise in Ruby hyperrealism :). + def pp(result, elapsed) + widths = compute_column_widths(result) + separator = build_separator(widths) + + pp = [] + + pp << separator + pp << build_cells(result.columns, widths) + pp << separator + + result.rows.each do |row| + pp << build_cells(row, widths) + end + + pp << separator + pp << build_footer(result.rows.length, elapsed) + + pp.join("\n") + "\n" + end + + private + + def compute_column_widths(result) + [].tap do |widths| + result.columns.each_with_index do |column, i| + cells_in_column = [column] + result.rows.map {|r| r[i].nil? ? 'NULL' : r[i].to_s} + widths << cells_in_column.map(&:length).max + end + end + end + + def build_separator(widths) + padding = 1 + '+' + widths.map {|w| '-' * (w + (padding*2))}.join('+') + '+' + end + + def build_cells(items, widths) + cells = [] + items.each_with_index do |item, i| + item = 'NULL' if item.nil? + justifier = item.is_a?(Numeric) ? 'rjust' : 'ljust' + cells << item.to_s.send(justifier, widths[i]) + end + '| ' + cells.join(' | ') + ' |' + end + + def build_footer(nrows, elapsed) + rows_label = nrows == 1 ? 'row' : 'rows' + "#{nrows} #{rows_label} in set (%.2f sec)" % elapsed + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/mysql/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/mysql/schema_definitions.rb index ca7dfda80d..157e75dbf7 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql/schema_definitions.rb @@ -11,6 +11,30 @@ module ActiveRecord args.each { |name| column(name, :blob, options) } end + def tinyblob(*args, **options) + args.each { |name| column(name, :tinyblob, options) } + end + + def mediumblob(*args, **options) + args.each { |name| column(name, :mediumblob, options) } + end + + def longblob(*args, **options) + args.each { |name| column(name, :longblob, options) } + end + + def tinytext(*args, **options) + args.each { |name| column(name, :tinytext, options) } + end + + def mediumtext(*args, **options) + args.each { |name| column(name, :mediumtext, options) } + end + + def longtext(*args, **options) + args.each { |name| column(name, :longtext, options) } + end + def json(*args, **options) args.each { |name| column(name, :json, options) } end diff --git a/activerecord/lib/active_record/connection_adapters/mysql/schema_dumper.rb b/activerecord/lib/active_record/connection_adapters/mysql/schema_dumper.rb index 9dee3172f4..ccf5b6cadc 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql/schema_dumper.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql/schema_dumper.rb @@ -12,7 +12,7 @@ module ActiveRecord spec[:unsigned] = 'true' if column.unsigned? return if spec.empty? else - spec[:id] = column.type.inspect + spec[:id] = schema_type(column).inspect spec.merge!(prepare_column_options(column).delete_if { |key, _| [:name, :type, :null].include?(key) }) end spec @@ -32,7 +32,7 @@ module ActiveRecord def schema_type(column) if column.sql_type == 'tinyblob' - 'blob' + :blob else super end diff --git a/activerecord/lib/active_record/connection_adapters/mysql/type_metadata.rb b/activerecord/lib/active_record/connection_adapters/mysql/type_metadata.rb new file mode 100644 index 0000000000..e1e3f7b472 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/mysql/type_metadata.rb @@ -0,0 +1,32 @@ +module ActiveRecord + module ConnectionAdapters + module MySQL + class TypeMetadata < DelegateClass(SqlTypeMetadata) # :nodoc: + attr_reader :extra, :strict + + def initialize(type_metadata, extra: "", strict: false) + super(type_metadata) + @type_metadata = type_metadata + @extra = extra + @strict = strict + end + + def ==(other) + other.is_a?(MySQL::TypeMetadata) && + attributes_for_hash == other.attributes_for_hash + end + alias eql? == + + def hash + attributes_for_hash.hash + end + + protected + + def attributes_for_hash + [self.class, @type_metadata, extra, strict] + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb index 773ecbe126..57d8867bb4 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb @@ -10,14 +10,18 @@ module ActiveRecord config = config.symbolize_keys config[:username] = 'root' if config[:username].nil? + config[:flags] ||= 0 if Mysql2::Client.const_defined? :FOUND_ROWS - config[:flags] = Mysql2::Client::FOUND_ROWS + if config[:flags].kind_of? Array + config[:flags].push "FOUND_ROWS".freeze + else + config[:flags] |= Mysql2::Client::FOUND_ROWS + end end client = Mysql2::Client.new(config) - options = [config[:host], config[:username], config[:password], config[:database], config[:port], config[:socket], 0] - ConnectionAdapters::Mysql2Adapter.new(client, logger, options, config) + ConnectionAdapters::Mysql2Adapter.new(client, logger, nil, config) rescue Mysql2::Error => error if error.message.include?("Unknown database") raise ActiveRecord::NoDatabaseError @@ -38,7 +42,7 @@ module ActiveRecord end def supports_json? - version >= '5.7.8' + !mariadb? && version >= '5.7.8' end # HELPER METHODS =========================================== @@ -95,33 +99,15 @@ module ActiveRecord # DATABASE STATEMENTS ====================================== #++ - # FIXME: re-enable the following once a "better" query_cache solution is in core - # - # The overrides below perform much better than the originals in AbstractAdapter - # because we're able to take advantage of mysql2's lazy-loading capabilities - # - # # Returns a record hash with the column names as keys and column values - # # as values. - # def select_one(sql, name = nil) - # result = execute(sql, name) - # result.each(as: :hash) do |r| - # return r - # end - # end - # - # # Returns a single value from a record - # def select_value(sql, name = nil) - # result = execute(sql, name) - # if first = result.first - # first.first - # end - # end - # - # # Returns an array of the values of the first column in a select: - # # select_values("SELECT id FROM companies LIMIT 3") => [1,2,3] - # def select_values(sql, name = nil) - # execute(sql, name).map { |row| row.first } - # end + # Returns a record hash with the column names as keys and column values + # as values. + def select_one(arel, name = nil, binds = []) + arel, binds = binds_from_relation(arel, binds) + execute(to_sql(arel, binds), name).each(as: :hash) do |row| + @connection.next_result while @connection.more_results? + return row + end + end # Returns an array of arrays containing the field values. # Order is the same as that returned by +columns+. @@ -148,14 +134,6 @@ module ActiveRecord ActiveRecord::Result.new(result.fields, result.to_a) end - alias exec_without_stmt exec_query - - def insert_sql(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) - super - id_value || @connection.last_id - end - alias :create :insert_sql - def exec_insert(sql, name, binds, pk = nil, sequence_name = nil) execute to_sql(sql, binds), name end @@ -185,10 +163,6 @@ module ActiveRecord def full_version @full_version ||= @connection.server_info[:version] end - - def set_field_encoding field_name - field_name - end end end end diff --git a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb deleted file mode 100644 index 89d18ee14e..0000000000 --- a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb +++ /dev/null @@ -1,475 +0,0 @@ -require 'active_record/connection_adapters/abstract_mysql_adapter' -require 'active_record/connection_adapters/statement_pool' -require 'active_support/core_ext/hash/keys' - -gem 'mysql', '~> 2.9' -require 'mysql' - -class Mysql # :nodoc: all - class Time - # Used for casting DateTime fields to a MySQL friendly Time. - # This was documented in 48498da0dfed5239ea1eafb243ce47d7e3ce9e8e - def to_date - Date.new(year, month, day) - end - end - class Stmt; include Enumerable end - class Result; include Enumerable end -end - -module ActiveRecord - module ConnectionHandling # :nodoc: - # Establishes a connection to the database that's used by all Active Record objects. - def mysql_connection(config) - config = config.symbolize_keys - host = config[:host] - port = config[:port] - socket = config[:socket] - username = config[:username] ? config[:username].to_s : 'root' - password = config[:password].to_s - database = config[:database] - - mysql = Mysql.init - mysql.ssl_set(config[:sslkey], config[:sslcert], config[:sslca], config[:sslcapath], config[:sslcipher]) if config[:sslca] || config[:sslkey] - - default_flags = Mysql.const_defined?(:CLIENT_MULTI_RESULTS) ? Mysql::CLIENT_MULTI_RESULTS : 0 - default_flags |= Mysql::CLIENT_FOUND_ROWS if Mysql.const_defined?(:CLIENT_FOUND_ROWS) - options = [host, username, password, database, port, socket, default_flags] - ConnectionAdapters::MysqlAdapter.new(mysql, logger, options, config) - rescue Mysql::Error => error - if error.message.include?("Unknown database") - raise ActiveRecord::NoDatabaseError - else - raise - end - end - end - - module ConnectionAdapters - # The MySQL adapter will work with both Ruby/MySQL, which is a Ruby-based MySQL adapter that comes bundled with Active Record, and with - # the faster C-based MySQL/Ruby adapter (available both as a gem and from http://www.tmtm.org/en/mysql/ruby/). - # - # Options: - # - # * <tt>:host</tt> - Defaults to "localhost". - # * <tt>:port</tt> - Defaults to 3306. - # * <tt>:socket</tt> - Defaults to "/tmp/mysql.sock". - # * <tt>:username</tt> - Defaults to "root" - # * <tt>:password</tt> - Defaults to nothing. - # * <tt>:database</tt> - The name of the database. No default, must be provided. - # * <tt>:encoding</tt> - (Optional) Sets the client encoding by executing "SET NAMES <encoding>" after connection. - # * <tt>:reconnect</tt> - Defaults to false (See MySQL documentation: http://dev.mysql.com/doc/refman/5.7/en/auto-reconnect.html). - # * <tt>:strict</tt> - Defaults to true. Enable STRICT_ALL_TABLES. (See MySQL documentation: http://dev.mysql.com/doc/refman/5.7/en/sql-mode.html) - # * <tt>:variables</tt> - (Optional) A hash session variables to send as <tt>SET @@SESSION.key = value</tt> on each database connection. Use the value +:default+ to set a variable to its DEFAULT value. (See MySQL documentation: http://dev.mysql.com/doc/refman/5.7/en/set-statement.html). - # * <tt>:sslca</tt> - Necessary to use MySQL with an SSL connection. - # * <tt>:sslkey</tt> - Necessary to use MySQL with an SSL connection. - # * <tt>:sslcert</tt> - Necessary to use MySQL with an SSL connection. - # * <tt>:sslcapath</tt> - Necessary to use MySQL with an SSL connection. - # * <tt>:sslcipher</tt> - Necessary to use MySQL with an SSL connection. - # - class MysqlAdapter < AbstractMysqlAdapter - ADAPTER_NAME = 'MySQL'.freeze - - class StatementPool < ConnectionAdapters::StatementPool - private - - def dealloc(stmt) - stmt[:stmt].close - end - end - - def initialize(connection, logger, connection_options, config) - super - @statements = StatementPool.new(self.class.type_cast_config_to_integer(config.fetch(:statement_limit) { 1000 })) - @client_encoding = nil - connect - end - - # Returns true, since this connection adapter supports prepared statement - # caching. - def supports_statement_cache? - true - end - - # HELPER METHODS =========================================== - - def each_hash(result) # :nodoc: - if block_given? - result.each_hash do |row| - row.symbolize_keys! - yield row - end - else - to_enum(:each_hash, result) - end - end - - def error_number(exception) # :nodoc: - exception.errno if exception.respond_to?(:errno) - end - - # QUOTING ================================================== - - def quote_string(string) #:nodoc: - @connection.quote(string) - end - - #-- - # CONNECTION MANAGEMENT ==================================== - #++ - - def active? - if @connection.respond_to?(:stat) - @connection.stat - else - @connection.query 'select 1' - end - - # mysql-ruby doesn't raise an exception when stat fails. - if @connection.respond_to?(:errno) - @connection.errno.zero? - else - true - end - rescue Mysql::Error - false - end - - def reconnect! - super - disconnect! - connect - end - - # Disconnects from the database if already connected. Otherwise, this - # method does nothing. - def disconnect! - super - @connection.close rescue nil - end - - def reset! - if @connection.respond_to?(:change_user) - # See http://bugs.mysql.com/bug.php?id=33540 -- the workaround way to - # reset the connection is to change the user to the same user. - @connection.change_user(@config[:username], @config[:password], @config[:database]) - configure_connection - end - end - - #-- - # DATABASE STATEMENTS ====================================== - #++ - - def select_all(arel, name = nil, binds = []) - if ExplainRegistry.collect? && prepared_statements - unprepared_statement { super } - else - super - end - end - - def select_rows(sql, name = nil, binds = []) - @connection.query_with_result = true - rows = exec_query(sql, name, binds).rows - @connection.more_results && @connection.next_result # invoking stored procedures with CLIENT_MULTI_RESULTS requires this to tidy up else connection will be dropped - rows - end - - # Clears the prepared statements cache. - def clear_cache! - super - @statements.clear - end - - # Taken from here: - # https://github.com/tmtm/ruby-mysql/blob/master/lib/mysql/charset.rb - # Author: TOMITA Masahiro <tommy@tmtm.org> - ENCODINGS = { - "armscii8" => nil, - "ascii" => Encoding::US_ASCII, - "big5" => Encoding::Big5, - "binary" => Encoding::ASCII_8BIT, - "cp1250" => Encoding::Windows_1250, - "cp1251" => Encoding::Windows_1251, - "cp1256" => Encoding::Windows_1256, - "cp1257" => Encoding::Windows_1257, - "cp850" => Encoding::CP850, - "cp852" => Encoding::CP852, - "cp866" => Encoding::IBM866, - "cp932" => Encoding::Windows_31J, - "dec8" => nil, - "eucjpms" => Encoding::EucJP_ms, - "euckr" => Encoding::EUC_KR, - "gb2312" => Encoding::EUC_CN, - "gbk" => Encoding::GBK, - "geostd8" => nil, - "greek" => Encoding::ISO_8859_7, - "hebrew" => Encoding::ISO_8859_8, - "hp8" => nil, - "keybcs2" => nil, - "koi8r" => Encoding::KOI8_R, - "koi8u" => Encoding::KOI8_U, - "latin1" => Encoding::ISO_8859_1, - "latin2" => Encoding::ISO_8859_2, - "latin5" => Encoding::ISO_8859_9, - "latin7" => Encoding::ISO_8859_13, - "macce" => Encoding::MacCentEuro, - "macroman" => Encoding::MacRoman, - "sjis" => Encoding::SHIFT_JIS, - "swe7" => nil, - "tis620" => Encoding::TIS_620, - "ucs2" => Encoding::UTF_16BE, - "ujis" => Encoding::EucJP_ms, - "utf8" => Encoding::UTF_8, - "utf8mb4" => Encoding::UTF_8, - } - - # Get the client encoding for this database - def client_encoding - return @client_encoding if @client_encoding - - result = exec_query( - "select @@character_set_client", - 'SCHEMA') - @client_encoding = ENCODINGS[result.rows.last.last] - end - - def exec_query(sql, name = 'SQL', binds = [], prepare: false) - if without_prepared_statement?(binds) - result_set, affected_rows = exec_without_stmt(sql, name) - else - result_set, affected_rows = exec_stmt(sql, name, binds, cache_stmt: prepare) - end - - yield affected_rows if block_given? - - result_set - end - - def last_inserted_id(result) - @connection.insert_id - end - - module Fields # :nodoc: - class DateTime < Type::DateTime # :nodoc: - def cast_value(value) - if Mysql::Time === value - new_time( - value.year, - value.month, - value.day, - value.hour, - value.minute, - value.second, - value.second_part) - else - super - end - end - end - - class Time < Type::Time # :nodoc: - def cast_value(value) - if Mysql::Time === value - new_time( - 2000, - 01, - 01, - value.hour, - value.minute, - value.second, - value.second_part) - else - super - end - end - end - - class << self - TYPES = Type::HashLookupTypeMap.new # :nodoc: - - delegate :register_type, :alias_type, to: :TYPES - - def find_type(field) - if field.type == Mysql::Field::TYPE_TINY && field.length > 1 - TYPES.lookup(Mysql::Field::TYPE_LONG) - else - TYPES.lookup(field.type) - end - end - end - - register_type Mysql::Field::TYPE_TINY, Type::Boolean.new - register_type Mysql::Field::TYPE_LONG, Type::Integer.new - alias_type Mysql::Field::TYPE_LONGLONG, Mysql::Field::TYPE_LONG - alias_type Mysql::Field::TYPE_NEWDECIMAL, Mysql::Field::TYPE_LONG - - register_type Mysql::Field::TYPE_DATE, Type::Date.new - register_type Mysql::Field::TYPE_DATETIME, Fields::DateTime.new - register_type Mysql::Field::TYPE_TIME, Fields::Time.new - register_type Mysql::Field::TYPE_FLOAT, Type::Float.new - end - - def initialize_type_map(m) # :nodoc: - super - register_class_with_precision m, %r(datetime)i, Fields::DateTime - register_class_with_precision m, %r(time)i, Fields::Time - end - - def exec_without_stmt(sql, name = 'SQL') # :nodoc: - # Some queries, like SHOW CREATE TABLE don't work through the prepared - # statement API. For those queries, we need to use this method. :'( - log(sql, name) do - result = @connection.query(sql) - affected_rows = @connection.affected_rows - - if result - types = {} - fields = [] - result.fetch_fields.each { |field| - field_name = field.name - fields << field_name - - if field.decimals > 0 - types[field_name] = Type::Decimal.new - else - types[field_name] = Fields.find_type field - end - } - - result_set = ActiveRecord::Result.new(fields, result.to_a, types) - result.free - else - result_set = ActiveRecord::Result.new([], []) - end - - [result_set, affected_rows] - end - end - - def execute_and_free(sql, name = nil) # :nodoc: - result = execute(sql, name) - ret = yield result - result.free - ret - end - - def insert_sql(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) #:nodoc: - super sql, name - id_value || @connection.insert_id - end - alias :create :insert_sql - - def exec_delete(sql, name, binds) # :nodoc: - affected_rows = 0 - - exec_query(sql, name, binds) do |n| - affected_rows = n - end - - affected_rows - end - alias :exec_update :exec_delete - - def begin_db_transaction #:nodoc: - exec_query "BEGIN" - end - - private - - def exec_stmt(sql, name, binds, cache_stmt: false) - cache = {} - type_casted_binds = binds.map { |attr| type_cast(attr.value_for_database) } - - log(sql, name, binds) do - if !cache_stmt - stmt = @connection.prepare(sql) - else - cache = @statements[sql] ||= { - :stmt => @connection.prepare(sql) - } - stmt = cache[:stmt] - end - - begin - stmt.execute(*type_casted_binds) - rescue Mysql::Error => e - # Older versions of MySQL leave the prepared statement in a bad - # place when an error occurs. To support older MySQL versions, we - # need to close the statement and delete the statement from the - # cache. - if !cache_stmt - stmt.close - else - @statements.delete sql - end - raise e - end - - cols = nil - if metadata = stmt.result_metadata - cols = cache[:cols] ||= metadata.fetch_fields.map(&:name) - metadata.free - end - - result_set = ActiveRecord::Result.new(cols, stmt.to_a) if cols - affected_rows = stmt.affected_rows - - stmt.free_result - stmt.close if !cache_stmt - - [result_set, affected_rows] - end - end - - def connect - encoding = @config[:encoding] - if encoding - @connection.options(Mysql::SET_CHARSET_NAME, encoding) rescue nil - end - - if @config[:sslca] || @config[:sslkey] - @connection.ssl_set(@config[:sslkey], @config[:sslcert], @config[:sslca], @config[:sslcapath], @config[:sslcipher]) - end - - @connection.options(Mysql::OPT_CONNECT_TIMEOUT, @config[:connect_timeout]) if @config[:connect_timeout] - @connection.options(Mysql::OPT_READ_TIMEOUT, @config[:read_timeout]) if @config[:read_timeout] - @connection.options(Mysql::OPT_WRITE_TIMEOUT, @config[:write_timeout]) if @config[:write_timeout] - - @connection.real_connect(*@connection_options) - - # reconnect must be set after real_connect is called, because real_connect sets it to false internally - @connection.reconnect = !!@config[:reconnect] if @connection.respond_to?(:reconnect=) - - configure_connection - end - - # Many Rails applications monkey-patch a replacement of the configure_connection method - # and don't call 'super', so leave this here even though it looks superfluous. - def configure_connection - super - end - - def select(sql, name = nil, binds = []) - @connection.query_with_result = true - rows = super - @connection.more_results && @connection.next_result # invoking stored procedures with CLIENT_MULTI_RESULTS requires this to tidy up else connection will be dropped - rows - end - - # Returns the full version of the connected MySQL server. - def full_version - @full_version ||= @connection.server_info - end - - def set_field_encoding field_name - field_name.force_encoding(client_encoding) - if internal_enc = Encoding.default_internal - field_name = field_name.encode!(internal_enc) - end - field_name - end - end - end -end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb b/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb index 0e0c0e993a..6f2e03b370 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb @@ -4,44 +4,7 @@ module ActiveRecord module DatabaseStatements def explain(arel, binds = []) sql = "EXPLAIN #{to_sql(arel, binds)}" - ExplainPrettyPrinter.new.pp(exec_query(sql, 'EXPLAIN', binds)) - end - - class ExplainPrettyPrinter # :nodoc: - # Pretty prints the result of an EXPLAIN in a way that resembles the output of the - # PostgreSQL shell: - # - # QUERY PLAN - # ------------------------------------------------------------------------------ - # Nested Loop Left Join (cost=0.00..37.24 rows=8 width=0) - # Join Filter: (posts.user_id = users.id) - # -> Index Scan using users_pkey on users (cost=0.00..8.27 rows=1 width=4) - # Index Cond: (id = 1) - # -> Seq Scan on posts (cost=0.00..28.88 rows=8 width=4) - # Filter: (posts.user_id = 1) - # (6 rows) - # - def pp(result) - header = result.columns.first - lines = result.rows.map(&:first) - - # We add 2 because there's one char of padding at both sides, note - # the extra hyphens in the example above. - width = [header, *lines].map(&:length).max + 2 - - pp = [] - - pp << header.center(width).rstrip - pp << '-' * width - - pp += lines.map {|line| " #{line}"} - - nrows = result.rows.length - rows_label = nrows == 1 ? 'row' : 'rows' - pp << "(#{nrows} #{rows_label})" - - pp.join("\n") + "\n" - end + PostgreSQL::ExplainPrettyPrinter.new.pp(exec_query(sql, 'EXPLAIN', binds)) end def select_value(arel, name = nil, binds = []) @@ -52,8 +15,8 @@ module ActiveRecord end end - def select_values(arel, name = nil) - arel, binds = binds_from_relation arel, [] + def select_values(arel, name = nil, binds = []) + arel, binds = binds_from_relation arel, binds sql = to_sql(arel, binds) execute_and_clear(sql, name, binds) do |result| if result.nfields > 0 @@ -72,28 +35,6 @@ module ActiveRecord end end - # Executes an INSERT query and returns the new record's ID - def insert_sql(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) - unless pk - # Extract the table from the insert sql. Yuck. - table_ref = extract_table_ref_from_insert_sql(sql) - pk = primary_key(table_ref) if table_ref - end - - if pk && use_insert_returning? - select_value("#{sql} RETURNING #{quote_column_name(pk)}") - elsif pk - super - last_insert_id_value(sequence_name || default_sequence_name(table_ref, pk)) - else - super - end - end - - def create - super.insert - end - # The internal PostgreSQL identifier of the money data type. MONEY_COLUMN_TYPE_OID = 790 #:nodoc: # The internal PostgreSQL identifier of the BYTEA data type. @@ -150,6 +91,8 @@ module ActiveRecord # Executes an SQL statement, returning a PGresult object on success # or raising a PGError exception otherwise. + # Note: the PGresult object is manually memory managed; if you don't + # need it specifically, you many want consider the exec_query wrapper. def execute(sql, name = nil) log(sql, name) do @connection.async_exec(sql) @@ -174,8 +117,8 @@ module ActiveRecord end alias :exec_update :exec_delete - def sql_for_insert(sql, pk, id_value, sequence_name, binds) - unless pk + def sql_for_insert(sql, pk, id_value, sequence_name, binds) # :nodoc: + if pk.nil? # Extract the table from the insert sql. Yuck. table_ref = extract_table_ref_from_insert_sql(sql) pk = primary_key(table_ref) if table_ref @@ -185,7 +128,7 @@ module ActiveRecord sql = "#{sql} RETURNING #{quote_column_name(pk)}" end - [sql, binds] + super end def exec_insert(sql, name, binds, pk = nil, sequence_name = nil) @@ -202,11 +145,6 @@ module ActiveRecord end end - # Executes an UPDATE query and returns the number of affected tuples. - def update_sql(sql, name = nil) - super.cmd_tuples - end - # Begins a transaction. def begin_db_transaction execute "BEGIN" diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/explain_pretty_printer.rb b/activerecord/lib/active_record/connection_adapters/postgresql/explain_pretty_printer.rb new file mode 100644 index 0000000000..789b88912c --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/explain_pretty_printer.rb @@ -0,0 +1,42 @@ +module ActiveRecord + module ConnectionAdapters + module PostgreSQL + class ExplainPrettyPrinter # :nodoc: + # Pretty prints the result of an EXPLAIN in a way that resembles the output of the + # PostgreSQL shell: + # + # QUERY PLAN + # ------------------------------------------------------------------------------ + # Nested Loop Left Join (cost=0.00..37.24 rows=8 width=0) + # Join Filter: (posts.user_id = users.id) + # -> Index Scan using users_pkey on users (cost=0.00..8.27 rows=1 width=4) + # Index Cond: (id = 1) + # -> Seq Scan on posts (cost=0.00..28.88 rows=8 width=4) + # Filter: (posts.user_id = 1) + # (6 rows) + # + def pp(result) + header = result.columns.first + lines = result.rows.map(&:first) + + # We add 2 because there's one char of padding at both sides, note + # the extra hyphens in the example above. + width = [header, *lines].map(&:length).max + 2 + + pp = [] + + pp << header.center(width).rstrip + pp << '-' * width + + pp += lines.map {|line| " #{line}"} + + nrows = result.rows.length + rows_label = nrows == 1 ? 'row' : 'rows' + pp << "(#{nrows} #{rows_label})" + + pp.join("\n") + "\n" + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb index 25961a9869..87593ef704 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb @@ -50,6 +50,10 @@ module ActiveRecord "[" + value.map { |v| subtype.type_cast_for_schema(v) }.join(", ") + "]" end + def map(value, &block) + value.map(&block) + end + private def type_cast_array(value, method) diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/money.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/money.rb index 2163674019..dcc12ae2a4 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/money.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/money.rb @@ -3,8 +3,6 @@ module ActiveRecord module PostgreSQL module OID # :nodoc: class Money < Type::Decimal # :nodoc: - class_attribute :precision - def type :money end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb index fc201f8fb9..a8d2310035 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb @@ -6,6 +6,7 @@ module ActiveRecord module OID # :nodoc: class Range < Type::Value # :nodoc: attr_reader :subtype, :type + delegate :user_input_in_time_zone, to: :subtype def initialize(subtype, type = :range) @subtype = subtype @@ -18,7 +19,7 @@ module ActiveRecord def cast_value(value) return if value == 'empty' - return value if value.is_a?(::Range) + return value unless value.is_a?(::String) extracted = extract_bounds(value) from = type_cast_single extracted[:from] @@ -46,6 +47,12 @@ module ActiveRecord other.type == type end + def map(value) # :nodoc: + new_begin = yield(value.begin) + new_end = yield(value.end) + ::Range.new(new_begin, new_end, value.exclude_end?) + end + private def type_cast_single(value) diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb index d5879ea7df..c1c77a967e 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb @@ -55,10 +55,11 @@ module ActiveRecord end end - # Does not quote function default values for UUID columns - def quote_default_expression(value, column) #:nodoc: - if column.type == :uuid && value =~ /\(\)/ - value + def quote_default_expression(value, column) # :nodoc: + if value.is_a?(Proc) + value.call + elsif column.type == :uuid && value =~ /\(\)/ + value # Does not quote function default values for UUID columns elsif column.respond_to?(:array?) value = type_cast_from_column(column, value) quote(value) diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/schema_dumper.rb b/activerecord/lib/active_record/connection_adapters/postgresql/schema_dumper.rb index a4f0742516..b82bdb8b0c 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_dumper.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_dumper.rb @@ -9,9 +9,9 @@ module ActiveRecord spec[:id] = ':bigserial' elsif column.type == :uuid spec[:id] = ':uuid' - spec[:default] = column.default_function.inspect + spec[:default] = schema_default(column) || 'nil' else - spec[:id] = column.type.inspect + spec[:id] = schema_type(column).inspect spec.merge!(prepare_column_options(column).delete_if { |key, _| [:name, :type, :null].include?(key) }) end spec @@ -35,18 +35,14 @@ module ActiveRecord return super unless column.serial? if column.bigint? - 'bigserial' + :bigserial else - 'serial' + :serial end end - def schema_default(column) - if column.default_function - column.default_function.inspect unless column.serial? - else - super - end + def schema_expression(column) + super unless column.serial? end end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb index aaf5b2898b..67e727d8ed 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb @@ -70,6 +70,12 @@ module ActiveRecord # Returns the list of all tables in the schema search path. def tables(name = nil) + if name + ActiveSupport::Deprecation.warn(<<-MSG.squish) + Passing arguments to #tables is deprecated without replacement. + MSG + end + select_values("SELECT tablename FROM pg_tables WHERE schemaname = ANY(current_schemas(false))", 'SCHEMA') end @@ -87,6 +93,16 @@ module ActiveRecord # If the schema is not specified as part of +name+ then it will only find tables within # the current schema search path (regardless of permissions to access tables in other schemas) def table_exists?(name) + ActiveSupport::Deprecation.warn(<<-MSG.squish) + #table_exists? currently checks both tables and views. + This behavior is deprecated and will be changed with Rails 5.1 to only check tables. + Use #data_source_exists? instead. + MSG + + data_source_exists?(name) + end + + def data_source_exists?(name) name = Utils.extract_schema_qualified_name(name.to_s) return false unless name.identifier @@ -99,7 +115,6 @@ module ActiveRecord AND n.nspname = #{name.schema ? "'#{name.schema}'" : 'ANY (current_schemas(false))'} SQL end - alias data_source_exists? table_exists? def views # :nodoc: select_values(<<-SQL, 'SCHEMA') @@ -154,15 +169,18 @@ module ActiveRecord # Returns an array of indexes for the given table. def indexes(table_name, name = nil) - result = query(<<-SQL, 'SCHEMA') - SELECT distinct i.relname, d.indisunique, d.indkey, pg_get_indexdef(d.indexrelid), t.oid - FROM pg_class t - INNER JOIN pg_index d ON t.oid = d.indrelid - INNER JOIN pg_class i ON d.indexrelid = i.oid - WHERE i.relkind = 'i' - AND d.indisprimary = 'f' - AND t.relname = '#{table_name}' - AND i.relnamespace IN (SELECT oid FROM pg_namespace WHERE nspname = ANY (current_schemas(false)) ) + table = Utils.extract_schema_qualified_name(table_name.to_s) + + result = query(<<-SQL, 'SCHEMA') + SELECT distinct i.relname, d.indisunique, d.indkey, pg_get_indexdef(d.indexrelid), t.oid + FROM pg_class t + INNER JOIN pg_index d ON t.oid = d.indrelid + INNER JOIN pg_class i ON d.indexrelid = i.oid + LEFT JOIN pg_namespace n ON n.oid = i.relnamespace + WHERE i.relkind = 'i' + AND d.indisprimary = 'f' + AND t.relname = '#{table.identifier}' + AND n.nspname = #{table.schema ? "'#{table.schema}'" : 'ANY (current_schemas(false))'} ORDER BY i.relname SQL @@ -489,14 +507,27 @@ module ActiveRecord end def remove_index(table_name, options = {}) #:nodoc: - index_name = index_name_for_remove(table_name, options) + table = Utils.extract_schema_qualified_name(table_name.to_s) + + if options.is_a?(Hash) && options.key?(:name) + provided_index = Utils.extract_schema_qualified_name(options[:name].to_s) + + options[:name] = provided_index.identifier + table = PostgreSQL::Name.new(provided_index.schema, table.identifier) unless table.schema.present? + + if provided_index.schema.present? && table.schema != provided_index.schema + raise ArgumentError.new("Index schema '#{provided_index.schema}' does not match table schema '#{table.schema}'") + end + end + + index_to_remove = PostgreSQL::Name.new(table.schema, index_name_for_remove(table.to_s, options)) algorithm = - if Hash === options && options.key?(:algorithm) + if options.is_a?(Hash) && options.key?(:algorithm) index_algorithms.fetch(options[:algorithm]) do raise ArgumentError.new("Algorithm must be one of the following: #{index_algorithms.keys.map(&:inspect).join(', ')}") end end - execute "DROP INDEX #{algorithm} #{quote_table_name(index_name)}" + execute "DROP INDEX #{algorithm} #{quote_table_name(index_to_remove)}" end # Renames an index of a table. Raises error if length of new diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index 7ab35cfb76..61c9628de3 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -5,6 +5,7 @@ require 'pg' require "active_record/connection_adapters/abstract_adapter" require "active_record/connection_adapters/postgresql/column" require "active_record/connection_adapters/postgresql/database_statements" +require "active_record/connection_adapters/postgresql/explain_pretty_printer" require "active_record/connection_adapters/postgresql/oid" require "active_record/connection_adapters/postgresql/quoting" require "active_record/connection_adapters/postgresql/referential_integrity" @@ -72,7 +73,6 @@ module ActiveRecord NATIVE_DATABASE_TYPES = { primary_key: "serial primary key", - bigserial: "bigserial", string: { name: "character varying" }, text: { name: "text" }, integer: { name: "integer" }, @@ -89,7 +89,6 @@ module ActiveRecord int8range: { name: "int8range" }, binary: { name: "bytea" }, boolean: { name: "boolean" }, - bigint: { name: "bigint" }, xml: { name: "xml" }, tsvector: { name: "tsvector" }, hstore: { name: "hstore" }, @@ -102,6 +101,12 @@ module ActiveRecord ltree: { name: "ltree" }, citext: { name: "citext" }, point: { name: "point" }, + line: { name: "line" }, + lseg: { name: "lseg" }, + box: { name: "box" }, + path: { name: "path" }, + polygon: { name: "polygon" }, + circle: { name: "circle" }, bit: { name: "bit" }, bit_varying: { name: "bit varying" }, money: { name: "money" }, @@ -188,7 +193,7 @@ module ActiveRecord # Initializes and connects a PostgreSQL adapter. def initialize(connection, logger, connection_parameters, config) - super(connection, logger) + super(connection, logger, config) @visitor = Arel::Visitors::PostgreSQL.new self if self.class.type_cast_config_to_boolean(config.fetch(:prepared_statements) { true }) @@ -198,7 +203,7 @@ module ActiveRecord @prepared_statements = false end - @connection_parameters, @config = connection_parameters, config + @connection_parameters = connection_parameters # @local_tz is initialized as nil to avoid warnings when connect tries to use it @local_tz = nil @@ -209,8 +214,8 @@ module ActiveRecord @statements = StatementPool.new @connection, self.class.type_cast_config_to_integer(config.fetch(:statement_limit) { 1000 }) - if postgresql_version < 80200 - raise "Your version of PostgreSQL (#{postgresql_version}) is too old, please upgrade!" + if postgresql_version < 90100 + raise "Your version of PostgreSQL (#{postgresql_version}) is too old. Active Record supports PostgreSQL >= 9.1." end add_pg_decoders @@ -292,9 +297,8 @@ module ActiveRecord true end - # Returns true if pg > 9.1 def supports_extensions? - postgresql_version >= 90100 + true end # Range datatypes weren't introduced until PostgreSQL 9.2 @@ -306,18 +310,18 @@ module ActiveRecord postgresql_version >= 90300 end - def get_advisory_lock(key) # :nodoc: - unless key.is_a?(Integer) && key.bit_length <= 63 - raise(ArgumentError, "Postgres requires advisory lock keys to be a signed 64 bit integer") + def get_advisory_lock(lock_id) # :nodoc: + unless lock_id.is_a?(Integer) && lock_id.bit_length <= 63 + raise(ArgumentError, "Postgres requires advisory lock ids to be a signed 64 bit integer") end - select_value("SELECT pg_try_advisory_lock(#{key});") + select_value("SELECT pg_try_advisory_lock(#{lock_id});") end - def release_advisory_lock(key) # :nodoc: - unless key.is_a?(Integer) && key.bit_length <= 63 - raise(ArgumentError, "Postgres requires advisory lock keys to be a signed 64 bit integer") + def release_advisory_lock(lock_id) # :nodoc: + unless lock_id.is_a?(Integer) && lock_id.bit_length <= 63 + raise(ArgumentError, "Postgres requires advisory lock ids to be a signed 64 bit integer") end - select_value("SELECT pg_advisory_unlock(#{key})") + select_value("SELECT pg_advisory_unlock(#{lock_id})") end def enable_extension(name) @@ -386,12 +390,12 @@ module ActiveRecord "average" => "avg", } - protected + # Returns the version of the connected PostgreSQL server. + def postgresql_version + @connection.server_version + end - # Returns the version of the connected PostgreSQL server. - def postgresql_version - @connection.server_version - end + protected # See http://www.postgresql.org/docs/current/static/errcodes-appendix.html FOREIGN_KEY_VIOLATION = "23503" @@ -457,15 +461,15 @@ module ActiveRecord m.register_type 'macaddr', OID::SpecializedString.new(:macaddr) m.register_type 'citext', OID::SpecializedString.new(:citext) m.register_type 'ltree', OID::SpecializedString.new(:ltree) + m.register_type 'line', OID::SpecializedString.new(:line) + m.register_type 'lseg', OID::SpecializedString.new(:lseg) + m.register_type 'box', OID::SpecializedString.new(:box) + m.register_type 'path', OID::SpecializedString.new(:path) + m.register_type 'polygon', OID::SpecializedString.new(:polygon) + m.register_type 'circle', OID::SpecializedString.new(:circle) # FIXME: why are we keeping these types as strings? m.alias_type 'interval', 'varchar' - m.alias_type 'path', 'varchar' - m.alias_type 'line', 'varchar' - m.alias_type 'polygon', 'varchar' - m.alias_type 'circle', 'varchar' - m.alias_type 'lseg', 'varchar' - m.alias_type 'box', 'varchar' register_class_with_precision m, 'time', Type::Time register_class_with_precision m, 'timestamp', OID::DateTime @@ -508,8 +512,13 @@ module ActiveRecord def extract_value_from_default(default) # :nodoc: case default # Quoted types - when /\A[\(B]?'(.*)'::/m - $1.gsub("''".freeze, "'".freeze) + when /\A[\(B]?'(.*)'.*::"?([\w. ]+)"?(?:\[\])?\z/m + # The default 'now'::date is CURRENT_DATE + if $1 == "now".freeze && $2 == "date".freeze + nil + else + $1.gsub("''".freeze, "'".freeze) + end # Boolean types when 'true'.freeze, 'false'.freeze default @@ -531,7 +540,7 @@ module ActiveRecord end def has_default_function?(default_value, default) # :nodoc: - !default_value && (%r{\w+\(.*\)} === default) + !default_value && (%r{\w+\(.*\)|\(.*\)::\w+} === default) end def load_additional_types(type_map, oids = nil) # :nodoc: @@ -652,12 +661,6 @@ module ActiveRecord # connected server's characteristics. def connect @connection = PGconn.connect(@connection_parameters) - - # Money type has a fixed precision of 10 in PostgreSQL 8.2 and below, and as of - # PostgreSQL 8.3 it has a fixed precision of 19. PostgreSQLColumn.extract_precision - # should know about this but can't detect it there, so deal with it here. - OID::Money.precision = (postgresql_version >= 80300) ? 19 : 10 - configure_connection rescue ::PG::Error => error if error.message.include?("does not exist") @@ -702,15 +705,7 @@ module ActiveRecord end # Returns the current ID of a table's sequence. - def last_insert_id(sequence_name) #:nodoc: - Integer(last_insert_id_value(sequence_name)) - end - - def last_insert_id_value(sequence_name) - last_insert_id_result(sequence_name).rows.first.first - end - - def last_insert_id_result(sequence_name) #:nodoc: + def last_insert_id_result(sequence_name) # :nodoc: exec_query("SELECT currval('#{sequence_name}')", 'SQL') end @@ -752,7 +747,7 @@ module ActiveRecord end def create_table_definition(name, temporary = false, options = nil, as = nil) # :nodoc: - PostgreSQL::TableDefinition.new native_database_types, name, temporary, options, as + PostgreSQL::TableDefinition.new(name, temporary, options, as) end def can_perform_case_insensitive_comparison_for?(column) @@ -828,9 +823,8 @@ module ActiveRecord ActiveRecord::Type.register(:json, OID::Json, adapter: :postgresql) ActiveRecord::Type.register(:jsonb, OID::Jsonb, adapter: :postgresql) ActiveRecord::Type.register(:money, OID::Money, adapter: :postgresql) - ActiveRecord::Type.register(:point, OID::Point, adapter: :postgresql) + ActiveRecord::Type.register(:point, OID::Rails51Point, adapter: :postgresql) ActiveRecord::Type.register(:legacy_point, OID::Point, adapter: :postgresql) - ActiveRecord::Type.register(:rails_5_1_point, OID::Rails51Point, adapter: :postgresql) ActiveRecord::Type.register(:uuid, OID::Uuid, adapter: :postgresql) ActiveRecord::Type.register(:vector, OID::Vector, adapter: :postgresql) ActiveRecord::Type.register(:xml, OID::Xml, adapter: :postgresql) diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3/explain_pretty_printer.rb b/activerecord/lib/active_record/connection_adapters/sqlite3/explain_pretty_printer.rb new file mode 100644 index 0000000000..a946f5ebd0 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/sqlite3/explain_pretty_printer.rb @@ -0,0 +1,19 @@ +module ActiveRecord + module ConnectionAdapters + module SQLite3 + class ExplainPrettyPrinter # :nodoc: + # Pretty prints the result of an EXPLAIN QUERY PLAN in a way that resembles + # the output of the SQLite shell: + # + # 0|0|0|SEARCH TABLE users USING INTEGER PRIMARY KEY (rowid=?) (~1 rows) + # 0|1|1|SCAN TABLE posts (~100000 rows) + # + def pp(result) + result.rows.map do |row| + row.join('|') + end.join("\n") + "\n" + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb index 32fe275bfb..c65d33ccb3 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb @@ -1,5 +1,6 @@ require 'active_record/connection_adapters/abstract_adapter' require 'active_record/connection_adapters/statement_pool' +require 'active_record/connection_adapters/sqlite3/explain_pretty_printer' require 'active_record/connection_adapters/sqlite3/schema_creation' gem 'sqlite3', '~> 1.3.6' @@ -7,7 +8,6 @@ require 'sqlite3' module ActiveRecord module ConnectionHandling # :nodoc: - # sqlite3 adapter reuses sqlite_connection. def sqlite3_connection(config) # Require database. unless config[:database] @@ -78,11 +78,10 @@ module ActiveRecord end def initialize(connection, logger, connection_options, config) - super(connection, logger) + super(connection, logger, config) @active = nil @statements = StatementPool.new(self.class.type_cast_config_to_integer(config.fetch(:statement_limit) { 1000 })) - @config = config @visitor = Arel::Visitors::SQLite.new self @quoted_column_names = {} @@ -130,6 +129,10 @@ module ActiveRecord true end + def supports_datetime_with_precision? + true + end + def active? @active != false end @@ -215,21 +218,7 @@ module ActiveRecord def explain(arel, binds = []) sql = "EXPLAIN QUERY PLAN #{to_sql(arel, binds)}" - ExplainPrettyPrinter.new.pp(exec_query(sql, 'EXPLAIN', [])) - end - - class ExplainPrettyPrinter - # Pretty prints the result of an EXPLAIN QUERY PLAN in a way that resembles - # the output of the SQLite shell: - # - # 0|0|0|SEARCH TABLE users USING INTEGER PRIMARY KEY (rowid=?) (~1 rows) - # 0|1|1|SCAN TABLE posts (~100000 rows) - # - def pp(result) # :nodoc: - result.rows.map do |row| - row.join('|') - end.join("\n") + "\n" - end + SQLite3::ExplainPrettyPrinter.new.pp(exec_query(sql, 'EXPLAIN', [])) end def exec_query(sql, name = nil, binds = [], prepare: false) @@ -277,22 +266,6 @@ module ActiveRecord log(sql, name) { @connection.execute(sql) } end - def update_sql(sql, name = nil) #:nodoc: - super - @connection.changes - end - - def delete_sql(sql, name = nil) #:nodoc: - sql += " WHERE 1=1" unless sql =~ /WHERE/i - super sql, name - end - - def insert_sql(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) #:nodoc: - super - id_value || @connection.last_insert_row_id - end - alias :create :insert_sql - def select_rows(sql, name = nil, binds = []) exec_query(sql, name, binds).rows end @@ -312,11 +285,36 @@ module ActiveRecord # SCHEMA STATEMENTS ======================================== def tables(name = nil) # :nodoc: + ActiveSupport::Deprecation.warn(<<-MSG.squish) + #tables currently returns both tables and views. + This behavior is deprecated and will be changed with Rails 5.1 to only return tables. + Use #data_sources instead. + MSG + + if name + ActiveSupport::Deprecation.warn(<<-MSG.squish) + Passing arguments to #tables is deprecated without replacement. + MSG + end + + data_sources + end + + def data_sources select_values("SELECT name FROM sqlite_master WHERE type IN ('table','view') AND name <> 'sqlite_sequence'", 'SCHEMA') end - alias data_sources tables def table_exists?(table_name) + ActiveSupport::Deprecation.warn(<<-MSG.squish) + #table_exists? currently checks both tables and views. + This behavior is deprecated and will be changed with Rails 5.1 to only check tables. + Use #data_source_exists? instead. + MSG + + data_source_exists?(table_name) + end + + def data_source_exists?(table_name) return false unless table_name.present? sql = "SELECT name FROM sqlite_master WHERE type IN ('table','view') AND name <> 'sqlite_sequence'" @@ -324,7 +322,6 @@ module ActiveRecord select_values(sql, 'SCHEMA').any? end - alias data_source_exists? table_exists? def views # :nodoc: select_values("SELECT name FROM sqlite_master WHERE type = 'view' AND name <> 'sqlite_sequence'", 'SCHEMA') diff --git a/activerecord/lib/active_record/connection_handling.rb b/activerecord/lib/active_record/connection_handling.rb index aedef54928..a8b3d03ba5 100644 --- a/activerecord/lib/active_record/connection_handling.rb +++ b/activerecord/lib/active_record/connection_handling.rb @@ -8,7 +8,7 @@ module ActiveRecord # example for regular databases (MySQL, PostgreSQL, etc): # # ActiveRecord::Base.establish_connection( - # adapter: "mysql", + # adapter: "mysql2", # host: "localhost", # username: "myuser", # password: "mypass", diff --git a/activerecord/lib/active_record/core.rb b/activerecord/lib/active_record/core.rb index 1250f8a3c3..24fd0aaecf 100644 --- a/activerecord/lib/active_record/core.rb +++ b/activerecord/lib/active_record/core.rb @@ -256,6 +256,11 @@ module ActiveRecord end end + def arel_attribute(name, table = arel_table) # :nodoc: + name = attribute_alias(name) if attribute_alias?(name) + table[name] + end + def predicate_builder # :nodoc: @predicate_builder ||= PredicateBuilder.new(table_metadata) end @@ -275,7 +280,7 @@ module ActiveRecord def relation # :nodoc: relation = Relation.create(self, arel_table, predicate_builder) - if finder_needs_type_condition? + if finder_needs_type_condition? && !ignore_default_scope? relation.where(type_condition).create_with(inheritance_column.to_sym => sti_name) else relation diff --git a/activerecord/lib/active_record/counter_cache.rb b/activerecord/lib/active_record/counter_cache.rb index 9e7d391c70..1b6817554d 100644 --- a/activerecord/lib/active_record/counter_cache.rb +++ b/activerecord/lib/active_record/counter_cache.rb @@ -97,8 +97,8 @@ module ActiveRecord # # ==== Examples # - # # Increment the post_count column for the record with an id of 5 - # DiscussionBoard.increment_counter(:post_count, 5) + # # Increment the posts_count column for the record with an id of 5 + # DiscussionBoard.increment_counter(:posts_count, 5) def increment_counter(counter_name, id) update_counters(id, counter_name => 1) end @@ -115,8 +115,8 @@ module ActiveRecord # # ==== Examples # - # # Decrement the post_count column for the record with an id of 5 - # DiscussionBoard.decrement_counter(:post_count, 5) + # # Decrement the posts_count column for the record with an id of 5 + # DiscussionBoard.decrement_counter(:posts_count, 5) def decrement_counter(counter_name, id) update_counters(id, counter_name => -1) end diff --git a/activerecord/lib/active_record/enum.rb b/activerecord/lib/active_record/enum.rb index 8fba6fcc35..903c63a7db 100644 --- a/activerecord/lib/active_record/enum.rb +++ b/activerecord/lib/active_record/enum.rb @@ -95,7 +95,7 @@ module ActiveRecord module Enum def self.extended(base) # :nodoc: - base.class_attribute(:defined_enums) + base.class_attribute(:defined_enums, instance_writer: false) base.defined_enums = {} end @@ -104,10 +104,11 @@ module ActiveRecord super end - class EnumType < Type::Value - def initialize(name, mapping) + class EnumType < Type::Value # :nodoc: + def initialize(name, mapping, subtype) @name = name @mapping = mapping + @subtype = subtype end def cast(value) @@ -124,7 +125,7 @@ module ActiveRecord def deserialize(value) return if value.nil? - mapping.key(value.to_i) + mapping.key(subtype.deserialize(value)) end def serialize(value) @@ -139,7 +140,7 @@ module ActiveRecord protected - attr_reader :name, :mapping + attr_reader :name, :mapping, :subtype end def enum(definitions) @@ -158,7 +159,9 @@ module ActiveRecord detect_enum_conflict!(name, name) detect_enum_conflict!(name, "#{name}=") - attribute name, EnumType.new(name, enum_values) + decorate_attribute_type(name, :enum) do |subtype| + EnumType.new(name, enum_values, subtype) + end _enum_methods_module.module_eval do pairs = values.respond_to?(:each_pair) ? values.each_pair : values.each_with_index @@ -187,7 +190,7 @@ module ActiveRecord # scope :active, -> { where status: 0 } klass.send(:detect_enum_conflict!, name, value_method_name, true) - klass.scope value_method_name, -> { klass.where name => value } + klass.scope value_method_name, -> { where(name => value) } end end defined_enums[name.to_s] = enum_values diff --git a/activerecord/lib/active_record/errors.rb b/activerecord/lib/active_record/errors.rb index ee7df61715..2ec9bf3d67 100644 --- a/activerecord/lib/active_record/errors.rb +++ b/activerecord/lib/active_record/errors.rb @@ -277,7 +277,12 @@ module ActiveRecord # * You are joining an existing open transaction # * You are creating a nested (savepoint) transaction # - # The mysql, mysql2 and postgresql adapters support setting the transaction isolation level. + # The mysql2 and postgresql adapters support setting the transaction isolation level. class TransactionIsolationError < ActiveRecordError end + + # IrreversibleOrderError is raised when a relation's order is too complex for + # +reverse_order+ to automatically reverse. + class IrreversibleOrderError < ActiveRecordError + end end diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb index deb7d7d06d..ed1bbf5dcd 100644 --- a/activerecord/lib/active_record/fixtures.rb +++ b/activerecord/lib/active_record/fixtures.rb @@ -401,7 +401,7 @@ module ActiveRecord # It's possible to set the fixture's model class directly in the YAML file. # This is helpful when fixtures are loaded outside tests and # +set_fixture_class+ is not available (e.g. - # when running <tt>rake db:fixtures:load</tt>). + # when running <tt>rails db:fixtures:load</tt>). # # _fixture: # model_class: User diff --git a/activerecord/lib/active_record/gem_version.rb b/activerecord/lib/active_record/gem_version.rb index a388b529c9..73be4cb271 100644 --- a/activerecord/lib/active_record/gem_version.rb +++ b/activerecord/lib/active_record/gem_version.rb @@ -8,7 +8,7 @@ module ActiveRecord MAJOR = 5 MINOR = 0 TINY = 0 - PRE = "alpha" + PRE = "beta3" 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 589c70db0d..899683ee4f 100644 --- a/activerecord/lib/active_record/inheritance.rb +++ b/activerecord/lib/active_record/inheritance.rb @@ -51,8 +51,12 @@ module ActiveRecord end attrs = args.first - if subclass_from_attributes?(attrs) + if has_attribute?(inheritance_column) subclass = subclass_from_attributes(attrs) + + if subclass.nil? && base_class == self + subclass = subclass_from_attributes(column_defaults) + end end if subclass && subclass != self @@ -163,10 +167,11 @@ module ActiveRecord end def using_single_table_inheritance?(record) - record[inheritance_column].present? && columns_hash.include?(inheritance_column) + record[inheritance_column].present? && has_attribute?(inheritance_column) end def find_sti_class(type_name) + type_name = base_class.type_for_attribute(inheritance_column).cast(type_name) subclass = begin if store_full_sti_class ActiveSupport::Dependencies.constantize(type_name) @@ -187,7 +192,7 @@ module ActiveRecord end def type_condition(table = arel_table) - sti_column = table[inheritance_column] + sti_column = arel_attribute(inheritance_column, table) sti_names = ([self] + descendants).map(&:sti_name) sti_column.in(sti_names) @@ -195,17 +200,14 @@ module ActiveRecord # Detect the subclass from the inheritance column of attrs. If the inheritance column value # is not self or a valid subclass, raises ActiveRecord::SubclassNotFound - # If this is a StrongParameters hash, and access to inheritance_column is not permitted, - # this will ignore the inheritance column and return nil - def subclass_from_attributes?(attrs) - attribute_names.include?(inheritance_column) && attrs.is_a?(Hash) - end - def subclass_from_attributes(attrs) - subclass_name = attrs.with_indifferent_access[inheritance_column] + attrs = attrs.to_h if attrs.respond_to?(:permitted?) + if attrs.is_a?(Hash) + subclass_name = attrs.with_indifferent_access[inheritance_column] - if subclass_name.present? - find_sti_class(subclass_name) + if subclass_name.present? + find_sti_class(subclass_name) + end end end end diff --git a/activerecord/lib/active_record/internal_metadata.rb b/activerecord/lib/active_record/internal_metadata.rb new file mode 100644 index 0000000000..81db96bffd --- /dev/null +++ b/activerecord/lib/active_record/internal_metadata.rb @@ -0,0 +1,56 @@ +require 'active_record/scoping/default' +require 'active_record/scoping/named' + +module ActiveRecord + # This class is used to create a table that keeps track of values and keys such + # as which environment migrations were run in. + class InternalMetadata < ActiveRecord::Base # :nodoc: + class << self + def primary_key + "key" + end + + def table_name + "#{table_name_prefix}#{ActiveRecord::Base.internal_metadata_table_name}#{table_name_suffix}" + end + + def original_table_name + "#{table_name_prefix}active_record_internal_metadatas#{table_name_suffix}" + end + + def []=(key, value) + first_or_initialize(key: key).update_attributes!(value: value) + end + + def [](key) + where(key: key).pluck(:value).first + end + + def table_exists? + ActiveSupport::Deprecation.silence { connection.table_exists?(table_name) } + end + + def original_table_exists? + # This method will be removed in Rails 5.1 + # Since it is only necessary when `active_record_internal_metadatas` could exist + ActiveSupport::Deprecation.silence { connection.table_exists?(original_table_name) } + end + + # Creates an internal metadata table with columns +key+ and +value+ + def create_table + if original_table_exists? + connection.rename_table(original_table_name, table_name) + end + unless table_exists? + key_options = connection.internal_string_options_for_primary_key + + connection.create_table(table_name, id: false) do |t| + t.string :key, key_options + t.string :value + t.timestamps + end + end + end + end + end +end diff --git a/activerecord/lib/active_record/log_subscriber.rb b/activerecord/lib/active_record/log_subscriber.rb index b63caa4473..efa2a4df02 100644 --- a/activerecord/lib/active_record/log_subscriber.rb +++ b/activerecord/lib/active_record/log_subscriber.rb @@ -67,7 +67,7 @@ module ActiveRecord case sql when /\A\s*rollback/mi RED - when /\s*.*?select .*for update/mi, /\A\s*lock/mi + when /select .*for update/mi, /\A\s*lock/mi WHITE when /\A\s*select/i BLUE diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb index 1e870d4c44..4419a7b1e7 100644 --- a/activerecord/lib/active_record/migration.rb +++ b/activerecord/lib/active_record/migration.rb @@ -13,7 +13,7 @@ module ActiveRecord # For example the following migration is not reversible. # Rolling back this migration will raise an ActiveRecord::IrreversibleMigration error. # - # class IrreversibleMigrationExample < ActiveRecord::Migration + # class IrreversibleMigrationExample < ActiveRecord::Migration[5.0] # def change # create_table :distributors do |t| # t.string :zipcode @@ -31,7 +31,7 @@ module ActiveRecord # # 1. Define <tt>#up</tt> and <tt>#down</tt> methods instead of <tt>#change</tt>: # - # class ReversibleMigrationExample < ActiveRecord::Migration + # class ReversibleMigrationExample < ActiveRecord::Migration[5.0] # def up # create_table :distributors do |t| # t.string :zipcode @@ -56,7 +56,7 @@ module ActiveRecord # # 2. Use the #reversible method in <tt>#change</tt> method: # - # class ReversibleMigrationExample < ActiveRecord::Migration + # class ReversibleMigrationExample < ActiveRecord::Migration[5.0] # def change # create_table :distributors do |t| # t.string :zipcode @@ -126,9 +126,9 @@ module ActiveRecord class PendingMigrationError < MigrationError#:nodoc: def initialize(message = nil) if !message && defined?(Rails.env) - super("Migrations are pending. To resolve this issue, run:\n\n\tbin/rake db:migrate RAILS_ENV=#{::Rails.env}.") + super("Migrations are pending. To resolve this issue, run:\n\n\tbin/rails db:migrate RAILS_ENV=#{::Rails.env}") elsif !message - super("Migrations are pending. To resolve this issue, run:\n\n\tbin/rake db:migrate.") + super("Migrations are pending. To resolve this issue, run:\n\n\tbin/rails db:migrate") else super end @@ -143,6 +143,40 @@ module ActiveRecord end end + class NoEnvironmentInSchemaError < MigrationError #:nodoc: + def initialize + msg = "Environment data not found in the schema. To resolve this issue, run: \n\n\tbin/rails db:environment:set" + if defined?(Rails.env) + super("#{msg} RAILS_ENV=#{::Rails.env}") + else + super(msg) + end + end + end + + class ProtectedEnvironmentError < ActiveRecordError #:nodoc: + def initialize(env = "production") + msg = "You are attempting to run a destructive action against your '#{env}' database\n" + msg << "If you are sure you want to continue, run the same command with the environment variable\n" + msg << "DISABLE_DATABASE_ENVIRONMENT_CHECK=1" + super(msg) + end + end + + class EnvironmentMismatchError < ActiveRecordError + def initialize(current: nil, stored: nil) + msg = "You are attempting to modify a database that was last run in `#{ stored }` environment.\n" + msg << "You are running in `#{ current }` environment." + msg << "If you are sure you want to continue, first set the environment using:\n\n" + msg << "\tbin/rails db:environment:set" + if defined?(Rails.env) + super("#{msg} RAILS_ENV=#{::Rails.env}") + else + super(msg) + end + end + end + # = Active Record Migrations # # Migrations can manage the evolution of a schema used by several physical @@ -155,7 +189,7 @@ module ActiveRecord # # Example of a simple migration: # - # class AddSsl < ActiveRecord::Migration + # class AddSsl < ActiveRecord::Migration[5.0] # def up # add_column :accounts, :ssl_enabled, :boolean, default: true # end @@ -175,7 +209,7 @@ module ActiveRecord # # Example of a more complex migration that also needs to initialize data: # - # class AddSystemSettings < ActiveRecord::Migration + # class AddSystemSettings < ActiveRecord::Migration[5.0] # def up # create_table :system_settings do |t| # t.string :name @@ -301,23 +335,23 @@ module ActiveRecord # rails generate migration add_fieldname_to_tablename fieldname:string # # This will generate the file <tt>timestamp_add_fieldname_to_tablename.rb</tt>, which will look like this: - # class AddFieldnameToTablename < ActiveRecord::Migration + # class AddFieldnameToTablename < ActiveRecord::Migration[5.0] # def change # add_column :tablenames, :fieldname, :string # end # end # # To run migrations against the currently configured database, use - # <tt>rake db:migrate</tt>. This will update the database by running all of the + # <tt>rails db:migrate</tt>. This will update the database by running all of the # pending migrations, creating the <tt>schema_migrations</tt> table # (see "About the schema_migrations table" section below) if missing. It will also # invoke the db:schema:dump task, which will update your db/schema.rb file # to match the structure of your database. # # To roll the database back to a previous migration version, use - # <tt>rake db:migrate VERSION=X</tt> where <tt>X</tt> is the version to which + # <tt>rails db:migrate VERSION=X</tt> where <tt>X</tt> is the version to which # you wish to downgrade. Alternatively, you can also use the STEP option if you - # wish to rollback last few migrations. <tt>rake db:migrate STEP=2</tt> will rollback + # wish to rollback last few migrations. <tt>rails db:migrate STEP=2</tt> will rollback # the latest two migrations. # # If any of the migrations throw an <tt>ActiveRecord::IrreversibleMigration</tt> exception, @@ -332,7 +366,7 @@ module ActiveRecord # # Not all migrations change the schema. Some just fix the data: # - # class RemoveEmptyTags < ActiveRecord::Migration + # class RemoveEmptyTags < ActiveRecord::Migration[5.0] # def up # Tag.all.each { |tag| tag.destroy if tag.pages.empty? } # end @@ -345,7 +379,7 @@ module ActiveRecord # # Others remove columns when they migrate up instead of down: # - # class RemoveUnnecessaryItemAttributes < ActiveRecord::Migration + # class RemoveUnnecessaryItemAttributes < ActiveRecord::Migration[5.0] # def up # remove_column :items, :incomplete_items_count # remove_column :items, :completed_items_count @@ -359,7 +393,7 @@ module ActiveRecord # # And sometimes you need to do something in SQL not abstracted directly by migrations: # - # class MakeJoinUnique < ActiveRecord::Migration + # class MakeJoinUnique < ActiveRecord::Migration[5.0] # def up # execute "ALTER TABLE `pages_linked_pages` ADD UNIQUE `page_id_linked_page_id` (`page_id`,`linked_page_id`)" # end @@ -376,7 +410,7 @@ module ActiveRecord # <tt>Base#reset_column_information</tt> in order to ensure that the model has the # latest column data from after the new column was added. Example: # - # class AddPeopleSalary < ActiveRecord::Migration + # class AddPeopleSalary < ActiveRecord::Migration[5.0] # def up # add_column :people, :salary, :integer # Person.reset_column_information @@ -434,7 +468,7 @@ module ActiveRecord # To define a reversible migration, define the +change+ method in your # migration like this: # - # class TenderloveMigration < ActiveRecord::Migration + # class TenderloveMigration < ActiveRecord::Migration[5.0] # def change # create_table(:horses) do |t| # t.column :content, :text @@ -464,7 +498,7 @@ module ActiveRecord # can't execute inside a transaction though, and for these situations # you can turn the automatic transactions off. # - # class ChangeEnum < ActiveRecord::Migration + # class ChangeEnum < ActiveRecord::Migration[5.0] # disable_ddl_transaction! # # def up @@ -476,6 +510,32 @@ module ActiveRecord # are in a Migration with <tt>self.disable_ddl_transaction!</tt>. class Migration autoload :CommandRecorder, 'active_record/migration/command_recorder' + autoload :Compatibility, 'active_record/migration/compatibility' + + # This must be defined before the inherited hook, below + class Current < Migration # :nodoc: + end + + def self.inherited(subclass) # :nodoc: + super + if subclass.superclass == Migration + subclass.include Compatibility::Legacy + end + end + + def self.[](version) + version = version.to_s + name = "V#{version.tr('.', '_')}" + unless Compatibility.const_defined?(name) + versions = Compatibility.constants.grep(/\AV[0-9_]+\z/).map { |s| s.to_s.delete('V').tr('_', '.').inspect } + raise "Unknown migration version #{version.inspect}; expected one of #{versions.sort.join(', ')}" + end + Compatibility.const_get(name) + end + + def self.current_version + Rails.version.to_f + end MigrationFilenameRegexp = /\A([0-9]+)_([_a-z0-9]*)\.?([_a-z0-9]*)?\.rb\z/ # :nodoc: @@ -509,6 +569,10 @@ module ActiveRecord attr_accessor :delegate # :nodoc: attr_accessor :disable_ddl_transaction # :nodoc: + def nearest_delegate # :nodoc: + delegate || superclass.nearest_delegate + end + # Raises <tt>ActiveRecord::PendingMigrationError</tt> error if any migrations are pending. def check_pending!(connection = Base.connection) raise ActiveRecord::PendingMigrationError if ActiveRecord::Migrator.needs_migration?(connection) @@ -520,7 +584,7 @@ module ActiveRecord FileUtils.cd Rails.root do current_config = Base.connection_config Base.clear_all_connections! - system("bin/rake db:test:prepare") + system("bin/rails db:test:prepare") # Establish a new connection, the old database may be gone (db:test:prepare uses purge) Base.establish_connection(current_config) end @@ -535,7 +599,7 @@ module ActiveRecord end def method_missing(name, *args, &block) # :nodoc: - (delegate || superclass.delegate).send(name, *args, &block) + nearest_delegate.send(name, *args, &block) end def migrate(direction) @@ -575,7 +639,7 @@ module ActiveRecord # and create the table 'apples' on the way up, and the reverse # on the way down. # - # class FixTLMigration < ActiveRecord::Migration + # class FixTLMigration < ActiveRecord::Migration[5.0] # def change # revert do # create_table(:horses) do |t| @@ -594,7 +658,7 @@ module ActiveRecord # # require_relative '20121212123456_tenderlove_migration' # - # class FixupTLMigration < ActiveRecord::Migration + # class FixupTLMigration < ActiveRecord::Migration[5.0] # def change # revert TenderloveMigration # @@ -647,7 +711,7 @@ module ActiveRecord # when the three columns 'first_name', 'last_name' and 'full_name' exist, # even when migrating down: # - # class SplitNameMigration < ActiveRecord::Migration + # class SplitNameMigration < ActiveRecord::Migration[5.0] # def change # add_column :users, :first_name, :string # add_column :users, :last_name, :string @@ -967,10 +1031,12 @@ module ActiveRecord end def get_all_versions(connection = Base.connection) - if connection.table_exists?(schema_migrations_table_name) - SchemaMigration.all.map { |x| x.version.to_i }.sort - else - [] + ActiveSupport::Deprecation.silence do + if connection.table_exists?(schema_migrations_table_name) + SchemaMigration.all.map { |x| x.version.to_i }.sort + else + [] + end end end @@ -1046,6 +1112,7 @@ module ActiveRecord validate(@migrations) Base.connection.initialize_schema_migrations_table + Base.connection.initialize_internal_metadata_table end def current_version @@ -1103,45 +1170,58 @@ module ActiveRecord private + # Used for running a specific migration. def run_without_lock migration = migrations.detect { |m| m.version == @target_version } raise UnknownMigrationVersionError.new(@target_version) if migration.nil? - unless (up? && migrated.include?(migration.version.to_i)) || (down? && !migrated.include?(migration.version.to_i)) - begin - execute_migration_in_transaction(migration, @direction) - rescue => e - canceled_msg = use_transaction?(migration) ? ", this migration was canceled" : "" - raise StandardError, "An error has occurred#{canceled_msg}:\n\n#{e}", e.backtrace - end - end + execute_migration_in_transaction(migration, @direction) + + record_environment end + # Used for running multiple migrations up to or down to a certain value. def migrate_without_lock - if !target && @target_version && @target_version > 0 + if invalid_target? raise UnknownMigrationVersionError.new(@target_version) end runnable.each do |migration| - Base.logger.info "Migrating to #{migration.name} (#{migration.version})" if Base.logger - - begin - execute_migration_in_transaction(migration, @direction) - rescue => e - canceled_msg = use_transaction?(migration) ? "this and " : "" - raise StandardError, "An error has occurred, #{canceled_msg}all later migrations canceled:\n\n#{e}", e.backtrace - end + execute_migration_in_transaction(migration, @direction) end + + record_environment + end + + # Stores the current environment in the database. + def record_environment + return if down? + ActiveRecord::InternalMetadata[:environment] = ActiveRecord::Migrator.current_environment end def ran?(migration) migrated.include?(migration.version.to_i) end + # Return true if a valid version is not provided. + def invalid_target? + !target && @target_version && @target_version > 0 + end + def execute_migration_in_transaction(migration, direction) + return if down? && !migrated.include?(migration.version.to_i) + return if up? && migrated.include?(migration.version.to_i) + + Base.logger.info "Migrating to #{migration.name} (#{migration.version})" if Base.logger + ddl_transaction(migration) do migration.migrate(direction) record_version_state_after_migrating(migration.version) end + rescue => e + msg = "An error has occurred, " + msg << "this and " if use_transaction?(migration) + msg << "all later migrations canceled:\n\n#{e}" + raise StandardError, msg, e.backtrace end def target @@ -1170,10 +1250,27 @@ module ActiveRecord ActiveRecord::SchemaMigration.where(:version => version.to_s).delete_all else migrated << version - ActiveRecord::SchemaMigration.create!(:version => version.to_s) + ActiveRecord::SchemaMigration.create!(version: version.to_s) end end + def self.last_stored_environment + return nil if current_version == 0 + raise NoEnvironmentInSchemaError unless ActiveRecord::InternalMetadata.table_exists? + + environment = ActiveRecord::InternalMetadata[:environment] + raise NoEnvironmentInSchemaError unless environment + environment + end + + def self.current_environment + ActiveRecord::ConnectionHandling::DEFAULT_ENV.call + end + + def self.protected_environment? + ActiveRecord::Base.protected_environments.include?(last_stored_environment) if last_stored_environment + end + def up? @direction == :up end @@ -1200,17 +1297,17 @@ module ActiveRecord end def with_advisory_lock - key = generate_migrator_advisory_lock_key - got_lock = Base.connection.get_advisory_lock(key) + lock_id = generate_migrator_advisory_lock_id + got_lock = Base.connection.get_advisory_lock(lock_id) raise ConcurrentMigrationError unless got_lock load_migrated # reload schema_migrations to be sure it wasn't changed by another process before we got the lock yield ensure - Base.connection.release_advisory_lock(key) if got_lock + Base.connection.release_advisory_lock(lock_id) if got_lock end MIGRATOR_SALT = 2053462845 - def generate_migrator_advisory_lock_key + def generate_migrator_advisory_lock_id db_name_hash = Zlib.crc32(Base.connection.current_database) MIGRATOR_SALT * db_name_hash end diff --git a/activerecord/lib/active_record/migration/compatibility.rb b/activerecord/lib/active_record/migration/compatibility.rb new file mode 100644 index 0000000000..09d55adcd7 --- /dev/null +++ b/activerecord/lib/active_record/migration/compatibility.rb @@ -0,0 +1,116 @@ +module ActiveRecord + class Migration + module Compatibility # :nodoc: all + V5_0 = Current + + module FourTwoShared + module TableDefinition + def references(*, **options) + options[:index] ||= false + super + end + alias :belongs_to :references + + def timestamps(*, **options) + options[:null] = true if options[:null].nil? + super + end + end + + def create_table(table_name, options = {}) + if block_given? + super(table_name, options) do |t| + class << t + prepend TableDefinition + end + yield t + end + else + super + end + end + + def change_table(table_name, options = {}) + if block_given? + super(table_name, options) do |t| + class << t + prepend TableDefinition + end + yield t + end + else + super + end + end + + def add_reference(*, **options) + options[:index] ||= false + super + end + alias :add_belongs_to :add_reference + + def add_timestamps(*, **options) + options[:null] = true if options[:null].nil? + super + end + + def index_exists?(table_name, column_name, options = {}) + column_names = Array(column_name).map(&:to_s) + options[:name] = + if options.key?(:name).present? + options[:name].to_s + else + index_name(table_name, column: column_names) + end + super + end + + def remove_index(table_name, options = {}) + options = { column: options } unless options.is_a?(Hash) + options[:name] = index_name_for_remove(table_name, options) + super(table_name, options) + end + + private + + def index_name_for_remove(table_name, options = {}) + index_name = index_name(table_name, options) + + unless index_name_exists?(table_name, index_name, true) + if options.is_a?(Hash) && options.has_key?(:name) + options_without_column = options.dup + options_without_column.delete :column + index_name_without_column = index_name(table_name, options_without_column) + + return index_name_without_column if index_name_exists?(table_name, index_name_without_column, false) + end + + raise ArgumentError, "Index name '#{index_name}' on table '#{table_name}' does not exist" + end + + index_name + end + end + + class V4_2 < V5_0 + # 4.2 is defined as a module because it needs to be shared with + # Legacy. When the time comes, V5_0 should be defined straight + # in its class. + include FourTwoShared + end + + module Legacy + include FourTwoShared + + def migrate(*) + ActiveSupport::Deprecation.warn \ + "Directly inheriting from ActiveRecord::Migration is deprecated. " \ + "Please specify the Rails release the migration was written for:\n" \ + "\n" \ + " class #{self.class.name} < ActiveRecord::Migration[4.2]" + super + end + end + end + end +end diff --git a/activerecord/lib/active_record/model_schema.rb b/activerecord/lib/active_record/model_schema.rb index a9bd094a66..ee52c3ae02 100644 --- a/activerecord/lib/active_record/model_schema.rb +++ b/activerecord/lib/active_record/model_schema.rb @@ -44,6 +44,19 @@ module ActiveRecord ## # :singleton-method: + # Accessor for the name of the internal metadata table. By default, the value is "ar_internal_metadata" + class_attribute :internal_metadata_table_name, instance_accessor: false + self.internal_metadata_table_name = "ar_internal_metadata" + + ## + # :singleton-method: + # Accessor for an array of names of environments where destructive actions should be prohibited. By default, + # the value is ["production"] + class_attribute :protected_environments, instance_accessor: false + self.protected_environments = ["production"] + + ## + # :singleton-method: # Indicates whether table names should be the pluralized versions of the corresponding class names. # If true, the default table name for a Product class will be +products+. If false, it would just be +product+. # See table_name for the full rules on table/class naming. This is true, by default. @@ -242,7 +255,18 @@ module ActiveRecord @attribute_types ||= Hash.new(Type::Value.new) end - def type_for_attribute(attr_name) # :nodoc: + # Returns the type of the attribute with the given name, after applying + # all modifiers. This method is the only valid source of information for + # anything related to the types of a model's attributes. This method will + # access the database and load the model's schema if it is required. + # + # The return value of this method will implement the interface described + # by ActiveModel::Type::Value (though the object itself may not subclass + # it). + # + # +attr_name+ The name of the attribute to retrieve the type for. Must be + # a string + def type_for_attribute(attr_name) attribute_types[attr_name] end @@ -275,7 +299,7 @@ module ActiveRecord # when just after creating a table you want to populate it with some default # values, eg: # - # class CreateJobLevels < ActiveRecord::Migration + # class CreateJobLevels < ActiveRecord::Migration[5.0] # def up # create_table :job_levels do |t| # t.integer :id @@ -339,6 +363,9 @@ module ActiveRecord @columns = nil @columns_hash = nil @attribute_names = nil + direct_descendants.each do |descendant| + descendant.send(:reload_schema_from_cache) + end end # Guesses the table name, but does not decorate it with prefix and suffix information. @@ -382,7 +409,7 @@ module ActiveRecord If you'd like the new behavior today, you can add this line: - attribute :#{column.name}, :rails_5_1_point#{array_arguments} + attribute :#{column.name}, :point#{array_arguments} WARNING end end diff --git a/activerecord/lib/active_record/nested_attributes.rb b/activerecord/lib/active_record/nested_attributes.rb index c5a1488588..ae78ceee01 100644 --- a/activerecord/lib/active_record/nested_attributes.rb +++ b/activerecord/lib/active_record/nested_attributes.rb @@ -195,15 +195,23 @@ module ActiveRecord # Nested attributes for an associated collection can also be passed in # the form of a hash of hashes instead of an array of hashes: # - # Member.create(name: 'joe', - # posts_attributes: { first: { title: 'Foo' }, - # second: { title: 'Bar' } }) + # Member.create( + # name: 'joe', + # posts_attributes: { + # first: { title: 'Foo' }, + # second: { title: 'Bar' } + # } + # ) # # has the same effect as # - # Member.create(name: 'joe', - # posts_attributes: [ { title: 'Foo' }, - # { title: 'Bar' } ]) + # Member.create( + # name: 'joe', + # posts_attributes: [ + # { title: 'Foo' }, + # { title: 'Bar' } + # ] + # ) # # The keys of the hash which is the value for +:posts_attributes+ are # ignored in this case. @@ -542,7 +550,7 @@ module ActiveRecord # has_destroy_flag? or if a <tt>:reject_if</tt> proc exists for this # association and evaluates to +true+. def reject_new_record?(association_name, attributes) - has_destroy_flag?(attributes) || call_reject_if(association_name, attributes) + will_be_destroyed?(association_name, attributes) || call_reject_if(association_name, attributes) end # Determines if a record with the particular +attributes+ should be @@ -551,7 +559,8 @@ module ActiveRecord # # Returns false if there is a +destroy_flag+ on the attributes. def call_reject_if(association_name, attributes) - return false if has_destroy_flag?(attributes) + return false if will_be_destroyed?(association_name, attributes) + case callback = self.nested_attributes_options[association_name][:reject_if] when Symbol method(callback).arity == 0 ? send(callback) : send(callback, attributes) @@ -560,6 +569,15 @@ module ActiveRecord end end + # Only take into account the destroy flag if <tt>:allow_destroy</tt> is true + def will_be_destroyed?(association_name, attributes) + allow_destroy?(association_name) && has_destroy_flag?(attributes) + end + + def allow_destroy?(association_name) + self.nested_attributes_options[association_name][:allow_destroy] + end + def raise_nested_attributes_record_not_found!(association_name, record_id) model = self.class._reflect_on_association(association_name).klass.name raise RecordNotFound.new("Couldn't find #{model} with ID=#{record_id} for #{self.class.name} with ID=#{id}", diff --git a/activerecord/lib/active_record/null_relation.rb b/activerecord/lib/active_record/null_relation.rb index 0b500346bc..1ab4e0404f 100644 --- a/activerecord/lib/active_record/null_relation.rb +++ b/activerecord/lib/active_record/null_relation.rb @@ -1,7 +1,7 @@ module ActiveRecord module NullRelation # :nodoc: def exec_queries - @records = [] + @records = [].freeze end def pluck(*column_names) diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb index 94316d5249..d9a394fb71 100644 --- a/activerecord/lib/active_record/persistence.rb +++ b/activerecord/lib/active_record/persistence.rb @@ -102,11 +102,11 @@ module ActiveRecord # Saves the model. # - # If the model is new a record gets created in the database, otherwise + # If the model is new, a record gets created in the database, otherwise # the existing record gets updated. # - # By default, save always run validations. If any of them fail the action - # is cancelled and #save returns +false+. However, if you supply + # By default, save always runs validations. If any of them fail the action + # is cancelled and #save returns +false+, and the record won't be saved. However, if you supply # validate: false, validations are bypassed altogether. See # ActiveRecord::Validations for more information. # @@ -132,9 +132,10 @@ module ActiveRecord # If the model is new, a record gets created in the database, otherwise # the existing record gets updated. # - # With #save! validations always run. If any of them fail - # ActiveRecord::RecordInvalid gets raised. See ActiveRecord::Validations - # for more information. + # By default, #save! always runs validations. If any of them fail + # ActiveRecord::RecordInvalid gets raised, and the record won't be saved. However, if you supply + # validate: false, validations are bypassed altogether. See + # ActiveRecord::Validations for more information. # # By default, #save! also sets the +updated_at+/+updated_on+ attributes to # the current time. However, if you supply <tt>touch: false</tt>, these @@ -215,7 +216,7 @@ module ActiveRecord became.instance_variable_set("@changed_attributes", attributes_changed_by_setter) became.instance_variable_set("@new_record", new_record?) became.instance_variable_set("@destroyed", destroyed?) - became.instance_variable_set("@errors", errors) + became.errors.copy!(errors) became end @@ -246,7 +247,7 @@ module ActiveRecord # This method raises an ActiveRecord::ActiveRecordError if the # attribute is marked as readonly. # - # See also #update_column. + # Also see #update_column. def update_attribute(name, value) name = name.to_s verify_readonly_attribute(name) @@ -269,7 +270,7 @@ module ActiveRecord alias update_attributes update # Updates its receiver just like #update but calls #save! instead - # of +save+, so an exception is raised if the record is invalid. + # of +save+, so an exception is raised if the record is invalid and saving will fail. def update!(attributes) # The following transaction covers any possible database side-effects of the # attributes assignment. For example, setting the IDs of a child collection. @@ -298,6 +299,7 @@ module ActiveRecord # * \Validations are skipped. # * \Callbacks are skipped. # * +updated_at+/+updated_on+ are not updated. + # * However, attributes are serialized with the same rules as ActiveRecord::Relation#update_all # # This method raises an ActiveRecord::ActiveRecordError when called on new # objects, or when at least one of the attributes is marked as readonly. @@ -347,7 +349,7 @@ module ActiveRecord end # Wrapper around #decrement that saves the record. This method differs from - # its non-bang version in that it passes through the attribute setter. + # its non-bang version in the sense that it passes through the attribute setter. # Saving is not subjected to validation checks. Returns +true+ if the # record could be saved. def decrement!(attribute, by = 1) @@ -358,13 +360,21 @@ module ActiveRecord # if the predicate returns +true+ the attribute will become +false+. This # method toggles directly the underlying value without calling any setter. # Returns +self+. + # + # Example: + # + # user = User.first + # user.banned? # => false + # user.toggle(:banned) + # user.banned? # => true + # def toggle(attribute) self[attribute] = !public_send("#{attribute}?") self end # Wrapper around #toggle that saves the record. This method differs from - # its non-bang version in that it passes through the attribute setter. + # its non-bang version in the sense that it passes through the attribute setter. # Saving is not subjected to validation checks. Returns +true+ if the # record could be saved. def toggle!(attribute) @@ -558,5 +568,9 @@ module ActiveRecord ensure @_association_destroy_exception = nil end + + def belongs_to_touch_method + :touch + end end end diff --git a/activerecord/lib/active_record/querying.rb b/activerecord/lib/active_record/querying.rb index 1f429cfd94..de5b42e987 100644 --- a/activerecord/lib/active_record/querying.rb +++ b/activerecord/lib/active_record/querying.rb @@ -1,7 +1,7 @@ module ActiveRecord module Querying delegate :find, :take, :take!, :first, :first!, :last, :last!, :exists?, :any?, :many?, to: :all - delegate :second, :second!, :third, :third!, :fourth, :fourth!, :fifth, :fifth!, :forty_two, :forty_two!, to: :all + delegate :second, :second!, :third, :third!, :fourth, :fourth!, :fifth, :fifth!, :forty_two, :forty_two!, :third_to_last, :third_to_last!, :second_to_last, :second_to_last!, to: :all delegate :first_or_create, :first_or_create!, :first_or_initialize, to: :all delegate :find_or_create_by, :find_or_create_by!, :find_or_initialize_by, to: :all delegate :find_by, :find_by!, to: :all @@ -35,8 +35,8 @@ module ActiveRecord # # Post.find_by_sql ["SELECT title FROM posts WHERE author = ? AND created > ?", author_id, start_date] # Post.find_by_sql ["SELECT body FROM comments WHERE author = :user_id OR approved_by = :user_id", { :user_id => user_id }] - def find_by_sql(sql, binds = []) - result_set = connection.select_all(sanitize_sql(sql), "#{name} Load", binds) + def find_by_sql(sql, binds = [], preparable: nil) + result_set = connection.select_all(sanitize_sql(sql), "#{name} Load", binds, preparable: preparable) column_types = result_set.column_types.dup columns_hash.each_key { |k| column_types.delete k } message_bus = ActiveSupport::Notifications.instrumenter diff --git a/activerecord/lib/active_record/railtie.rb b/activerecord/lib/active_record/railtie.rb index 2744673c12..f4200e96b7 100644 --- a/activerecord/lib/active_record/railtie.rb +++ b/activerecord/lib/active_record/railtie.rb @@ -40,7 +40,7 @@ module ActiveRecord task :load_config do ActiveRecord::Tasks::DatabaseTasks.database_configuration = Rails.application.config.database_configuration - if defined?(ENGINE_PATH) && engine = Rails::Engine.find(ENGINE_PATH) + if defined?(ENGINE_ROOT) && engine = Rails::Engine.find(ENGINE_ROOT) if engine.paths['db/migrate'].existent ActiveRecord::Tasks::DatabaseTasks.migrations_paths += engine.paths['db/migrate'].to_a end @@ -57,8 +57,10 @@ module ActiveRecord console do |app| require "active_record/railties/console_sandbox" if app.sandbox? require "active_record/base" - console = ActiveSupport::Logger.new(STDERR) - Rails.logger.extend ActiveSupport::Logger.broadcast console + unless ActiveSupport::Logger.logger_outputs_to?(Rails.logger, STDERR, STDOUT) + console = ActiveSupport::Logger.new(STDERR) + Rails.logger.extend ActiveSupport::Logger.broadcast console + end end runner do @@ -69,6 +71,7 @@ module ActiveRecord ActiveSupport.on_load(:active_record) do self.time_zone_aware_attributes = true self.default_timezone = :utc + self.time_zone_aware_types = ActiveRecord::Base.time_zone_aware_types end end @@ -134,8 +137,8 @@ Oops - You have a database configured, but it doesn't exist yet! Here's how to get started: 1. Configure your database in config/database.yml. - 2. Run `bin/rake db:create` to create the database. - 3. Run `bin/rake db:setup` to load your database schema. + 2. Run `bin/rails db:create` to create the database. + 3. Run `bin/rails db:setup` to load your database schema. end_warning raise end diff --git a/activerecord/lib/active_record/railties/databases.rake b/activerecord/lib/active_record/railties/databases.rake index 5b1ea16f0b..69a7838001 100644 --- a/activerecord/lib/active_record/railties/databases.rake +++ b/activerecord/lib/active_record/railties/databases.rake @@ -1,6 +1,16 @@ require 'active_record' db_namespace = namespace :db do + desc "Set the environment value for the database" + task "environment:set" => [:environment, :load_config] do + ActiveRecord::InternalMetadata.create_table + ActiveRecord::InternalMetadata[:environment] = ActiveRecord::Migrator.current_environment + end + + task :check_protected_environments => [:environment, :load_config] do + ActiveRecord::Tasks::DatabaseTasks.check_protected_environments! + end + task :load_config do ActiveRecord::Base.configurations = ActiveRecord::Tasks::DatabaseTasks.database_configuration || {} ActiveRecord::Migrator.migrations_paths = ActiveRecord::Tasks::DatabaseTasks.migrations_paths @@ -18,24 +28,28 @@ db_namespace = namespace :db do end namespace :drop do - task :all => :load_config do + task :all => [:load_config, :check_protected_environments] do ActiveRecord::Tasks::DatabaseTasks.drop_all end end desc 'Drops the database from DATABASE_URL or config/database.yml for the current RAILS_ENV (use db:drop:all to drop all databases in the config). Without RAILS_ENV, it defaults to dropping the development and test databases.' - task :drop => [:load_config] do + task :drop => [:load_config, :check_protected_environments] do + db_namespace["drop:_unsafe"].invoke + end + + task "drop:_unsafe" => [:load_config] do ActiveRecord::Tasks::DatabaseTasks.drop_current end namespace :purge do - task :all => :load_config do + task :all => [:load_config, :check_protected_environments] do ActiveRecord::Tasks::DatabaseTasks.purge_all end end # desc "Empty the database from DATABASE_URL or config/database.yml for the current RAILS_ENV (use db:purge:all to purge all databases in the config). Without RAILS_ENV it defaults to purging the development and test databases." - task :purge => [:load_config] do + task :purge => [:load_config, :check_protected_environments] do ActiveRecord::Tasks::DatabaseTasks.purge_current end @@ -169,7 +183,7 @@ db_namespace = namespace :db do pending_migrations.each do |pending_migration| puts ' %4d %s' % [pending_migration.version, pending_migration.name] end - abort %{Run `rake db:migrate` to update your database then try again.} + abort %{Run `rails db:migrate` to update your database then try again.} end end @@ -288,7 +302,7 @@ db_namespace = namespace :db do end desc "Recreates the databases from the structure.sql file" - task :load => [:load_config] do + task :load => [:environment, :load_config] do ActiveRecord::Tasks::DatabaseTasks.load_schema_current(:sql, ENV['SCHEMA']) end @@ -351,7 +365,7 @@ db_namespace = namespace :db do task :clone_structure => %w(db:test:deprecated db:structure:dump db:test:load_structure) # desc "Empty the test database" - task :purge => %w(environment load_config) do + task :purge => %w(environment load_config check_protected_environments) do ActiveRecord::Tasks::DatabaseTasks.purge ActiveRecord::Base.configurations['test'] end diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb index 5b9d45d871..43f573f193 100644 --- a/activerecord/lib/active_record/reflection.rb +++ b/activerecord/lib/active_record/reflection.rb @@ -7,8 +7,8 @@ module ActiveRecord extend ActiveSupport::Concern included do - class_attribute :_reflections - class_attribute :aggregate_reflections + class_attribute :_reflections, instance_writer: false + class_attribute :aggregate_reflections, instance_writer: false self._reflections = {} self.aggregate_reflections = {} end @@ -124,8 +124,19 @@ module ActiveRecord end end - # Holds all the methods that are shared between MacroReflection, AssociationReflection - # and ThroughReflection + # Holds all the methods that are shared between MacroReflection and ThroughReflection. + # + # AbstractReflection + # MacroReflection + # AggregateReflection + # AssociationReflection + # HasManyReflection + # HasOneReflection + # BelongsToReflection + # HasAndBelongsToManyReflection + # ThroughReflection + # PolymorphicReflection + # RuntimeReflection class AbstractReflection # :nodoc: def table_name klass.table_name @@ -228,18 +239,14 @@ module ActiveRecord def alias_candidate(name) "#{plural_name}_#{name}" end + + def chain + collect_join_chain + end end # Base class for AggregateReflection and AssociationReflection. Objects of # AggregateReflection and AssociationReflection are returned by the Reflection::ClassMethods. - # - # MacroReflection - # AggregateReflection - # AssociationReflection - # HasManyReflection - # HasOneReflection - # BelongsToReflection - # ThroughReflection class MacroReflection < AbstractReflection # Returns the name of the macro. # @@ -371,7 +378,7 @@ module ActiveRecord end def foreign_key - @foreign_key ||= options[:foreign_key] || derive_foreign_key + @foreign_key ||= options[:foreign_key] || derive_foreign_key.freeze end def association_foreign_key @@ -418,7 +425,7 @@ module ActiveRecord # A chain of reflections from this one back to the owner. For more see the explanation in # ThroughReflection. - def chain + def collect_join_chain [self] end @@ -483,28 +490,7 @@ module ActiveRecord # Returns +true+ if +self+ is a +has_one+ reflection. def has_one?; false; end - def association_class - case macro - when :belongs_to - if polymorphic? - Associations::BelongsToPolymorphicAssociation - else - Associations::BelongsToAssociation - end - when :has_many - if options[:through] - Associations::HasManyThroughAssociation - else - Associations::HasManyAssociation - end - when :has_one - if options[:through] - Associations::HasOneThroughAssociation - else - Associations::HasOneAssociation - end - end - end + def association_class; raise NotImplementedError; end def polymorphic? options[:polymorphic] @@ -513,6 +499,18 @@ module ActiveRecord VALID_AUTOMATIC_INVERSE_MACROS = [:has_many, :has_one, :belongs_to] INVALID_AUTOMATIC_INVERSE_OPTIONS = [:conditions, :through, :polymorphic, :foreign_key] + def add_as_source(seed) + seed + end + + def add_as_polymorphic_through(reflection, seed) + seed + [PolymorphicReflection.new(self, reflection)] + end + + def add_as_through(seed) + seed + [self] + end + protected def actual_source_reflection # FIXME: this is a horrible name @@ -522,14 +520,7 @@ module ActiveRecord private def calculate_constructable(macro, options) - case macro - when :belongs_to - !polymorphic? - when :has_one - !options[:through] - else - true - end + true end # Attempts to find the inverse association name automatically. @@ -545,7 +536,7 @@ module ActiveRecord end end - # returns either nil or the inverse association name that it finds. + # returns either false or the inverse association name that it finds. def automatic_inverse_of if can_find_inverse_of_automatically?(self) inverse_name = ActiveSupport::Inflector.underscore(options[:as] || active_record.name.demodulize).to_sym @@ -622,34 +613,52 @@ module ActiveRecord end class HasManyReflection < AssociationReflection # :nodoc: - def initialize(name, scope, options, active_record) - super(name, scope, options, active_record) - end - def macro; :has_many; end def collection?; true; end - end - class HasOneReflection < AssociationReflection # :nodoc: - def initialize(name, scope, options, active_record) - super(name, scope, options, active_record) + def association_class + if options[:through] + Associations::HasManyThroughAssociation + else + Associations::HasManyAssociation + end end + end + class HasOneReflection < AssociationReflection # :nodoc: def macro; :has_one; end def has_one?; true; end - end - class BelongsToReflection < AssociationReflection # :nodoc: - def initialize(name, scope, options, active_record) - super(name, scope, options, active_record) + def association_class + if options[:through] + Associations::HasOneThroughAssociation + else + Associations::HasOneAssociation + end end + private + + def calculate_constructable(macro, options) + !options[:through] + end + end + + class BelongsToReflection < AssociationReflection # :nodoc: def macro; :belongs_to; end def belongs_to?; true; end + def association_class + if polymorphic? + Associations::BelongsToPolymorphicAssociation + else + Associations::BelongsToAssociation + end + end + def join_keys(association_klass) key = polymorphic? ? association_primary_key(association_klass) : association_primary_key JoinKeys.new(key, foreign_key) @@ -658,6 +667,12 @@ module ActiveRecord def join_id_for(owner) # :nodoc: owner[foreign_key] end + + private + + def calculate_constructable(macro, options) + !polymorphic? + end end class HasAndBelongsToManyReflection < AssociationReflection # :nodoc: @@ -743,19 +758,8 @@ module ActiveRecord # # => [<ActiveRecord::Reflection::ThroughReflection: @delegate_reflection=#<ActiveRecord::Reflection::HasManyReflection: @name=:tags...>, # <ActiveRecord::Reflection::HasManyReflection: @name=:taggings, @options={}, @active_record=Post>] # - def chain - @chain ||= begin - a = source_reflection.chain - b = through_reflection.chain.map(&:dup) - - if options[:source_type] - b[0] = PolymorphicReflection.new(b[0], self) - end - - chain = a + b - chain[0] = self # Use self so we don't lose the information from :source_type - chain - end + def collect_join_chain + collect_join_reflections [self] end # This is for clearing cache on the reflection. Useful for tests that need to compare @@ -914,6 +918,27 @@ module ActiveRecord scope_chain end + def add_as_source(seed) + collect_join_reflections seed + end + + def add_as_polymorphic_through(reflection, seed) + collect_join_reflections(seed + [PolymorphicReflection.new(self, reflection)]) + end + + def add_as_through(seed) + collect_join_reflections(seed + [self]) + end + + def collect_join_reflections(seed) + a = source_reflection.add_as_source seed + if options[:source_type] + through_reflection.add_as_polymorphic_through self, a + else + through_reflection.add_as_through a + end + end + protected def actual_source_reflection # FIXME: this is a horrible name @@ -970,7 +995,7 @@ module ActiveRecord end def constraints - [source_type_info] + @reflection.constraints + [source_type_info] end def source_type_info diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb index f100476374..09afdc6c69 100644 --- a/activerecord/lib/active_record/relation.rb +++ b/activerecord/lib/active_record/relation.rb @@ -47,7 +47,7 @@ module ActiveRecord if !primary_key_value && connection.prefetch_primary_key?(klass.table_name) primary_key_value = connection.next_sequence_value(klass.sequence_name) - values[klass.arel_table[klass.primary_key]] = primary_key_value + values[arel_attribute(klass.primary_key)] = primary_key_value end end @@ -99,12 +99,16 @@ module ActiveRecord end substitutes = values.map do |(arel_attr, _)| - [arel_attr, connection.substitute_at(klass.columns_hash[arel_attr.name])] + [arel_attr, Arel::Nodes::BindParam.new] end [substitutes, binds] end + def arel_attribute(name) # :nodoc: + klass.arel_attribute(name, table) + end + # Initializes new record from relation while maintaining the current # scope. # @@ -132,7 +136,7 @@ module ActiveRecord # ==== Examples # # users = User.where(name: 'Oscar') - # users.create # => #<User id: 3, name: "oscar", ...> + # users.create # => #<User id: 3, name: "Oscar", ...> # # users.create(name: 'fxn') # users.create # => #<User id: 4, name: "fxn", ...> @@ -249,17 +253,21 @@ module ActiveRecord # Converts relation objects to Array. def to_a + records.dup + end + + def records # :nodoc: load @records end # Serializes the relation objects Array. def encode_with(coder) - coder.represent_seq(nil, to_a) + coder.represent_seq(nil, records) end def as_json(options = nil) #:nodoc: - to_a.as_json(options) + records.as_json(options) end # Returns size of the records. @@ -294,13 +302,13 @@ module ActiveRecord # Returns true if there is exactly one record. def one? return super if block_given? - limit_value ? to_a.one? : size == 1 + limit_value ? records.one? : size == 1 end # Returns true if there is more than one record. def many? return super if block_given? - limit_value ? to_a.many? : size > 1 + limit_value ? records.many? : size > 1 end # Returns a cache key that can be used to identify the records fetched by @@ -347,9 +355,8 @@ module ActiveRecord # Updates all records in the current relation with details given. This method constructs a single SQL UPDATE # statement and sends it straight to the database. It does not instantiate the involved models and it does not - # trigger Active Record callbacks or validations. Values passed to #update_all will not go through - # Active Record's type-casting behavior. It should receive only values that can be passed as-is to the SQL - # database. + # trigger Active Record callbacks or validations. However, values passed to #update_all will still go through + # Active Record's normal type casting and serialization. # # ==== Parameters # @@ -372,11 +379,11 @@ module ActiveRecord stmt.set Arel.sql(@klass.send(:sanitize_sql_for_assignment, updates)) stmt.table(table) - stmt.key = table[primary_key] if joins_values.any? - @klass.connection.join_to_update(stmt, arel) + @klass.connection.join_to_update(stmt, arel, arel_attribute(primary_key)) else + stmt.key = arel_attribute(primary_key) stmt.take(arel.limit) stmt.order(*arel.orders) stmt.wheres = arel.constraints @@ -415,7 +422,7 @@ module ActiveRecord if id.is_a?(Array) id.map.with_index { |one_id, idx| update(one_id, attributes[idx]) } elsif id == :all - to_a.each { |record| record.update(attributes) } + records.each { |record| record.update(attributes) } else if ActiveRecord::Base === id id = id.id @@ -454,7 +461,7 @@ module ActiveRecord MESSAGE where(conditions).destroy_all else - to_a.each(&:destroy).tap { reset } + records.each(&:destroy).tap { reset } end end @@ -528,7 +535,7 @@ module ActiveRecord stmt.from(table) if joins_values.any? - @klass.connection.join_to_delete(stmt, arel, table[primary_key]) + @klass.connection.join_to_delete(stmt, arel, arel_attribute(primary_key)) else stmt.wheres = arel.constraints end @@ -584,7 +591,7 @@ module ActiveRecord def reset @last = @to_sql = @order_clause = @scope_for_create = @arel = @loaded = nil @should_eager_load = @join_dependency = nil - @records = [] + @records = [].freeze @offsets = {} self end @@ -651,21 +658,21 @@ module ActiveRecord def ==(other) case other when Associations::CollectionProxy, AssociationRelation - self == other.to_a + self == other.records when Relation other.to_sql == to_sql when Array - to_a == other + records == other end end def pretty_print(q) - q.pp(self.to_a) + q.pp(self.records) end # Returns true if relation is blank. def blank? - to_a.blank? + records.blank? end def values @@ -673,7 +680,7 @@ module ActiveRecord end def inspect - entries = to_a.take([limit_value, 11].compact.min).map!(&:inspect) + entries = records.take([limit_value, 11].compact.min).map!(&:inspect) entries[10] = '...' if entries.size == 11 "#<#{self.class.name} [#{entries.join(', ')}]>" @@ -682,14 +689,14 @@ module ActiveRecord protected def load_records(records) - @records = records + @records = records.freeze @loaded = true end private def exec_queries - @records = eager_loading? ? find_with_associations : @klass.find_by_sql(arel, bound_attributes) + @records = eager_loading? ? find_with_associations.freeze : @klass.find_by_sql(arel, bound_attributes).freeze preload = preload_values preload += includes_values unless eager_loading? diff --git a/activerecord/lib/active_record/relation/batches.rb b/activerecord/lib/active_record/relation/batches.rb index 221bc73680..243ef0eae9 100644 --- a/activerecord/lib/active_record/relation/batches.rb +++ b/activerecord/lib/active_record/relation/batches.rb @@ -29,15 +29,15 @@ module ActiveRecord # # ==== Options # * <tt>:batch_size</tt> - Specifies the size of the batch. Default to 1000. - # * <tt>:begin_at</tt> - Specifies the primary key value to start from, inclusive of the value. - # * <tt>:end_at</tt> - Specifies the primary key value to end at, inclusive of the value. + # * <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. # This is especially useful if you want multiple workers dealing with # the same processing queue. You can make worker 1 handle all the records # between id 0 and 10,000 and worker 2 handle from 10,000 and beyond - # (by setting the +:begin_at+ and +:end_at+ option on each worker). + # (by setting the +:start+ and +:finish+ option on each worker). # # # Let's process for a batch of 2000 records, skipping the first 2000 rows - # Person.find_each(begin_at: 2000, batch_size: 2000) do |person| + # Person.find_each(start: 2000, batch_size: 2000) do |person| # person.party_all_night! # end # @@ -48,22 +48,15 @@ module ActiveRecord # # NOTE: You can't set the limit either, that's used to control # the batch sizes. - def find_each(begin_at: nil, end_at: nil, batch_size: 1000, start: nil) - if start - begin_at = start - ActiveSupport::Deprecation.warn(<<-MSG.squish) - Passing `start` value to find_each is deprecated, and will be removed in Rails 5.1. - Please pass `begin_at` instead. - MSG - end + def find_each(start: nil, finish: nil, batch_size: 1000) if block_given? - find_in_batches(begin_at: begin_at, end_at: end_at, batch_size: batch_size) do |records| + find_in_batches(start: start, finish: finish, batch_size: batch_size) do |records| records.each { |record| yield record } end else - enum_for(:find_each, begin_at: begin_at, end_at: end_at, batch_size: batch_size) do + enum_for(:find_each, start: start, finish: finish, batch_size: batch_size) do relation = self - apply_limits(relation, begin_at, end_at).size + apply_limits(relation, start, finish).size end end end @@ -88,15 +81,15 @@ module ActiveRecord # # ==== Options # * <tt>:batch_size</tt> - Specifies the size of the batch. Default to 1000. - # * <tt>:begin_at</tt> - Specifies the primary key value to start from, inclusive of the value. - # * <tt>:end_at</tt> - Specifies the primary key value to end at, inclusive of the value. + # * <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. # This is especially useful if you want multiple workers dealing with # the same processing queue. You can make worker 1 handle all the records # between id 0 and 10,000 and worker 2 handle from 10,000 and beyond - # (by setting the +:begin_at+ and +:end_at+ option on each worker). + # (by setting the +:start+ and +:finish+ option on each worker). # # # Let's process the next 2000 records - # Person.find_in_batches(begin_at: 2000, batch_size: 2000) do |group| + # Person.find_in_batches(start: 2000, batch_size: 2000) do |group| # group.each { |person| person.party_all_night! } # end # @@ -107,24 +100,16 @@ module ActiveRecord # # NOTE: You can't set the limit either, that's used to control # the batch sizes. - def find_in_batches(begin_at: nil, end_at: nil, batch_size: 1000, start: nil) - if start - begin_at = start - ActiveSupport::Deprecation.warn(<<-MSG.squish) - Passing `start` value to find_in_batches is deprecated, and will be removed in Rails 5.1. - Please pass `begin_at` instead. - MSG - end - + def find_in_batches(start: nil, finish: nil, batch_size: 1000) relation = self unless block_given? - return to_enum(:find_in_batches, begin_at: begin_at, end_at: end_at, batch_size: batch_size) do - total = apply_limits(relation, begin_at, end_at).size + return to_enum(:find_in_batches, start: start, finish: finish, batch_size: batch_size) do + total = apply_limits(relation, start, finish).size (total - 1).div(batch_size) + 1 end end - in_batches(of: batch_size, begin_at: begin_at, end_at: end_at, load: true) do |batch| + in_batches(of: batch_size, start: start, finish: finish, load: true) do |batch| yield batch.to_a end end @@ -153,18 +138,18 @@ module ActiveRecord # ==== 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>:begin_at</tt> - Specifies the primary key value to start from, inclusive of the value. - # * <tt>:end_at</tt> - Specifies the primary key value to end at, inclusive of the value. + # * <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. # # This is especially useful if you want to work with the # ActiveRecord::Relation object instead of the array of records, or if # you want multiple workers dealing with the same processing queue. You can # make worker 1 handle all the records between id 0 and 10,000 and worker 2 - # handle from 10,000 and beyond (by setting the +:begin_at+ and +:end_at+ + # handle from 10,000 and beyond (by setting the +:start+ and +:finish+ # option on each worker). # # # Let's process the next 2000 records - # Person.in_batches(of: 2000, begin_at: 2000).update_all(awesome: true) + # Person.in_batches(of: 2000, start: 2000).update_all(awesome: true) # # An example of calling where query method on the relation: # @@ -186,10 +171,10 @@ module ActiveRecord # # NOTE: You can't set the limit either, that's used to control the batch # sizes. - def in_batches(of: 1000, begin_at: nil, end_at: nil, load: false) + def in_batches(of: 1000, start: nil, finish: nil, load: false) relation = self unless block_given? - return BatchEnumerator.new(of: of, begin_at: begin_at, end_at: end_at, relation: self) + return BatchEnumerator.new(of: of, start: start, finish: finish, relation: self) end if logger && (arel.orders.present? || arel.taken.present?) @@ -197,12 +182,12 @@ module ActiveRecord end relation = relation.reorder(batch_order).limit(of) - relation = apply_limits(relation, begin_at, end_at) + relation = apply_limits(relation, start, finish) batch_relation = relation loop do if load - records = batch_relation.to_a + records = batch_relation.records ids = records.map(&:id) yielded_relation = self.where(primary_key => ids) yielded_relation.load_records(records) @@ -219,15 +204,15 @@ module ActiveRecord yield yielded_relation break if ids.length < of - batch_relation = relation.where(table[primary_key].gt(primary_key_offset)) + batch_relation = relation.where(arel_attribute(primary_key).gt(primary_key_offset)) end end private - def apply_limits(relation, begin_at, end_at) - relation = relation.where(table[primary_key].gteq(begin_at)) if begin_at - relation = relation.where(table[primary_key].lteq(end_at)) if end_at + def apply_limits(relation, start, finish) + relation = relation.where(arel_attribute(primary_key).gteq(start)) if start + relation = relation.where(arel_attribute(primary_key).lteq(finish)) if finish relation end diff --git a/activerecord/lib/active_record/relation/batches/batch_enumerator.rb b/activerecord/lib/active_record/relation/batches/batch_enumerator.rb index 153aae9584..13393dc605 100644 --- a/activerecord/lib/active_record/relation/batches/batch_enumerator.rb +++ b/activerecord/lib/active_record/relation/batches/batch_enumerator.rb @@ -3,11 +3,11 @@ module ActiveRecord class BatchEnumerator include Enumerable - def initialize(of: 1000, begin_at: nil, end_at: nil, relation:) #:nodoc: + def initialize(of: 1000, start: nil, finish: nil, relation:) #:nodoc: @of = of @relation = relation - @begin_at = begin_at - @end_at = end_at + @start = start + @finish = finish end # Looping through a collection of records from the database (using the @@ -34,8 +34,8 @@ module ActiveRecord def each_record return to_enum(:each_record) unless block_given? - @relation.to_enum(:in_batches, of: @of, begin_at: @begin_at, end_at: @end_at, load: true).each do |relation| - relation.to_a.each { |record| yield record } + @relation.to_enum(:in_batches, of: @of, start: @start, finish: @finish, load: true).each do |relation| + relation.records.each { |record| yield record } end end @@ -46,7 +46,7 @@ module ActiveRecord # People.in_batches.update_all('age = age + 1') [:delete_all, :update_all, :destroy_all].each do |method| define_method(method) do |*args, &block| - @relation.to_enum(:in_batches, of: @of, begin_at: @begin_at, end_at: @end_at, load: false).each do |relation| + @relation.to_enum(:in_batches, of: @of, start: @start, finish: @finish, load: false).each do |relation| relation.send(method, *args, &block) end end @@ -58,7 +58,7 @@ module ActiveRecord # relation.update_all(awesome: true) # end def each - enum = @relation.to_enum(:in_batches, of: @of, begin_at: @begin_at, end_at: @end_at, load: false) + enum = @relation.to_enum(:in_batches, of: @of, start: @start, finish: @finish, load: false) return enum.each { |relation| yield relation } if block_given? enum end diff --git a/activerecord/lib/active_record/relation/calculations.rb b/activerecord/lib/active_record/relation/calculations.rb index f45844a9ea..54c9af4898 100644 --- a/activerecord/lib/active_record/relation/calculations.rb +++ b/activerecord/lib/active_record/relation/calculations.rb @@ -155,15 +155,7 @@ module ActiveRecord # See also #ids. # def pluck(*column_names) - column_names.map! do |column_name| - if column_name.is_a?(Symbol) && attribute_alias?(column_name) - attribute_alias(column_name) - else - column_name.to_s - end - end - - if loaded? && (column_names - @klass.column_names).empty? + if loaded? && (column_names.map(&:to_s) - @klass.attribute_names - @klass.attribute_aliases.keys).empty? return @records.pluck(*column_names) end @@ -172,7 +164,7 @@ module ActiveRecord else relation = spawn relation.select_values = column_names.map { |cn| - columns_hash.key?(cn) ? arel_table[cn] : cn + @klass.has_attribute?(cn) || @klass.attribute_alias?(cn) ? arel_attribute(cn) : cn } result = klass.connection.select_all(relation.arel, nil, bound_attributes) result.cast_values(klass.attribute_types) diff --git a/activerecord/lib/active_record/relation/delegation.rb b/activerecord/lib/active_record/relation/delegation.rb index 27de313d05..f2578f5f96 100644 --- a/activerecord/lib/active_record/relation/delegation.rb +++ b/activerecord/lib/active_record/relation/delegation.rb @@ -36,13 +36,9 @@ 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. - BLACKLISTED_ARRAY_METHODS = [ - :compact!, :flatten!, :reject!, :reverse!, :rotate!, :map!, - :shuffle!, :slice!, :sort!, :sort_by!, :delete_if, - :keep_if, :pop, :shift, :delete_at, :select! - ].to_set # :nodoc: - - delegate :to_xml, :to_yaml, :length, :collect, :map, :each, :all?, :include?, :to_ary, :join, to: :to_a + delegate :to_xml, :to_yaml, :length, :collect, :map, :each, :all?, :include?, :to_ary, :join, + :[], :&, :|, :+, :-, :sample, :reverse, :compact, :in_groups, :in_groups_of, + :shuffle, :split, to: :records delegate :table_name, :quoted_table_name, :primary_key, :quoted_primary_key, :connection, :columns_hash, :to => :klass @@ -114,21 +110,14 @@ module ActiveRecord def respond_to?(method, include_private = false) super || @klass.respond_to?(method, include_private) || - array_delegable?(method) || arel.respond_to?(method, include_private) end protected - def array_delegable?(method) - Array.method_defined?(method) && BLACKLISTED_ARRAY_METHODS.exclude?(method) - end - def method_missing(method, *args, &block) if @klass.respond_to?(method) scoping { @klass.public_send(method, *args, &block) } - elsif array_delegable?(method) - to_a.public_send(method, *args, &block) elsif arel.respond_to?(method) arel.public_send(method, *args, &block) else diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb index 435cef901b..0037398554 100644 --- a/activerecord/lib/active_record/relation/finder_methods.rb +++ b/activerecord/lib/active_record/relation/finder_methods.rb @@ -117,9 +117,9 @@ module ActiveRecord # def first(limit = nil) if limit - find_nth_with_limit(offset_index, limit) + find_nth_with_limit_and_offset(0, limit, offset: offset_index) else - find_nth(0, offset_index) + find_nth 0 end end @@ -145,15 +145,21 @@ module ActiveRecord # # [#<Person id:4>, #<Person id:3>, #<Person id:2>] def last(limit = nil) - if limit - if order_values.empty? && primary_key - order(arel_table[primary_key].desc).limit(limit).reverse - else - to_a.last(limit) - end - else - find_last - end + return find_last(limit) if loaded? || limit_value + + result = limit(limit || 1) + result.order!(arel_attribute(primary_key)) if order_values.empty? && primary_key + result = result.reverse_order! + + limit ? result.reverse : result.first + rescue ActiveRecord::IrreversibleOrderError + ActiveSupport::Deprecation.warn(<<-WARNING.squish) + Finding a last element by loading the relation when SQL ORDER + can not be reversed is deprecated. + Rails 5.1 will raise ActiveRecord::IrreversibleOrderError in this case. + Please call `to_a.last` if you still want to load the relation. + WARNING + find_last(limit) end # Same as #last but raises ActiveRecord::RecordNotFound if no record @@ -169,7 +175,7 @@ module ActiveRecord # Person.offset(3).second # returns the second object from OFFSET 3 (which is OFFSET 4) # Person.where(["user_name = :u", { u: user_name }]).second def second - find_nth(1, offset_index) + find_nth 1 end # Same as #second but raises ActiveRecord::RecordNotFound if no record @@ -185,7 +191,7 @@ module ActiveRecord # Person.offset(3).third # returns the third object from OFFSET 3 (which is OFFSET 5) # Person.where(["user_name = :u", { u: user_name }]).third def third - find_nth(2, offset_index) + find_nth 2 end # Same as #third but raises ActiveRecord::RecordNotFound if no record @@ -201,7 +207,7 @@ module ActiveRecord # Person.offset(3).fourth # returns the fourth object from OFFSET 3 (which is OFFSET 6) # Person.where(["user_name = :u", { u: user_name }]).fourth def fourth - find_nth(3, offset_index) + find_nth 3 end # Same as #fourth but raises ActiveRecord::RecordNotFound if no record @@ -217,7 +223,7 @@ module ActiveRecord # Person.offset(3).fifth # returns the fifth object from OFFSET 3 (which is OFFSET 7) # Person.where(["user_name = :u", { u: user_name }]).fifth def fifth - find_nth(4, offset_index) + find_nth 4 end # Same as #fifth but raises ActiveRecord::RecordNotFound if no record @@ -233,7 +239,7 @@ module ActiveRecord # Person.offset(3).forty_two # returns the forty-second object from OFFSET 3 (which is OFFSET 44) # Person.where(["user_name = :u", { u: user_name }]).forty_two def forty_two - find_nth(41, offset_index) + find_nth 41 end # Same as #forty_two but raises ActiveRecord::RecordNotFound if no record @@ -242,6 +248,38 @@ module ActiveRecord find_nth! 41 end + # Find the third-to-last record. + # If no order is defined it will order by primary key. + # + # Person.third_to_last # returns the third-to-last object fetched by SELECT * FROM people + # Person.offset(3).third_to_last # returns the third-to-last object from OFFSET 3 + # Person.where(["user_name = :u", { u: user_name }]).third_to_last + def third_to_last + find_nth(-3) + end + + # Same as #third_to_last but raises ActiveRecord::RecordNotFound if no record + # is found. + def third_to_last! + find_nth!(-3) + end + + # Find the second-to-last record. + # If no order is defined it will order by primary key. + # + # Person.second_to_last # returns the second-to-last object fetched by SELECT * FROM people + # Person.offset(3).second_to_last # returns the second-to-last object from OFFSET 3 + # Person.where(["user_name = :u", { u: user_name }]).second_to_last + def second_to_last + find_nth(-2) + end + + # Same as #second_to_last but raises ActiveRecord::RecordNotFound if no record + # is found. + def second_to_last! + find_nth!(-2) + end + # Returns true if a record exists in the table that matches the +id+ or # conditions given, or false otherwise. The argument can take six forms: # @@ -298,7 +336,7 @@ module ActiveRecord end # This method is called whenever no records are found with either a single - # id or multiple ids and raises a ActiveRecord::RecordNotFound exception. + # id or multiple ids and raises an ActiveRecord::RecordNotFound exception. # # The error message is different depending on whether a single id or # multiple ids are provided. If multiple ids are provided, then the number @@ -442,6 +480,8 @@ module ActiveRecord end def find_some(ids) + return find_some_ordered(ids) unless order_values.present? + result = where(primary_key => ids).to_a expected_size = @@ -463,49 +503,77 @@ module ActiveRecord end end + def find_some_ordered(ids) + ids = ids.slice(offset_value || 0, limit_value || ids.size) || [] + + result = except(:limit, :offset).where(primary_key => ids).records + + if result.size == ids.size + pk_type = @klass.type_for_attribute(primary_key) + + records_by_id = result.index_by(&:id) + ids.map { |id| records_by_id.fetch(pk_type.cast(id)) } + else + raise_record_not_found_exception!(ids, result.size, ids.size) + end + end + def find_take if loaded? @records.first else - @take ||= limit(1).to_a.first + @take ||= limit(1).records.first end end - def find_nth(index, offset) + def find_nth(index, offset = nil) + # TODO: once the offset argument is removed we rely on offset_index + # within find_nth_with_limit, rather than pass it in via + # find_nth_with_limit_and_offset + if offset + ActiveSupport::Deprecation.warn(<<-MSG.squish) + Passing an offset argument to find_nth is deprecated, + please use Relation#offset instead. + MSG + end if loaded? @records[index] else - offset += index - @offsets[offset] ||= find_nth_with_limit(offset, 1).first + offset ||= offset_index + @offsets[offset + index] ||= find_nth_with_limit_and_offset(index, 1, offset: offset).first end end def find_nth!(index) - find_nth(index, offset_index) or raise RecordNotFound.new("Couldn't find #{@klass.name} with [#{arel.where_sql(@klass.arel_engine)}]") + find_nth(index) or raise RecordNotFound.new("Couldn't find #{@klass.name} with [#{arel.where_sql(@klass.arel_engine)}]") end - def find_nth_with_limit(offset, limit) + def find_nth_with_limit(index, limit) + # TODO: once the offset argument is removed from find_nth, + # find_nth_with_limit_and_offset can be merged into this method relation = if order_values.empty? && primary_key - order(arel_table[primary_key].asc) + order(arel_attribute(primary_key).asc) else self end - relation = relation.offset(offset) unless offset.zero? + relation = relation.offset(index) unless index.zero? relation.limit(limit).to_a end - def find_last + private + + def find_nth_with_limit_and_offset(index, limit, offset:) # :nodoc: if loaded? - @records.last + @records[index, limit] else - @last ||= - if limit_value - to_a.last - else - reverse_order.limit(1).to_a.first - end + index += offset + find_nth_with_limit(index, limit) end end + + def find_last(limit) + limit ? records.last(limit) : records.last + end end end diff --git a/activerecord/lib/active_record/relation/from_clause.rb b/activerecord/lib/active_record/relation/from_clause.rb index 92340216ed..8945cb0cc5 100644 --- a/activerecord/lib/active_record/relation/from_clause.rb +++ b/activerecord/lib/active_record/relation/from_clause.rb @@ -25,7 +25,7 @@ module ActiveRecord end def self.empty - new(nil, nil) + @empty ||= new(nil, nil) end end end diff --git a/activerecord/lib/active_record/relation/merger.rb b/activerecord/lib/active_record/relation/merger.rb index cb971eb255..396638d74d 100644 --- a/activerecord/lib/active_record/relation/merger.rb +++ b/activerecord/lib/active_record/relation/merger.rb @@ -141,6 +141,9 @@ module ActiveRecord end def merge_single_values + if relation.from_clause.empty? + relation.from_clause = other.from_clause + end relation.lock_value ||= other.lock_value unless other.create_with_value.blank? diff --git a/activerecord/lib/active_record/relation/predicate_builder.rb b/activerecord/lib/active_record/relation/predicate_builder.rb index 39e7b42629..953495a8b6 100644 --- a/activerecord/lib/active_record/relation/predicate_builder.rb +++ b/activerecord/lib/active_record/relation/predicate_builder.rb @@ -5,6 +5,7 @@ module ActiveRecord require 'active_record/relation/predicate_builder/base_handler' require 'active_record/relation/predicate_builder/basic_object_handler' require 'active_record/relation/predicate_builder/class_handler' + require 'active_record/relation/predicate_builder/polymorphic_array_handler' require 'active_record/relation/predicate_builder/range_handler' require 'active_record/relation/predicate_builder/relation_handler' @@ -18,9 +19,11 @@ module ActiveRecord register_handler(Class, ClassHandler.new(self)) register_handler(Base, BaseHandler.new(self)) register_handler(Range, RangeHandler.new(self)) + register_handler(RangeHandler::RangeWithBinds, RangeHandler.new(self)) 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) @@ -39,10 +42,7 @@ module ActiveRecord # # For polymorphic relationships, find the foreign key and type: # PriceEstimate.where(estimate_of: treasure) - if table.associated_with?(column) - value = AssociationQueryValue.new(table.associated_table(column), value) - end - + value = AssociationQueryHandler.value_for(table, column, value) if table.associated_with?(column) build(table.arel_attribute(column), value) end @@ -105,10 +105,23 @@ module ActiveRecord binds += bvs when Relation binds += value.bound_attributes + when Range + first = value.begin + last = value.end + unless first.respond_to?(:infinite?) && first.infinite? + binds << build_bind_param(column_name, first) + first = Arel::Nodes::BindParam.new + end + unless last.respond_to?(:infinite?) && last.infinite? + binds << build_bind_param(column_name, last) + last = Arel::Nodes::BindParam.new + end + + result[column_name] = RangeHandler::RangeWithBinds.new(first, last, value.exclude_end?) else if can_be_bound?(column_name, value) result[column_name] = Arel::Nodes::BindParam.new - binds << Relation::QueryAttribute.new(column_name.to_s, value, table.type(column_name)) + binds << build_bind_param(column_name, value) end end end @@ -145,5 +158,9 @@ module ActiveRecord handler_for(value).is_a?(BasicObjectHandler) && !table.associated_with?(column_name) end + + def build_bind_param(column_name, value) + Relation::QueryAttribute.new(column_name.to_s, value, table.type(column_name)) + 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 index e81be63cd3..d7fd878265 100644 --- a/activerecord/lib/active_record/relation/predicate_builder/association_query_handler.rb +++ b/activerecord/lib/active_record/relation/predicate_builder/association_query_handler.rb @@ -1,6 +1,16 @@ module ActiveRecord class PredicateBuilder class AssociationQueryHandler # :nodoc: + def self.value_for(table, column, value) + klass = if table.associated_table(column).polymorphic_association? && ::Array === value && value.first.is_a?(Base) + PolymorphicArrayValue + else + AssociationQueryValue + end + + klass.new(table.associated_table(column), value) + end + def initialize(predicate_builder) @predicate_builder = predicate_builder 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 new file mode 100644 index 0000000000..b6c6240343 --- /dev/null +++ b/activerecord/lib/active_record/relation/predicate_builder/polymorphic_array_handler.rb @@ -0,0 +1,57 @@ +module ActiveRecord + class PredicateBuilder + class PolymorphicArrayHandler # :nodoc: + def initialize(predicate_builder) + @predicate_builder = predicate_builder + end + + def call(attribute, value) + table = value.associated_table + queries = value.type_to_ids_mapping.map do |type, ids| + { table.association_foreign_type.to_s => type, table.association_foreign_key.to_s => ids } + end + + predicates = queries.map { |query| predicate_builder.build_from_hash(query) } + + if predicates.size > 1 + type_and_ids_predicates = predicates.map { |type_predicate, id_predicate| Arel::Nodes::Grouping.new(type_predicate.and(id_predicate)) } + type_and_ids_predicates.inject(&:or) + else + predicates.first + end + end + + protected + + attr_reader :predicate_builder + end + + class PolymorphicArrayValue # :nodoc: + attr_reader :associated_table, :values + + def initialize(associated_table, values) + @associated_table = associated_table + @values = values + end + + def type_to_ids_mapping + default_hash = Hash.new { |hsh, key| hsh[key] = [] } + values.each_with_object(default_hash) { |value, hash| hash[base_class(value).name] << convert_to_id(value) } + end + + private + + def primary_key(value) + associated_table.association_primary_key(base_class(value)) + end + + def base_class(value) + value.class.base_class + end + + def convert_to_id(value) + value._read_attribute(primary_key(value)) + end + end + end +end diff --git a/activerecord/lib/active_record/relation/predicate_builder/range_handler.rb b/activerecord/lib/active_record/relation/predicate_builder/range_handler.rb index 1b3849e3ad..306d4694ae 100644 --- a/activerecord/lib/active_record/relation/predicate_builder/range_handler.rb +++ b/activerecord/lib/active_record/relation/predicate_builder/range_handler.rb @@ -1,12 +1,28 @@ module ActiveRecord class PredicateBuilder class RangeHandler # :nodoc: + RangeWithBinds = Struct.new(:begin, :end, :exclude_end?) + def initialize(predicate_builder) @predicate_builder = predicate_builder end def call(attribute, value) - attribute.between(value) + if value.begin.respond_to?(:infinite?) && value.begin.infinite? + if value.end.respond_to?(:infinite?) && value.end.infinite? + attribute.not_in([]) + elsif value.exclude_end? + attribute.lt(value.end) + else + attribute.lteq(value.end) + end + elsif value.end.respond_to?(:infinite?) && value.end.infinite? + attribute.gteq(value.begin) + elsif value.exclude_end? + attribute.gteq(value.begin).and(attribute.lt(value.end)) + else + attribute.between(value) + end end protected diff --git a/activerecord/lib/active_record/relation/predicate_builder/relation_handler.rb b/activerecord/lib/active_record/relation/predicate_builder/relation_handler.rb index 063150958a..8a910a82fe 100644 --- a/activerecord/lib/active_record/relation/predicate_builder/relation_handler.rb +++ b/activerecord/lib/active_record/relation/predicate_builder/relation_handler.rb @@ -3,7 +3,7 @@ module ActiveRecord class RelationHandler # :nodoc: def call(attribute, value) if value.select_values.empty? - value = value.select(value.klass.arel_table[value.klass.primary_key]) + value = value.select(value.arel_attribute(value.klass.primary_key)) end attribute.in(value.arel) diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb index 2dc52982c9..4533f3263f 100644 --- a/activerecord/lib/active_record/relation/query_methods.rb +++ b/activerecord/lib/active_record/relation/query_methods.rb @@ -3,6 +3,7 @@ require "active_record/relation/query_attribute" require "active_record/relation/where_clause" require "active_record/relation/where_clause_factory" require 'active_model/forbidden_attributes_protection' +require 'active_support/core_ext/string/filters' module ActiveRecord module QueryMethods @@ -13,6 +14,8 @@ module ActiveRecord # WhereChain objects act as placeholder for queries in which #where does not have any parameter. # In this case, #where must be chained with #not to return a new relation. class WhereChain + include ActiveModel::ForbiddenAttributesProtection + def initialize(scope) @scope = scope end @@ -41,6 +44,8 @@ module ActiveRecord # User.where.not(name: "Jon", role: "admin") # # SELECT * FROM users WHERE name != 'Jon' AND role != 'admin' def not(opts, *rest) + opts = sanitize_forbidden_attributes(opts) + where_clause = @scope.send(:where_clause_factory).build(opts, rest) @scope.references!(PredicateBuilder.references(opts)) if Hash === opts @@ -49,16 +54,17 @@ module ActiveRecord end end + FROZEN_EMPTY_ARRAY = [].freeze Relation::MULTI_VALUE_METHODS.each do |name| class_eval <<-CODE, __FILE__, __LINE__ + 1 - def #{name}_values # def select_values - @values[:#{name}] || [] # @values[:select] || [] - end # end - # - def #{name}_values=(values) # def select_values=(values) - assert_mutability! # assert_mutability! - @values[:#{name}] = values # @values[:select] = values - end # end + def #{name}_values + @values[:#{name}] || FROZEN_EMPTY_ARRAY + end + + def #{name}_values=(values) + assert_mutability! + @values[:#{name}] = values + end CODE end @@ -93,11 +99,27 @@ module ActiveRecord end def bound_attributes - from_clause.binds + arel.bind_values + where_clause.binds + having_clause.binds + result = from_clause.binds + arel.bind_values + where_clause.binds + having_clause.binds + if limit_value && !string_containing_comma?(limit_value) + result << Attribute.with_cast_value( + "LIMIT".freeze, + connection.sanitize_limit(limit_value), + Type::Value.new, + ) + end + if offset_value + result << Attribute.with_cast_value( + "OFFSET".freeze, + offset_value.to_i, + Type::Value.new, + ) + end + result end + FROZEN_EMPTY_HASH = {}.freeze def create_with_value # :nodoc: - @values[:create_with] || {} + @values[:create_with] || FROZEN_EMPTY_HASH end alias extensions extending_values @@ -407,10 +429,30 @@ module ActiveRecord self end - # Performs a joins on +args+: + # Performs a joins on +args+. The given symbol(s) should match the name of + # the association(s). # # User.joins(:posts) - # # SELECT "users".* FROM "users" INNER JOIN "posts" ON "posts"."user_id" = "users"."id" + # # SELECT "users".* + # # FROM "users" + # # INNER JOIN "posts" ON "posts"."user_id" = "users"."id" + # + # Multiple joins: + # + # User.joins(:posts, :account) + # # SELECT "users".* + # # FROM "users" + # # INNER JOIN "posts" ON "posts"."user_id" = "users"."id" + # # INNER JOIN "accounts" ON "accounts"."id" = "users"."account_id" + # + # Nested joins: + # + # User.joins(posts: [:comments]) + # # SELECT "users".* + # # FROM "users" + # # INNER JOIN "posts" ON "posts"."user_id" = "users"."id" + # # INNER JOIN "comments" "comments_posts" + # # ON "comments_posts"."post_id" = "posts"."id" # # You can use strings in order to customize your joins: # @@ -609,16 +651,22 @@ module ActiveRecord # they must differ only by #where (if no #group has been defined) or #having (if a #group is # present). Neither relation may have a #limit, #offset, or #distinct set. # - # Post.where("id = 1").or(Post.where("id = 2")) - # # SELECT `posts`.* FROM `posts` WHERE (('id = 1' OR 'id = 2')) + # Post.where("id = 1").or(Post.where("author_id = 3")) + # # SELECT `posts`.* FROM `posts` WHERE (('id = 1' OR 'author_id = 3')) # def or(other) + unless other.is_a? Relation + raise ArgumentError, "You have passed #{other.class.name} object to #or. Pass an ActiveRecord::Relation object instead." + end + spawn.or!(other) end def or!(other) # :nodoc: - unless structurally_compatible_for_or?(other) - raise ArgumentError, 'Relation passed to #or must be structurally compatible' + incompatible_values = structurally_incompatible_values_for_or(other) + + unless incompatible_values.empty? + raise ArgumentError, "Relation passed to #or must be structurally compatible. Incompatible values: #{incompatible_values}" end self.where_clause = self.where_clause.or(other.where_clause) @@ -653,6 +701,13 @@ module ActiveRecord end def limit!(value) # :nodoc: + if string_containing_comma?(value) + # Remove `string_containing_comma?` when removing this deprecation + ActiveSupport::Deprecation.warn(<<-WARNING.squish) + Passing a string to limit in the form "1,2" is deprecated and will be + removed in Rails 5.1. Please call `offset` explicitly instead. + WARNING + end self.limit_value = value self end @@ -903,8 +958,14 @@ module ActiveRecord arel.where(where_clause.ast) unless where_clause.empty? arel.having(having_clause.ast) unless having_clause.empty? - arel.take(connection.sanitize_limit(limit_value)) if limit_value - arel.skip(offset_value.to_i) if offset_value + if limit_value + if string_containing_comma?(limit_value) + arel.take(connection.sanitize_limit(limit_value)) + else + arel.take(Arel::Nodes::BindParam.new) + end + end + arel.skip(Arel::Nodes::BindParam.new) if offset_value arel.group(*arel_columns(group_values.uniq.reject(&:blank?))) unless group_values.empty? build_order(arel) @@ -1036,8 +1097,8 @@ module ActiveRecord def arel_columns(columns) columns.map do |field| - if (Symbol === field || String === field) && columns_hash.key?(field.to_s) && !from_clause.value - arel_table[field] + if (Symbol === field || String === field) && (klass.has_attribute?(field) || klass.attribute_alias?(field)) && !from_clause.value + arel_attribute(field) elsif Symbol === field connection.quote_table_name(field.to_s) else @@ -1047,14 +1108,23 @@ module ActiveRecord end def reverse_sql_order(order_query) - order_query = ["#{quoted_table_name}.#{quoted_primary_key} ASC"] if order_query.empty? + if order_query.empty? + return [arel_attribute(primary_key).desc] if primary_key + raise IrreversibleOrderError, + "Relation has no current order and table has no primary key to be used as default order" + end order_query.flat_map do |o| case o + when Arel::Attribute + o.desc when Arel::Nodes::Ordering o.reverse when String - o.to_s.split(',').map! do |s| + if does_not_support_reverse?(o) + raise IrreversibleOrderError, "Order #{o.inspect} can not be reversed automatically" + end + o.split(',').map! do |s| s.strip! s.gsub!(/\sasc\Z/i, ' DESC') || s.gsub!(/\sdesc\Z/i, ' ASC') || s.concat(' DESC') end @@ -1064,6 +1134,13 @@ module ActiveRecord end end + def does_not_support_reverse?(order) + #uses sql function with multiple arguments + order =~ /\([^()]*,[^()]*\)/ || + # uses "nulls first" like construction + order =~ /nulls (first|last)\Z/i + end + def build_order(arel) orders = order_values.uniq orders.reject!(&:blank?) @@ -1099,12 +1176,10 @@ module ActiveRecord order_args.map! do |arg| case arg when Symbol - arg = klass.attribute_alias(arg) if klass.attribute_alias?(arg) - table[arg].asc + arel_attribute(arg).asc when Hash arg.map { |field, dir| - field = klass.attribute_alias(field) if klass.attribute_alias?(field) - table[field].send(dir.downcase) + arel_attribute(field).send(dir.downcase) } else arg @@ -1134,10 +1209,10 @@ module ActiveRecord end end - def structurally_compatible_for_or?(other) - Relation::SINGLE_VALUE_METHODS.all? { |m| send("#{m}_value") == other.send("#{m}_value") } && - (Relation::MULTI_VALUE_METHODS - [:extending]).all? { |m| send("#{m}_values") == other.send("#{m}_values") } && - (Relation::CLAUSE_METHODS - [:having, :where]).all? { |m| send("#{m}_clause") != other.send("#{m}_clause") } + def structurally_incompatible_values_for_or(other) + Relation::SINGLE_VALUE_METHODS.reject { |m| send("#{m}_value") == other.send("#{m}_value") } + + (Relation::MULTI_VALUE_METHODS - [:extending]).reject { |m| send("#{m}_values") == other.send("#{m}_values") } + + (Relation::CLAUSE_METHODS - [:having, :where]).reject { |m| send("#{m}_clause") == other.send("#{m}_clause") } end def new_where_clause @@ -1153,5 +1228,9 @@ module ActiveRecord def new_from_clause Relation::FromClause.empty end + + def string_containing_comma?(value) + ::String === value && value.include?(",") + end end end diff --git a/activerecord/lib/active_record/relation/record_fetch_warning.rb b/activerecord/lib/active_record/relation/record_fetch_warning.rb index 14e1bf89fa..dbd08811fa 100644 --- a/activerecord/lib/active_record/relation/record_fetch_warning.rb +++ b/activerecord/lib/active_record/relation/record_fetch_warning.rb @@ -23,23 +23,23 @@ module ActiveRecord end end - ActiveSupport::Notifications.subscribe("sql.active_record") do |*args| - payload = args.last - + # :stopdoc: + ActiveSupport::Notifications.subscribe("sql.active_record") do |*, payload| QueryRegistry.queries << payload[:sql] end + # :startdoc: class QueryRegistry # :nodoc: extend ActiveSupport::PerThreadRegistry - attr_accessor :queries + attr_reader :queries def initialize - reset + @queries = [] end def reset - @queries = [] + @queries.clear end end end diff --git a/activerecord/lib/active_record/relation/spawn_methods.rb b/activerecord/lib/active_record/relation/spawn_methods.rb index 5c3318651a..d5c18a2a4a 100644 --- a/activerecord/lib/active_record/relation/spawn_methods.rb +++ b/activerecord/lib/active_record/relation/spawn_methods.rb @@ -12,6 +12,7 @@ module ActiveRecord # Merges in the conditions from <tt>other</tt>, if <tt>other</tt> is an ActiveRecord::Relation. # Returns an array representing the intersection of the resulting records with <tt>other</tt>, if <tt>other</tt> is an array. + # # Post.where(published: true).joins(:comments).merge( Comment.where(spam: false) ) # # Performs a single join query with both where conditions. # @@ -28,7 +29,7 @@ module ActiveRecord # This is mainly intended for sharing common conditions between multiple associations. def merge(other) if other.is_a?(Array) - to_a & other + records & other elsif other spawn.merge!(other) else @@ -37,11 +38,14 @@ module ActiveRecord end def merge!(other) # :nodoc: - if !other.is_a?(Relation) && other.respond_to?(:to_proc) + if other.is_a?(Hash) + Relation::HashMerger.new(self, other).merge + elsif other.is_a?(Relation) + Relation::Merger.new(self, other).merge + elsif other.respond_to?(:to_proc) instance_exec(&other) else - klass = other.is_a?(Hash) ? Relation::HashMerger : Relation::Merger - klass.new(self, other).merge + raise ArgumentError, "#{other.inspect} is not an ActiveRecord::Relation" end end diff --git a/activerecord/lib/active_record/relation/where_clause.rb b/activerecord/lib/active_record/relation/where_clause.rb index 1f000b3f0f..2c2d6cfa47 100644 --- a/activerecord/lib/active_record/relation/where_clause.rb +++ b/activerecord/lib/active_record/relation/where_clause.rb @@ -81,7 +81,7 @@ module ActiveRecord end def self.empty - new([], []) + @empty ||= new([], []) end protected diff --git a/activerecord/lib/active_record/relation/where_clause_factory.rb b/activerecord/lib/active_record/relation/where_clause_factory.rb index a81ff98e49..dbf172a577 100644 --- a/activerecord/lib/active_record/relation/where_clause_factory.rb +++ b/activerecord/lib/active_record/relation/where_clause_factory.rb @@ -22,6 +22,7 @@ module ActiveRecord parts = predicate_builder.build_from_hash(attributes) when Arel::Nodes::Node parts = [opts] + binds = other else raise ArgumentError, "Unsupported argument type: #{opts} (#{opts.class})" end diff --git a/activerecord/lib/active_record/sanitization.rb b/activerecord/lib/active_record/sanitization.rb index 0c15f45db9..a9e1fd0dad 100644 --- a/activerecord/lib/active_record/sanitization.rb +++ b/activerecord/lib/active_record/sanitization.rb @@ -18,6 +18,9 @@ module ActiveRecord # sanitize_sql_for_conditions(["name=? and group_id=?", "foo'bar", 4]) # # => "name='foo''bar' and group_id=4" # + # sanitize_sql_for_conditions(["name=:name and group_id=:group_id", name: "foo'bar", group_id: 4]) + # # => "name='foo''bar' and group_id='4'" + # # sanitize_sql_for_conditions(["name='%s' and group_id='%s'", "foo'bar", 4]) # # => "name='foo''bar' and group_id='4'" # @@ -40,6 +43,9 @@ module ActiveRecord # sanitize_sql_for_assignment(["name=? and group_id=?", nil, 4]) # # => "name=NULL and group_id=4" # + # sanitize_sql_for_assignment(["name=:name and group_id=:group_id", name: nil, group_id: 4]) + # # => "name=NULL and group_id=4" + # # Post.send(:sanitize_sql_for_assignment, { name: nil, group_id: 4 }) # # => "`posts`.`name` = NULL, `posts`.`group_id` = 4" # @@ -54,7 +60,7 @@ module ActiveRecord end # Accepts an array, or string of SQL conditions and sanitizes - # them into a valid SQL fragment for a ORDER clause. + # them into a valid SQL fragment for an ORDER clause. # # sanitize_sql_for_order(["field(id, ?)", [1,3,2]]) # # => "field(id, 1,3,2)" @@ -140,6 +146,9 @@ module ActiveRecord # sanitize_sql_array(["name=? and group_id=?", "foo'bar", 4]) # # => "name='foo''bar' and group_id=4" # + # sanitize_sql_array(["name=:name and group_id=:group_id", name: "foo'bar", group_id: 4]) + # # => "name='foo''bar' and group_id=4" + # # sanitize_sql_array(["name='%s' and group_id='%s'", "foo'bar", 4]) # # => "name='foo''bar' and group_id='4'" def sanitize_sql_array(ary) @@ -204,7 +213,7 @@ module ActiveRecord end # TODO: Deprecate this - def quoted_id + def quoted_id # :nodoc: self.class.quote_value(@attributes[self.class.primary_key].value_for_database) end end diff --git a/activerecord/lib/active_record/schema.rb b/activerecord/lib/active_record/schema.rb index 31dd584538..784a02d2c3 100644 --- a/activerecord/lib/active_record/schema.rb +++ b/activerecord/lib/active_record/schema.rb @@ -27,7 +27,7 @@ module ActiveRecord # # ActiveRecord::Schema is only supported by database adapters that also # support migrations, the two features being very similar. - class Schema < Migration + class Schema < Migration::Current # Eval the given block. All methods available to the current connection # adapter are available within the block, so you can easily use the # database definition DSL to build up your schema ( @@ -51,6 +51,9 @@ module ActiveRecord initialize_schema_migrations_table connection.assume_migrated_upto_version(info[:version], migrations_paths) end + + ActiveRecord::InternalMetadata.create_table + ActiveRecord::InternalMetadata[:environment] = ActiveRecord::Migrator.current_environment end private diff --git a/activerecord/lib/active_record/schema_dumper.rb b/activerecord/lib/active_record/schema_dumper.rb index 2362dae9fc..f115c7542b 100644 --- a/activerecord/lib/active_record/schema_dumper.rb +++ b/activerecord/lib/active_record/schema_dumper.rb @@ -178,11 +178,11 @@ HEADER tbl.puts end - indexes(table, tbl) - tbl.puts " end" tbl.puts + indexes(table, tbl) + tbl.rewind stream.print tbl.read rescue => e @@ -198,7 +198,8 @@ HEADER if (indexes = @connection.indexes(table)).any? add_index_statements = indexes.map do |index| statement_parts = [ - "t.index #{index.columns.inspect}", + "add_index #{remove_prefix_and_suffix(index.table).inspect}", + index.columns.inspect, "name: #{index.name.inspect}", ] statement_parts << 'unique: true' if index.unique @@ -212,10 +213,11 @@ HEADER statement_parts << "using: #{index.using.inspect}" if index.using statement_parts << "type: #{index.type.inspect}" if index.type - " #{statement_parts.join(', ')}" + " #{statement_parts.join(', ')}" end stream.puts add_index_statements.sort.join("\n") + stream.puts end end @@ -254,7 +256,7 @@ HEADER end def ignored?(table_name) - [ActiveRecord::Base.schema_migrations_table_name, ignore_tables].flatten.any? do |ignored| + [ActiveRecord::Base.schema_migrations_table_name, ActiveRecord::Base.internal_metadata_table_name, ignore_tables].flatten.any? do |ignored| ignored === remove_prefix_and_suffix(table_name) end end diff --git a/activerecord/lib/active_record/schema_migration.rb b/activerecord/lib/active_record/schema_migration.rb index b384529e75..b6cb233e03 100644 --- a/activerecord/lib/active_record/schema_migration.rb +++ b/activerecord/lib/active_record/schema_migration.rb @@ -9,38 +9,29 @@ module ActiveRecord class SchemaMigration < ActiveRecord::Base # :nodoc: class << self def primary_key - nil + "version" end def table_name "#{table_name_prefix}#{ActiveRecord::Base.schema_migrations_table_name}#{table_name_suffix}" end - def index_name - "#{table_name_prefix}unique_#{ActiveRecord::Base.schema_migrations_table_name}#{table_name_suffix}" - end - def table_exists? - connection.table_exists?(table_name) + ActiveSupport::Deprecation.silence { connection.table_exists?(table_name) } end - def create_table(limit=nil) + def create_table unless table_exists? - version_options = {null: false} - version_options[:limit] = limit if limit + version_options = connection.internal_string_options_for_primary_key connection.create_table(table_name, id: false) do |t| - t.column :version, :string, version_options + t.string :version, version_options end - connection.add_index table_name, :version, unique: true, name: index_name end end def drop_table - if table_exists? - connection.remove_index table_name, name: index_name - connection.drop_table(table_name) - end + connection.drop_table table_name, if_exists: true end def normalize_migration_number(number) diff --git a/activerecord/lib/active_record/scoping.rb b/activerecord/lib/active_record/scoping.rb index e395970dc6..7794af8ca4 100644 --- a/activerecord/lib/active_record/scoping.rb +++ b/activerecord/lib/active_record/scoping.rb @@ -11,11 +11,11 @@ module ActiveRecord module ClassMethods def current_scope #:nodoc: - ScopeRegistry.value_for(:current_scope, self.to_s) + ScopeRegistry.value_for(:current_scope, self) end def current_scope=(scope) #:nodoc: - ScopeRegistry.set_value_for(:current_scope, self.to_s, scope) + ScopeRegistry.set_value_for(:current_scope, self, scope) end # Collects attributes from scopes that should be applied when creating @@ -53,18 +53,18 @@ module ActiveRecord # following code: # # registry = ActiveRecord::Scoping::ScopeRegistry - # registry.set_value_for(:current_scope, "Board", some_new_scope) + # registry.set_value_for(:current_scope, Board, some_new_scope) # # Now when you run: # - # registry.value_for(:current_scope, "Board") + # registry.value_for(:current_scope, Board) # # You will obtain whatever was defined in +some_new_scope+. The #value_for # and #set_value_for methods are delegated to the current ScopeRegistry # object, so the above example code can also be called as: # # ActiveRecord::Scoping::ScopeRegistry.set_value_for(:current_scope, - # "Board", some_new_scope) + # Board, some_new_scope) class ScopeRegistry # :nodoc: extend ActiveSupport::PerThreadRegistry @@ -74,16 +74,22 @@ module ActiveRecord @registry = Hash.new { |hash, key| hash[key] = {} } end - # Obtains the value for a given +scope_name+ and +variable_name+. - def value_for(scope_type, variable_name) + # Obtains the value for a given +scope_type+ and +model+. + def value_for(scope_type, model) raise_invalid_scope_type!(scope_type) - @registry[scope_type][variable_name] + klass = model + base = model.base_class + while klass <= base + value = @registry[scope_type][klass.name] + return value if value + klass = klass.superclass + end end - # Sets the +value+ for a given +scope_type+ and +variable_name+. - def set_value_for(scope_type, variable_name, value) + # Sets the +value+ for a given +scope_type+ and +model+. + def set_value_for(scope_type, model, value) raise_invalid_scope_type!(scope_type) - @registry[scope_type][variable_name] = value + @registry[scope_type][model.name] = value end private diff --git a/activerecord/lib/active_record/scoping/default.rb b/activerecord/lib/active_record/scoping/default.rb index cdcb73382f..f6b6768ce3 100644 --- a/activerecord/lib/active_record/scoping/default.rb +++ b/activerecord/lib/active_record/scoping/default.rb @@ -6,7 +6,7 @@ module ActiveRecord included do # Stores the default scope for the class. class_attribute :default_scopes, instance_writer: false, instance_predicate: false - class_attribute :default_scope_override, instance_predicate: false + class_attribute :default_scope_override, instance_writer: false, instance_predicate: false self.default_scopes = [] self.default_scope_override = nil @@ -122,11 +122,11 @@ module ActiveRecord end def ignore_default_scope? # :nodoc: - ScopeRegistry.value_for(:ignore_default_scope, self) + ScopeRegistry.value_for(:ignore_default_scope, base_class) end def ignore_default_scope=(ignore) # :nodoc: - ScopeRegistry.set_value_for(:ignore_default_scope, self, ignore) + ScopeRegistry.set_value_for(:ignore_default_scope, base_class, ignore) end # The ignore_default_scope flag is used to prevent an infinite recursion diff --git a/activerecord/lib/active_record/scoping/named.rb b/activerecord/lib/active_record/scoping/named.rb index 103569c84d..5395bd6076 100644 --- a/activerecord/lib/active_record/scoping/named.rb +++ b/activerecord/lib/active_record/scoping/named.rb @@ -151,6 +151,7 @@ module ActiveRecord "a class method with the same name." end + valid_scope_name?(name) extension = Module.new(&block) if block if body.respond_to?(:to_proc) @@ -169,6 +170,15 @@ module ActiveRecord end end end + + protected + + def valid_scope_name?(name) + if respond_to?(name, true) + logger.warn "Creating scope :#{name}. " \ + "Overwriting existing method #{self.name}.#{name}." + end + end end end end diff --git a/activerecord/lib/active_record/statement_cache.rb b/activerecord/lib/active_record/statement_cache.rb index f6b0efb88a..6c896ccea6 100644 --- a/activerecord/lib/active_record/statement_cache.rb +++ b/activerecord/lib/active_record/statement_cache.rb @@ -106,7 +106,7 @@ module ActiveRecord sql = query_builder.sql_for bind_values, connection - klass.find_by_sql sql, bind_values + klass.find_by_sql(sql, bind_values, preparable: true) end alias :call :execute end diff --git a/activerecord/lib/active_record/suppressor.rb b/activerecord/lib/active_record/suppressor.rb index b3644bf569..8ec4b48d31 100644 --- a/activerecord/lib/active_record/suppressor.rb +++ b/activerecord/lib/active_record/suppressor.rb @@ -37,7 +37,11 @@ module ActiveRecord end end - def create_or_update(*args) # :nodoc: + def save(*) # :nodoc: + SuppressorRegistry.suppressed[self.class.name] ? true : super + end + + def save!(*) # :nodoc: SuppressorRegistry.suppressed[self.class.name] ? true : super end end diff --git a/activerecord/lib/active_record/table_metadata.rb b/activerecord/lib/active_record/table_metadata.rb index f9bb1cf5e0..0faad48ce3 100644 --- a/activerecord/lib/active_record/table_metadata.rb +++ b/activerecord/lib/active_record/table_metadata.rb @@ -22,7 +22,11 @@ module ActiveRecord end def arel_attribute(column_name) - arel_table[column_name] + if klass + klass.arel_attribute(column_name, arel_table) + else + arel_table[column_name] + end end def type(column_name) diff --git a/activerecord/lib/active_record/tasks/database_tasks.rb b/activerecord/lib/active_record/tasks/database_tasks.rb index c0c29a618c..7dc41fa98c 100644 --- a/activerecord/lib/active_record/tasks/database_tasks.rb +++ b/activerecord/lib/active_record/tasks/database_tasks.rb @@ -42,6 +42,22 @@ module ActiveRecord LOCAL_HOSTS = ['127.0.0.1', 'localhost'] + def check_protected_environments! + unless ENV['DISABLE_DATABASE_ENVIRONMENT_CHECK'] + current = ActiveRecord::Migrator.current_environment + stored = ActiveRecord::Migrator.last_stored_environment + + if ActiveRecord::Migrator.protected_environment? + raise ActiveRecord::ProtectedEnvironmentError.new(stored) + end + + if stored && stored != current + raise ActiveRecord::EnvironmentMismatchError.new(current: current, stored: stored) + end + end + rescue ActiveRecord::NoDatabaseError + end + def register_task(pattern, task) @tasks ||= {} @tasks[pattern] = task @@ -100,7 +116,11 @@ module ActiveRecord end def create_all + old_pool = ActiveRecord::Base.connection_handler.retrieve_connection_pool(ActiveRecord::Base) each_local_configuration { |configuration| create configuration } + if old_pool + ActiveRecord::Base.connection_handler.establish_connection(ActiveRecord::Base, old_pool.spec) + end end def create_current(environment = env) @@ -204,6 +224,8 @@ module ActiveRecord else raise ArgumentError, "unknown format #{format.inspect}" end + ActiveRecord::InternalMetadata.create_table + ActiveRecord::InternalMetadata[:environment] = ActiveRecord::Migrator.current_environment end def load_schema_for(*args) @@ -232,7 +254,7 @@ module ActiveRecord def check_schema_file(filename) unless File.exist?(filename) - message = %{#{filename} doesn't exist yet. Run `rake db:migrate` to create it, then try again.} + message = %{#{filename} doesn't exist yet. Run `rails db:migrate` to create it, then try again.} message << %{ If you do not intend to use a database, you should instead alter #{Rails.root}/config/application.rb to limit the frameworks that will be loaded.} if defined?(::Rails) Kernel.abort message end diff --git a/activerecord/lib/active_record/tasks/mysql_database_tasks.rb b/activerecord/lib/active_record/tasks/mysql_database_tasks.rb index 100df9c6c6..7a49322e06 100644 --- a/activerecord/lib/active_record/tasks/mysql_database_tasks.rb +++ b/activerecord/lib/active_record/tasks/mysql_database_tasks.rb @@ -94,8 +94,6 @@ module ActiveRecord ArJdbcMySQL::Error elsif defined?(Mysql2) Mysql2::Error - elsif defined?(Mysql) - Mysql::Error else StandardError end diff --git a/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb b/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb index cd7d949239..8b4874044c 100644 --- a/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb +++ b/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb @@ -56,9 +56,9 @@ module ActiveRecord args = ['-s', '-x', '-O', '-f', filename] unless search_path.blank? - args << search_path.split(',').map do |part| + args += search_path.split(',').map do |part| "--schema=#{part.strip}" - end.join(' ') + end end args << configuration['database'] run_cmd('pg_dump', args, 'dumping') diff --git a/activerecord/lib/active_record/timestamp.rb b/activerecord/lib/active_record/timestamp.rb index a572c109d8..d9c18a5e38 100644 --- a/activerecord/lib/active_record/timestamp.rb +++ b/activerecord/lib/active_record/timestamp.rb @@ -16,7 +16,7 @@ module ActiveRecord # == Time Zone aware attributes # # Active Record keeps all the <tt>datetime</tt> and <tt>time</tt> columns - # time-zone aware. By default, these values are stored in the database as UTC + # timezone aware. By default, these values are stored in the database as UTC # and converted back to the current <tt>Time.zone</tt> when pulled from the database. # # This feature can be turned off completely by setting: @@ -28,6 +28,10 @@ module ActiveRecord # # ActiveRecord::Base.time_zone_aware_types = [:datetime] # + # You can also add database specific timezone aware types. For example, for PostgreSQL: + # + # ActiveRecord::Base.time_zone_aware_types += [:tsrange, :tstzrange] + # # Finally, you can indicate specific attributes of a model for which time zone # conversion should not applied, for instance by setting: # diff --git a/activerecord/lib/active_record/touch_later.rb b/activerecord/lib/active_record/touch_later.rb index 4352a0ffea..9a80a63e28 100644 --- a/activerecord/lib/active_record/touch_later.rb +++ b/activerecord/lib/active_record/touch_later.rb @@ -16,6 +16,13 @@ module ActiveRecord surreptitiously_touch @_defer_touch_attrs self.class.connection.add_transaction_record self + + # touch the parents as we are not calling the after_save callbacks + self.class.reflect_on_all_associations(:belongs_to).each do |r| + if touch = r.options[:touch] + ActiveRecord::Associations::Builder::BelongsTo.touch_record(self, r.foreign_key, r.name, touch, :touch_later) + end + end end def touch(*names, time: nil) # :nodoc: @@ -26,6 +33,7 @@ module ActiveRecord end private + def surreptitiously_touch(attrs) attrs.each { |attr| write_attribute attr, @_touch_time } clear_attribute_changes attrs @@ -33,9 +41,8 @@ module ActiveRecord def touch_deferred_attributes if has_defer_touch_attrs? && persisted? - @_touching_delayed_records = true touch(*@_defer_touch_attrs, time: @_touch_time) - @_touching_delayed_records, @_defer_touch_attrs, @_touch_time = nil, nil, nil + @_defer_touch_attrs, @_touch_time = nil, nil end end @@ -43,8 +50,9 @@ module ActiveRecord defined?(@_defer_touch_attrs) && @_defer_touch_attrs.present? end - def touching_delayed_records? - defined?(@_touching_delayed_records) && @_touching_delayed_records + def belongs_to_touch_method + :touch_later end + end end diff --git a/activerecord/lib/active_record/transactions.rb b/activerecord/lib/active_record/transactions.rb index 8de82feae3..77c2845d88 100644 --- a/activerecord/lib/active_record/transactions.rb +++ b/activerecord/lib/active_record/transactions.rb @@ -233,6 +233,24 @@ module ActiveRecord set_callback(:commit, :after, *args, &block) end + # Shortcut for <tt>after_commit :hook, on: :create</tt>. + def after_create_commit(*args, &block) + set_options_for_callbacks!(args, on: :create) + set_callback(:commit, :after, *args, &block) + end + + # Shortcut for <tt>after_commit :hook, on: :update</tt>. + def after_update_commit(*args, &block) + set_options_for_callbacks!(args, on: :update) + set_callback(:commit, :after, *args, &block) + end + + # Shortcut for <tt>after_commit :hook, on: :destroy</tt>. + def after_destroy_commit(*args, &block) + set_options_for_callbacks!(args, on: :destroy) + set_callback(:commit, :after, *args, &block) + end + # This callback is called after a create, update, or destroy are rolled back. # # Please check the documentation of #after_commit for options. @@ -268,9 +286,11 @@ module ActiveRecord private - def set_options_for_callbacks!(args) - options = args.last - if options.is_a?(Hash) && options[:on] + def set_options_for_callbacks!(args, enforced_options = {}) + options = args.extract_options!.merge!(enforced_options) + args << options + + if options[:on] fire_on = Array(options[:on]) assert_valid_transaction_action(fire_on) options[:if] = Array(options[:if]) diff --git a/activerecord/lib/active_record/type/type_map.rb b/activerecord/lib/active_record/type/type_map.rb index 81d7ed39bb..850a7a4e09 100644 --- a/activerecord/lib/active_record/type/type_map.rb +++ b/activerecord/lib/active_record/type/type_map.rb @@ -1,4 +1,4 @@ -require 'concurrent' +require 'concurrent/map' module ActiveRecord module Type diff --git a/activerecord/lib/active_record/type_caster.rb b/activerecord/lib/active_record/type_caster.rb index 63ba10c289..accc339d00 100644 --- a/activerecord/lib/active_record/type_caster.rb +++ b/activerecord/lib/active_record/type_caster.rb @@ -2,6 +2,6 @@ require 'active_record/type_caster/map' require 'active_record/type_caster/connection' module ActiveRecord - module TypeCaster + module TypeCaster # :nodoc: end end diff --git a/activerecord/lib/active_record/type_caster/connection.rb b/activerecord/lib/active_record/type_caster/connection.rb index 868d08ed44..7ed8dcc313 100644 --- a/activerecord/lib/active_record/type_caster/connection.rb +++ b/activerecord/lib/active_record/type_caster/connection.rb @@ -1,6 +1,6 @@ module ActiveRecord module TypeCaster - class Connection + class Connection # :nodoc: def initialize(klass, table_name) @klass = klass @table_name = table_name diff --git a/activerecord/lib/active_record/type_caster/map.rb b/activerecord/lib/active_record/type_caster/map.rb index 4b1941351c..3a367b3999 100644 --- a/activerecord/lib/active_record/type_caster/map.rb +++ b/activerecord/lib/active_record/type_caster/map.rb @@ -1,6 +1,6 @@ module ActiveRecord module TypeCaster - class Map + class Map # :nodoc: def initialize(types) @types = types end diff --git a/activerecord/lib/active_record/validations.rb b/activerecord/lib/active_record/validations.rb index 6677e6dc5f..ecaf04e39e 100644 --- a/activerecord/lib/active_record/validations.rb +++ b/activerecord/lib/active_record/validations.rb @@ -45,7 +45,7 @@ module ActiveRecord end # Attempts to save the record just like {ActiveRecord::Base#save}[rdoc-ref:Base#save] but - # will raise a ActiveRecord::RecordInvalid exception instead of returning +false+ if the record is not valid. + # will raise an ActiveRecord::RecordInvalid exception instead of returning +false+ if the record is not valid. def save!(options={}) perform_validations(options) ? super : raise_validation_error end diff --git a/activerecord/lib/active_record/validations/absence.rb b/activerecord/lib/active_record/validations/absence.rb index 2e19e6dc5c..641d041f3d 100644 --- a/activerecord/lib/active_record/validations/absence.rb +++ b/activerecord/lib/active_record/validations/absence.rb @@ -2,7 +2,6 @@ module ActiveRecord module Validations class AbsenceValidator < ActiveModel::Validations::AbsenceValidator # :nodoc: def validate_each(record, attribute, association_or_value) - return unless should_validate?(record) if record.class._reflect_on_association(attribute) association_or_value = Array.wrap(association_or_value).reject(&:marked_for_destruction?) end diff --git a/activerecord/lib/active_record/validations/associated.rb b/activerecord/lib/active_record/validations/associated.rb index 32fbaf0a91..b14db85167 100644 --- a/activerecord/lib/active_record/validations/associated.rb +++ b/activerecord/lib/active_record/validations/associated.rb @@ -2,10 +2,16 @@ module ActiveRecord module Validations class AssociatedValidator < ActiveModel::EachValidator #:nodoc: def validate_each(record, attribute, value) - if Array.wrap(value).reject {|r| r.marked_for_destruction? || r.valid?}.any? - record.errors.add(attribute, :invalid, options.merge(:value => value)) + if Array(value).reject { |r| valid_object?(r) }.any? + record.errors.add(attribute, :invalid, options.merge(value: value)) end end + + private + + def valid_object?(record) + (record.respond_to?(:marked_for_destruction?) && record.marked_for_destruction?) || record.valid? + end end module ClassMethods diff --git a/activerecord/lib/active_record/validations/length.rb b/activerecord/lib/active_record/validations/length.rb index 69e048eef1..0e0cebce4a 100644 --- a/activerecord/lib/active_record/validations/length.rb +++ b/activerecord/lib/active_record/validations/length.rb @@ -2,23 +2,11 @@ module ActiveRecord module Validations class LengthValidator < ActiveModel::Validations::LengthValidator # :nodoc: def validate_each(record, attribute, association_or_value) - return unless should_validate?(record) || associations_are_dirty?(record) if association_or_value.respond_to?(:loaded?) && association_or_value.loaded? association_or_value = association_or_value.target.reject(&:marked_for_destruction?) end super end - - def associations_are_dirty?(record) - attributes.any? do |attribute| - value = record.read_attribute_for_validation(attribute) - if value.respond_to?(:loaded?) && value.loaded? - value.target.any?(&:marked_for_destruction?) - else - false - end - end - end end module ClassMethods diff --git a/activerecord/lib/active_record/validations/presence.rb b/activerecord/lib/active_record/validations/presence.rb index 7e85ed43ac..ad82ea66c4 100644 --- a/activerecord/lib/active_record/validations/presence.rb +++ b/activerecord/lib/active_record/validations/presence.rb @@ -2,7 +2,6 @@ module ActiveRecord module Validations class PresenceValidator < ActiveModel::Validations::PresenceValidator # :nodoc: def validate_each(record, attribute, association_or_value) - return unless should_validate?(record) if record.class._reflect_on_association(attribute) association_or_value = Array.wrap(association_or_value).reject(&:marked_for_destruction?) end diff --git a/activerecord/lib/active_record/validations/uniqueness.rb b/activerecord/lib/active_record/validations/uniqueness.rb index aa2794f120..4a80cda0b8 100644 --- a/activerecord/lib/active_record/validations/uniqueness.rb +++ b/activerecord/lib/active_record/validations/uniqueness.rb @@ -11,15 +11,14 @@ module ActiveRecord end def validate_each(record, attribute, value) - return unless should_validate?(record) finder_class = find_finder_class_for(record) table = finder_class.arel_table value = map_enum_attribute(finder_class, attribute, value) relation = build_relation(finder_class, table, attribute, value) - if record.persisted? && finder_class.primary_key.to_s != attribute.to_s + if record.persisted? if finder_class.primary_key - relation = relation.where.not(finder_class.primary_key => record.id) + relation = relation.where.not(finder_class.primary_key => record.id_was || record.id) else raise UnknownPrimaryKey.new(finder_class, "Can not validate uniqueness for persisted record without primary key.") end @@ -57,14 +56,13 @@ module ActiveRecord value = value.attributes[reflection.klass.primary_key] unless value.nil? end - attribute_name = attribute.to_s - # the attribute may be an aliased attribute - if klass.attribute_aliases[attribute_name] - attribute = klass.attribute_aliases[attribute_name] - attribute_name = attribute.to_s + if klass.attribute_alias?(attribute) + attribute = klass.attribute_alias(attribute) end + attribute_name = attribute.to_s + column = klass.columns_hash[attribute_name] cast_type = klass.type_for_attribute(attribute_name) value = cast_type.serialize(value) @@ -73,15 +71,18 @@ module ActiveRecord value = value.to_s[0, column.limit] end - value = Arel::Nodes::Quoted.new(value) - comparison = if !options[:case_sensitive] && !value.nil? # will use SQL LOWER function before comparison, unless it detects a case insensitive collation klass.connection.case_insensitive_comparison(table, attribute, column, value) else klass.connection.case_sensitive_comparison(table, attribute, column, value) end - klass.unscoped.where(comparison) + if value.nil? + klass.unscoped.where(comparison) + else + bind = Relation::QueryAttribute.new(attribute_name, value, Type::Value.new) + klass.unscoped.where(comparison, bind) + end rescue RangeError klass.none end @@ -231,7 +232,6 @@ module ActiveRecord # # The following bundled adapters throw the ActiveRecord::RecordNotUnique exception: # - # * ActiveRecord::ConnectionAdapters::MysqlAdapter. # * ActiveRecord::ConnectionAdapters::Mysql2Adapter. # * ActiveRecord::ConnectionAdapters::SQLite3Adapter. # * ActiveRecord::ConnectionAdapters::PostgreSQLAdapter. diff --git a/activerecord/lib/rails/generators/active_record/migration/templates/create_table_migration.rb b/activerecord/lib/rails/generators/active_record/migration/templates/create_table_migration.rb index fadab2a1e6..5f7201cfe1 100644 --- a/activerecord/lib/rails/generators/active_record/migration/templates/create_table_migration.rb +++ b/activerecord/lib/rails/generators/active_record/migration/templates/create_table_migration.rb @@ -1,4 +1,4 @@ -class <%= migration_class_name %> < ActiveRecord::Migration +class <%= migration_class_name %> < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>] def change create_table :<%= table_name %><%= primary_key_type %> do |t| <% attributes.each do |attribute| -%> diff --git a/activerecord/lib/rails/generators/active_record/migration/templates/migration.rb b/activerecord/lib/rails/generators/active_record/migration/templates/migration.rb index 23a377db6a..481c70201b 100644 --- a/activerecord/lib/rails/generators/active_record/migration/templates/migration.rb +++ b/activerecord/lib/rails/generators/active_record/migration/templates/migration.rb @@ -1,4 +1,4 @@ -class <%= migration_class_name %> < ActiveRecord::Migration +class <%= migration_class_name %> < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>] <%- if migration_action == 'add' -%> def change <% attributes.each do |attribute| -%> @@ -19,7 +19,11 @@ class <%= migration_class_name %> < ActiveRecord::Migration def change create_join_table :<%= join_tables.first %>, :<%= join_tables.second %> do |t| <%- attributes.each do |attribute| -%> + <%- if attribute.reference? -%> + t.references :<%= attribute.name %><%= attribute.inject_options %> + <%- else -%> <%= '# ' unless attribute.has_index? -%>t.index <%= attribute.index_name %><%= attribute.inject_index_options %> + <%- end -%> <%- end -%> end end diff --git a/activerecord/lib/rails/generators/active_record/model/model_generator.rb b/activerecord/lib/rails/generators/active_record/model/model_generator.rb index 395951ac9d..f191eff5bf 100644 --- a/activerecord/lib/rails/generators/active_record/model/model_generator.rb +++ b/activerecord/lib/rails/generators/active_record/model/model_generator.rb @@ -22,30 +22,56 @@ module ActiveRecord def create_model_file template 'model.rb', File.join('app/models', class_path, "#{file_name}.rb") + generate_application_record end def create_module_file return if regular_class_path.empty? template 'module.rb', File.join('app/models', "#{class_path.join('/')}.rb") if behavior == :invoke - end - - def attributes_with_index - attributes.select { |a| !a.reference? && a.has_index? } - end - - def accessible_attributes - attributes.reject(&:reference?) + generate_application_record end hook_for :test_framework protected + def attributes_with_index + attributes.select { |a| !a.reference? && a.has_index? } + end + + # FIXME: Change this file to a symlink once RubyGems 2.5.0 is required. + def generate_application_record + if self.behavior == :invoke && !application_record_exist? + template 'application_record.rb', application_record_file_name + end + end + # Used by the migration template to determine the parent name of the model def parent_class_name - options[:parent] || "ActiveRecord::Base" + options[:parent] || determine_default_parent_class + end + + def application_record_exist? + file_exist = nil + in_root { file_exist = File.exist?(application_record_file_name) } + file_exist end + def application_record_file_name + @application_record_file_name ||= if mountable_engine? + "app/models/#{namespaced_path}/application_record.rb" + else + 'app/models/application_record.rb' + end + end + + def determine_default_parent_class + if application_record_exist? + "ApplicationRecord" + else + "ActiveRecord::Base" + end + end end end end diff --git a/activerecord/lib/rails/generators/active_record/model/templates/application_record.rb b/activerecord/lib/rails/generators/active_record/model/templates/application_record.rb new file mode 100644 index 0000000000..60050e0bf8 --- /dev/null +++ b/activerecord/lib/rails/generators/active_record/model/templates/application_record.rb @@ -0,0 +1,5 @@ +<% module_namespacing do -%> +class ApplicationRecord < ActiveRecord::Base + self.abstract_class = true +end +<% end -%> diff --git a/activerecord/test/cases/adapter_test.rb b/activerecord/test/cases/adapter_test.rb index 77c47f12d5..0ee147cdba 100644 --- a/activerecord/test/cases/adapter_test.rb +++ b/activerecord/test/cases/adapter_test.rb @@ -23,7 +23,8 @@ module ActiveRecord end def test_tables - tables = @connection.tables + tables = nil + ActiveSupport::Deprecation.silence { tables = @connection.tables } assert tables.include?("accounts") assert tables.include?("authors") assert tables.include?("tasks") @@ -31,9 +32,15 @@ module ActiveRecord end def test_table_exists? - assert @connection.table_exists?("accounts") - assert !@connection.table_exists?("nonexistingtable") - assert !@connection.table_exists?(nil) + ActiveSupport::Deprecation.silence do + assert @connection.table_exists?("accounts") + assert !@connection.table_exists?("nonexistingtable") + assert !@connection.table_exists?(nil) + end + end + + def test_table_exists_checking_both_tables_and_views_is_deprecated + assert_deprecated { @connection.table_exists?("accounts") } end def test_data_sources @@ -78,7 +85,7 @@ module ActiveRecord end end - if current_adapter?(:MysqlAdapter, :Mysql2Adapter) + if current_adapter?(:Mysql2Adapter) def test_charset assert_not_nil @connection.charset assert_not_equal 'character_set_database', @connection.charset @@ -246,6 +253,16 @@ module ActiveRecord assert_not_nil error.cause end end + + if current_adapter?(:Mysql2Adapter, :SQLite3Adapter) + def test_tables_returning_both_tables_and_views_is_deprecated + assert_deprecated { @connection.tables } + end + end + + def test_passing_arguments_to_tables_is_deprecated + assert_deprecated { @connection.tables(:books) } + end end class AdapterTestWithoutTransaction < ActiveRecord::TestCase diff --git a/activerecord/test/cases/adapters/mysql/active_schema_test.rb b/activerecord/test/cases/adapters/mysql/active_schema_test.rb deleted file mode 100644 index 0b5c9e1798..0000000000 --- a/activerecord/test/cases/adapters/mysql/active_schema_test.rb +++ /dev/null @@ -1,190 +0,0 @@ -require "cases/helper" -require 'support/connection_helper' - -class MysqlActiveSchemaTest < ActiveRecord::MysqlTestCase - include ConnectionHelper - - def setup - ActiveRecord::Base.connection.singleton_class.class_eval do - alias_method :execute_without_stub, :execute - def execute(sql, name = nil) return sql end - end - end - - teardown do - reset_connection - end - - def test_add_index - # add_index calls table_exists? and index_name_exists? which can't work since execute is stubbed - def (ActiveRecord::Base.connection).table_exists?(*); true; end - def (ActiveRecord::Base.connection).index_name_exists?(*); false; end - - expected = "CREATE INDEX `index_people_on_last_name` ON `people` (`last_name`) " - assert_equal expected, add_index(:people, :last_name, :length => nil) - - expected = "CREATE INDEX `index_people_on_last_name` ON `people` (`last_name`(10)) " - assert_equal expected, add_index(:people, :last_name, :length => 10) - - expected = "CREATE INDEX `index_people_on_last_name_and_first_name` ON `people` (`last_name`(15), `first_name`(15)) " - assert_equal expected, add_index(:people, [:last_name, :first_name], :length => 15) - - expected = "CREATE INDEX `index_people_on_last_name_and_first_name` ON `people` (`last_name`(15), `first_name`) " - assert_equal expected, add_index(:people, [:last_name, :first_name], :length => {:last_name => 15}) - - expected = "CREATE INDEX `index_people_on_last_name_and_first_name` ON `people` (`last_name`(15), `first_name`(10)) " - assert_equal expected, add_index(:people, [:last_name, :first_name], :length => {:last_name => 15, :first_name => 10}) - - %w(SPATIAL FULLTEXT UNIQUE).each do |type| - expected = "CREATE #{type} INDEX `index_people_on_last_name` ON `people` (`last_name`) " - assert_equal expected, add_index(:people, :last_name, :type => type) - end - - %w(btree hash).each do |using| - expected = "CREATE INDEX `index_people_on_last_name` USING #{using} ON `people` (`last_name`) " - assert_equal expected, add_index(:people, :last_name, :using => using) - end - - expected = "CREATE INDEX `index_people_on_last_name` USING btree ON `people` (`last_name`(10)) " - assert_equal expected, add_index(:people, :last_name, :length => 10, :using => :btree) - - expected = "CREATE INDEX `index_people_on_last_name` USING btree ON `people` (`last_name`(10)) ALGORITHM = COPY" - assert_equal expected, add_index(:people, :last_name, :length => 10, using: :btree, algorithm: :copy) - - assert_raise ArgumentError do - add_index(:people, :last_name, algorithm: :coyp) - end - - expected = "CREATE INDEX `index_people_on_last_name_and_first_name` USING btree ON `people` (`last_name`(15), `first_name`(15)) " - assert_equal expected, add_index(:people, [:last_name, :first_name], :length => 15, :using => :btree) - end - - def test_index_in_create - def (ActiveRecord::Base.connection).table_exists?(*); false; end - - %w(SPATIAL FULLTEXT UNIQUE).each do |type| - expected = "CREATE TABLE `people` (#{type} INDEX `index_people_on_last_name` (`last_name`) ) ENGINE=InnoDB" - actual = ActiveRecord::Base.connection.create_table(:people, id: false) do |t| - t.index :last_name, type: type - end - assert_equal expected, actual - end - - expected = "CREATE TABLE `people` ( INDEX `index_people_on_last_name` USING btree (`last_name`(10)) ) ENGINE=InnoDB" - actual = ActiveRecord::Base.connection.create_table(:people, id: false) do |t| - t.index :last_name, length: 10, using: :btree - end - assert_equal expected, actual - end - - def test_index_in_bulk_change - def (ActiveRecord::Base.connection).table_exists?(*); true; end - def (ActiveRecord::Base.connection).index_name_exists?(*); false; end - - %w(SPATIAL FULLTEXT UNIQUE).each do |type| - expected = "ALTER TABLE `people` ADD #{type} INDEX `index_people_on_last_name` (`last_name`)" - actual = ActiveRecord::Base.connection.change_table(:people, bulk: true) do |t| - t.index :last_name, type: type - end - assert_equal expected, actual - end - - expected = "ALTER TABLE `peaple` ADD INDEX `index_peaple_on_last_name` USING btree (`last_name`(10)), ALGORITHM = COPY" - actual = ActiveRecord::Base.connection.change_table(:peaple, bulk: true) do |t| - t.index :last_name, length: 10, using: :btree, algorithm: :copy - end - assert_equal expected, actual - end - - def test_drop_table - assert_equal "DROP TABLE `people`", drop_table(:people) - end - - def test_create_mysql_database_with_encoding - assert_equal "CREATE DATABASE `matt` DEFAULT CHARACTER SET `utf8`", create_database(:matt) - assert_equal "CREATE DATABASE `aimonetti` DEFAULT CHARACTER SET `latin1`", create_database(:aimonetti, {:charset => 'latin1'}) - assert_equal "CREATE DATABASE `matt_aimonetti` DEFAULT CHARACTER SET `big5` COLLATE `big5_chinese_ci`", create_database(:matt_aimonetti, {:charset => :big5, :collation => :big5_chinese_ci}) - end - - def test_recreate_mysql_database_with_encoding - create_database(:luca, {:charset => 'latin1'}) - assert_equal "CREATE DATABASE `luca` DEFAULT CHARACTER SET `latin1`", recreate_database(:luca, {:charset => 'latin1'}) - end - - def test_add_column - assert_equal "ALTER TABLE `people` ADD `last_name` varchar(255)", add_column(:people, :last_name, :string) - end - - def test_add_column_with_limit - assert_equal "ALTER TABLE `people` ADD `key` varchar(32)", add_column(:people, :key, :string, :limit => 32) - end - - def test_drop_table_with_specific_database - assert_equal "DROP TABLE `otherdb`.`people`", drop_table('otherdb.people') - end - - def test_add_timestamps - with_real_execute do - begin - ActiveRecord::Base.connection.create_table :delete_me - ActiveRecord::Base.connection.add_timestamps :delete_me, null: true - assert column_present?('delete_me', 'updated_at', 'datetime') - assert column_present?('delete_me', 'created_at', 'datetime') - ensure - ActiveRecord::Base.connection.drop_table :delete_me rescue nil - end - end - end - - def test_remove_timestamps - with_real_execute do - begin - ActiveRecord::Base.connection.create_table :delete_me do |t| - t.timestamps null: true - end - ActiveRecord::Base.connection.remove_timestamps :delete_me, { null: true } - assert !column_present?('delete_me', 'updated_at', 'datetime') - assert !column_present?('delete_me', 'created_at', 'datetime') - ensure - ActiveRecord::Base.connection.drop_table :delete_me rescue nil - end - end - end - - def test_indexes_in_create - ActiveRecord::Base.connection.stubs(:table_exists?).with(:temp).returns(false) - ActiveRecord::Base.connection.stubs(:index_name_exists?).with(:index_temp_on_zip).returns(false) - - expected = "CREATE TEMPORARY TABLE `temp` ( INDEX `index_temp_on_zip` (`zip`) ) ENGINE=InnoDB AS SELECT id, name, zip FROM a_really_complicated_query" - actual = ActiveRecord::Base.connection.create_table(:temp, temporary: true, as: "SELECT id, name, zip FROM a_really_complicated_query") do |t| - t.index :zip - end - - assert_equal expected, actual - end - - private - def with_real_execute - ActiveRecord::Base.connection.singleton_class.class_eval do - alias_method :execute_with_stub, :execute - remove_method :execute - alias_method :execute, :execute_without_stub - end - - yield - ensure - ActiveRecord::Base.connection.singleton_class.class_eval do - remove_method :execute - alias_method :execute, :execute_with_stub - end - end - - def method_missing(method_symbol, *arguments) - ActiveRecord::Base.connection.send(method_symbol, *arguments) - end - - def column_present?(table_name, column_name, type) - results = ActiveRecord::Base.connection.select_all("SHOW FIELDS FROM #{table_name} LIKE '#{column_name}'") - results.first && results.first['Type'] == type - end -end diff --git a/activerecord/test/cases/adapters/mysql/case_sensitivity_test.rb b/activerecord/test/cases/adapters/mysql/case_sensitivity_test.rb deleted file mode 100644 index 98d44315dd..0000000000 --- a/activerecord/test/cases/adapters/mysql/case_sensitivity_test.rb +++ /dev/null @@ -1,54 +0,0 @@ -require "cases/helper" - -class MysqlCaseSensitivityTest < ActiveRecord::MysqlTestCase - class CollationTest < ActiveRecord::Base - end - - repair_validations(CollationTest) - - def test_columns_include_collation_different_from_table - assert_equal 'utf8_bin', CollationTest.columns_hash['string_cs_column'].collation - assert_equal 'utf8_general_ci', CollationTest.columns_hash['string_ci_column'].collation - end - - def test_case_sensitive - assert !CollationTest.columns_hash['string_ci_column'].case_sensitive? - assert CollationTest.columns_hash['string_cs_column'].case_sensitive? - end - - def test_case_insensitive_comparison_for_ci_column - CollationTest.validates_uniqueness_of(:string_ci_column, :case_sensitive => false) - CollationTest.create!(:string_ci_column => 'A') - invalid = CollationTest.new(:string_ci_column => 'a') - queries = assert_sql { invalid.save } - ci_uniqueness_query = queries.detect { |q| q.match(/string_ci_column/) } - assert_no_match(/lower/i, ci_uniqueness_query) - end - - def test_case_insensitive_comparison_for_cs_column - CollationTest.validates_uniqueness_of(:string_cs_column, :case_sensitive => false) - CollationTest.create!(:string_cs_column => 'A') - invalid = CollationTest.new(:string_cs_column => 'a') - queries = assert_sql { invalid.save } - cs_uniqueness_query = queries.detect { |q| q.match(/string_cs_column/) } - assert_match(/lower/i, cs_uniqueness_query) - end - - def test_case_sensitive_comparison_for_ci_column - CollationTest.validates_uniqueness_of(:string_ci_column, :case_sensitive => true) - CollationTest.create!(:string_ci_column => 'A') - invalid = CollationTest.new(:string_ci_column => 'A') - queries = assert_sql { invalid.save } - ci_uniqueness_query = queries.detect { |q| q.match(/string_ci_column/) } - assert_match(/binary/i, ci_uniqueness_query) - end - - def test_case_sensitive_comparison_for_cs_column - CollationTest.validates_uniqueness_of(:string_cs_column, :case_sensitive => true) - CollationTest.create!(:string_cs_column => 'A') - invalid = CollationTest.new(:string_cs_column => 'A') - queries = assert_sql { invalid.save } - cs_uniqueness_query = queries.detect { |q| q.match(/string_cs_column/) } - assert_no_match(/binary/i, cs_uniqueness_query) - end -end diff --git a/activerecord/test/cases/adapters/mysql/charset_collation_test.rb b/activerecord/test/cases/adapters/mysql/charset_collation_test.rb deleted file mode 100644 index f2117a97e6..0000000000 --- a/activerecord/test/cases/adapters/mysql/charset_collation_test.rb +++ /dev/null @@ -1,54 +0,0 @@ -require "cases/helper" -require 'support/schema_dumping_helper' - -class MysqlCharsetCollationTest < ActiveRecord::MysqlTestCase - include SchemaDumpingHelper - self.use_transactional_tests = false - - setup do - @connection = ActiveRecord::Base.connection - @connection.create_table :charset_collations, force: true do |t| - t.string :string_ascii_bin, charset: 'ascii', collation: 'ascii_bin' - t.text :text_ucs2_unicode_ci, charset: 'ucs2', collation: 'ucs2_unicode_ci' - end - end - - teardown do - @connection.drop_table :charset_collations, if_exists: true - end - - test "string column with charset and collation" do - column = @connection.columns(:charset_collations).find { |c| c.name == 'string_ascii_bin' } - assert_equal :string, column.type - assert_equal 'ascii_bin', column.collation - end - - test "text column with charset and collation" do - column = @connection.columns(:charset_collations).find { |c| c.name == 'text_ucs2_unicode_ci' } - assert_equal :text, column.type - assert_equal 'ucs2_unicode_ci', column.collation - end - - test "add column with charset and collation" do - @connection.add_column :charset_collations, :title, :string, charset: 'utf8', collation: 'utf8_bin' - - column = @connection.columns(:charset_collations).find { |c| c.name == 'title' } - assert_equal :string, column.type - assert_equal 'utf8_bin', column.collation - end - - test "change column with charset and collation" do - @connection.add_column :charset_collations, :description, :string, charset: 'utf8', collation: 'utf8_unicode_ci' - @connection.change_column :charset_collations, :description, :text, charset: 'utf8', collation: 'utf8_general_ci' - - column = @connection.columns(:charset_collations).find { |c| c.name == 'description' } - assert_equal :text, column.type - assert_equal 'utf8_general_ci', column.collation - end - - test "schema dump includes collation" do - output = dump_table_schema("charset_collations") - assert_match %r{t.string\s+"string_ascii_bin",\s+limit: 255,\s+collation: "ascii_bin"$}, output - assert_match %r{t.text\s+"text_ucs2_unicode_ci",\s+limit: 65535,\s+collation: "ucs2_unicode_ci"$}, output - end -end diff --git a/activerecord/test/cases/adapters/mysql/connection_test.rb b/activerecord/test/cases/adapters/mysql/connection_test.rb deleted file mode 100644 index 75653ee9af..0000000000 --- a/activerecord/test/cases/adapters/mysql/connection_test.rb +++ /dev/null @@ -1,210 +0,0 @@ -require "cases/helper" -require 'support/connection_helper' -require 'support/ddl_helper' - -class MysqlConnectionTest < ActiveRecord::MysqlTestCase - include ConnectionHelper - include DdlHelper - - class Klass < ActiveRecord::Base - end - - def setup - super - @connection = ActiveRecord::Base.connection - end - - def test_mysql_reconnect_attribute_after_connection_with_reconnect_true - run_without_connection do |orig_connection| - ActiveRecord::Base.establish_connection(orig_connection.merge({:reconnect => true})) - assert ActiveRecord::Base.connection.raw_connection.reconnect - end - end - - unless ARTest.connection_config['arunit']['socket'] - def test_connect_with_url - run_without_connection do - ar_config = ARTest.connection_config['arunit'] - - url = "mysql://#{ar_config["username"]}:#{ar_config["password"]}@localhost/#{ar_config["database"]}" - Klass.establish_connection(url) - assert_equal ar_config['database'], Klass.connection.current_database - end - end - end - - def test_mysql_reconnect_attribute_after_connection_with_reconnect_false - run_without_connection do |orig_connection| - ActiveRecord::Base.establish_connection(orig_connection.merge({:reconnect => false})) - assert !ActiveRecord::Base.connection.raw_connection.reconnect - end - end - - def test_no_automatic_reconnection_after_timeout - assert @connection.active? - @connection.update('set @@wait_timeout=1') - sleep 2 - assert !@connection.active? - - # Repair all fixture connections so other tests won't break. - @fixture_connections.each(&:verify!) - end - - def test_successful_reconnection_after_timeout_with_manual_reconnect - assert @connection.active? - @connection.update('set @@wait_timeout=1') - sleep 2 - @connection.reconnect! - assert @connection.active? - end - - def test_successful_reconnection_after_timeout_with_verify - assert @connection.active? - @connection.update('set @@wait_timeout=1') - sleep 2 - @connection.verify! - assert @connection.active? - end - - def test_bind_value_substitute - bind_param = @connection.substitute_at('foo') - assert_equal Arel.sql('?'), bind_param.to_sql - end - - def test_exec_no_binds - with_example_table do - result = @connection.exec_query('SELECT id, data FROM ex') - assert_equal 0, result.rows.length - assert_equal 2, result.columns.length - assert_equal %w{ id data }, result.columns - - @connection.exec_query('INSERT INTO ex (id, data) VALUES (1, "foo")') - - # if there are no bind parameters, it will return a string (due to - # the libmysql api) - result = @connection.exec_query('SELECT id, data FROM ex') - assert_equal 1, result.rows.length - assert_equal 2, result.columns.length - - assert_equal [['1', 'foo']], result.rows - end - end - - def test_exec_with_binds - with_example_table do - @connection.exec_query('INSERT INTO ex (id, data) VALUES (1, "foo")') - result = @connection.exec_query( - 'SELECT id, data FROM ex WHERE id = ?', nil, [ActiveRecord::Relation::QueryAttribute.new("id", 1, ActiveRecord::Type::Value.new)]) - - assert_equal 1, result.rows.length - assert_equal 2, result.columns.length - - assert_equal [[1, 'foo']], result.rows - end - end - - def test_exec_typecasts_bind_vals - with_example_table do - @connection.exec_query('INSERT INTO ex (id, data) VALUES (1, "foo")') - bind = ActiveRecord::Relation::QueryAttribute.new("id", "1-fuu", ActiveRecord::Type::Integer.new) - - result = @connection.exec_query( - 'SELECT id, data FROM ex WHERE id = ?', nil, [bind]) - - assert_equal 1, result.rows.length - assert_equal 2, result.columns.length - - assert_equal [[1, 'foo']], result.rows - end - end - - def test_mysql_connection_collation_is_configured - assert_equal 'utf8_unicode_ci', @connection.show_variable('collation_connection') - assert_equal 'utf8_general_ci', ARUnit2Model.connection.show_variable('collation_connection') - end - - def test_mysql_default_in_strict_mode - result = @connection.exec_query "SELECT @@SESSION.sql_mode" - assert_equal [["STRICT_ALL_TABLES"]], result.rows - end - - def test_mysql_strict_mode_disabled - run_without_connection do |orig_connection| - ActiveRecord::Base.establish_connection(orig_connection.merge({:strict => false})) - result = ActiveRecord::Base.connection.exec_query "SELECT @@SESSION.sql_mode" - assert_equal [['']], result.rows - end - end - - def test_mysql_strict_mode_specified_default - run_without_connection do |orig_connection| - ActiveRecord::Base.establish_connection(orig_connection.merge({strict: :default})) - global_sql_mode = ActiveRecord::Base.connection.exec_query "SELECT @@GLOBAL.sql_mode" - session_sql_mode = ActiveRecord::Base.connection.exec_query "SELECT @@SESSION.sql_mode" - assert_equal global_sql_mode.rows, session_sql_mode.rows - end - end - - def test_mysql_set_session_variable - run_without_connection do |orig_connection| - ActiveRecord::Base.establish_connection(orig_connection.deep_merge({:variables => {:default_week_format => 3}})) - session_mode = ActiveRecord::Base.connection.exec_query "SELECT @@SESSION.DEFAULT_WEEK_FORMAT" - assert_equal 3, session_mode.rows.first.first.to_i - end - end - - def test_mysql_sql_mode_variable_overrides_strict_mode - run_without_connection do |orig_connection| - ActiveRecord::Base.establish_connection(orig_connection.deep_merge(variables: { 'sql_mode' => 'ansi' })) - result = ActiveRecord::Base.connection.exec_query 'SELECT @@SESSION.sql_mode' - assert_not_equal [['STRICT_ALL_TABLES']], result.rows - end - end - - def test_mysql_set_session_variable_to_default - run_without_connection do |orig_connection| - ActiveRecord::Base.establish_connection(orig_connection.deep_merge({:variables => {:default_week_format => :default}})) - global_mode = ActiveRecord::Base.connection.exec_query "SELECT @@GLOBAL.DEFAULT_WEEK_FORMAT" - session_mode = ActiveRecord::Base.connection.exec_query "SELECT @@SESSION.DEFAULT_WEEK_FORMAT" - assert_equal global_mode.rows, session_mode.rows - end - end - - def test_get_and_release_advisory_lock - key = "test_key" - - got_lock = @connection.get_advisory_lock(key) - assert got_lock, "get_advisory_lock should have returned true but it didn't" - - assert_equal test_lock_free(key), false, - "expected the test advisory lock to be held but it wasn't" - - released_lock = @connection.release_advisory_lock(key) - assert released_lock, "expected release_advisory_lock to return true but it didn't" - - assert test_lock_free(key), 'expected the test key to be available after releasing' - end - - def test_release_non_existent_advisory_lock - fake_key = "fake_key" - released_non_existent_lock = @connection.release_advisory_lock(fake_key) - assert_equal released_non_existent_lock, false, - 'expected release_advisory_lock to return false when there was no lock to release' - end - - protected - - def test_lock_free(key) - @connection.select_value("SELECT IS_FREE_LOCK('#{key}');") == '1' - end - - private - - def with_example_table(&block) - definition ||= <<-SQL - `id` int auto_increment PRIMARY KEY, - `data` varchar(255) - SQL - super(@connection, 'ex', definition, &block) - end -end diff --git a/activerecord/test/cases/adapters/mysql/consistency_test.rb b/activerecord/test/cases/adapters/mysql/consistency_test.rb deleted file mode 100644 index 743f6436e4..0000000000 --- a/activerecord/test/cases/adapters/mysql/consistency_test.rb +++ /dev/null @@ -1,49 +0,0 @@ -require "cases/helper" - -class MysqlConsistencyTest < ActiveRecord::MysqlTestCase - self.use_transactional_tests = false - - class Consistency < ActiveRecord::Base - self.table_name = "mysql_consistency" - end - - setup do - @old_emulate_booleans = ActiveRecord::ConnectionAdapters::MysqlAdapter.emulate_booleans - ActiveRecord::ConnectionAdapters::MysqlAdapter.emulate_booleans = false - - @connection = ActiveRecord::Base.connection - @connection.clear_cache! - @connection.create_table("mysql_consistency") do |t| - t.boolean "a_bool" - t.string "a_string" - end - Consistency.reset_column_information - Consistency.create! - end - - teardown do - ActiveRecord::ConnectionAdapters::MysqlAdapter.emulate_booleans = @old_emulate_booleans - @connection.drop_table "mysql_consistency" - end - - test "boolean columns with random value type cast to 0 when emulate_booleans is false" do - with_new = Consistency.new - with_last = Consistency.last - with_new.a_bool = 'wibble' - with_last.a_bool = 'wibble' - - assert_equal 0, with_new.a_bool - assert_equal 0, with_last.a_bool - end - - test "string columns call #to_s" do - with_new = Consistency.new - with_last = Consistency.last - thing = Object.new - with_new.a_string = thing - with_last.a_string = thing - - assert_equal thing.to_s, with_new.a_string - assert_equal thing.to_s, with_last.a_string - end -end diff --git a/activerecord/test/cases/adapters/mysql/enum_test.rb b/activerecord/test/cases/adapters/mysql/enum_test.rb deleted file mode 100644 index ef8ee0a6e3..0000000000 --- a/activerecord/test/cases/adapters/mysql/enum_test.rb +++ /dev/null @@ -1,10 +0,0 @@ -require "cases/helper" - -class MysqlEnumTest < ActiveRecord::MysqlTestCase - class EnumTest < ActiveRecord::Base - end - - def test_enum_limit - assert_equal 6, EnumTest.columns.first.limit - end -end diff --git a/activerecord/test/cases/adapters/mysql/explain_test.rb b/activerecord/test/cases/adapters/mysql/explain_test.rb deleted file mode 100644 index c44c1e6648..0000000000 --- a/activerecord/test/cases/adapters/mysql/explain_test.rb +++ /dev/null @@ -1,21 +0,0 @@ -require "cases/helper" -require 'models/developer' -require 'models/computer' - -class MysqlExplainTest < ActiveRecord::MysqlTestCase - fixtures :developers - - def test_explain_for_one_query - explain = Developer.where(id: 1).explain - assert_match %(EXPLAIN for: SELECT `developers`.* FROM `developers` WHERE `developers`.`id` = 1), explain - assert_match %r(developers |.* const), explain - end - - def test_explain_with_eager_loading - explain = Developer.where(id: 1).includes(:audit_logs).explain - assert_match %(EXPLAIN for: SELECT `developers`.* FROM `developers` WHERE `developers`.`id` = 1), explain - assert_match %r(developers |.* const), explain - assert_match %(EXPLAIN for: SELECT `audit_logs`.* FROM `audit_logs` WHERE `audit_logs`.`developer_id` = 1), explain - assert_match %r(audit_logs |.* ALL), explain - end -end diff --git a/activerecord/test/cases/adapters/mysql/mysql_adapter_test.rb b/activerecord/test/cases/adapters/mysql/mysql_adapter_test.rb deleted file mode 100644 index d2ce48fc00..0000000000 --- a/activerecord/test/cases/adapters/mysql/mysql_adapter_test.rb +++ /dev/null @@ -1,119 +0,0 @@ -require "cases/helper" -require 'support/ddl_helper' - -module ActiveRecord - module ConnectionAdapters - class MysqlAdapterTest < ActiveRecord::MysqlTestCase - include DdlHelper - - def setup - @conn = ActiveRecord::Base.connection - end - - def test_bad_connection_mysql - assert_raise ActiveRecord::NoDatabaseError do - configuration = ActiveRecord::Base.configurations['arunit'].merge(database: 'inexistent_activerecord_unittest') - connection = ActiveRecord::Base.mysql_connection(configuration) - connection.drop_table 'ex', if_exists: true - end - end - - def test_valid_column - with_example_table do - column = @conn.columns('ex').find { |col| col.name == 'id' } - assert @conn.valid_type?(column.type) - end - end - - def test_invalid_column - assert_not @conn.valid_type?(:foobar) - end - - def test_client_encoding - assert_equal Encoding::UTF_8, @conn.client_encoding - end - - def test_exec_insert_number - with_example_table do - insert(@conn, 'number' => 10) - - result = @conn.exec_query('SELECT number FROM ex WHERE number = 10') - - assert_equal 1, result.rows.length - # if there are no bind parameters, it will return a string (due to - # the libmysql api) - assert_equal '10', result.rows.last.last - end - end - - def test_exec_insert_string - with_example_table do - str = 'ã„ãŸã ãã¾ã™ï¼' - insert(@conn, 'number' => 10, 'data' => str) - - result = @conn.exec_query('SELECT number, data FROM ex WHERE number = 10') - - value = result.rows.last.last - - # FIXME: this should probably be inside the mysql AR adapter? - value.force_encoding(@conn.client_encoding) - - # The strings in this file are utf-8, so transcode to utf-8 - value.encode!(Encoding::UTF_8) - - assert_equal str, value - end - end - - def test_composite_primary_key - with_example_table '`id` INT, `number` INT, foo INT, PRIMARY KEY (`id`, `number`)' do - assert_nil @conn.primary_key('ex') - end - end - - def test_tinyint_integer_typecasting - with_example_table '`status` TINYINT(4)' do - insert(@conn, { 'status' => 2 }, 'ex') - - result = @conn.exec_query('SELECT status FROM ex') - - assert_equal 2, result.column_types['status'].deserialize(result.last['status']) - end - end - - def test_supports_extensions - assert_not @conn.supports_extensions?, 'does not support extensions' - end - - def test_respond_to_enable_extension - assert @conn.respond_to?(:enable_extension) - end - - def test_respond_to_disable_extension - assert @conn.respond_to?(:disable_extension) - end - - private - def insert(ctx, data, table='ex') - binds = data.map { |name, value| - Relation::QueryAttribute.new(name, value, Type::Value.new) - } - columns = binds.map(&:name) - - sql = "INSERT INTO #{table} (#{columns.join(", ")}) - VALUES (#{(['?'] * columns.length).join(', ')})" - - ctx.exec_insert(sql, 'SQL', binds) - end - - def with_example_table(definition = nil, &block) - definition ||= <<-SQL - `id` int auto_increment PRIMARY KEY, - `number` integer, - `data` varchar(255) - SQL - super(@conn, 'ex', definition, &block) - end - end - end -end diff --git a/activerecord/test/cases/adapters/mysql/quoting_test.rb b/activerecord/test/cases/adapters/mysql/quoting_test.rb deleted file mode 100644 index 2024aa36ab..0000000000 --- a/activerecord/test/cases/adapters/mysql/quoting_test.rb +++ /dev/null @@ -1,29 +0,0 @@ -require "cases/helper" - -class MysqlQuotingTest < ActiveRecord::MysqlTestCase - def setup - @conn = ActiveRecord::Base.connection - end - - def test_type_cast_true - assert_equal 1, @conn.type_cast(true) - end - - def test_type_cast_false - assert_equal 0, @conn.type_cast(false) - end - - def test_quoted_date_precision_for_gte_564 - @conn.stubs(:full_version).returns('5.6.4') - @conn.remove_instance_variable(:@version) if @conn.instance_variable_defined?(:@version) - t = Time.now.change(usec: 1) - assert_match(/\.000001\z/, @conn.quoted_date(t)) - end - - def test_quoted_date_precision_for_lt_564 - @conn.stubs(:full_version).returns('5.6.3') - @conn.remove_instance_variable(:@version) if @conn.instance_variable_defined?(:@version) - t = Time.now.change(usec: 1) - assert_no_match(/\.000001\z/, @conn.quoted_date(t)) - end -end diff --git a/activerecord/test/cases/adapters/mysql/reserved_word_test.rb b/activerecord/test/cases/adapters/mysql/reserved_word_test.rb deleted file mode 100644 index 4ea1d9ad36..0000000000 --- a/activerecord/test/cases/adapters/mysql/reserved_word_test.rb +++ /dev/null @@ -1,153 +0,0 @@ -require "cases/helper" - -# a suite of tests to ensure the ConnectionAdapters#MysqlAdapter can handle tables with -# reserved word names (ie: group, order, values, etc...) -class MysqlReservedWordTest < ActiveRecord::MysqlTestCase - class Group < ActiveRecord::Base - Group.table_name = 'group' - belongs_to :select - has_one :values - end - - class Select < ActiveRecord::Base - Select.table_name = 'select' - has_many :groups - end - - class Values < ActiveRecord::Base - Values.table_name = 'values' - end - - class Distinct < ActiveRecord::Base - Distinct.table_name = 'distinct' - has_and_belongs_to_many :selects - has_many :values, :through => :groups - end - - def setup - @connection = ActiveRecord::Base.connection - - # we call execute directly here (and do similar below) because ActiveRecord::Base#create_table() - # will fail with these table names if these test cases fail - - create_tables_directly 'group'=>'id int auto_increment primary key, `order` varchar(255), select_id int', - 'select'=>'id int auto_increment primary key', - 'values'=>'id int auto_increment primary key, group_id int', - 'distinct'=>'id int auto_increment primary key', - 'distinct_select'=>'distinct_id int, select_id int' - end - - teardown do - drop_tables_directly ['group', 'select', 'values', 'distinct', 'distinct_select', 'order'] - end - - # create tables with reserved-word names and columns - def test_create_tables - assert_nothing_raised { - @connection.create_table :order do |t| - t.column :group, :string - end - } - end - - # rename tables with reserved-word names - def test_rename_tables - assert_nothing_raised { @connection.rename_table(:group, :order) } - end - - # alter column with a reserved-word name in a table with a reserved-word name - def test_change_columns - assert_nothing_raised { @connection.change_column_default(:group, :order, 'whatever') } - #the quoting here will reveal any double quoting issues in change_column's interaction with the column method in the adapter - assert_nothing_raised { @connection.change_column('group', 'order', :Int, :default => 0) } - assert_nothing_raised { @connection.rename_column(:group, :order, :values) } - end - - # introspect table with reserved word name - def test_introspect - assert_nothing_raised { @connection.columns(:group) } - assert_nothing_raised { @connection.indexes(:group) } - end - - #fixtures - self.use_instantiated_fixtures = true - self.use_transactional_tests = false - - #activerecord model class with reserved-word table name - def test_activerecord_model - create_test_fixtures :select, :distinct, :group, :values, :distinct_select - x = nil - assert_nothing_raised { x = Group.new } - x.order = 'x' - assert_nothing_raised { x.save } - x.order = 'y' - assert_nothing_raised { x.save } - assert_nothing_raised { Group.find_by_order('y') } - assert_nothing_raised { Group.find(1) } - Group.find(1) - end - - # has_one association with reserved-word table name - def test_has_one_associations - create_test_fixtures :select, :distinct, :group, :values, :distinct_select - v = nil - assert_nothing_raised { v = Group.find(1).values } - assert_equal 2, v.id - end - - # belongs_to association with reserved-word table name - def test_belongs_to_associations - create_test_fixtures :select, :distinct, :group, :values, :distinct_select - gs = nil - assert_nothing_raised { gs = Select.find(2).groups } - assert_equal gs.length, 2 - assert(gs.collect(&:id).sort == [2, 3]) - end - - # has_and_belongs_to_many with reserved-word table name - def test_has_and_belongs_to_many - create_test_fixtures :select, :distinct, :group, :values, :distinct_select - s = nil - assert_nothing_raised { s = Distinct.find(1).selects } - assert_equal s.length, 2 - assert(s.collect(&:id).sort == [1, 2]) - end - - # activerecord model introspection with reserved-word table and column names - def test_activerecord_introspection - assert_nothing_raised { Group.table_exists? } - assert_nothing_raised { Group.columns } - end - - # Calculations - def test_calculations_work_with_reserved_words - assert_nothing_raised { Group.count } - end - - def test_associations_work_with_reserved_words - assert_nothing_raised { Select.all.merge!(:includes => [:groups]).to_a } - end - - #the following functions were added to DRY test cases - - private - # custom fixture loader, uses FixtureSet#create_fixtures and appends base_path to the current file's path - def create_test_fixtures(*fixture_names) - ActiveRecord::FixtureSet.create_fixtures(FIXTURES_ROOT + "/reserved_words", fixture_names) - end - - # custom drop table, uses execute on connection to drop a table if it exists. note: escapes table_name - def drop_tables_directly(table_names, connection = @connection) - table_names.each do |name| - connection.drop_table name, if_exists: true - end - end - - # custom create table, uses execute on connection to create a table, note: escapes table_name, does NOT escape columns - def create_tables_directly (tables, connection = @connection) - tables.each do |table_name, column_properties| - connection.execute("CREATE TABLE `#{table_name}` ( #{column_properties} )") - end - end - -end diff --git a/activerecord/test/cases/adapters/mysql/schema_test.rb b/activerecord/test/cases/adapters/mysql/schema_test.rb deleted file mode 100644 index 566324dcae..0000000000 --- a/activerecord/test/cases/adapters/mysql/schema_test.rb +++ /dev/null @@ -1,100 +0,0 @@ -require "cases/helper" -require 'models/post' -require 'models/comment' - -module ActiveRecord - module ConnectionAdapters - class MysqlSchemaTest < ActiveRecord::MysqlTestCase - fixtures :posts - - def setup - @connection = ActiveRecord::Base.connection - db = Post.connection_pool.spec.config[:database] - table = Post.table_name - @db_name = db - - @omgpost = Class.new(ActiveRecord::Base) do - self.inheritance_column = :disabled - self.table_name = "#{db}.#{table}" - def self.name; 'Post'; end - end - end - - class MysqlDouble < ActiveRecord::Base - self.table_name = "mysql_doubles" - end - - def test_float_limits - @connection.create_table :mysql_doubles do |t| - t.float :float_no_limit - t.float :float_short, limit: 5 - t.float :float_long, limit: 53 - - t.float :float_23, limit: 23 - t.float :float_24, limit: 24 - t.float :float_25, limit: 25 - end - - MysqlDouble.reset_column_information - - column_no_limit = MysqlDouble.columns.find { |c| c.name == 'float_no_limit' } - column_short = MysqlDouble.columns.find { |c| c.name == 'float_short' } - column_long = MysqlDouble.columns.find { |c| c.name == 'float_long' } - - column_23 = MysqlDouble.columns.find { |c| c.name == 'float_23' } - column_24 = MysqlDouble.columns.find { |c| c.name == 'float_24' } - column_25 = MysqlDouble.columns.find { |c| c.name == 'float_25' } - - # Mysql floats are precision 0..24, Mysql doubles are precision 25..53 - assert_equal 24, column_no_limit.limit - assert_equal 24, column_short.limit - assert_equal 53, column_long.limit - - assert_equal 24, column_23.limit - assert_equal 24, column_24.limit - assert_equal 53, column_25.limit - ensure - @connection.drop_table "mysql_doubles", if_exists: true - end - - def test_schema - assert @omgpost.first - end - - def test_primary_key - assert_equal 'id', @omgpost.primary_key - end - - def test_table_exists? - name = @omgpost.table_name - assert @connection.table_exists?(name), "#{name} table should exist" - end - - def test_table_exists_wrong_schema - assert(!@connection.table_exists?("#{@db_name}.zomg"), "table should not exist") - end - - def test_dump_indexes - index_a_name = 'index_key_tests_on_snack' - index_b_name = 'index_key_tests_on_pizza' - index_c_name = 'index_key_tests_on_awesome' - - table = 'key_tests' - - indexes = @connection.indexes(table).sort_by(&:name) - assert_equal 3,indexes.size - - index_a = indexes.select{|i| i.name == index_a_name}[0] - index_b = indexes.select{|i| i.name == index_b_name}[0] - index_c = indexes.select{|i| i.name == index_c_name}[0] - assert_equal :btree, index_a.using - assert_nil index_a.type - assert_equal :btree, index_b.using - assert_nil index_b.type - - assert_nil index_c.using - assert_equal :fulltext, index_c.type - end - end - end -end diff --git a/activerecord/test/cases/adapters/mysql/sp_test.rb b/activerecord/test/cases/adapters/mysql/sp_test.rb deleted file mode 100644 index 7849248dcc..0000000000 --- a/activerecord/test/cases/adapters/mysql/sp_test.rb +++ /dev/null @@ -1,30 +0,0 @@ -require "cases/helper" -require 'models/topic' -require 'models/reply' - -class MysqlStoredProcedureTest < ActiveRecord::MysqlTestCase - fixtures :topics - - def setup - @connection = ActiveRecord::Base.connection - unless ActiveRecord::Base.connection.version >= '5.6.0' || Mysql.const_defined?(:CLIENT_MULTI_RESULTS) - skip("no stored procedure support") - end - end - - # Test that MySQL allows multiple results for stored procedures - # - # In MySQL 5.6, CLIENT_MULTI_RESULTS is enabled by default. - # http://dev.mysql.com/doc/refman/5.6/en/call.html - def test_multi_results - rows = @connection.select_rows('CALL ten();') - assert_equal 10, rows[0][0].to_i, "ten() did not return 10 as expected: #{rows.inspect}" - assert @connection.active?, "Bad connection use by 'MysqlAdapter.select_rows'" - end - - def test_multi_results_from_find_by_sql - topics = Topic.find_by_sql 'CALL topics(3);' - assert_equal 3, topics.size - assert @connection.active?, "Bad connection use by 'MysqlAdapter.select'" - end -end diff --git a/activerecord/test/cases/adapters/mysql/sql_types_test.rb b/activerecord/test/cases/adapters/mysql/sql_types_test.rb deleted file mode 100644 index d18579f242..0000000000 --- a/activerecord/test/cases/adapters/mysql/sql_types_test.rb +++ /dev/null @@ -1,14 +0,0 @@ -require "cases/helper" - -class MysqlSqlTypesTest < ActiveRecord::MysqlTestCase - def test_binary_types - assert_equal 'varbinary(64)', type_to_sql(:binary, 64) - assert_equal 'varbinary(4095)', type_to_sql(:binary, 4095) - assert_equal 'blob', type_to_sql(:binary, 4096) - assert_equal 'blob', type_to_sql(:binary) - end - - def type_to_sql(*args) - ActiveRecord::Base.connection.type_to_sql(*args) - end -end diff --git a/activerecord/test/cases/adapters/mysql/statement_pool_test.rb b/activerecord/test/cases/adapters/mysql/statement_pool_test.rb deleted file mode 100644 index 0d1f968022..0000000000 --- a/activerecord/test/cases/adapters/mysql/statement_pool_test.rb +++ /dev/null @@ -1,19 +0,0 @@ -require 'cases/helper' - -class MysqlStatementPoolTest < ActiveRecord::MysqlTestCase - if Process.respond_to?(:fork) - def test_cache_is_per_pid - cache = ActiveRecord::ConnectionAdapters::MysqlAdapter::StatementPool.new(10) - cache['foo'] = 'bar' - assert_equal 'bar', cache['foo'] - - pid = fork { - lookup = cache['foo']; - exit!(!lookup) - } - - Process.waitpid pid - assert $?.success?, 'process should exit successfully' - end - end -end diff --git a/activerecord/test/cases/adapters/mysql/table_options_test.rb b/activerecord/test/cases/adapters/mysql/table_options_test.rb deleted file mode 100644 index 99df6d6cba..0000000000 --- a/activerecord/test/cases/adapters/mysql/table_options_test.rb +++ /dev/null @@ -1,42 +0,0 @@ -require "cases/helper" -require 'support/schema_dumping_helper' - -class MysqlTableOptionsTest < ActiveRecord::MysqlTestCase - include SchemaDumpingHelper - - def setup - @connection = ActiveRecord::Base.connection - end - - def teardown - @connection.drop_table "mysql_table_options", if_exists: true - end - - test "table options with ENGINE" do - @connection.create_table "mysql_table_options", force: true, options: "ENGINE=MyISAM" - output = dump_table_schema("mysql_table_options") - options = %r{create_table "mysql_table_options", force: :cascade, options: "(?<options>.*)"}.match(output)[:options] - assert_match %r{ENGINE=MyISAM}, options - end - - test "table options with ROW_FORMAT" do - @connection.create_table "mysql_table_options", force: true, options: "ROW_FORMAT=REDUNDANT" - output = dump_table_schema("mysql_table_options") - options = %r{create_table "mysql_table_options", force: :cascade, options: "(?<options>.*)"}.match(output)[:options] - assert_match %r{ROW_FORMAT=REDUNDANT}, options - end - - test "table options with CHARSET" do - @connection.create_table "mysql_table_options", force: true, options: "CHARSET=utf8mb4" - output = dump_table_schema("mysql_table_options") - options = %r{create_table "mysql_table_options", force: :cascade, options: "(?<options>.*)"}.match(output)[:options] - assert_match %r{CHARSET=utf8mb4}, options - end - - test "table options with COLLATE" do - @connection.create_table "mysql_table_options", force: true, options: "COLLATE=utf8mb4_bin" - output = dump_table_schema("mysql_table_options") - options = %r{create_table "mysql_table_options", force: :cascade, options: "(?<options>.*)"}.match(output)[:options] - assert_match %r{COLLATE=utf8mb4_bin}, options - end -end diff --git a/activerecord/test/cases/adapters/mysql/unsigned_type_test.rb b/activerecord/test/cases/adapters/mysql/unsigned_type_test.rb deleted file mode 100644 index 84c5394c2e..0000000000 --- a/activerecord/test/cases/adapters/mysql/unsigned_type_test.rb +++ /dev/null @@ -1,65 +0,0 @@ -require "cases/helper" -require "support/schema_dumping_helper" - -class MysqlUnsignedTypeTest < ActiveRecord::MysqlTestCase - include SchemaDumpingHelper - self.use_transactional_tests = false - - class UnsignedType < ActiveRecord::Base - end - - setup do - @connection = ActiveRecord::Base.connection - @connection.create_table("unsigned_types", force: true) do |t| - t.integer :unsigned_integer, unsigned: true - t.bigint :unsigned_bigint, unsigned: true - t.float :unsigned_float, unsigned: true - t.decimal :unsigned_decimal, unsigned: true, precision: 10, scale: 2 - end - end - - teardown do - @connection.drop_table "unsigned_types", if_exists: true - end - - test "unsigned int max value is in range" do - assert expected = UnsignedType.create(unsigned_integer: 4294967295) - assert_equal expected, UnsignedType.find_by(unsigned_integer: 4294967295) - end - - test "minus value is out of range" do - assert_raise(RangeError) do - UnsignedType.create(unsigned_integer: -10) - end - assert_raise(RangeError) do - UnsignedType.create(unsigned_bigint: -10) - end - assert_raise(ActiveRecord::StatementInvalid) do - UnsignedType.create(unsigned_float: -10.0) - end - assert_raise(ActiveRecord::StatementInvalid) do - UnsignedType.create(unsigned_decimal: -10.0) - end - end - - test "schema definition can use unsigned as the type" do - @connection.change_table("unsigned_types") do |t| - t.unsigned_integer :unsigned_integer_t - t.unsigned_bigint :unsigned_bigint_t - t.unsigned_float :unsigned_float_t - t.unsigned_decimal :unsigned_decimal_t, precision: 10, scale: 2 - end - - @connection.columns("unsigned_types").select { |c| /^unsigned_/ === c.name }.each do |column| - assert column.unsigned? - end - end - - test "schema dump includes unsigned option" do - schema = dump_table_schema "unsigned_types" - assert_match %r{t.integer\s+"unsigned_integer",\s+limit: 4,\s+unsigned: true$}, schema - assert_match %r{t.integer\s+"unsigned_bigint",\s+limit: 8,\s+unsigned: true$}, schema - assert_match %r{t.float\s+"unsigned_float",\s+limit: 24,\s+unsigned: true$}, schema - assert_match %r{t.decimal\s+"unsigned_decimal",\s+precision: 10,\s+scale: 2,\s+unsigned: true$}, schema - end -end diff --git a/activerecord/test/cases/adapters/mysql2/active_schema_test.rb b/activerecord/test/cases/adapters/mysql2/active_schema_test.rb index 31dc69a45b..99f97c7914 100644 --- a/activerecord/test/cases/adapters/mysql2/active_schema_test.rb +++ b/activerecord/test/cases/adapters/mysql2/active_schema_test.rb @@ -16,8 +16,8 @@ class Mysql2ActiveSchemaTest < ActiveRecord::Mysql2TestCase end def test_add_index - # add_index calls table_exists? and index_name_exists? which can't work since execute is stubbed - def (ActiveRecord::Base.connection).table_exists?(*); true; end + # add_index calls data_source_exists? and index_name_exists? which can't work since execute is stubbed + def (ActiveRecord::Base.connection).data_source_exists?(*); true; end def (ActiveRecord::Base.connection).index_name_exists?(*); false; end expected = "CREATE INDEX `index_people_on_last_name` ON `people` (`last_name`) " @@ -60,7 +60,7 @@ class Mysql2ActiveSchemaTest < ActiveRecord::Mysql2TestCase end def test_index_in_create - def (ActiveRecord::Base.connection).table_exists?(*); false; end + def (ActiveRecord::Base.connection).data_source_exists?(*); false; end %w(SPATIAL FULLTEXT UNIQUE).each do |type| expected = "CREATE TABLE `people` (#{type} INDEX `index_people_on_last_name` (`last_name`) ) ENGINE=InnoDB" @@ -78,7 +78,7 @@ class Mysql2ActiveSchemaTest < ActiveRecord::Mysql2TestCase end def test_index_in_bulk_change - def (ActiveRecord::Base.connection).table_exists?(*); true; end + def (ActiveRecord::Base.connection).data_source_exists?(*); true; end def (ActiveRecord::Base.connection).index_name_exists?(*); false; end %w(SPATIAL FULLTEXT UNIQUE).each do |type| @@ -152,7 +152,7 @@ class Mysql2ActiveSchemaTest < ActiveRecord::Mysql2TestCase end def test_indexes_in_create - ActiveRecord::Base.connection.stubs(:table_exists?).with(:temp).returns(false) + ActiveRecord::Base.connection.stubs(:data_source_exists?).with(:temp).returns(false) ActiveRecord::Base.connection.stubs(:index_name_exists?).with(:index_temp_on_zip).returns(false) expected = "CREATE TEMPORARY TABLE `temp` ( INDEX `index_temp_on_zip` (`zip`) ) ENGINE=InnoDB AS SELECT id, name, zip FROM a_really_complicated_query" diff --git a/activerecord/test/cases/adapters/mysql2/charset_collation_test.rb b/activerecord/test/cases/adapters/mysql2/charset_collation_test.rb index 4fd34def15..668c07dacb 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+limit: 255,\s+collation: "ascii_bin"$}, 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+limit: 65535,\s+collation: "ucs2_unicode_ci"$}, output end end diff --git a/activerecord/test/cases/adapters/mysql2/connection_test.rb b/activerecord/test/cases/adapters/mysql2/connection_test.rb index 71c4028675..575138eb2a 100644 --- a/activerecord/test/cases/adapters/mysql2/connection_test.rb +++ b/activerecord/test/cases/adapters/mysql2/connection_test.rb @@ -68,9 +68,6 @@ class Mysql2ConnectionTest < ActiveRecord::Mysql2TestCase assert_equal 'utf8_general_ci', ARUnit2Model.connection.show_variable('collation_connection') end - # TODO: Below is a straight up copy/paste from mysql/connection_test.rb - # I'm not sure what the correct way is to share these tests between - # adapters in minitest. def test_mysql_default_in_strict_mode result = @connection.exec_query "SELECT @@SESSION.sql_mode" assert_equal [["STRICT_ALL_TABLES"]], result.rows @@ -84,6 +81,20 @@ class Mysql2ConnectionTest < ActiveRecord::Mysql2TestCase end end + def test_passing_arbitary_flags_to_adapter + run_without_connection do |orig_connection| + ActiveRecord::Base.establish_connection(orig_connection.merge({flags: Mysql2::Client::COMPRESS})) + assert_equal (Mysql2::Client::COMPRESS | Mysql2::Client::FOUND_ROWS), ActiveRecord::Base.connection.raw_connection.query_options[:flags] + end + end + + def test_passing_flags_by_array_to_adapter + run_without_connection do |orig_connection| + ActiveRecord::Base.establish_connection(orig_connection.merge({flags: ['COMPRESS'] })) + assert_equal ["COMPRESS", "FOUND_ROWS"], ActiveRecord::Base.connection.raw_connection.query_options[:flags] + end + end + def test_mysql_strict_mode_specified_default run_without_connection do |orig_connection| ActiveRecord::Base.establish_connection(orig_connection.merge({strict: :default})) @@ -133,30 +144,30 @@ class Mysql2ConnectionTest < ActiveRecord::Mysql2TestCase end def test_get_and_release_advisory_lock - key = "test_key" + lock_name = "test_lock_name" - got_lock = @connection.get_advisory_lock(key) + got_lock = @connection.get_advisory_lock(lock_name) assert got_lock, "get_advisory_lock should have returned true but it didn't" - assert_equal test_lock_free(key), false, + assert_equal test_lock_free(lock_name), false, "expected the test advisory lock to be held but it wasn't" - released_lock = @connection.release_advisory_lock(key) + released_lock = @connection.release_advisory_lock(lock_name) assert released_lock, "expected release_advisory_lock to return true but it didn't" - assert test_lock_free(key), 'expected the test key to be available after releasing' + assert test_lock_free(lock_name), 'expected the test lock to be available after releasing' end def test_release_non_existent_advisory_lock - fake_key = "fake_key" - released_non_existent_lock = @connection.release_advisory_lock(fake_key) + lock_name = "fake_lock_name" + released_non_existent_lock = @connection.release_advisory_lock(lock_name) assert_equal released_non_existent_lock, false, 'expected release_advisory_lock to return false when there was no lock to release' end protected - def test_lock_free(key) - @connection.select_value("SELECT IS_FREE_LOCK('#{key}');") == 1 + def test_lock_free(lock_name) + @connection.select_value("SELECT IS_FREE_LOCK('#{lock_name}');") == 1 end end diff --git a/activerecord/test/cases/adapters/mysql2/enum_test.rb b/activerecord/test/cases/adapters/mysql2/enum_test.rb index bd732b5eca..35dbc76d1b 100644 --- a/activerecord/test/cases/adapters/mysql2/enum_test.rb +++ b/activerecord/test/cases/adapters/mysql2/enum_test.rb @@ -5,6 +5,22 @@ class Mysql2EnumTest < ActiveRecord::Mysql2TestCase end def test_enum_limit - assert_equal 6, EnumTest.columns.first.limit + column = EnumTest.columns_hash['enum_column'] + assert_equal 8, column.limit + end + + def test_should_not_be_blob_or_text_column + column = EnumTest.columns_hash['enum_column'] + assert_not column.blob_or_text_column? + end + + def test_should_not_be_unsigned + column = EnumTest.columns_hash['enum_column'] + assert_not column.unsigned? + end + + def test_should_not_be_bigint + column = EnumTest.columns_hash['enum_column'] + assert_not column.bigint? end end diff --git a/activerecord/test/cases/adapters/mysql2/mysql2_adapter_test.rb b/activerecord/test/cases/adapters/mysql2/mysql2_adapter_test.rb new file mode 100644 index 0000000000..4efd728754 --- /dev/null +++ b/activerecord/test/cases/adapters/mysql2/mysql2_adapter_test.rb @@ -0,0 +1,44 @@ +require "cases/helper" + +class Mysql2AdapterTest < ActiveRecord::Mysql2TestCase + def setup + @conn = ActiveRecord::Base.connection + end + + def test_columns_for_distinct_zero_orders + assert_equal "posts.id", + @conn.columns_for_distinct("posts.id", []) + end + + def test_columns_for_distinct_one_order + assert_equal "posts.id, posts.created_at AS alias_0", + @conn.columns_for_distinct("posts.id", ["posts.created_at desc"]) + end + + def test_columns_for_distinct_few_orders + assert_equal "posts.id, posts.created_at AS alias_0, posts.position AS alias_1", + @conn.columns_for_distinct("posts.id", ["posts.created_at desc", "posts.position asc"]) + end + + def test_columns_for_distinct_with_case + assert_equal( + 'posts.id, CASE WHEN author.is_active THEN UPPER(author.name) ELSE UPPER(author.email) END AS alias_0', + @conn.columns_for_distinct('posts.id', + ["CASE WHEN author.is_active THEN UPPER(author.name) ELSE UPPER(author.email) END"]) + ) + end + + def test_columns_for_distinct_blank_not_nil_orders + assert_equal "posts.id, posts.created_at AS alias_0", + @conn.columns_for_distinct("posts.id", ["posts.created_at desc", "", " "]) + end + + def test_columns_for_distinct_with_arel_order + order = Object.new + def order.to_sql + "posts.created_at desc" + end + assert_equal "posts.id, posts.created_at AS alias_0", + @conn.columns_for_distinct("posts.id", [order]) + 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 396f235e77..7c89fda582 100644 --- a/activerecord/test/cases/adapters/mysql2/schema_migrations_test.rb +++ b/activerecord/test/cases/adapters/mysql2/schema_migrations_test.rb @@ -12,9 +12,30 @@ class SchemaMigrationsTest < ActiveRecord::Mysql2TestCase end def test_initializes_schema_migrations_for_encoding_utf8mb4 - smtn = ActiveRecord::Migrator.schema_migrations_table_name - connection.drop_table smtn, if_exists: true + with_encoding_utf8mb4 do + table_name = ActiveRecord::SchemaMigration.table_name + connection.drop_table table_name, if_exists: true + connection.initialize_schema_migrations_table + + assert connection.column_exists?(table_name, :version, :string, collation: 'utf8_general_ci') + end + end + + def test_initializes_internal_metadata_for_encoding_utf8mb4 + with_encoding_utf8mb4 do + table_name = ActiveRecord::InternalMetadata.table_name + connection.drop_table table_name, if_exists: true + + connection.initialize_internal_metadata_table + + assert connection.column_exists?(table_name, :key, :string, collation: 'utf8_general_ci') + end + end + + private + + def with_encoding_utf8mb4 database_name = connection.current_database database_info = connection.select_one("SELECT * FROM information_schema.schemata WHERE schema_name = '#{database_name}'") @@ -23,15 +44,11 @@ class SchemaMigrationsTest < ActiveRecord::Mysql2TestCase execute("ALTER DATABASE #{database_name} DEFAULT CHARACTER SET utf8mb4") - connection.initialize_schema_migrations_table - - limit = ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter::MAX_INDEX_LENGTH_FOR_CHARSETS_OF_4BYTES_MAXLEN - assert connection.column_exists?(smtn, :version, :string, limit: limit) + yield ensure execute("ALTER DATABASE #{database_name} DEFAULT CHARACTER SET #{original_charset} COLLATE #{original_collation}") end - private def connection @connection ||= ActiveRecord::Base.connection end diff --git a/activerecord/test/cases/adapters/mysql2/schema_test.rb b/activerecord/test/cases/adapters/mysql2/schema_test.rb index 17e674e652..43957791b1 100644 --- a/activerecord/test/cases/adapters/mysql2/schema_test.rb +++ b/activerecord/test/cases/adapters/mysql2/schema_test.rb @@ -20,10 +20,6 @@ module ActiveRecord end end - class MysqlDouble < ActiveRecord::Base - self.table_name = "mysql_doubles" - end - def test_float_limits @connection.create_table :mysql_doubles do |t| t.float :float_no_limit @@ -35,15 +31,13 @@ module ActiveRecord t.float :float_25, limit: 25 end - MysqlDouble.reset_column_information - - column_no_limit = MysqlDouble.columns.find { |c| c.name == 'float_no_limit' } - column_short = MysqlDouble.columns.find { |c| c.name == 'float_short' } - column_long = MysqlDouble.columns.find { |c| c.name == 'float_long' } + column_no_limit = @connection.columns(:mysql_doubles).find { |c| c.name == 'float_no_limit' } + column_short = @connection.columns(:mysql_doubles).find { |c| c.name == 'float_short' } + column_long = @connection.columns(:mysql_doubles).find { |c| c.name == 'float_long' } - column_23 = MysqlDouble.columns.find { |c| c.name == 'float_23' } - column_24 = MysqlDouble.columns.find { |c| c.name == 'float_24' } - column_25 = MysqlDouble.columns.find { |c| c.name == 'float_25' } + column_23 = @connection.columns(:mysql_doubles).find { |c| c.name == 'float_23' } + column_24 = @connection.columns(:mysql_doubles).find { |c| c.name == 'float_24' } + column_25 = @connection.columns(:mysql_doubles).find { |c| c.name == 'float_25' } # Mysql floats are precision 0..24, Mysql doubles are precision 25..53 assert_equal 24, column_no_limit.limit @@ -65,13 +59,13 @@ module ActiveRecord assert_equal 'id', @omgpost.primary_key end - def test_table_exists? + def test_data_source_exists? name = @omgpost.table_name - assert @connection.table_exists?(name), "#{name} table should exist" + assert @connection.data_source_exists?(name), "#{name} data_source should exist" end - def test_table_exists_wrong_schema - assert(!@connection.table_exists?("#{@db_name}.zomg"), "table should not exist") + def test_data_source_exists_wrong_schema + assert(!@connection.data_source_exists?("#{@db_name}.zomg"), "data_source should not exist") end def test_dump_indexes diff --git a/activerecord/test/cases/adapters/mysql2/sp_test.rb b/activerecord/test/cases/adapters/mysql2/sp_test.rb index cdaa2cca44..4197ba45f1 100644 --- a/activerecord/test/cases/adapters/mysql2/sp_test.rb +++ b/activerecord/test/cases/adapters/mysql2/sp_test.rb @@ -22,6 +22,12 @@ class Mysql2StoredProcedureTest < ActiveRecord::Mysql2TestCase assert @connection.active?, "Bad connection use by 'Mysql2Adapter.select_rows'" end + def test_multi_results_from_select_one + row = @connection.select_one('CALL topics(1);') + assert_equal 'David', row['author_name'] + assert @connection.active?, "Bad connection use by 'Mysql2Adapter.select_one'" + end + def test_multi_results_from_find_by_sql topics = Topic.find_by_sql 'CALL topics(3);' assert_equal 3, topics.size diff --git a/activerecord/test/cases/adapters/mysql2/unsigned_type_test.rb b/activerecord/test/cases/adapters/mysql2/unsigned_type_test.rb index a6f6dd21bb..c95a64cc16 100644 --- a/activerecord/test/cases/adapters/mysql2/unsigned_type_test.rb +++ b/activerecord/test/cases/adapters/mysql2/unsigned_type_test.rb @@ -57,7 +57,7 @@ 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+limit: 4,\s+unsigned: true$}, schema + assert_match %r{t.integer\s+"unsigned_integer",\s+unsigned: true$}, schema assert_match %r{t.integer\s+"unsigned_bigint",\s+limit: 8,\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 diff --git a/activerecord/test/cases/adapters/postgresql/active_schema_test.rb b/activerecord/test/cases/adapters/postgresql/active_schema_test.rb index 24def31e36..ed44bf7362 100644 --- a/activerecord/test/cases/adapters/postgresql/active_schema_test.rb +++ b/activerecord/test/cases/adapters/postgresql/active_schema_test.rb @@ -54,8 +54,10 @@ class PostgresqlActiveSchemaTest < ActiveRecord::PostgreSQLTestCase end def test_remove_index - # remove_index calls index_name_exists? which can't work since execute is stubbed - ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.send(:define_method, :index_name_exists?) { |*| true } + # remove_index calls index_name_for_remove which can't work since execute is stubbed + ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.send(:define_method, :index_name_for_remove) do |*| + 'index_people_on_last_name' + end expected = %(DROP INDEX CONCURRENTLY "index_people_on_last_name") assert_equal expected, remove_index(:people, name: "index_people_on_last_name", algorithm: :concurrently) @@ -64,7 +66,7 @@ class PostgresqlActiveSchemaTest < ActiveRecord::PostgreSQLTestCase add_index(:people, :last_name, algorithm: :copy) end - ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.send :remove_method, :index_name_exists? + ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.send :remove_method, :index_name_for_remove end private diff --git a/activerecord/test/cases/adapters/postgresql/connection_test.rb b/activerecord/test/cases/adapters/postgresql/connection_test.rb index b12beb91de..d559de3e28 100644 --- a/activerecord/test/cases/adapters/postgresql/connection_test.rb +++ b/activerecord/test/cases/adapters/postgresql/connection_test.rb @@ -90,7 +90,7 @@ module ActiveRecord end def test_tables_logs_name - @connection.tables('hello') + ActiveSupport::Deprecation.silence { @connection.tables('hello') } assert_equal 'SCHEMA', @subscriber.logged[0][1] end @@ -100,7 +100,7 @@ module ActiveRecord end def test_table_exists_logs_name - @connection.table_exists?('items') + ActiveSupport::Deprecation.silence { @connection.table_exists?('items') } assert_equal 'SCHEMA', @subscriber.logged[0][1] end @@ -211,33 +211,33 @@ module ActiveRecord end def test_get_and_release_advisory_lock - key = 5295901941911233559 + lock_id = 5295901941911233559 list_advisory_locks = <<-SQL SELECT locktype, - (classid::bigint << 32) | objid::bigint AS lock_key + (classid::bigint << 32) | objid::bigint AS lock_id FROM pg_locks WHERE locktype = 'advisory' SQL - got_lock = @connection.get_advisory_lock(key) + got_lock = @connection.get_advisory_lock(lock_id) assert got_lock, "get_advisory_lock should have returned true but it didn't" - advisory_lock = @connection.query(list_advisory_locks).find {|l| l[1] == key} + advisory_lock = @connection.query(list_advisory_locks).find {|l| l[1] == lock_id} assert advisory_lock, - "expected to find an advisory lock with key #{key} but there wasn't one" + "expected to find an advisory lock with lock_id #{lock_id} but there wasn't one" - released_lock = @connection.release_advisory_lock(key) + released_lock = @connection.release_advisory_lock(lock_id) assert released_lock, "expected release_advisory_lock to return true but it didn't" - advisory_locks = @connection.query(list_advisory_locks).select {|l| l[1] == key} + advisory_locks = @connection.query(list_advisory_locks).select {|l| l[1] == lock_id} assert_empty advisory_locks, - "expected to have released advisory lock with key #{key} but it was still held" + "expected to have released advisory lock with lock_id #{lock_id} but it was still held" end def test_release_non_existent_advisory_lock - fake_key = 2940075057017742022 + fake_lock_id = 2940075057017742022 with_warning_suppression do - released_non_existent_lock = @connection.release_advisory_lock(fake_key) + released_non_existent_lock = @connection.release_advisory_lock(fake_lock_id) assert_equal released_non_existent_lock, false, 'expected release_advisory_lock to return false when there was no lock to release' end diff --git a/activerecord/test/cases/adapters/postgresql/extension_migration_test.rb b/activerecord/test/cases/adapters/postgresql/extension_migration_test.rb index 9cfc133308..b56c226763 100644 --- a/activerecord/test/cases/adapters/postgresql/extension_migration_test.rb +++ b/activerecord/test/cases/adapters/postgresql/extension_migration_test.rb @@ -3,13 +3,13 @@ require "cases/helper" class PostgresqlExtensionMigrationTest < ActiveRecord::PostgreSQLTestCase self.use_transactional_tests = false - class EnableHstore < ActiveRecord::Migration + class EnableHstore < ActiveRecord::Migration::Current def change enable_extension "hstore" end end - class DisableHstore < ActiveRecord::Migration + class DisableHstore < ActiveRecord::Migration::Current def change disable_extension "hstore" end @@ -24,9 +24,9 @@ class PostgresqlExtensionMigrationTest < ActiveRecord::PostgreSQLTestCase return skip("no extension support") end - @old_schema_migration_tabel_name = ActiveRecord::SchemaMigration.table_name - @old_tabel_name_prefix = ActiveRecord::Base.table_name_prefix - @old_tabel_name_suffix = ActiveRecord::Base.table_name_suffix + @old_schema_migration_table_name = ActiveRecord::SchemaMigration.table_name + @old_table_name_prefix = ActiveRecord::Base.table_name_prefix + @old_table_name_suffix = ActiveRecord::Base.table_name_suffix ActiveRecord::Base.table_name_prefix = "p_" ActiveRecord::Base.table_name_suffix = "_s" @@ -36,11 +36,11 @@ class PostgresqlExtensionMigrationTest < ActiveRecord::PostgreSQLTestCase end def teardown - ActiveRecord::Base.table_name_prefix = @old_tabel_name_prefix - ActiveRecord::Base.table_name_suffix = @old_tabel_name_suffix + ActiveRecord::Base.table_name_prefix = @old_table_name_prefix + ActiveRecord::Base.table_name_suffix = @old_table_name_suffix ActiveRecord::SchemaMigration.delete_all rescue nil ActiveRecord::Migration.verbose = true - ActiveRecord::SchemaMigration.table_name = @old_schema_migration_tabel_name + ActiveRecord::SchemaMigration.table_name = @old_schema_migration_table_name super end diff --git a/activerecord/test/cases/adapters/postgresql/geometric_test.rb b/activerecord/test/cases/adapters/postgresql/geometric_test.rb index 0baf985654..9e250c2b7c 100644 --- a/activerecord/test/cases/adapters/postgresql/geometric_test.rb +++ b/activerecord/test/cases/adapters/postgresql/geometric_test.rb @@ -7,10 +7,10 @@ class PostgresqlPointTest < ActiveRecord::PostgreSQLTestCase include SchemaDumpingHelper class PostgresqlPoint < ActiveRecord::Base - attribute :x, :rails_5_1_point - attribute :y, :rails_5_1_point - attribute :z, :rails_5_1_point - attribute :array_of_points, :rails_5_1_point, array: true + attribute :x, :point + attribute :y, :point + attribute :z, :point + attribute :array_of_points, :point, array: true attribute :legacy_x, :legacy_point attribute :legacy_y, :legacy_point attribute :legacy_z, :legacy_point @@ -167,16 +167,18 @@ class PostgresqlPointTest < ActiveRecord::PostgreSQLTestCase end class PostgresqlGeometricTest < ActiveRecord::PostgreSQLTestCase + include SchemaDumpingHelper + class PostgresqlGeometric < ActiveRecord::Base; end setup do @connection = ActiveRecord::Base.connection @connection.create_table("postgresql_geometrics") do |t| - t.column :a_line_segment, :lseg - t.column :a_box, :box - t.column :a_path, :path - t.column :a_polygon, :polygon - t.column :a_circle, :circle + t.lseg :a_line_segment + t.box :a_box + t.path :a_path + t.polygon :a_polygon + t.circle :a_circle end end @@ -233,4 +235,144 @@ class PostgresqlGeometricTest < ActiveRecord::PostgreSQLTestCase objs = PostgresqlGeometric.find_by_sql "SELECT isclosed(a_path) FROM postgresql_geometrics ORDER BY id ASC" assert_equal [false, true], objs.map(&:isclosed) end + + def test_schema_dumping + output = dump_table_schema("postgresql_geometrics") + assert_match %r{t\.lseg\s+"a_line_segment"$}, output + assert_match %r{t\.box\s+"a_box"$}, output + assert_match %r{t\.path\s+"a_path"$}, output + assert_match %r{t\.polygon\s+"a_polygon"$}, output + assert_match %r{t\.circle\s+"a_circle"$}, output + end +end + +class PostgreSQLGeometricLineTest < ActiveRecord::PostgreSQLTestCase + include SchemaDumpingHelper + + class PostgresqlLine < ActiveRecord::Base; end + + setup do + unless ActiveRecord::Base.connection.send(:postgresql_version) >= 90400 + skip("line type is not fully implemented") + end + @connection = ActiveRecord::Base.connection + @connection.create_table("postgresql_lines") do |t| + t.line :a_line + end + end + + teardown do + if defined?(@connection) + @connection.drop_table 'postgresql_lines', if_exists: true + end + end + + def test_geometric_line_type + g = PostgresqlLine.new( + a_line: '{2.0, 3, 5.5}' + ) + g.save! + + h = PostgresqlLine.find(g.id) + assert_equal '{2,3,5.5}', h.a_line + end + + def test_alternative_format_line_type + g = PostgresqlLine.new( + a_line: '(2.0, 3), (4.0, 6.0)' + ) + g.save! + + h = PostgresqlLine.find(g.id) + assert_equal '{1.5,-1,0}', h.a_line + end + + def test_schema_dumping_for_line_type + output = dump_table_schema("postgresql_lines") + assert_match %r{t\.line\s+"a_line"$}, output + end +end + +class PostgreSQLGeometricTypesTest < ActiveRecord::PostgreSQLTestCase + attr_reader :connection, :table_name + + def setup + super + @connection = ActiveRecord::Base.connection + @table_name = :testings + end + + def test_creating_column_with_point_type + connection.create_table(table_name) do |t| + t.point :foo_point + end + + assert_column_exists(:foo_point) + assert_type_correct(:foo_point, :point) + end + + def test_creating_column_with_line_type + connection.create_table(table_name) do |t| + t.line :foo_line + end + + assert_column_exists(:foo_line) + assert_type_correct(:foo_line, :line) + end + + def test_creating_column_with_lseg_type + connection.create_table(table_name) do |t| + t.lseg :foo_lseg + end + + assert_column_exists(:foo_lseg) + assert_type_correct(:foo_lseg, :lseg) + end + + def test_creating_column_with_box_type + connection.create_table(table_name) do |t| + t.box :foo_box + end + + assert_column_exists(:foo_box) + assert_type_correct(:foo_box, :box) + end + + def test_creating_column_with_path_type + connection.create_table(table_name) do |t| + t.path :foo_path + end + + assert_column_exists(:foo_path) + assert_type_correct(:foo_path, :path) + end + + def test_creating_column_with_polygon_type + connection.create_table(table_name) do |t| + t.polygon :foo_polygon + end + + assert_column_exists(:foo_polygon) + assert_type_correct(:foo_polygon, :polygon) + end + + def test_creating_column_with_circle_type + connection.create_table(table_name) do |t| + t.circle :foo_circle + end + + assert_column_exists(:foo_circle) + assert_type_correct(:foo_circle, :circle) + end + + private + + def assert_column_exists(column_name) + assert connection.column_exists?(table_name, column_name) + end + + def assert_type_correct(column_name, type) + column = connection.columns(table_name).find { |c| c.name == column_name.to_s } + assert_equal type, column.type + end end diff --git a/activerecord/test/cases/adapters/postgresql/hstore_test.rb b/activerecord/test/cases/adapters/postgresql/hstore_test.rb index 6a2d501646..27cc65a643 100644 --- a/activerecord/test/cases/adapters/postgresql/hstore_test.rb +++ b/activerecord/test/cases/adapters/postgresql/hstore_test.rb @@ -86,7 +86,7 @@ if ActiveRecord::Base.connection.supports_extensions? end def test_hstore_migration - hstore_migration = Class.new(ActiveRecord::Migration) do + hstore_migration = Class.new(ActiveRecord::Migration::Current) do def change change_table("hstores") do |t| t.hstore :keys diff --git a/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb b/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb index e361521155..31e87722d9 100644 --- a/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb +++ b/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb @@ -53,12 +53,6 @@ module ActiveRecord end end - def test_composite_primary_key - with_example_table 'id serial, number serial, PRIMARY KEY (id, number)' do - assert_nil @connection.primary_key('ex') - end - end - def test_primary_key_raises_error_if_table_not_found assert_raises(ActiveRecord::StatementInvalid) do @connection.primary_key('unobtainium') @@ -139,8 +133,8 @@ module ActiveRecord def test_sql_for_insert_with_returning_disabled connection = connection_without_insert_returning - result = connection.sql_for_insert('sql', nil, nil, nil, 'binds') - assert_equal ['sql', 'binds'], result + sql, binds = connection.sql_for_insert('sql', nil, nil, nil, 'binds') + assert_equal ['sql', 'binds'], [sql, binds] end def test_serial_sequence @@ -322,11 +316,6 @@ module ActiveRecord end end - def test_substitute_at - bind = @connection.substitute_at(nil) - assert_equal Arel.sql('$1'), bind.to_sql - end - def test_partial_index with_example_table do @connection.add_index 'ex', %w{ id number }, :name => 'partial', :where => "number > 100" diff --git a/activerecord/test/cases/adapters/postgresql/prepared_statements_test.rb b/activerecord/test/cases/adapters/postgresql/prepared_statements_test.rb new file mode 100644 index 0000000000..f1519db48b --- /dev/null +++ b/activerecord/test/cases/adapters/postgresql/prepared_statements_test.rb @@ -0,0 +1,22 @@ +require "cases/helper" +require "models/developer" + +class PreparedStatementsTest < ActiveRecord::PostgreSQLTestCase + fixtures :developers + + def setup + @default_prepared_statements = Developer.connection_config[:prepared_statements] + Developer.connection_config[:prepared_statements] = false + end + + def teardown + Developer.connection_config[:prepared_statements] = @default_prepared_statements + end + + def nothing_raised_with_falsy_prepared_statements + assert_nothing_raised do + Developer.where(id: 1) + end + end + +end diff --git a/activerecord/test/cases/adapters/postgresql/range_test.rb b/activerecord/test/cases/adapters/postgresql/range_test.rb index 02b1083430..0edfa4ed9d 100644 --- a/activerecord/test/cases/adapters/postgresql/range_test.rb +++ b/activerecord/test/cases/adapters/postgresql/range_test.rb @@ -4,11 +4,13 @@ require 'support/connection_helper' if ActiveRecord::Base.connection.respond_to?(:supports_ranges?) && ActiveRecord::Base.connection.supports_ranges? class PostgresqlRange < ActiveRecord::Base self.table_name = "postgresql_ranges" + self.time_zone_aware_types += [:tsrange, :tstzrange] end class PostgresqlRangeTest < ActiveRecord::PostgreSQLTestCase self.use_transactional_tests = false include ConnectionHelper + include InTimeZone def setup @connection = PostgresqlRange.connection @@ -160,6 +162,26 @@ _SQL assert_nil @empty_range.float_range end + def test_timezone_awareness_tzrange + tz = "Pacific Time (US & Canada)" + + in_time_zone tz do + PostgresqlRange.reset_column_information + time_string = Time.current.to_s + time = Time.zone.parse(time_string) + + record = PostgresqlRange.new(tstz_range: time_string..time_string) + assert_equal time..time, record.tstz_range + assert_equal ActiveSupport::TimeZone[tz], record.tstz_range.begin.time_zone + + record.save! + record.reload + + assert_equal time..time, record.tstz_range + assert_equal ActiveSupport::TimeZone[tz], record.tstz_range.begin.time_zone + end + end + def test_create_tstzrange tstzrange = Time.parse('2010-01-01 14:30:00 +0100')...Time.parse('2011-02-02 14:30:00 CDT') round_trip(@new_range, :tstz_range, tstzrange) @@ -188,6 +210,26 @@ _SQL Time.send(tz, 2010, 1, 1, 14, 30, 0)...Time.send(tz, 2010, 1, 1, 14, 30, 0)) end + def test_timezone_awareness_tsrange + tz = "Pacific Time (US & Canada)" + + in_time_zone tz do + PostgresqlRange.reset_column_information + time_string = Time.current.to_s + time = Time.zone.parse(time_string) + + record = PostgresqlRange.new(ts_range: time_string..time_string) + assert_equal time..time, record.ts_range + assert_equal ActiveSupport::TimeZone[tz], record.ts_range.begin.time_zone + + record.save! + record.reload + + assert_equal time..time, record.ts_range + assert_equal ActiveSupport::TimeZone[tz], record.ts_range.begin.time_zone + end + end + def test_create_numrange assert_equal_round_trip(@new_range, :num_range, BigDecimal.new('0.5')...BigDecimal.new('1')) diff --git a/activerecord/test/cases/adapters/postgresql/schema_test.rb b/activerecord/test/cases/adapters/postgresql/schema_test.rb index 93e98ec872..f50fe88b9b 100644 --- a/activerecord/test/cases/adapters/postgresql/schema_test.rb +++ b/activerecord/test/cases/adapters/postgresql/schema_test.rb @@ -166,7 +166,7 @@ class SchemaTest < ActiveRecord::PostgreSQLTestCase end end - def test_raise_wraped_exception_on_bad_prepare + 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)] end @@ -184,42 +184,42 @@ class SchemaTest < ActiveRecord::PostgreSQLTestCase @connection.exec_query("alter table developers drop column zomg", 'sql', []) if altered end - def test_table_exists? + def test_data_source_exists? [Thing1, Thing2, Thing3, Thing4].each do |klass| name = klass.table_name - assert @connection.table_exists?(name), "'#{name}' table should exist" + assert @connection.data_source_exists?(name), "'#{name}' data_source should exist" end end - def test_table_exists_when_on_schema_search_path + def test_data_source_exists_when_on_schema_search_path with_schema_search_path(SCHEMA_NAME) do - assert(@connection.table_exists?(TABLE_NAME), "table should exist and be found") + assert(@connection.data_source_exists?(TABLE_NAME), "data_source should exist and be found") end end - def test_table_exists_when_not_on_schema_search_path + def test_data_source_exists_when_not_on_schema_search_path with_schema_search_path('PUBLIC') do - assert(!@connection.table_exists?(TABLE_NAME), "table exists but should not be found") + assert(!@connection.data_source_exists?(TABLE_NAME), "data_source exists but should not be found") end end - def test_table_exists_wrong_schema - assert(!@connection.table_exists?("foo.things"), "table should not exist") + def test_data_source_exists_wrong_schema + assert(!@connection.data_source_exists?("foo.things"), "data_source should not exist") end - def test_table_exists_quoted_names + def test_data_source_exists_quoted_names [ %("#{SCHEMA_NAME}"."#{TABLE_NAME}"), %(#{SCHEMA_NAME}."#{TABLE_NAME}"), %(#{SCHEMA_NAME}."#{TABLE_NAME}")].each do |given| - assert(@connection.table_exists?(given), "table should exist when specified as #{given}") + assert(@connection.data_source_exists?(given), "data_source should exist when specified as #{given}") end with_schema_search_path(SCHEMA_NAME) do given = %("#{TABLE_NAME}") - assert(@connection.table_exists?(given), "table should exist when specified as #{given}") + assert(@connection.data_source_exists?(given), "data_source should exist when specified as #{given}") end end - def test_table_exists_quoted_table + def test_data_source_exists_quoted_table with_schema_search_path(SCHEMA_NAME) do - assert(@connection.table_exists?('"things.table"'), "table should exist") + assert(@connection.data_source_exists?('"things.table"'), "data_source should exist") end end @@ -321,16 +321,33 @@ class SchemaTest < ActiveRecord::PostgreSQLTestCase do_dump_index_tests_for_schema("public, #{SCHEMA_NAME}", INDEX_A_COLUMN, INDEX_B_COLUMN_S1, INDEX_D_COLUMN, INDEX_E_COLUMN) end + def test_dump_indexes_for_table_with_scheme_specified_in_name + indexes = @connection.indexes("#{SCHEMA_NAME}.#{TABLE_NAME}") + assert_equal 4, indexes.size + end + def test_with_uppercase_index_name @connection.execute "CREATE INDEX \"things_Index\" ON #{SCHEMA_NAME}.things (name)" - assert_nothing_raised { @connection.remove_index "things", name: "#{SCHEMA_NAME}.things_Index"} - @connection.execute "CREATE INDEX \"things_Index\" ON #{SCHEMA_NAME}.things (name)" with_schema_search_path SCHEMA_NAME do assert_nothing_raised { @connection.remove_index "things", name: "things_Index"} end end + def test_remove_index_when_schema_specified + @connection.execute "CREATE INDEX \"things_Index\" ON #{SCHEMA_NAME}.things (name)" + assert_nothing_raised { @connection.remove_index "things", name: "#{SCHEMA_NAME}.things_Index" } + + @connection.execute "CREATE INDEX \"things_Index\" ON #{SCHEMA_NAME}.things (name)" + assert_nothing_raised { @connection.remove_index "#{SCHEMA_NAME}.things", name: "things_Index" } + + @connection.execute "CREATE INDEX \"things_Index\" ON #{SCHEMA_NAME}.things (name)" + assert_nothing_raised { @connection.remove_index "#{SCHEMA_NAME}.things", name: "#{SCHEMA_NAME}.things_Index" } + + @connection.execute "CREATE INDEX \"things_Index\" ON #{SCHEMA_NAME}.things (name)" + assert_raises(ArgumentError) { @connection.remove_index "#{SCHEMA2_NAME}.things", name: "#{SCHEMA_NAME}.things_Index" } + end + def test_primary_key_with_schema_specified [ %("#{SCHEMA_NAME}"."#{PK_TABLE_NAME}"), diff --git a/activerecord/test/cases/adapters/postgresql/uuid_test.rb b/activerecord/test/cases/adapters/postgresql/uuid_test.rb index 7127d69e9e..7628075ad2 100644 --- a/activerecord/test/cases/adapters/postgresql/uuid_test.rb +++ b/activerecord/test/cases/adapters/postgresql/uuid_test.rb @@ -20,6 +20,8 @@ class PostgresqlUUIDTest < ActiveRecord::PostgreSQLTestCase end setup do + enable_extension!('uuid-ossp', connection) + connection.create_table "uuid_data_type" do |t| t.uuid 'guid' end @@ -144,8 +146,6 @@ class PostgresqlUUIDGenerationTest < ActiveRecord::PostgreSQLTestCase end setup do - enable_extension!('uuid-ossp', connection) - connection.create_table('pg_uuids', id: :uuid, default: 'uuid_generate_v1()') do |t| t.string 'name' t.uuid 'other_uuid', default: 'uuid_generate_v4()' @@ -170,7 +170,6 @@ class PostgresqlUUIDGenerationTest < ActiveRecord::PostgreSQLTestCase drop_table "pg_uuids" drop_table 'pg_uuids_2' connection.execute 'DROP FUNCTION IF EXISTS my_uuid_generator();' - disable_extension!('uuid-ossp', connection) end if ActiveRecord::Base.connection.supports_extensions? @@ -198,14 +197,14 @@ class PostgresqlUUIDGenerationTest < ActiveRecord::PostgreSQLTestCase def test_schema_dumper_for_uuid_primary_key schema = dump_table_schema "pg_uuids" - assert_match(/\bcreate_table "pg_uuids", id: :uuid, default: "uuid_generate_v1\(\)"/, schema) - assert_match(/t\.uuid "other_uuid", default: "uuid_generate_v4\(\)"/, schema) + assert_match(/\bcreate_table "pg_uuids", id: :uuid, default: -> { "uuid_generate_v1\(\)" }/, schema) + assert_match(/t\.uuid "other_uuid", default: -> { "uuid_generate_v4\(\)" }/, schema) end def test_schema_dumper_for_uuid_primary_key_with_custom_default schema = dump_table_schema "pg_uuids_2" - assert_match(/\bcreate_table "pg_uuids_2", id: :uuid, default: "my_uuid_generator\(\)"/, schema) - assert_match(/t\.uuid "other_uuid_2", default: "my_uuid_generator\(\)"/, schema) + assert_match(/\bcreate_table "pg_uuids_2", id: :uuid, default: -> { "my_uuid_generator\(\)" }/, schema) + assert_match(/t\.uuid "other_uuid_2", default: -> { "my_uuid_generator\(\)" }/, schema) end end end @@ -215,8 +214,6 @@ class PostgresqlUUIDTestNilDefault < ActiveRecord::PostgreSQLTestCase include SchemaDumpingHelper setup do - enable_extension!('uuid-ossp', connection) - connection.create_table('pg_uuids', id: false) do |t| t.primary_key :id, :uuid, default: nil t.string 'name' @@ -225,7 +222,6 @@ class PostgresqlUUIDTestNilDefault < ActiveRecord::PostgreSQLTestCase teardown do drop_table "pg_uuids" - disable_extension!('uuid-ossp', connection) end if ActiveRecord::Base.connection.supports_extensions? @@ -258,8 +254,6 @@ class PostgresqlUUIDTestInverseOf < ActiveRecord::PostgreSQLTestCase end setup do - enable_extension!('uuid-ossp', connection) - connection.transaction do connection.create_table('pg_uuid_posts', id: :uuid) do |t| t.string 'title' @@ -274,7 +268,6 @@ class PostgresqlUUIDTestInverseOf < ActiveRecord::PostgreSQLTestCase teardown do drop_table "pg_uuid_comments" drop_table "pg_uuid_posts" - disable_extension!('uuid-ossp', connection) end if ActiveRecord::Base.connection.supports_extensions? diff --git a/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb b/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb index 640df31e2e..02c3358ba6 100644 --- a/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb +++ b/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb @@ -130,11 +130,6 @@ module ActiveRecord assert_equal 'UTF-8', @conn.encoding end - def test_bind_value_substitute - bind_param = @conn.substitute_at('foo') - assert_equal Arel.sql('?'), bind_param.to_sql - end - def test_exec_no_binds with_example_table 'id int, data string' do result = @conn.exec_query('SELECT id, data FROM ex') @@ -284,9 +279,9 @@ module ActiveRecord def test_tables with_example_table do - assert_equal %w{ ex }, @conn.tables + ActiveSupport::Deprecation.silence { assert_equal %w{ ex }, @conn.tables } with_example_table 'id integer PRIMARY KEY AUTOINCREMENT, number integer', 'people' do - assert_equal %w{ ex people }.sort, @conn.tables.sort + ActiveSupport::Deprecation.silence { assert_equal %w{ ex people }.sort, @conn.tables.sort } end end end @@ -297,7 +292,9 @@ module ActiveRecord WHERE type IN ('table','view') AND name <> 'sqlite_sequence' SQL assert_logged [[sql.squish, 'SCHEMA', []]] do - @conn.tables('hello') + ActiveSupport::Deprecation.silence do + @conn.tables('hello') + end end end @@ -316,7 +313,9 @@ module ActiveRecord WHERE type IN ('table','view') AND name <> 'sqlite_sequence' AND name = 'ex' SQL assert_logged [[sql.squish, 'SCHEMA', []]] do - assert @conn.table_exists?('ex') + ActiveSupport::Deprecation.silence do + assert @conn.table_exists?('ex') + end end end end @@ -401,12 +400,6 @@ module ActiveRecord end end - def test_composite_primary_key - with_example_table 'id integer, number integer, foo integer, PRIMARY KEY (id, number)' do - assert_nil @conn.primary_key('ex') - end - end - def test_supports_extensions assert_not @conn.supports_extensions?, 'does not support extensions' end diff --git a/activerecord/test/cases/adapters/sqlite3/sqlite3_create_folder_test.rb b/activerecord/test/cases/adapters/sqlite3/sqlite3_create_folder_test.rb index 887dcfc96c..9b675b804b 100644 --- a/activerecord/test/cases/adapters/sqlite3/sqlite3_create_folder_test.rb +++ b/activerecord/test/cases/adapters/sqlite3/sqlite3_create_folder_test.rb @@ -6,13 +6,17 @@ module ActiveRecord class SQLite3CreateFolder < ActiveRecord::SQLite3TestCase def test_sqlite_creates_directory Dir.mktmpdir do |dir| - dir = Pathname.new(dir) - @conn = Base.sqlite3_connection :database => dir.join("db/foo.sqlite3"), - :adapter => 'sqlite3', - :timeout => 100 + begin + dir = Pathname.new(dir) + @conn = Base.sqlite3_connection :database => dir.join("db/foo.sqlite3"), + :adapter => 'sqlite3', + :timeout => 100 - assert Dir.exist? dir.join('db') - assert File.exist? dir.join('db/foo.sqlite3') + assert Dir.exist? dir.join('db') + assert File.exist? dir.join('db/foo.sqlite3') + ensure + @conn.disconnect! if @conn + end end end end diff --git a/activerecord/test/cases/ar_schema_test.rb b/activerecord/test/cases/ar_schema_test.rb index 9d5327bf35..9c99689c1e 100644 --- a/activerecord/test/cases/ar_schema_test.rb +++ b/activerecord/test/cases/ar_schema_test.rb @@ -21,10 +21,10 @@ if ActiveRecord::Base.connection.supports_migrations? ActiveRecord::Migration.verbose = @original_verbose end - def test_has_no_primary_key + def test_has_primary_key old_primary_key_prefix_type = ActiveRecord::Base.primary_key_prefix_type ActiveRecord::Base.primary_key_prefix_type = :table_name_with_underscore - assert_nil ActiveRecord::SchemaMigration.primary_key + assert_equal "version", ActiveRecord::SchemaMigration.primary_key ActiveRecord::SchemaMigration.create_table assert_difference "ActiveRecord::SchemaMigration.count", 1 do diff --git a/activerecord/test/cases/associations/belongs_to_associations_test.rb b/activerecord/test/cases/associations/belongs_to_associations_test.rb index 938350627f..4f99c57c3c 100644 --- a/activerecord/test/cases/associations/belongs_to_associations_test.rb +++ b/activerecord/test/cases/associations/belongs_to_associations_test.rb @@ -53,7 +53,7 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase def test_belongs_to_with_primary_key_joins_on_correct_column sql = Client.joins(:firm_with_primary_key).to_sql - if current_adapter?(:MysqlAdapter, :Mysql2Adapter) + if current_adapter?(:Mysql2Adapter) assert_no_match(/`firm_with_primary_keys_companies`\.`id`/, sql) assert_match(/`firm_with_primary_keys_companies`\.`name`/, sql) elsif current_adapter?(:OracleAdapter) diff --git a/activerecord/test/cases/associations/callbacks_test.rb b/activerecord/test/cases/associations/callbacks_test.rb index a531e0e02c..a4298a25a6 100644 --- a/activerecord/test/cases/associations/callbacks_test.rb +++ b/activerecord/test/cases/associations/callbacks_test.rb @@ -177,14 +177,14 @@ class AssociationCallbacksTest < ActiveRecord::TestCase end def test_dont_add_if_before_callback_raises_exception - assert !@david.unchangable_posts.include?(@authorless) + assert !@david.unchangeable_posts.include?(@authorless) begin - @david.unchangable_posts << @authorless + @david.unchangeable_posts << @authorless rescue Exception end assert @david.post_log.empty? - assert !@david.unchangable_posts.include?(@authorless) + assert !@david.unchangeable_posts.include?(@authorless) @david.reload - assert !@david.unchangable_posts.include?(@authorless) + assert !@david.unchangeable_posts.include?(@authorless) end end diff --git a/activerecord/test/cases/associations/eager_test.rb b/activerecord/test/cases/associations/eager_test.rb index 0c09713971..3ee84fb66c 100644 --- a/activerecord/test/cases/associations/eager_test.rb +++ b/activerecord/test/cases/associations/eager_test.rb @@ -749,6 +749,38 @@ class EagerAssociationTest < ActiveRecord::TestCase } end + def test_eager_has_many_through_with_order + tag = OrderedTag.create(name: 'Foo') + post1 = Post.create!(title: 'Beaches', body: "I like beaches!") + post2 = Post.create!(title: 'Pools', body: "I like pools!") + + Tagging.create!(taggable_type: 'Post', taggable_id: post1.id, tag: tag) + Tagging.create!(taggable_type: 'Post', taggable_id: post2.id, tag: tag) + + tag_with_includes = OrderedTag.includes(:tagged_posts).find(tag.id) + assert_equal(tag_with_includes.taggings.map(&:taggable).map(&:title), tag_with_includes.tagged_posts.map(&:title)) + end + + def test_eager_has_many_through_multiple_with_order + tag1 = OrderedTag.create!(name: 'Bar') + tag2 = OrderedTag.create!(name: 'Foo') + + post1 = Post.create!(title: 'Beaches', body: "I like beaches!") + post2 = Post.create!(title: 'Pools', body: "I like pools!") + + Tagging.create!(taggable: post1, tag: tag1) + Tagging.create!(taggable: post2, tag: tag1) + Tagging.create!(taggable: post2, tag: tag2) + Tagging.create!(taggable: post1, tag: tag2) + + tags_with_includes = OrderedTag.where(id: [tag1, tag2].map(&:id)).includes(:tagged_posts).order(:id).to_a + tag1_with_includes = tags_with_includes.first + tag2_with_includes = tags_with_includes.last + + assert_equal([post2, post1].map(&:title), tag1_with_includes.tagged_posts.map(&:title)) + assert_equal([post1, post2].map(&:title), tag2_with_includes.tagged_posts.map(&:title)) + end + def test_eager_with_default_scope developer = EagerDeveloperWithDefaultScope.where(:name => 'David').first projects = Project.order(:id).to_a @@ -1216,7 +1248,7 @@ class EagerAssociationTest < ActiveRecord::TestCase end def test_join_eager_with_empty_order_should_generate_valid_sql - assert_nothing_raised(ActiveRecord::StatementInvalid) do + assert_nothing_raised do Post.includes(:comments).order("").where(:comments => {:body => "Thank you for the welcome"}).first end end @@ -1402,4 +1434,10 @@ class EagerAssociationTest < ActiveRecord::TestCase post = Post.eager_load(:tags).where('tags.name = ?', 'General').first assert_equal posts(:welcome), post end + + # CollectionProxy#reader is expensive, so the preloader avoids calling it. + test "preloading has_many_through association avoids calling association.reader" do + ActiveRecord::Associations::HasManyAssociation.any_instance.expects(:reader).never + Author.preload(:readonly_comments).first! + end end diff --git a/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb b/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb index ccb062f991..1bbca84bb2 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 @@ -146,6 +146,19 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase assert_equal 1, country.treaties.count end + def test_join_table_composite_primary_key_should_not_warn + country = Country.new(:name => 'India') + country.country_id = 'c1' + country.save! + + treaty = Treaty.new(:name => 'peace') + treaty.treaty_id = 't1' + warning = capture(:stderr) do + country.treaties << treaty + end + assert_no_match(/WARNING: Rails does not support composite primary key\./, warning) + end + def test_has_and_belongs_to_many david = Developer.find(1) @@ -827,12 +840,12 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase assert_no_queries { david.projects.columns } end - def test_attributes_are_being_set_when_initialized_from_habm_association_with_where_clause + def test_attributes_are_being_set_when_initialized_from_habtm_association_with_where_clause new_developer = projects(:action_controller).developers.where(:name => "Marcelo").build assert_equal new_developer.name, "Marcelo" end - def test_attributes_are_being_set_when_initialized_from_habm_association_with_multiple_where_clauses + def test_attributes_are_being_set_when_initialized_from_habtm_association_with_multiple_where_clauses new_developer = projects(:action_controller).developers.where(:name => "Marcelo").where(:salary => 90_000).build assert_equal new_developer.name, "Marcelo" assert_equal new_developer.salary, 90_000 @@ -925,7 +938,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase end def test_with_symbol_class_name - assert_nothing_raised NoMethodError do + assert_nothing_raised do DeveloperWithSymbolClassName.new 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 eb94870a35..e975f4fbdd 100644 --- a/activerecord/test/cases/associations/has_many_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_associations_test.rb @@ -203,9 +203,22 @@ class HasManyAssociationsTest < ActiveRecord::TestCase bulb = car.bulbs.create assert_equal 'defaulty', bulb.name + end + + def test_build_and_create_from_association_should_respect_passed_attributes_over_default_scope + car = Car.create(name: 'honda') + + bulb = car.bulbs.build(name: 'exotic') + assert_equal 'exotic', bulb.name - bulb = car.bulbs.create(:name => 'exotic') + bulb = car.bulbs.create(name: 'exotic') assert_equal 'exotic', bulb.name + + bulb = car.awesome_bulbs.build(frickinawesome: false) + assert_equal false, bulb.frickinawesome + + bulb = car.awesome_bulbs.create(frickinawesome: false) + assert_equal false, bulb.frickinawesome end def test_build_from_association_should_respect_scope @@ -395,6 +408,16 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end assert_no_queries do + bulbs.third_to_last() + bulbs.third_to_last({}) + end + + assert_no_queries do + bulbs.second_to_last() + bulbs.second_to_last({}) + end + + assert_no_queries do bulbs.last() bulbs.last({}) end @@ -2258,7 +2281,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_equal [], authors(:david).posts_with_signature.map(&:title) end - test 'associations autosaves when object is already persited' do + test 'associations autosaves when object is already persisted' do bulb = Bulb.create! tyre = Tyre.create! @@ -2335,6 +2358,12 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_equal [first_bulb, second_bulb], car.bulbs end + test 'double insertion of new object to association when same association used in the after create callback of a new object' do + car = Car.create! + car.bulbs << TrickyBulb.new + assert_equal 1, car.bulbs.size + end + def test_association_force_reload_with_only_true_is_deprecated company = Company.find(1) 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 226ecf5447..bb8c9fa19c 100644 --- a/activerecord/test/cases/associations/has_many_through_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_through_associations_test.rb @@ -884,7 +884,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase def test_collection_singular_ids_setter_raises_exception_when_invalid_ids_set company = companies(:rails_core) ids = [Developer.first.id, -9999] - assert_raises(ActiveRecord::RecordNotFound) {company.developer_ids= ids} + assert_raises(ActiveRecord::AssociationTypeMismatch) {company.developer_ids= ids} end def test_build_a_model_from_hm_through_association_with_where_clause diff --git a/activerecord/test/cases/associations/inverse_associations_test.rb b/activerecord/test/cases/associations/inverse_associations_test.rb index 57d1c8feda..c9743e80d3 100644 --- a/activerecord/test/cases/associations/inverse_associations_test.rb +++ b/activerecord/test/cases/associations/inverse_associations_test.rb @@ -130,15 +130,15 @@ end class InverseAssociationTests < ActiveRecord::TestCase def test_should_allow_for_inverse_of_options_in_associations - assert_nothing_raised(ArgumentError, 'ActiveRecord should allow the inverse_of options on has_many') do + assert_nothing_raised do Class.new(ActiveRecord::Base).has_many(:wheels, :inverse_of => :car) end - assert_nothing_raised(ArgumentError, 'ActiveRecord should allow the inverse_of options on has_one') do + assert_nothing_raised do Class.new(ActiveRecord::Base).has_one(:engine, :inverse_of => :car) end - assert_nothing_raised(ArgumentError, 'ActiveRecord should allow the inverse_of options on belongs_to') do + assert_nothing_raised do Class.new(ActiveRecord::Base).belongs_to(:car, :inverse_of => :driver) end end @@ -666,7 +666,7 @@ class InversePolymorphicBelongsToTests < ActiveRecord::TestCase def test_trying_to_access_inverses_that_dont_exist_shouldnt_raise_an_error # Ideally this would, if only for symmetry's sake with other association types - assert_nothing_raised(ActiveRecord::InverseOfAssociationNotFoundError) { Face.first.horrible_polymorphic_man } + assert_nothing_raised { Face.first.horrible_polymorphic_man } end def test_trying_to_set_polymorphic_inverses_that_dont_exist_at_all_should_raise_an_error @@ -676,7 +676,7 @@ class InversePolymorphicBelongsToTests < ActiveRecord::TestCase def test_trying_to_set_polymorphic_inverses_that_dont_exist_on_the_instance_being_set_should_raise_an_error # passes because Man does have the correct inverse_of - assert_nothing_raised(ActiveRecord::InverseOfAssociationNotFoundError) { Face.first.polymorphic_man = Man.first } + assert_nothing_raised { Face.first.polymorphic_man = Man.first } # fails because Interest does have the correct inverse_of assert_raise(ActiveRecord::InverseOfAssociationNotFoundError) { Face.first.polymorphic_man = Interest.first } end @@ -688,7 +688,7 @@ class InverseMultipleHasManyInversesForSameModel < ActiveRecord::TestCase fixtures :men, :interests, :zines def test_that_we_can_load_associations_that_have_the_same_reciprocal_name_from_different_models - assert_nothing_raised(ActiveRecord::AssociationTypeMismatch) do + assert_nothing_raised do i = Interest.first i.zine i.man @@ -696,7 +696,7 @@ class InverseMultipleHasManyInversesForSameModel < ActiveRecord::TestCase end def test_that_we_can_create_associations_that_have_the_same_reciprocal_name_from_different_models - assert_nothing_raised(ActiveRecord::AssociationTypeMismatch) do + assert_nothing_raised do i = Interest.first i.build_zine(:title => 'Get Some in Winter! 2008') i.build_man(:name => 'Gordon') diff --git a/activerecord/test/cases/associations/join_model_test.rb b/activerecord/test/cases/associations/join_model_test.rb index f6dddaf5b4..1d892a0956 100644 --- a/activerecord/test/cases/associations/join_model_test.rb +++ b/activerecord/test/cases/associations/join_model_test.rb @@ -88,7 +88,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase def test_polymorphic_has_many_going_through_join_model_with_custom_select_and_joins assert_equal tags(:general), tag = posts(:welcome).tags.add_joins_and_select.first - assert_nothing_raised(NoMethodError) { tag.author_id } + assert_nothing_raised { tag.author_id } end def test_polymorphic_has_many_going_through_join_model_with_custom_foreign_key @@ -363,6 +363,13 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase assert_equal posts(:welcome, :thinking).sort_by(&:id), tags(:general).tagged_posts.sort_by(&:id) end + def test_has_many_polymorphic_associations_merges_through_scope + Tag.has_many :null_taggings, -> { none }, class_name: :Tagging + Tag.has_many :null_tagged_posts, :through => :null_taggings, :source => 'taggable', :source_type => 'Post' + assert_equal [], tags(:general).null_tagged_posts + refute_equal [], tags(:general).tagged_posts + end + def test_eager_has_many_polymorphic_with_source_type tag_with_include = Tag.all.merge!(:includes => :tagged_posts).find(tags(:general).id) desired = posts(:welcome, :thinking) diff --git a/activerecord/test/cases/attribute_methods_test.rb b/activerecord/test/cases/attribute_methods_test.rb index 52d197718e..1db52af59b 100644 --- a/activerecord/test/cases/attribute_methods_test.rb +++ b/activerecord/test/cases/attribute_methods_test.rb @@ -175,7 +175,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase assert_equal category_attrs , category.attributes_before_type_cast end - if current_adapter?(:MysqlAdapter, :Mysql2Adapter) + if current_adapter?(:Mysql2Adapter) def test_read_attributes_before_type_cast_on_boolean bool = Boolean.create!({ "value" => false }) if RUBY_PLATFORM =~ /java/ @@ -714,6 +714,13 @@ class AttributeMethodsTest < ActiveRecord::TestCase end end + def test_time_zone_aware_attributes_dont_recurse_infinitely_on_invalid_values + in_time_zone "Pacific Time (US & Canada)" do + record = @target.new(bonus_time: []) + assert_equal nil, record.bonus_time + end + end + def test_setting_time_zone_conversion_for_attributes_should_write_value_on_class_variable Topic.skip_time_zone_conversion_for_attributes = [:field_a] Minimalistic.skip_time_zone_conversion_for_attributes = [:field_b] @@ -791,7 +798,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase assert_nil computer.system end - def test_global_methods_are_overwritte_when_subclassing + def test_global_methods_are_overwritten_when_subclassing klass = Class.new(ActiveRecord::Base) { self.abstract_class = true } subklass = Class.new(klass) do diff --git a/activerecord/test/cases/attributes_test.rb b/activerecord/test/cases/attributes_test.rb index 264b275181..2991ca8b76 100644 --- a/activerecord/test/cases/attributes_test.rb +++ b/activerecord/test/cases/attributes_test.rb @@ -172,5 +172,18 @@ module ActiveRecord assert_equal int_range, klass.type_for_attribute("my_int_range") end end + + test "attributes added after subclasses load are inherited" do + parent = Class.new(ActiveRecord::Base) do + self.table_name = "topics" + end + + child = Class.new(parent) + child.new # => force a schema load + + parent.attribute(:foo, Type::Value.new) + + assert_equal(:bar, child.new(foo: :bar).foo) + end end end diff --git a/activerecord/test/cases/autosave_association_test.rb b/activerecord/test/cases/autosave_association_test.rb index 0df8f1f798..3608063b01 100644 --- a/activerecord/test/cases/autosave_association_test.rb +++ b/activerecord/test/cases/autosave_association_test.rb @@ -433,6 +433,53 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociationWithAcceptsNestedAttrib ActiveRecord::Base.index_nested_attribute_errors = old_attribute_config end + def test_errors_details_should_be_set + molecule = Molecule.new + valid_electron = Electron.new(name: 'electron') + invalid_electron = Electron.new + + molecule.electrons = [valid_electron, invalid_electron] + + assert_not invalid_electron.valid? + assert valid_electron.valid? + assert_not molecule.valid? + assert_equal [{error: :blank}], molecule.errors.details["electrons.name"] + end + + def test_errors_details_should_be_indexed_when_passed_as_array + guitar = Guitar.new + tuning_peg_valid = TuningPeg.new + tuning_peg_valid.pitch = 440.0 + tuning_peg_invalid = TuningPeg.new + + guitar.tuning_pegs = [tuning_peg_valid, tuning_peg_invalid] + + assert_not tuning_peg_invalid.valid? + assert tuning_peg_valid.valid? + assert_not guitar.valid? + assert_equal [{error: :not_a_number, value: nil}] , guitar.errors.details["tuning_pegs[1].pitch"] + assert_equal [], guitar.errors.details["tuning_pegs.pitch"] + end + + def test_errors_details_should_be_indexed_when_global_flag_is_set + old_attribute_config = ActiveRecord::Base.index_nested_attribute_errors + ActiveRecord::Base.index_nested_attribute_errors = true + + molecule = Molecule.new + valid_electron = Electron.new(name: 'electron') + invalid_electron = Electron.new + + molecule.electrons = [valid_electron, invalid_electron] + + assert_not invalid_electron.valid? + assert valid_electron.valid? + assert_not molecule.valid? + assert_equal [{error: :blank}], molecule.errors.details["electrons[1].name"] + assert_equal [], molecule.errors.details["electrons.name"] + ensure + ActiveRecord::Base.index_nested_attribute_errors = old_attribute_config + end + def test_valid_adding_with_nested_attributes molecule = Molecule.new valid_electron = Electron.new(name: 'electron') diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb index 23fd1d6571..eef2d29d02 100644 --- a/activerecord/test/cases/base_test.rb +++ b/activerecord/test/cases/base_test.rb @@ -26,7 +26,7 @@ require 'models/bird' require 'models/car' require 'models/bulb' require 'rexml/document' -require 'concurrent/atomics' +require 'concurrent/atomic/count_down_latch' class FirstAbstractClass < ActiveRecord::Base self.abstract_class = true @@ -82,7 +82,6 @@ class BasicsTest < ActiveRecord::TestCase classname = conn.class.name[/[^:]*$/] badchar = { 'SQLite3Adapter' => '"', - 'MysqlAdapter' => '`', 'Mysql2Adapter' => '`', 'PostgreSQLAdapter' => '"', 'OracleAdapter' => '"', @@ -112,7 +111,9 @@ class BasicsTest < ActiveRecord::TestCase unless current_adapter?(:PostgreSQLAdapter, :OracleAdapter, :SQLServerAdapter, :FbAdapter) def test_limit_with_comma - assert Topic.limit("1,2").to_a + assert_deprecated do + assert Topic.limit("1,2").to_a + end end end @@ -138,14 +139,10 @@ class BasicsTest < ActiveRecord::TestCase end def test_limit_should_sanitize_sql_injection_for_limit_with_commas - assert_raises(ArgumentError) do - Topic.limit("1, 7 procedure help()").to_a - end - end - - unless current_adapter?(:MysqlAdapter, :Mysql2Adapter) - def test_limit_should_allow_sql_literal - assert_equal 1, Topic.limit(Arel.sql('2-1')).to_a.length + assert_deprecated do + assert_raises(ArgumentError) do + Topic.limit("1, 7 procedure help()").to_a + end end end @@ -213,7 +210,7 @@ class BasicsTest < ActiveRecord::TestCase end def test_preserving_time_objects_with_local_time_conversion_to_default_timezone_utc - with_env_tz 'America/New_York' do + with_env_tz eastern_time_zone do with_timezone_config default: :utc do time = Time.local(2000) topic = Topic.create('written_on' => time) @@ -226,7 +223,7 @@ class BasicsTest < ActiveRecord::TestCase end def test_preserving_time_objects_with_time_with_zone_conversion_to_default_timezone_utc - with_env_tz 'America/New_York' do + with_env_tz eastern_time_zone do with_timezone_config default: :utc do Time.use_zone 'Central Time (US & Canada)' do time = Time.zone.local(2000) @@ -241,7 +238,7 @@ class BasicsTest < ActiveRecord::TestCase end def test_preserving_time_objects_with_utc_time_conversion_to_default_timezone_local - with_env_tz 'America/New_York' do + with_env_tz eastern_time_zone do with_timezone_config default: :local do time = Time.utc(2000) topic = Topic.create('written_on' => time) @@ -254,7 +251,7 @@ class BasicsTest < ActiveRecord::TestCase end def test_preserving_time_objects_with_time_with_zone_conversion_to_default_timezone_local - with_env_tz 'America/New_York' do + with_env_tz eastern_time_zone do with_timezone_config default: :local do Time.use_zone 'Central Time (US & Canada)' do time = Time.zone.local(2000) @@ -268,6 +265,14 @@ class BasicsTest < ActiveRecord::TestCase end end + def eastern_time_zone + if Gem.win_platform? + "EST5EDT" + else + "America/New_York" + end + end + def test_custom_mutator topic = Topic.find(1) # This mutator is protected in the class definition @@ -438,7 +443,7 @@ class BasicsTest < ActiveRecord::TestCase Post.reset_table_name end - if current_adapter?(:MysqlAdapter, :Mysql2Adapter) + if current_adapter?(:Mysql2Adapter) def test_update_all_with_order_and_limit assert_equal 1, Topic.limit(1).order('id DESC').update_all(:content => 'bulk updated!') end @@ -519,7 +524,8 @@ class BasicsTest < ActiveRecord::TestCase end def test_find_by_slug_with_array - assert_equal Topic.find(['1-meowmeow', '2-hello']), Topic.find([1, 2]) + assert_equal Topic.find([1, 2]), Topic.find(['1-meowmeow', '2-hello']) + assert_equal 'The Second Topic of the day', Topic.find(['2-hello', '1-meowmeow']).first.title end def test_find_by_slug_with_range @@ -798,7 +804,7 @@ class BasicsTest < ActiveRecord::TestCase assert_equal dev.salary.amount, dup.salary.amount assert !dup.persisted? - # test if the attributes have been dupd + # test if the attributes have been duped original_amount = dup.salary.amount dev.salary.amount = 1 assert_equal original_amount, dup.salary.amount @@ -1246,6 +1252,7 @@ class BasicsTest < ActiveRecord::TestCase original_logger = ActiveRecord::Base.logger log = StringIO.new ActiveRecord::Base.logger = ActiveSupport::Logger.new(log) + ActiveRecord::Base.logger.level = Logger::DEBUG ActiveRecord::Base.benchmark("Logging", :level => :debug, :silence => false) { ActiveRecord::Base.logger.debug "Quiet" } assert_match(/Quiet/, log.string) ensure @@ -1268,9 +1275,10 @@ class BasicsTest < ActiveRecord::TestCase UnloadablePost.send(:current_scope=, UnloadablePost.all) UnloadablePost.unloadable - assert_not_nil ActiveRecord::Scoping::ScopeRegistry.value_for(:current_scope, "UnloadablePost") + klass = UnloadablePost + assert_not_nil ActiveRecord::Scoping::ScopeRegistry.value_for(:current_scope, klass) ActiveSupport::Dependencies.remove_unloadable_constants! - assert_nil ActiveRecord::Scoping::ScopeRegistry.value_for(:current_scope, "UnloadablePost") + assert_nil ActiveRecord::Scoping::ScopeRegistry.value_for(:current_scope, klass) ensure Object.class_eval{ remove_const :UnloadablePost } if defined?(UnloadablePost) end @@ -1345,6 +1353,19 @@ class BasicsTest < ActiveRecord::TestCase Company.attribute_names end + def test_has_attribute + assert Company.has_attribute?('id') + assert Company.has_attribute?('type') + assert Company.has_attribute?('name') + assert_not Company.has_attribute?('lastname') + assert_not Company.has_attribute?('age') + end + + def test_has_attribute_with_symbol + assert Company.has_attribute?(:id) + assert_not Company.has_attribute?(:age) + end + def test_attribute_names_on_table_not_exists assert_equal [], NonExistentTable.attribute_names end diff --git a/activerecord/test/cases/batches_test.rb b/activerecord/test/cases/batches_test.rb index da65336305..84aac3e721 100644 --- a/activerecord/test/cases/batches_test.rb +++ b/activerecord/test/cases/batches_test.rb @@ -38,7 +38,7 @@ class EachTest < ActiveRecord::TestCase 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, begin_at: 7).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 end @@ -101,16 +101,16 @@ class EachTest < ActiveRecord::TestCase def test_find_in_batches_should_start_from_the_start_option assert_queries(@total) do - Post.find_in_batches(batch_size: 1, begin_at: 2) do |batch| + Post.find_in_batches(batch_size: 1, start: 2) do |batch| assert_kind_of Array, batch assert_kind_of Post, batch.first end end end - def test_find_in_batches_should_end_at_the_end_option + def test_find_in_batches_should_end_at_the_finish_option assert_queries(6) do - Post.find_in_batches(batch_size: 1, end_at: 5) do |batch| + Post.find_in_batches(batch_size: 1, finish: 5) do |batch| assert_kind_of Array, batch assert_kind_of Post, batch.first end @@ -175,7 +175,7 @@ class EachTest < ActiveRecord::TestCase def test_find_in_batches_should_not_modify_passed_options assert_nothing_raised do - Post.find_in_batches({ batch_size: 42, begin_at: 1 }.freeze){} + Post.find_in_batches({ batch_size: 42, start: 1 }.freeze){} end end @@ -184,7 +184,7 @@ class EachTest < ActiveRecord::TestCase start_nick = nick_order_subscribers.second.nick subscribers = [] - Subscriber.find_in_batches(batch_size: 1, begin_at: start_nick) do |batch| + Subscriber.find_in_batches(batch_size: 1, start: start_nick) do |batch| subscribers.concat(batch) end @@ -311,15 +311,15 @@ class EachTest < ActiveRecord::TestCase def test_in_batches_should_start_from_the_start_option post = Post.order('id ASC').where('id >= ?', 2).first assert_queries(2) do - relation = Post.in_batches(of: 1, begin_at: 2).first + relation = Post.in_batches(of: 1, start: 2).first assert_equal post, relation.first end end - def test_in_batches_should_end_at_the_end_option + def test_in_batches_should_end_at_the_finish_option post = Post.order('id DESC').where('id <= ?', 5).first assert_queries(7) do - relation = Post.in_batches(of: 1, end_at: 5, load: true).reverse_each.first + relation = Post.in_batches(of: 1, finish: 5, load: true).reverse_each.first assert_equal post, relation.last end end @@ -371,7 +371,7 @@ class EachTest < ActiveRecord::TestCase def test_in_batches_should_not_modify_passed_options assert_nothing_raised do - Post.in_batches({ of: 42, begin_at: 1 }.freeze){} + Post.in_batches({ of: 42, start: 1 }.freeze){} end end @@ -380,7 +380,7 @@ class EachTest < ActiveRecord::TestCase start_nick = nick_order_subscribers.second.nick subscribers = [] - Subscriber.in_batches(of: 1, begin_at: start_nick) do |relation| + Subscriber.in_batches(of: 1, start: start_nick) do |relation| subscribers.concat(relation) end @@ -441,32 +441,11 @@ class EachTest < ActiveRecord::TestCase assert_equal 2, person.reload.author_id # incremented only once end - def test_find_in_batches_start_deprecated - assert_deprecated do - assert_queries(@total) do - Post.find_in_batches(batch_size: 1, start: 2) do |batch| - assert_kind_of Array, batch - assert_kind_of Post, batch.first - end - end - end - end - - def test_find_each_start_deprecated - assert_deprecated do - assert_queries(@total) do - Post.find_each(batch_size: 1, start: 2) do |post| - assert_kind_of Post, post - end - end - end - 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, begin_at: 4).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 diff --git a/activerecord/test/cases/binary_test.rb b/activerecord/test/cases/binary_test.rb index 86dee929bf..9eb5352150 100644 --- a/activerecord/test/cases/binary_test.rb +++ b/activerecord/test/cases/binary_test.rb @@ -20,10 +20,6 @@ unless current_adapter?(:DB2Adapter) name = binary.name - # MySQL adapter doesn't properly encode things, so we have to do it - if current_adapter?(:MysqlAdapter) - name.force_encoding(Encoding::UTF_8) - end assert_equal 'ã„ãŸã ãã¾ã™ï¼', name end diff --git a/activerecord/test/cases/bind_parameter_test.rb b/activerecord/test/cases/bind_parameter_test.rb index 1e38b97c4a..cd9c76f1f0 100644 --- a/activerecord/test/cases/bind_parameter_test.rb +++ b/activerecord/test/cases/bind_parameter_test.rb @@ -39,7 +39,7 @@ module ActiveRecord end def test_binds_are_logged - sub = @connection.substitute_at(@pk) + sub = Arel::Nodes::BindParam.new binds = [Relation::QueryAttribute.new("id", 1, Type::Value.new)] sql = "select * from topics where id = #{sub.to_sql}" diff --git a/activerecord/test/cases/calculations_test.rb b/activerecord/test/cases/calculations_test.rb index 4a0e6f497f..8f2682c781 100644 --- a/activerecord/test/cases/calculations_test.rb +++ b/activerecord/test/cases/calculations_test.rb @@ -124,7 +124,7 @@ class CalculationsTest < ActiveRecord::TestCase end def test_should_generate_valid_sql_with_joins_and_group - assert_nothing_raised ActiveRecord::StatementInvalid do + assert_nothing_raised do AuditLog.joins(:developer).group(:id).count end end @@ -545,8 +545,8 @@ class CalculationsTest < ActiveRecord::TestCase assert_equal 7, Company.includes(:contracts).sum(:developer_id) end - def test_from_option_with_specified_index - if Edge.connection.adapter_name == 'MySQL' or Edge.connection.adapter_name == 'Mysql2' + if current_adapter?(:Mysql2Adapter) + def test_from_option_with_specified_index assert_equal Edge.count(:all), Edge.from('edges USE INDEX(unique_edge_index)').count(:all) assert_equal Edge.where('sink_id < 5').count(:all), Edge.from('edges USE INDEX(unique_edge_index)').where('sink_id < 5').count(:all) @@ -742,7 +742,7 @@ class CalculationsTest < ActiveRecord::TestCase end def test_should_reference_correct_aliases_while_joining_tables_of_has_many_through_association - assert_nothing_raised ActiveRecord::StatementInvalid do + assert_nothing_raised do developer = Developer.create!(name: 'developer') developer.ratings.includes(comment: :post).where(posts: { id: 1 }).count end diff --git a/activerecord/test/cases/callbacks_test.rb b/activerecord/test/cases/callbacks_test.rb index 73ac30e547..4f70ae3a1d 100644 --- a/activerecord/test/cases/callbacks_test.rb +++ b/activerecord/test/cases/callbacks_test.rb @@ -33,7 +33,7 @@ class CallbackDeveloper < ActiveRecord::Base ActiveRecord::Callbacks::CALLBACKS.each do |callback_method| next if callback_method.to_s =~ /^around_/ define_callback_method(callback_method) - send(callback_method, callback_string(callback_method)) + ActiveSupport::Deprecation.silence { send(callback_method, callback_string(callback_method)) } send(callback_method, callback_proc(callback_method)) send(callback_method, callback_object(callback_method)) send(callback_method) { |model| model.history << [callback_method, :block] } diff --git a/activerecord/test/cases/collection_cache_key_test.rb b/activerecord/test/cases/collection_cache_key_test.rb index 53058c5a4a..a2874438c1 100644 --- a/activerecord/test/cases/collection_cache_key_test.rb +++ b/activerecord/test/cases/collection_cache_key_test.rb @@ -66,5 +66,24 @@ module ActiveRecord developers = projects(:active_record).developers assert_match(/\Adevelopers\/query-(\h+)-(\d+)-(\d+)\Z/, developers.cache_key) end + + test "cache_key for loaded collection with zero size" do + Comment.delete_all + posts = Post.includes(:comments) + empty_loaded_collection = posts.first.comments + + 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) + 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) + end end end diff --git a/activerecord/test/cases/column_definition_test.rb b/activerecord/test/cases/column_definition_test.rb index 14b95ecab1..81162b7e98 100644 --- a/activerecord/test/cases/column_definition_test.rb +++ b/activerecord/test/cases/column_definition_test.rb @@ -38,42 +38,52 @@ module ActiveRecord assert_equal %Q{title varchar(20) DEFAULT 'Hello' NOT NULL}, @viz.accept(column_def) end - if current_adapter?(:MysqlAdapter, :Mysql2Adapter) + if current_adapter?(:Mysql2Adapter) def test_should_set_default_for_mysql_binary_data_types type = SqlTypeMetadata.new(type: :binary, sql_type: "binary(1)") - binary_column = AbstractMysqlAdapter::Column.new("title", "a", type) + binary_column = MySQL::Column.new("title", "a", type) assert_equal "a", binary_column.default type = SqlTypeMetadata.new(type: :binary, sql_type: "varbinary") - varbinary_column = AbstractMysqlAdapter::Column.new("title", "a", type) + varbinary_column = MySQL::Column.new("title", "a", type) assert_equal "a", varbinary_column.default end + def test_should_be_empty_string_default_for_mysql_binary_data_types + type = SqlTypeMetadata.new(type: :binary, sql_type: "binary(1)") + binary_column = MySQL::Column.new("title", "", type, false) + assert_equal "", binary_column.default + + type = SqlTypeMetadata.new(type: :binary, sql_type: "varbinary") + varbinary_column = MySQL::Column.new("title", "", type, false) + assert_equal "", varbinary_column.default + end + def test_should_not_set_default_for_blob_and_text_data_types assert_raise ArgumentError do - AbstractMysqlAdapter::Column.new("title", "a", SqlTypeMetadata.new(sql_type: "blob")) + MySQL::Column.new("title", "a", SqlTypeMetadata.new(sql_type: "blob")) end - text_type = AbstractMysqlAdapter::MysqlTypeMetadata.new( + text_type = MySQL::TypeMetadata.new( SqlTypeMetadata.new(type: :text)) assert_raise ArgumentError do - AbstractMysqlAdapter::Column.new("title", "Hello", text_type) + MySQL::Column.new("title", "Hello", text_type) end - text_column = AbstractMysqlAdapter::Column.new("title", nil, text_type) + text_column = MySQL::Column.new("title", nil, text_type) assert_equal nil, text_column.default - not_null_text_column = AbstractMysqlAdapter::Column.new("title", nil, text_type, false) + not_null_text_column = MySQL::Column.new("title", nil, text_type, false) assert_equal "", not_null_text_column.default end def test_has_default_should_return_false_for_blob_and_text_data_types binary_type = SqlTypeMetadata.new(sql_type: "blob") - blob_column = AbstractMysqlAdapter::Column.new("title", nil, binary_type) + blob_column = MySQL::Column.new("title", nil, binary_type) assert !blob_column.has_default? text_type = SqlTypeMetadata.new(type: :text) - text_column = AbstractMysqlAdapter::Column.new("title", nil, text_type) + text_column = MySQL::Column.new("title", nil, text_type) assert !text_column.has_default? end end diff --git a/activerecord/test/cases/connection_adapters/mysql_type_lookup_test.rb b/activerecord/test/cases/connection_adapters/mysql_type_lookup_test.rb index 2749273884..f2b1d9e4e7 100644 --- a/activerecord/test/cases/connection_adapters/mysql_type_lookup_test.rb +++ b/activerecord/test/cases/connection_adapters/mysql_type_lookup_test.rb @@ -1,6 +1,6 @@ require "cases/helper" -if current_adapter?(:MysqlAdapter, :Mysql2Adapter) +if current_adapter?(:Mysql2Adapter) module ActiveRecord module ConnectionAdapters class MysqlTypeLookupTest < ActiveRecord::TestCase diff --git a/activerecord/test/cases/connection_management_test.rb b/activerecord/test/cases/connection_management_test.rb index dff6ea0fb0..d43668e57c 100644 --- a/activerecord/test/cases/connection_management_test.rb +++ b/activerecord/test/cases/connection_management_test.rb @@ -92,7 +92,14 @@ module ActiveRecord app = lambda { |_| [200, {}, body] } response_body = ConnectionManagement.new(app).call(@env)[2] assert response_body.respond_to?(:to_path) - assert_equal response_body.to_path, "/path" + assert_equal "/path", response_body.to_path + end + + test "doesn't mutate the original response" do + original_response = [200, {}, 'hi'] + app = lambda { |_| original_response } + ConnectionManagement.new(app).call(@env)[2] + assert_equal 'hi', original_response.last end end end diff --git a/activerecord/test/cases/connection_pool_test.rb b/activerecord/test/cases/connection_pool_test.rb index 7ef5c93a48..efa3e0455e 100644 --- a/activerecord/test/cases/connection_pool_test.rb +++ b/activerecord/test/cases/connection_pool_test.rb @@ -1,5 +1,5 @@ require "cases/helper" -require 'concurrent/atomics' +require 'concurrent/atomic/count_down_latch' module ActiveRecord module ConnectionAdapters diff --git a/activerecord/test/cases/connection_specification/resolver_test.rb b/activerecord/test/cases/connection_specification/resolver_test.rb index 3c2f5d4219..358b6ad537 100644 --- a/activerecord/test/cases/connection_specification/resolver_test.rb +++ b/activerecord/test/cases/connection_specification/resolver_test.rb @@ -57,6 +57,12 @@ module ActiveRecord "encoding" => "utf8" }, spec) end + def test_url_missing_scheme + spec = resolve 'foo' + assert_equal({ + "database" => "foo" }, spec) + end + def test_url_host_db spec = resolve 'abstract://foo/bar?encoding=utf8' assert_equal({ diff --git a/activerecord/test/cases/counter_cache_test.rb b/activerecord/test/cases/counter_cache_test.rb index 922cb59280..66b4c3f1ff 100644 --- a/activerecord/test/cases/counter_cache_test.rb +++ b/activerecord/test/cases/counter_cache_test.rb @@ -151,7 +151,7 @@ class CounterCacheTest < ActiveRecord::TestCase test "reset the right counter if two have the same foreign key" do michael = people(:michael) - assert_nothing_raised(ActiveRecord::StatementInvalid) do + assert_nothing_raised do Person.reset_counters(michael.id, :friends_too) end end diff --git a/activerecord/test/cases/custom_locking_test.rb b/activerecord/test/cases/custom_locking_test.rb index e8290297e3..26d015bf71 100644 --- a/activerecord/test/cases/custom_locking_test.rb +++ b/activerecord/test/cases/custom_locking_test.rb @@ -6,7 +6,7 @@ module ActiveRecord fixtures :people def test_custom_lock - if current_adapter?(:MysqlAdapter, :Mysql2Adapter) + if current_adapter?(:Mysql2Adapter) assert_match 'SHARE MODE', Person.lock('LOCK IN SHARE MODE').to_sql assert_sql(/LOCK IN SHARE MODE/) do Person.all.merge!(:lock => 'LOCK IN SHARE MODE').find(1) diff --git a/activerecord/test/cases/database_statements_test.rb b/activerecord/test/cases/database_statements_test.rb index c689e97d83..ba085991e0 100644 --- a/activerecord/test/cases/database_statements_test.rb +++ b/activerecord/test/cases/database_statements_test.rb @@ -6,14 +6,23 @@ class DatabaseStatementsTest < ActiveRecord::TestCase end def test_insert_should_return_the_inserted_id + assert_not_nil return_the_inserted_id(method: :insert) + end + + def test_create_should_return_the_inserted_id + assert_not_nil return_the_inserted_id(method: :create) + end + + private + + def return_the_inserted_id(method:) # Oracle adapter uses prefetched primary key values from sequence and passes them to connection adapter insert method if current_adapter?(:OracleAdapter) sequence_name = "accounts_seq" id_value = @connection.next_sequence_value(sequence_name) - id = @connection.insert("INSERT INTO accounts (id, firm_id,credit_limit) VALUES (accounts_seq.nextval,42,5000)", nil, :id, id_value, sequence_name) + @connection.send(method, "INSERT INTO accounts (id, firm_id,credit_limit) VALUES (accounts_seq.nextval,42,5000)", nil, :id, id_value, sequence_name) else - id = @connection.insert("INSERT INTO accounts (firm_id,credit_limit) VALUES (42,5000)") + @connection.send(method, "INSERT INTO accounts (firm_id,credit_limit) VALUES (42,5000)") end - assert_not_nil id end end diff --git a/activerecord/test/cases/date_time_precision_test.rb b/activerecord/test/cases/date_time_precision_test.rb index 698f1b852e..e996d142a2 100644 --- a/activerecord/test/cases/date_time_precision_test.rb +++ b/activerecord/test/cases/date_time_precision_test.rb @@ -10,6 +10,7 @@ class DateTimePrecisionTest < ActiveRecord::TestCase setup do @connection = ActiveRecord::Base.connection + Foo.reset_column_information end teardown do @@ -20,24 +21,24 @@ class DateTimePrecisionTest < ActiveRecord::TestCase @connection.create_table(:foos, force: true) @connection.add_column :foos, :created_at, :datetime, precision: 0 @connection.add_column :foos, :updated_at, :datetime, precision: 5 - assert_equal 0, activerecord_column_option('foos', 'created_at', 'precision') - assert_equal 5, activerecord_column_option('foos', 'updated_at', 'precision') + assert_equal 0, Foo.columns_hash['created_at'].precision + assert_equal 5, Foo.columns_hash['updated_at'].precision end def test_timestamps_helper_with_custom_precision @connection.create_table(:foos, force: true) do |t| t.timestamps precision: 4 end - assert_equal 4, activerecord_column_option('foos', 'created_at', 'precision') - assert_equal 4, activerecord_column_option('foos', 'updated_at', 'precision') + assert_equal 4, Foo.columns_hash['created_at'].precision + assert_equal 4, Foo.columns_hash['updated_at'].precision end def test_passing_precision_to_datetime_does_not_set_limit @connection.create_table(:foos, force: true) do |t| t.timestamps precision: 4 end - assert_nil activerecord_column_option('foos', 'created_at', 'limit') - assert_nil activerecord_column_option('foos', 'updated_at', 'limit') + assert_nil Foo.columns_hash['created_at'].limit + assert_nil Foo.columns_hash['updated_at'].limit end def test_invalid_datetime_precision_raises_error @@ -48,14 +49,6 @@ class DateTimePrecisionTest < ActiveRecord::TestCase end end - def test_database_agrees_with_activerecord_about_precision - @connection.create_table(:foos, force: true) do |t| - t.timestamps precision: 4 - end - assert_equal 4, database_datetime_precision('foos', 'created_at') - assert_equal 4, database_datetime_precision('foos', 'updated_at') - end - def test_formatting_datetime_according_to_precision @connection.create_table(:foos, force: true) do |t| t.datetime :created_at, precision: 0 @@ -91,21 +84,5 @@ class DateTimePrecisionTest < ActiveRecord::TestCase end end - private - - def database_datetime_precision(table_name, column_name) - results = @connection.exec_query("SELECT column_name, datetime_precision FROM information_schema.columns WHERE table_name = '#{table_name}'") - result = results.find do |result_hash| - result_hash["column_name"] == column_name - end - result && result["datetime_precision"].to_i - end - - def activerecord_column_option(tablename, column_name, option) - result = @connection.columns(tablename).find do |column| - column.name == column_name - end - result && result.send(option) - end end end diff --git a/activerecord/test/cases/defaults_test.rb b/activerecord/test/cases/defaults_test.rb index 67fddebf45..067513e24c 100644 --- a/activerecord/test/cases/defaults_test.rb +++ b/activerecord/test/cases/defaults_test.rb @@ -1,20 +1,14 @@ require "cases/helper" +require 'support/schema_dumping_helper' require 'models/default' require 'models/entrant' class DefaultTest < ActiveRecord::TestCase def test_nil_defaults_for_not_null_columns - column_defaults = - if current_adapter?(:MysqlAdapter) && (Mysql.client_version < 50051 || (50100..50122).include?(Mysql.client_version)) - { 'id' => nil, 'name' => '', 'course_id' => nil } - else - { 'id' => nil, 'name' => nil, 'course_id' => nil } - end - - column_defaults.each do |name, default| + %w(id name course_id).each do |name| column = Entrant.columns_hash[name] assert !column.null, "#{name} column should be NOT NULL" - assert_equal default, column.default, "#{name} column should be DEFAULT #{default.inspect}" + assert_not column.default, "#{name} column should be DEFAULT 'nil'" end end @@ -87,7 +81,32 @@ class DefaultStringsTest < ActiveRecord::TestCase end end -if current_adapter?(:MysqlAdapter, :Mysql2Adapter) +if current_adapter?(:PostgreSQLAdapter) + class PostgresqlDefaultExpressionTest < ActiveRecord::TestCase + include SchemaDumpingHelper + + 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 + 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 +end + +if current_adapter?(:Mysql2Adapter) + class MysqlDefaultExpressionTest < ActiveRecord::TestCase + include SchemaDumpingHelper + + if ActiveRecord::Base.connection.version >= '5.6.0' + test "schema dump includes default expression" do + output = dump_table_schema("datetime_defaults") + assert_match %r/t\.datetime\s+"modified_datetime",\s+default: -> { "CURRENT_TIMESTAMP" }/, output + end + end + end + class DefaultsTestWithoutTransactionalFixtures < ActiveRecord::TestCase # ActiveRecord::Base#create! (and #save and other related methods) will # open a new transaction. When in transactional tests mode, this will @@ -182,8 +201,7 @@ if current_adapter?(:MysqlAdapter, :Mysql2Adapter) assert_equal '0', klass.columns_hash['zero'].default assert !klass.columns_hash['zero'].null - # 0 in MySQL 4, nil in 5. - assert [0, nil].include?(klass.columns_hash['omit'].default) + assert_equal nil, klass.columns_hash['omit'].default assert !klass.columns_hash['omit'].null assert_raise(ActiveRecord::StatementInvalid) { klass.create! } diff --git a/activerecord/test/cases/enum_test.rb b/activerecord/test/cases/enum_test.rb index 7c930de97b..babacd1ee9 100644 --- a/activerecord/test/cases/enum_test.rb +++ b/activerecord/test/cases/enum_test.rb @@ -411,4 +411,14 @@ class EnumTest < ActiveRecord::TestCase assert book.proposed?, "expected fixture to default to proposed status" assert book.in_english?, "expected fixture to default to english language" end + + test "uses default value from database on initialization" do + book = Book.new + assert book.proposed? + end + + test "uses default value from database on initialization when using custom mapping" do + book = Book.new + assert book.hard? + end end diff --git a/activerecord/test/cases/finder_test.rb b/activerecord/test/cases/finder_test.rb index 6686ce012d..3e31874455 100644 --- a/activerecord/test/cases/finder_test.rb +++ b/activerecord/test/cases/finder_test.rb @@ -19,7 +19,7 @@ require 'models/car' require 'models/tyre' class FinderTest < ActiveRecord::TestCase - fixtures :companies, :topics, :entrants, :developers, :developers_projects, :posts, :comments, :accounts, :authors, :customers, :categories, :categorizations, :cars + fixtures :companies, :topics, :entrants, :developers, :developers_projects, :posts, :comments, :accounts, :authors, :author_addresses, :customers, :categories, :categorizations, :cars def test_find_by_id_with_hash assert_raises(ActiveRecord::StatementInvalid) do @@ -43,11 +43,80 @@ class FinderTest < ActiveRecord::TestCase end assert_equal "should happen", exception.message - assert_nothing_raised(RuntimeError) do + assert_nothing_raised do Topic.all.find(-> { raise "should not happen" }) { |e| e.title == topics(:first).title } end end + def test_find_with_ids_returning_ordered + records = Topic.find([4,2,5]) + assert_equal 'The Fourth Topic of the day', records[0].title + assert_equal 'The Second Topic of the day', records[1].title + assert_equal 'The Fifth Topic of the day', records[2].title + + records = Topic.find(4,2,5) + assert_equal 'The Fourth Topic of the day', records[0].title + assert_equal 'The Second Topic of the day', records[1].title + assert_equal 'The Fifth Topic of the day', records[2].title + + records = Topic.find(['4','2','5']) + assert_equal 'The Fourth Topic of the day', records[0].title + assert_equal 'The Second Topic of the day', records[1].title + assert_equal 'The Fifth Topic of the day', records[2].title + + records = Topic.find('4','2','5') + assert_equal 'The Fourth Topic of the day', records[0].title + assert_equal 'The Second Topic of the day', records[1].title + assert_equal 'The Fifth Topic of the day', records[2].title + end + + def test_find_with_ids_and_order_clause + # The order clause takes precedence over the informed ids + records = Topic.order(:author_name).find([5,3,1]) + assert_equal 'The Third Topic of the day', records[0].title + assert_equal 'The First Topic', records[1].title + assert_equal 'The Fifth Topic of the day', records[2].title + + records = Topic.order(:id).find([5,3,1]) + assert_equal 'The First Topic', records[0].title + assert_equal 'The Third Topic of the day', records[1].title + assert_equal 'The Fifth Topic of the day', records[2].title + end + + def test_find_with_ids_with_limit_and_order_clause + # The order clause takes precedence over the informed ids + records = Topic.limit(2).order(:id).find([5,3,1]) + assert_equal 2, records.size + assert_equal 'The First Topic', records[0].title + assert_equal 'The Third Topic of the day', records[1].title + end + + def test_find_with_ids_and_limit + records = Topic.limit(3).find([3,2,5,1,4]) + assert_equal 3, records.size + assert_equal 'The Third Topic of the day', records[0].title + assert_equal 'The Second Topic of the day', records[1].title + assert_equal 'The Fifth Topic of the day', records[2].title + end + + def test_find_with_ids_where_and_limit + # Please note that Topic 1 is the only not approved so + # if it were among the first 3 it would raise an ActiveRecord::RecordNotFound + records = Topic.where(approved: true).limit(3).find([3,2,5,1,4]) + assert_equal 3, records.size + assert_equal 'The Third Topic of the day', records[0].title + assert_equal 'The Second Topic of the day', records[1].title + assert_equal 'The Fifth Topic of the day', records[2].title + end + + def test_find_with_ids_and_offset + records = Topic.offset(2).find([3,2,5,1,4]) + assert_equal 3, records.size + assert_equal 'The Fifth Topic of the day', records[0].title + assert_equal 'The First Topic', records[1].title + assert_equal 'The Fourth Topic of the day', records[2].title + end + def test_find_passing_active_record_object_is_deprecated assert_deprecated do Topic.find(Topic.last) @@ -195,7 +264,9 @@ class FinderTest < ActiveRecord::TestCase def test_find_by_ids_with_limit_and_offset assert_equal 2, Entrant.limit(2).find([1,3,2]).size - assert_equal 1, Entrant.limit(3).offset(2).find([1,3,2]).size + entrants = Entrant.limit(3).offset(2).find([1,3,2]) + assert_equal 1, entrants.size + assert_equal 'Ruby Guru', entrants.first.name # Also test an edge case: If you have 11 results, and you set a # limit of 3 and offset of 9, then you should find that there @@ -203,6 +274,8 @@ class FinderTest < ActiveRecord::TestCase devs = Developer.all last_devs = Developer.limit(3).offset(9).find devs.map(&:id) assert_equal 2, last_devs.size + assert_equal 'fixture_10', last_devs[0].name + assert_equal 'Jamis', last_devs[1].name end def test_find_with_large_number @@ -434,25 +507,53 @@ class FinderTest < ActiveRecord::TestCase end def test_take_and_first_and_last_with_integer_should_use_sql_limit - assert_sql(/LIMIT 3|ROWNUM <= 3/) { Topic.take(3).entries } - assert_sql(/LIMIT 2|ROWNUM <= 2/) { Topic.first(2).entries } - assert_sql(/LIMIT 5|ROWNUM <= 5/) { Topic.last(5).entries } + assert_sql(/LIMIT|ROWNUM <=/) { Topic.take(3).entries } + assert_sql(/LIMIT|ROWNUM <=/) { Topic.first(2).entries } + assert_sql(/LIMIT|ROWNUM <=/) { Topic.last(5).entries } end def test_last_with_integer_and_order_should_keep_the_order assert_equal Topic.order("title").to_a.last(2), Topic.order("title").last(2) end - def test_last_with_integer_and_order_should_not_use_sql_limit - query = assert_sql { Topic.order("title").last(5).entries } - assert_equal 1, query.length - assert_no_match(/LIMIT/, query.first) + def test_last_with_integer_and_order_should_use_sql_limit + relation = Topic.order("title") + assert_queries(1) { relation.last(5) } + assert !relation.loaded? + end + + def test_last_with_integer_and_reorder_should_use_sql_limit + relation = Topic.reorder("title") + assert_queries(1) { relation.last(5) } + assert !relation.loaded? + end + + def test_last_on_loaded_relation_should_not_use_sql + relation = Topic.limit(10).load + assert_no_queries do + relation.last + relation.last(2) + end end - def test_last_with_integer_and_reorder_should_not_use_sql_limit - query = assert_sql { Topic.reorder("title").last(5).entries } - assert_equal 1, query.length - assert_no_match(/LIMIT/, query.first) + def test_last_with_irreversible_order + assert_deprecated do + Topic.order("coalesce(author_name, title)").last + end + end + + def test_last_on_relation_with_limit_and_offset + post = posts('sti_comments') + + comments = post.comments.order(id: :asc) + assert_equal comments.limit(2).to_a.last, comments.limit(2).last + assert_equal comments.limit(2).to_a.last(2), comments.limit(2).last(2) + assert_equal comments.limit(2).to_a.last(3), comments.limit(2).last(3) + + comments = comments.offset(1) + assert_equal comments.limit(2).to_a.last, comments.limit(2).last + assert_equal comments.limit(2).to_a.last(2), comments.limit(2).last(2) + assert_equal comments.limit(2).to_a.last(3), comments.limit(2).last(3) end def test_take_and_first_and_last_with_integer_should_return_an_array @@ -706,96 +807,13 @@ class FinderTest < ActiveRecord::TestCase assert Company.where(["name = :name", {name: "37signals' go'es agains"}]).first end - def test_bind_arity - assert_nothing_raised { bind '' } - assert_raise(ActiveRecord::PreparedStatementInvalid) { bind '', 1 } - - assert_raise(ActiveRecord::PreparedStatementInvalid) { bind '?' } - assert_nothing_raised { bind '?', 1 } - assert_raise(ActiveRecord::PreparedStatementInvalid) { bind '?', 1, 1 } - end - def test_named_bind_variables - assert_equal '1', bind(':a', :a => 1) # ' ruby-mode - assert_equal '1 1', bind(':a :a', :a => 1) # ' ruby-mode - - assert_nothing_raised { bind("'+00:00'", :foo => "bar") } - assert_kind_of Firm, Company.where(["name = :name", { name: "37signals" }]).first assert_nil Company.where(["name = :name", { name: "37signals!" }]).first assert_nil Company.where(["name = :name", { name: "37signals!' OR 1=1" }]).first assert_kind_of Time, Topic.where(["id = :id", { id: 1 }]).first.written_on end - def test_named_bind_arity - assert_nothing_raised { bind "name = :name", { name: "37signals" } } - assert_nothing_raised { bind "name = :name", { name: "37signals", id: 1 } } - assert_raise(ActiveRecord::PreparedStatementInvalid) { bind "name = :name", { id: 1 } } - end - - class SimpleEnumerable - include Enumerable - - def initialize(ary) - @ary = ary - end - - def each(&b) - @ary.each(&b) - end - end - - def test_bind_enumerable - quoted_abc = %(#{ActiveRecord::Base.connection.quote('a')},#{ActiveRecord::Base.connection.quote('b')},#{ActiveRecord::Base.connection.quote('c')}) - - assert_equal '1,2,3', bind('?', [1, 2, 3]) - assert_equal quoted_abc, bind('?', %w(a b c)) - - assert_equal '1,2,3', bind(':a', :a => [1, 2, 3]) - assert_equal quoted_abc, bind(':a', :a => %w(a b c)) # ' - - assert_equal '1,2,3', bind('?', SimpleEnumerable.new([1, 2, 3])) - assert_equal quoted_abc, bind('?', SimpleEnumerable.new(%w(a b c))) - - assert_equal '1,2,3', bind(':a', :a => SimpleEnumerable.new([1, 2, 3])) - assert_equal quoted_abc, bind(':a', :a => SimpleEnumerable.new(%w(a b c))) # ' - end - - def test_bind_empty_enumerable - quoted_nil = ActiveRecord::Base.connection.quote(nil) - assert_equal quoted_nil, bind('?', []) - assert_equal " in (#{quoted_nil})", bind(' in (?)', []) - assert_equal "foo in (#{quoted_nil})", bind('foo in (?)', []) - end - - def test_bind_empty_string - quoted_empty = ActiveRecord::Base.connection.quote('') - assert_equal quoted_empty, bind('?', '') - end - - def test_bind_chars - quoted_bambi = ActiveRecord::Base.connection.quote("Bambi") - quoted_bambi_and_thumper = ActiveRecord::Base.connection.quote("Bambi\nand\nThumper") - assert_equal "name=#{quoted_bambi}", bind('name=?', "Bambi") - assert_equal "name=#{quoted_bambi_and_thumper}", bind('name=?', "Bambi\nand\nThumper") - assert_equal "name=#{quoted_bambi}", bind('name=?', "Bambi".mb_chars) - assert_equal "name=#{quoted_bambi_and_thumper}", bind('name=?', "Bambi\nand\nThumper".mb_chars) - end - - def test_bind_record - o = Struct.new(:quoted_id).new(1) - assert_equal '1', bind('?', o) - - os = [o] * 3 - assert_equal '1,1,1', bind('?', os) - end - - def test_named_bind_with_postgresql_type_casts - l = Proc.new { bind(":a::integer '2009-01-01'::date", :a => '10') } - assert_nothing_raised(&l) - assert_equal "#{ActiveRecord::Base.connection.quote('10')}::integer '2009-01-01'::date", l.call - end - def test_string_sanitation assert_not_equal "'something ' 1=1'", ActiveRecord::Base.sanitize("something ' 1=1") assert_equal "'something; select table'", ActiveRecord::Base.sanitize("something; select table") @@ -1003,10 +1021,13 @@ class FinderTest < ActiveRecord::TestCase end def test_find_with_order_on_included_associations_with_construct_finder_sql_for_association_limiting_and_is_distinct - assert_equal 2, Post.includes(authors: :author_address).order('author_addresses.id DESC ').limit(2).to_a.size + assert_equal 2, Post.includes(authors: :author_address). + where.not(author_addresses: { id: nil }). + order('author_addresses.id DESC').limit(2).to_a.size assert_equal 3, Post.includes(author: :author_address, authors: :author_address). - order('author_addresses_authors.id DESC ').limit(3).to_a.size + where.not(author_addresses_authors: { id: nil }). + order('author_addresses_authors.id DESC').limit(3).to_a.size end def test_find_with_nil_inside_set_passed_for_one_attribute @@ -1065,7 +1086,7 @@ class FinderTest < ActiveRecord::TestCase end def test_finder_with_offset_string - assert_nothing_raised(ActiveRecord::StatementInvalid) { Topic.offset("3").to_a } + assert_nothing_raised { Topic.offset("3").to_a } end test "find_by with hash conditions returns the first matching record" do @@ -1136,14 +1157,6 @@ class FinderTest < ActiveRecord::TestCase end protected - def bind(statement, *vars) - if vars.first.is_a?(Hash) - ActiveRecord::Base.send(:replace_named_bind_variables, statement, vars.first) - else - ActiveRecord::Base.send(:replace_bind_variables, statement, vars) - end - end - def table_with_custom_primary_key yield(Class.new(Toy) do def self.name diff --git a/activerecord/test/cases/fixtures_test.rb b/activerecord/test/cases/fixtures_test.rb index f30ed4fcc8..da934ab8fe 100644 --- a/activerecord/test/cases/fixtures_test.rb +++ b/activerecord/test/cases/fixtures_test.rb @@ -184,7 +184,6 @@ class FixturesTest < ActiveRecord::TestCase end def test_fixtures_from_root_yml_with_instantiation - # assert_equal 2, @accounts.size assert_equal 50, @unknown.credit_limit end @@ -224,6 +223,10 @@ class FixturesTest < ActiveRecord::TestCase assert_equal(%(table "parrots" has no column named "arrr".), e.message) end + def test_yaml_file_with_symbol_columns + ActiveRecord::FixtureSet.create_fixtures(FIXTURES_ROOT + "/naked/yml", "trees") + end + def test_omap_fixtures assert_nothing_raised do fixtures = ActiveRecord::FixtureSet.new(Account.connection, 'categories', Category, FIXTURES_ROOT + "/categories_ordered") diff --git a/activerecord/test/cases/forbidden_attributes_protection_test.rb b/activerecord/test/cases/forbidden_attributes_protection_test.rb index f4e7646f03..91921469b8 100644 --- a/activerecord/test/cases/forbidden_attributes_protection_test.rb +++ b/activerecord/test/cases/forbidden_attributes_protection_test.rb @@ -1,14 +1,20 @@ require 'cases/helper' require 'active_support/core_ext/hash/indifferent_access' -require 'models/person' + require 'models/company' +require 'models/person' +require 'models/ship' +require 'models/ship_part' +require 'models/treasure' -class ProtectedParams < ActiveSupport::HashWithIndifferentAccess +class ProtectedParams attr_accessor :permitted alias :permitted? :permitted + delegate :keys, :key?, :has_key?, :empty?, to: :@parameters + def initialize(attributes) - super(attributes) + @parameters = attributes.with_indifferent_access @permitted = false end @@ -17,6 +23,18 @@ class ProtectedParams < ActiveSupport::HashWithIndifferentAccess self end + def [](key) + @parameters[key] + end + + def to_h + @parameters + end + + def stringify_keys + dup + end + def dup super.tap do |duplicate| duplicate.instance_variable_set :@permitted, @permitted @@ -75,6 +93,13 @@ class ForbiddenAttributesProtectionTest < ActiveRecord::TestCase end end + def test_create_with_works_with_permitted_params + params = ProtectedParams.new(first_name: 'Guille').permit! + + person = Person.create_with(params).create! + assert_equal 'Guille', person.first_name + end + def test_create_with_works_with_params_values params = ProtectedParams.new(first_name: 'Guille') @@ -90,10 +115,51 @@ class ForbiddenAttributesProtectionTest < ActiveRecord::TestCase end end + def test_where_works_with_permitted_params + params = ProtectedParams.new(first_name: 'Guille').permit! + + person = Person.where(params).create! + assert_equal 'Guille', person.first_name + end + def test_where_works_with_params_values params = ProtectedParams.new(first_name: 'Guille') person = Person.where(first_name: params[:first_name]).create! assert_equal 'Guille', person.first_name end + + def test_where_not_checks_permitted + params = ProtectedParams.new(first_name: 'Guille', gender: 'm') + + assert_raises(ActiveModel::ForbiddenAttributesError) do + Person.where().not(params) + end + end + + def test_where_not_works_with_permitted_params + params = ProtectedParams.new(first_name: 'Guille').permit! + Person.create!(params) + assert_empty Person.where.not(params).select {|p| p.first_name == 'Guille' } + end + + def test_strong_params_style_objects_work_with_singular_associations + params = ProtectedParams.new( name: "Stern", ship_attributes: ProtectedParams.new(name: "The Black Rock").permit!).permit! + part = ShipPart.new(params) + + assert_equal "Stern", part.name + assert_equal "The Black Rock", part.ship.name + end + + def test_strong_params_style_objects_work_with_collection_associations + params = ProtectedParams.new( + trinkets_attributes: ProtectedParams.new( + "0" => ProtectedParams.new(name: "Necklace").permit!, + "1" => ProtectedParams.new(name: "Spoon").permit! ) ).permit! + part = ShipPart.new(params) + + assert_equal "Necklace", part.trinkets[0].name + assert_equal "Spoon", part.trinkets[1].name + end + end diff --git a/activerecord/test/cases/helper.rb b/activerecord/test/cases/helper.rb index d82a3040fc..95f8706d73 100644 --- a/activerecord/test/cases/helper.rb +++ b/activerecord/test/cases/helper.rb @@ -47,11 +47,11 @@ def in_memory_db? end def subsecond_precision_supported? - !current_adapter?(:MysqlAdapter, :Mysql2Adapter) || ActiveRecord::Base.connection.version >= '5.6.4' + ActiveRecord::Base.connection.supports_datetime_with_precision? end def mysql_enforcing_gtid_consistency? - current_adapter?(:MysqlAdapter, :Mysql2Adapter) && 'ON' == ActiveRecord::Base.connection.show_variable('enforce_gtid_consistency') + current_adapter?(:Mysql2Adapter) && 'ON' == ActiveRecord::Base.connection.show_variable('enforce_gtid_consistency') end def supports_savepoints? diff --git a/activerecord/test/cases/inheritance_test.rb b/activerecord/test/cases/inheritance_test.rb index 52e3734dd0..7da6842047 100644 --- a/activerecord/test/cases/inheritance_test.rb +++ b/activerecord/test/cases/inheritance_test.rb @@ -7,6 +7,7 @@ require 'models/project' require 'models/subscriber' require 'models/vegetables' require 'models/shop' +require 'models/sponsor' module InheritanceTestHelper def with_store_full_sti_class(&block) @@ -478,4 +479,128 @@ class InheritanceComputeTypeTest < ActiveRecord::TestCase product = Shop::Product.new(:type => phone) assert product.save end + + def test_inheritance_new_with_subclass_as_default + original_type = Company.columns_hash["type"].default + ActiveRecord::Base.connection.change_column_default :companies, :type, 'Firm' + Company.reset_column_information + + firm = Company.new # without arguments + assert_equal 'Firm', firm.type + assert_instance_of Firm, firm + + firm = Company.new(firm_name: 'Shri Hans Plastic') # with arguments + assert_equal 'Firm', firm.type + assert_instance_of Firm, firm + + client = Client.new + assert_equal 'Client', client.type + assert_instance_of Client, client + + firm = Company.new(type: 'Client') # overwrite the default type + assert_equal 'Client', firm.type + assert_instance_of Client, firm + ensure + ActiveRecord::Base.connection.change_column_default :companies, :type, original_type + Company.reset_column_information + end +end + +class InheritanceAttributeTest < ActiveRecord::TestCase + + class Company < ActiveRecord::Base + self.table_name = 'companies' + attribute :type, :string, default: "InheritanceAttributeTest::Startup" + end + + class Startup < Company + end + + class Empire < Company + end + + def test_inheritance_new_with_subclass_as_default + startup = Company.new # without arguments + assert_equal 'InheritanceAttributeTest::Startup', startup.type + assert_instance_of Startup, startup + + empire = Company.new(type: 'InheritanceAttributeTest::Empire') # without arguments + assert_equal 'InheritanceAttributeTest::Empire', empire.type + assert_instance_of Empire, empire + end +end + +class InheritanceAttributeMappingTest < ActiveRecord::TestCase + setup do + @old_registry = ActiveRecord::Type.registry + ActiveRecord::Type.registry = ActiveRecord::Type::AdapterSpecificRegistry.new + ActiveRecord::Type.register :omg_sti, InheritanceAttributeMappingTest::OmgStiType + Company.delete_all + Sponsor.delete_all + end + + teardown do + ActiveRecord::Type.registry = @old_registry + end + + class OmgStiType < ActiveRecord::Type::String + def cast_value(value) + if value =~ /\Aomg_(.+)\z/ + $1.classify + else + value + end + end + + def serialize(value) + if value + "omg_%s" % value.underscore + end + end + end + + class Company < ActiveRecord::Base + self.table_name = 'companies' + attribute :type, :omg_sti + end + + class Startup < Company; end + class Empire < Company; end + + class Sponsor < ActiveRecord::Base + self.table_name = 'sponsors' + attribute :sponsorable_type, :omg_sti + + belongs_to :sponsorable, polymorphic: true + end + + def test_sti_with_custom_type + Startup.create! name: 'a Startup' + Empire.create! name: 'an Empire' + + assert_equal [["a Startup", "omg_inheritance_attribute_mapping_test/startup"], + ["an Empire", "omg_inheritance_attribute_mapping_test/empire"]], ActiveRecord::Base.connection.select_rows('SELECT name, type FROM companies').sort + assert_equal [["a Startup", "InheritanceAttributeMappingTest::Startup"], + ["an Empire", "InheritanceAttributeMappingTest::Empire"]], Company.all.map { |a| [a.name, a.type] }.sort + + startup = Startup.first + startup.becomes! Empire + startup.save! + + assert_equal [["a Startup", "omg_inheritance_attribute_mapping_test/empire"], + ["an Empire", "omg_inheritance_attribute_mapping_test/empire"]], ActiveRecord::Base.connection.select_rows('SELECT name, type FROM companies').sort + + assert_equal [["a Startup", "InheritanceAttributeMappingTest::Empire"], + ["an Empire", "InheritanceAttributeMappingTest::Empire"]], Company.all.map { |a| [a.name, a.type] }.sort + end + + def test_polymorphic_associations_custom_type + startup = Startup.create! name: 'a Startup' + sponsor = Sponsor.create! sponsorable: startup + + assert_equal ["omg_inheritance_attribute_mapping_test/company"], ActiveRecord::Base.connection.select_values('SELECT sponsorable_type FROM sponsors') + + sponsor = Sponsor.first + assert_equal startup, sponsor.sponsorable + end end diff --git a/activerecord/test/cases/invalid_connection_test.rb b/activerecord/test/cases/invalid_connection_test.rb index 6523fc29fd..a16b52751a 100644 --- a/activerecord/test/cases/invalid_connection_test.rb +++ b/activerecord/test/cases/invalid_connection_test.rb @@ -1,5 +1,6 @@ require "cases/helper" +if current_adapter?(:Mysql2Adapter) class TestAdapterWithInvalidConnection < ActiveRecord::TestCase self.use_transactional_tests = false @@ -9,7 +10,7 @@ class TestAdapterWithInvalidConnection < ActiveRecord::TestCase def setup # Can't just use current adapter; sqlite3 will create a database # file on the fly. - Bird.establish_connection adapter: 'mysql', database: 'i_do_not_exist' + Bird.establish_connection adapter: 'mysql2', database: 'i_do_not_exist' end teardown do @@ -20,3 +21,4 @@ class TestAdapterWithInvalidConnection < ActiveRecord::TestCase assert_equal "#{Bird.name} (call '#{Bird.name}.connection' to establish a connection)", Bird.inspect end end +end diff --git a/activerecord/test/cases/invertible_migration_test.rb b/activerecord/test/cases/invertible_migration_test.rb index 84b0ff8fcb..e030f6c588 100644 --- a/activerecord/test/cases/invertible_migration_test.rb +++ b/activerecord/test/cases/invertible_migration_test.rb @@ -5,7 +5,7 @@ end module ActiveRecord class InvertibleMigrationTest < ActiveRecord::TestCase - class SilentMigration < ActiveRecord::Migration + class SilentMigration < ActiveRecord::Migration::Current def write(text = '') # sssshhhhh!! end @@ -105,7 +105,7 @@ module ActiveRecord end end - class LegacyMigration < ActiveRecord::Migration + class LegacyMigration < ActiveRecord::Migration::Current def self.up create_table("horses") do |t| t.column :content, :text @@ -157,8 +157,10 @@ module ActiveRecord teardown do %w[horses new_horses].each do |table| - if ActiveRecord::Base.connection.table_exists?(table) - ActiveRecord::Base.connection.drop_table(table) + ActiveSupport::Deprecation.silence do + if ActiveRecord::Base.connection.table_exists?(table) + ActiveRecord::Base.connection.drop_table(table) + end end end ActiveRecord::Migration.verbose = @verbose_was @@ -189,14 +191,14 @@ module ActiveRecord def test_migrate_up migration = InvertibleMigration.new migration.migrate(:up) - assert migration.connection.table_exists?("horses"), "horses should exist" + ActiveSupport::Deprecation.silence { assert migration.connection.table_exists?("horses"), "horses should exist" } end def test_migrate_down migration = InvertibleMigration.new migration.migrate :up migration.migrate :down - assert !migration.connection.table_exists?("horses") + ActiveSupport::Deprecation.silence { assert !migration.connection.table_exists?("horses") } end def test_migrate_revert @@ -204,11 +206,11 @@ module ActiveRecord revert = InvertibleRevertMigration.new migration.migrate :up revert.migrate :up - assert !migration.connection.table_exists?("horses") + ActiveSupport::Deprecation.silence { assert !migration.connection.table_exists?("horses") } revert.migrate :down - assert migration.connection.table_exists?("horses") + ActiveSupport::Deprecation.silence { assert migration.connection.table_exists?("horses") } migration.migrate :down - assert !migration.connection.table_exists?("horses") + ActiveSupport::Deprecation.silence { assert !migration.connection.table_exists?("horses") } end def test_migrate_revert_by_part @@ -216,18 +218,24 @@ module ActiveRecord received = [] migration = InvertibleByPartsMigration.new migration.test = ->(dir){ - assert migration.connection.table_exists?("horses") - assert migration.connection.table_exists?("new_horses") + ActiveSupport::Deprecation.silence do + assert migration.connection.table_exists?("horses") + assert migration.connection.table_exists?("new_horses") + end received << dir } migration.migrate :up assert_equal [:both, :up], received - assert !migration.connection.table_exists?("horses") - assert migration.connection.table_exists?("new_horses") + ActiveSupport::Deprecation.silence do + assert !migration.connection.table_exists?("horses") + assert migration.connection.table_exists?("new_horses") + end migration.migrate :down assert_equal [:both, :up, :both, :down], received - assert migration.connection.table_exists?("horses") - assert !migration.connection.table_exists?("new_horses") + ActiveSupport::Deprecation.silence do + assert migration.connection.table_exists?("horses") + assert !migration.connection.table_exists?("new_horses") + end end def test_migrate_revert_whole_migration @@ -236,20 +244,20 @@ module ActiveRecord revert = RevertWholeMigration.new(klass) migration.migrate :up revert.migrate :up - assert !migration.connection.table_exists?("horses") + ActiveSupport::Deprecation.silence { assert !migration.connection.table_exists?("horses") } revert.migrate :down - assert migration.connection.table_exists?("horses") + ActiveSupport::Deprecation.silence { assert migration.connection.table_exists?("horses") } migration.migrate :down - assert !migration.connection.table_exists?("horses") + ActiveSupport::Deprecation.silence { assert !migration.connection.table_exists?("horses") } end end def test_migrate_nested_revert_whole_migration revert = NestedRevertWholeMigration.new(InvertibleRevertMigration) revert.migrate :down - assert revert.connection.table_exists?("horses") + ActiveSupport::Deprecation.silence { assert revert.connection.table_exists?("horses") } revert.migrate :up - assert !revert.connection.table_exists?("horses") + ActiveSupport::Deprecation.silence { assert !revert.connection.table_exists?("horses") } end def test_migrate_revert_change_column_default @@ -314,24 +322,24 @@ module ActiveRecord def test_legacy_up LegacyMigration.migrate :up - assert ActiveRecord::Base.connection.table_exists?("horses"), "horses should exist" + ActiveSupport::Deprecation.silence { assert ActiveRecord::Base.connection.table_exists?("horses"), "horses should exist" } end def test_legacy_down LegacyMigration.migrate :up LegacyMigration.migrate :down - assert !ActiveRecord::Base.connection.table_exists?("horses"), "horses should not exist" + ActiveSupport::Deprecation.silence { assert !ActiveRecord::Base.connection.table_exists?("horses"), "horses should not exist" } end def test_up LegacyMigration.up - assert ActiveRecord::Base.connection.table_exists?("horses"), "horses should exist" + ActiveSupport::Deprecation.silence { assert ActiveRecord::Base.connection.table_exists?("horses"), "horses should exist" } end def test_down LegacyMigration.up LegacyMigration.down - assert !ActiveRecord::Base.connection.table_exists?("horses"), "horses should not exist" + ActiveSupport::Deprecation.silence { assert !ActiveRecord::Base.connection.table_exists?("horses"), "horses should not exist" } end def test_migrate_down_with_table_name_prefix @@ -340,13 +348,13 @@ module ActiveRecord migration = InvertibleMigration.new migration.migrate(:up) assert_nothing_raised { migration.migrate(:down) } - assert !ActiveRecord::Base.connection.table_exists?("p_horses_s"), "p_horses_s should not exist" + ActiveSupport::Deprecation.silence { assert !ActiveRecord::Base.connection.table_exists?("p_horses_s"), "p_horses_s should not exist" } ensure ActiveRecord::Base.table_name_prefix = ActiveRecord::Base.table_name_suffix = '' end # MySQL 5.7 and Oracle do not allow to create duplicate indexes on the same columns - unless current_adapter?(:MysqlAdapter, :Mysql2Adapter, :OracleAdapter) + unless current_adapter?(:Mysql2Adapter, :OracleAdapter) def test_migrate_revert_add_index_with_name RevertNamedIndexMigration1.new.migrate(:up) RevertNamedIndexMigration2.new.migrate(:up) diff --git a/activerecord/test/cases/locking_test.rb b/activerecord/test/cases/locking_test.rb index 2e1363334d..4fe76e563a 100644 --- a/activerecord/test/cases/locking_test.rb +++ b/activerecord/test/cases/locking_test.rb @@ -441,7 +441,7 @@ unless in_memory_db? def test_lock_sending_custom_lock_statement Person.transaction do person = Person.find(1) - assert_sql(/LIMIT 1 FOR SHARE NOWAIT/) do + assert_sql(/LIMIT \$\d FOR SHARE NOWAIT/) do person.lock!('FOR SHARE NOWAIT') end end diff --git a/activerecord/test/cases/log_subscriber_test.rb b/activerecord/test/cases/log_subscriber_test.rb index 3846ba8e7f..707a2d1da1 100644 --- a/activerecord/test/cases/log_subscriber_test.rb +++ b/activerecord/test/cases/log_subscriber_test.rb @@ -209,7 +209,7 @@ class LogSubscriberTest < ActiveRecord::TestCase Thread.new { assert_equal 0, ActiveRecord::LogSubscriber.runtime }.join end - unless current_adapter?(:Mysql2Adapter) + if ActiveRecord::Base.connection.prepared_statements def test_binary_data_is_not_logged Binary.create(data: 'some binary data') wait diff --git a/activerecord/test/cases/migration/change_schema_test.rb b/activerecord/test/cases/migration/change_schema_test.rb index 83e50048ec..d6963b48d7 100644 --- a/activerecord/test/cases/migration/change_schema_test.rb +++ b/activerecord/test/cases/migration/change_schema_test.rb @@ -50,7 +50,7 @@ module ActiveRecord def test_create_table_with_defaults # MySQL doesn't allow defaults on TEXT or BLOB columns. - mysql = current_adapter?(:MysqlAdapter, :Mysql2Adapter) + mysql = current_adapter?(:Mysql2Adapter) connection.create_table :testings do |t| t.column :one, :string, :default => "hello" @@ -141,7 +141,7 @@ module ActiveRecord assert_equal 'smallint', one.sql_type assert_equal 'integer', four.sql_type assert_equal 'bigint', eight.sql_type - elsif current_adapter?(:MysqlAdapter, :Mysql2Adapter) + elsif current_adapter?(:Mysql2Adapter) assert_match 'int(11)', default.sql_type assert_match 'tinyint', one.sql_type assert_match 'int', four.sql_type @@ -339,7 +339,7 @@ module ActiveRecord def test_change_column_null testing_table_with_only_foo_attribute do - notnull_migration = Class.new(ActiveRecord::Migration) do + notnull_migration = Class.new(ActiveRecord::Migration::Current) do def change change_column_null :testings, :foo, false end @@ -405,9 +405,9 @@ module ActiveRecord def test_drop_table_if_exists connection.create_table(:testings) - assert connection.table_exists?(:testings) + ActiveSupport::Deprecation.silence { assert connection.table_exists?(:testings) } connection.drop_table(:testings, if_exists: true) - assert_not connection.table_exists?(:testings) + ActiveSupport::Deprecation.silence { assert_not connection.table_exists?(:testings) } end def test_drop_table_if_exists_nothing_raised @@ -442,7 +442,7 @@ module ActiveRecord end def test_create_table_with_force_cascade_drops_dependent_objects - skip "MySQL > 5.5 does not drop dependent objects with DROP TABLE CASCADE" if current_adapter?(:MysqlAdapter, :Mysql2Adapter) + skip "MySQL > 5.5 does not drop dependent objects with DROP TABLE CASCADE" if current_adapter?(:Mysql2Adapter) # can't re-create table referenced by foreign key assert_raises(ActiveRecord::StatementInvalid) do @connection.create_table :trains, force: true diff --git a/activerecord/test/cases/migration/column_attributes_test.rb b/activerecord/test/cases/migration/column_attributes_test.rb index 8d8e661aa5..c7a1b81a75 100644 --- a/activerecord/test/cases/migration/column_attributes_test.rb +++ b/activerecord/test/cases/migration/column_attributes_test.rb @@ -37,13 +37,13 @@ module ActiveRecord def test_add_column_without_limit # TODO: limit: nil should work with all adapters. - skip "MySQL wrongly enforces a limit of 255" if current_adapter?(:MysqlAdapter, :Mysql2Adapter) + skip "MySQL wrongly enforces a limit of 255" if current_adapter?(:Mysql2Adapter) add_column :test_models, :description, :string, limit: nil TestModel.reset_column_information assert_nil TestModel.columns_hash["description"].limit end - if current_adapter?(:MysqlAdapter, :Mysql2Adapter) + if current_adapter?(:Mysql2Adapter) def test_unabstracted_database_dependent_types add_column :test_models, :intelligence_quotient, :tinyint TestModel.reset_column_information @@ -63,8 +63,6 @@ module ActiveRecord # Do a manual insertion if current_adapter?(:OracleAdapter) connection.execute "insert into test_models (id, wealth) values (people_seq.nextval, 12345678901234567890.0123456789)" - elsif current_adapter?(:MysqlAdapter) && Mysql.client_version < 50003 #before MySQL 5.0.3 decimals stored as strings - connection.execute "insert into test_models (wealth) values ('12345678901234567890.0123456789')" elsif current_adapter?(:PostgreSQLAdapter) connection.execute "insert into test_models (wealth) values (12345678901234567890.0123456789)" else @@ -171,7 +169,7 @@ module ActiveRecord end end - if current_adapter?(:MysqlAdapter, :Mysql2Adapter, :PostgreSQLAdapter) + if current_adapter?(:Mysql2Adapter, :PostgreSQLAdapter) def test_out_of_range_limit_should_raise assert_raise(ActiveRecordError) { add_column :test_models, :integer_too_big, :integer, :limit => 10 } diff --git a/activerecord/test/cases/migration/column_positioning_test.rb b/activerecord/test/cases/migration/column_positioning_test.rb index 4637970ce0..8294da0373 100644 --- a/activerecord/test/cases/migration/column_positioning_test.rb +++ b/activerecord/test/cases/migration/column_positioning_test.rb @@ -23,7 +23,7 @@ module ActiveRecord ActiveRecord::Base.primary_key_prefix_type = nil end - if current_adapter?(:MysqlAdapter, :Mysql2Adapter) + if current_adapter?(:Mysql2Adapter) def test_column_positioning assert_equal %w(first second third), conn.columns(:testings).map(&:name) end diff --git a/activerecord/test/cases/migration/columns_test.rb b/activerecord/test/cases/migration/columns_test.rb index ab3f584350..fca1cb7e97 100644 --- a/activerecord/test/cases/migration/columns_test.rb +++ b/activerecord/test/cases/migration/columns_test.rb @@ -62,7 +62,7 @@ module ActiveRecord assert_equal '70000', default_after end - if current_adapter?(:MysqlAdapter, :Mysql2Adapter) + if current_adapter?(:Mysql2Adapter) def test_mysql_rename_column_preserves_auto_increment rename_column "test_models", "id", "id_test" assert connection.columns("test_models").find { |c| c.name == "id_test" }.auto_increment? diff --git a/activerecord/test/cases/migration/compatibility_test.rb b/activerecord/test/cases/migration/compatibility_test.rb new file mode 100644 index 0000000000..60ca90464d --- /dev/null +++ b/activerecord/test/cases/migration/compatibility_test.rb @@ -0,0 +1,118 @@ +require 'cases/helper' + +module ActiveRecord + class Migration + class CompatibilityTest < ActiveRecord::TestCase + attr_reader :connection + self.use_transactional_tests = false + + def setup + super + @connection = ActiveRecord::Base.connection + @verbose_was = ActiveRecord::Migration.verbose + ActiveRecord::Migration.verbose = false + + connection.create_table :testings do |t| + t.column :foo, :string, :limit => 100 + t.column :bar, :string, :limit => 100 + end + end + + teardown do + connection.drop_table :testings rescue nil + ActiveRecord::Migration.verbose = @verbose_was + ActiveRecord::SchemaMigration.delete_all rescue nil + end + + def test_migration_doesnt_remove_named_index + connection.add_index :testings, :foo, :name => "custom_index_name" + + migration = Class.new(ActiveRecord::Migration[4.2]) { + def version; 101 end + def migrate(x) + remove_index :testings, :foo + end + }.new + + assert connection.index_exists?(:testings, :foo, name: "custom_index_name") + assert_raise(StandardError) { ActiveRecord::Migrator.new(:up, [migration]).migrate } + assert connection.index_exists?(:testings, :foo, name: "custom_index_name") + end + + def test_migration_does_remove_unnamed_index + connection.add_index :testings, :bar + + migration = Class.new(ActiveRecord::Migration[4.2]) { + def version; 101 end + def migrate(x) + remove_index :testings, :bar + end + }.new + + assert connection.index_exists?(:testings, :bar) + ActiveRecord::Migrator.new(:up, [migration]).migrate + assert_not connection.index_exists?(:testings, :bar) + end + + def test_references_does_not_add_index_by_default + migration = Class.new(ActiveRecord::Migration) { + def migrate(x) + create_table :more_testings do |t| + t.references :foo + t.belongs_to :bar, index: false + end + end + }.new + + ActiveRecord::Migrator.new(:up, [migration]).migrate + + assert_not connection.index_exists?(:more_testings, :foo_id) + assert_not connection.index_exists?(:more_testings, :bar_id) + ensure + connection.drop_table :more_testings rescue nil + end + + def test_timestamps_have_null_constraints_if_not_present_in_migration_of_create_table + migration = Class.new(ActiveRecord::Migration) { + def migrate(x) + create_table :more_testings do |t| + t.timestamps + end + end + }.new + + ActiveRecord::Migrator.new(:up, [migration]).migrate + + assert connection.columns(:more_testings).find { |c| c.name == 'created_at' }.null + assert connection.columns(:more_testings).find { |c| c.name == 'updated_at' }.null + ensure + connection.drop_table :more_testings rescue nil + end + + def test_timestamps_have_null_constraints_if_not_present_in_migration_for_adding_timestamps_to_existing_table + migration = Class.new(ActiveRecord::Migration) { + def migrate(x) + add_timestamps :testings + end + }.new + + ActiveRecord::Migrator.new(:up, [migration]).migrate + + assert connection.columns(:testings).find { |c| c.name == 'created_at' }.null + assert connection.columns(:testings).find { |c| c.name == 'updated_at' }.null + end + + def test_legacy_migrations_get_deprecation_warning_when_run + migration = Class.new(ActiveRecord::Migration) { + def up + add_column :testings, :baz, :string + end + } + + assert_deprecated do + migration.migrate :up + end + end + end + end +end diff --git a/activerecord/test/cases/migration/create_join_table_test.rb b/activerecord/test/cases/migration/create_join_table_test.rb index 8fd08fe4ce..0a7b57455c 100644 --- a/activerecord/test/cases/migration/create_join_table_test.rb +++ b/activerecord/test/cases/migration/create_join_table_test.rb @@ -12,7 +12,9 @@ module ActiveRecord teardown do %w(artists_musics musics_videos catalog).each do |table_name| - connection.drop_table table_name if connection.tables.include?(table_name) + ActiveSupport::Deprecation.silence do + connection.drop_table table_name if connection.table_exists?(table_name) + end end end @@ -82,62 +84,62 @@ module ActiveRecord connection.create_join_table :artists, :musics connection.drop_join_table :artists, :musics - assert !connection.tables.include?('artists_musics') + ActiveSupport::Deprecation.silence { assert !connection.table_exists?('artists_musics') } end def test_drop_join_table_with_strings connection.create_join_table :artists, :musics connection.drop_join_table 'artists', 'musics' - assert !connection.tables.include?('artists_musics') + ActiveSupport::Deprecation.silence { assert !connection.table_exists?('artists_musics') } end def test_drop_join_table_with_the_proper_order connection.create_join_table :videos, :musics connection.drop_join_table :videos, :musics - assert !connection.tables.include?('musics_videos') + ActiveSupport::Deprecation.silence { assert !connection.table_exists?('musics_videos') } end def test_drop_join_table_with_the_table_name connection.create_join_table :artists, :musics, table_name: :catalog connection.drop_join_table :artists, :musics, table_name: :catalog - assert !connection.tables.include?('catalog') + ActiveSupport::Deprecation.silence { assert !connection.table_exists?('catalog') } end def test_drop_join_table_with_the_table_name_as_string connection.create_join_table :artists, :musics, table_name: 'catalog' connection.drop_join_table :artists, :musics, table_name: 'catalog' - assert !connection.tables.include?('catalog') + ActiveSupport::Deprecation.silence { assert !connection.table_exists?('catalog') } end def test_drop_join_table_with_column_options connection.create_join_table :artists, :musics, column_options: {null: true} connection.drop_join_table :artists, :musics, column_options: {null: true} - assert !connection.tables.include?('artists_musics') + ActiveSupport::Deprecation.silence { assert !connection.table_exists?('artists_musics') } end def test_create_and_drop_join_table_with_common_prefix with_table_cleanup do connection.create_join_table 'audio_artists', 'audio_musics' - assert_includes connection.tables, 'audio_artists_musics' + ActiveSupport::Deprecation.silence { assert connection.table_exists?('audio_artists_musics') } connection.drop_join_table 'audio_artists', 'audio_musics' - assert !connection.tables.include?('audio_artists_musics'), "Should have dropped join table, but didn't" + ActiveSupport::Deprecation.silence { assert !connection.table_exists?('audio_artists_musics'), "Should have dropped join table, but didn't" } end end private def with_table_cleanup - tables_before = connection.tables + tables_before = connection.data_sources yield ensure - tables_after = connection.tables - tables_before + tables_after = connection.data_sources - tables_before tables_after.each do |table| connection.drop_table table diff --git a/activerecord/test/cases/migration/foreign_key_test.rb b/activerecord/test/cases/migration/foreign_key_test.rb index 72f2fa95f1..01162dcefe 100644 --- a/activerecord/test/cases/migration/foreign_key_test.rb +++ b/activerecord/test/cases/migration/foreign_key_test.rb @@ -99,7 +99,7 @@ module ActiveRecord assert_equal 1, foreign_keys.size fk = foreign_keys.first - if current_adapter?(:MysqlAdapter, :Mysql2Adapter) + if current_adapter?(:Mysql2Adapter) # ON DELETE RESTRICT is the default on MySQL assert_equal nil, fk.on_delete else @@ -224,7 +224,7 @@ module ActiveRecord assert_match %r{\s+add_foreign_key "astronauts",.+on_update: :cascade,.+on_delete: :nullify$}, output end - class CreateCitiesAndHousesMigration < ActiveRecord::Migration + class CreateCitiesAndHousesMigration < ActiveRecord::Migration::Current def change create_table("cities") { |t| } @@ -243,7 +243,7 @@ module ActiveRecord silence_stream($stdout) { migration.migrate(:down) } end - class CreateSchoolsAndClassesMigration < ActiveRecord::Migration + class CreateSchoolsAndClassesMigration < ActiveRecord::Migration::Current def change create_table(:schools) diff --git a/activerecord/test/cases/migration/index_test.rb b/activerecord/test/cases/migration/index_test.rb index b23b9a679f..5abd37bfa2 100644 --- a/activerecord/test/cases/migration/index_test.rb +++ b/activerecord/test/cases/migration/index_test.rb @@ -130,7 +130,17 @@ module ActiveRecord def test_named_index_exists connection.add_index :testings, :foo, :name => "custom_index_name" + assert connection.index_exists?(:testings, :foo) assert connection.index_exists?(:testings, :foo, :name => "custom_index_name") + assert !connection.index_exists?(:testings, :foo, :name => "other_index_name") + end + + def test_remove_named_index + connection.add_index :testings, :foo, :name => "custom_index_name" + + assert connection.index_exists?(:testings, :foo) + connection.remove_index :testings, :foo + assert !connection.index_exists?(:testings, :foo) end def test_add_index_attribute_length_limit @@ -176,7 +186,7 @@ module ActiveRecord connection.remove_index("testings", :name => "named_admin") # Selected adapters support index sort order - if current_adapter?(:SQLite3Adapter, :MysqlAdapter, :Mysql2Adapter, :PostgreSQLAdapter) + if current_adapter?(:SQLite3Adapter, :Mysql2Adapter, :PostgreSQLAdapter) connection.add_index("testings", ["last_name"], :order => {:last_name => :desc}) connection.remove_index("testings", ["last_name"]) connection.add_index("testings", ["last_name", "first_name"], :order => {:last_name => :desc}) diff --git a/activerecord/test/cases/migration/postgresql_geometric_types_test.rb b/activerecord/test/cases/migration/postgresql_geometric_types_test.rb deleted file mode 100644 index e4772905bb..0000000000 --- a/activerecord/test/cases/migration/postgresql_geometric_types_test.rb +++ /dev/null @@ -1,93 +0,0 @@ -require 'cases/helper' - -module ActiveRecord - class Migration - class PostgreSQLGeometricTypesTest < ActiveRecord::TestCase - attr_reader :connection, :table_name - - def setup - super - @connection = ActiveRecord::Base.connection - @table_name = :testings - end - - if current_adapter?(:PostgreSQLAdapter) - def test_creating_column_with_point_type - connection.create_table(table_name) do |t| - t.point :foo_point - end - - assert_column_exists(:foo_point) - assert_type_correct(:foo_point, :point) - end - - def test_creating_column_with_line_type - connection.create_table(table_name) do |t| - t.line :foo_line - end - - assert_column_exists(:foo_line) - assert_type_correct(:foo_line, :line) - end - - def test_creating_column_with_lseg_type - connection.create_table(table_name) do |t| - t.lseg :foo_lseg - end - - assert_column_exists(:foo_lseg) - assert_type_correct(:foo_lseg, :lseg) - end - - def test_creating_column_with_box_type - connection.create_table(table_name) do |t| - t.box :foo_box - end - - assert_column_exists(:foo_box) - assert_type_correct(:foo_box, :box) - end - - def test_creating_column_with_path_type - connection.create_table(table_name) do |t| - t.path :foo_path - end - - assert_column_exists(:foo_path) - assert_type_correct(:foo_path, :path) - end - - def test_creating_column_with_polygon_type - connection.create_table(table_name) do |t| - t.polygon :foo_polygon - end - - assert_column_exists(:foo_polygon) - assert_type_correct(:foo_polygon, :polygon) - end - - def test_creating_column_with_circle_type - connection.create_table(table_name) do |t| - t.circle :foo_circle - end - - assert_column_exists(:foo_circle) - assert_type_correct(:foo_circle, :circle) - end - end - - private - def assert_column_exists(column_name) - columns = connection.columns(table_name) - assert columns.map(&:name).include?(column_name.to_s) - end - - def assert_type_correct(column_name, type) - columns = connection.columns(table_name) - column = columns.select{ |c| c.name == column_name.to_s }.first - assert_equal type.to_s, column.sql_type - end - - end - end -end
\ No newline at end of file diff --git a/activerecord/test/cases/migration/references_foreign_key_test.rb b/activerecord/test/cases/migration/references_foreign_key_test.rb index 84ec657398..85435f4dbc 100644 --- a/activerecord/test/cases/migration/references_foreign_key_test.rb +++ b/activerecord/test/cases/migration/references_foreign_key_test.rb @@ -32,10 +32,10 @@ module ActiveRecord assert_equal [], @connection.foreign_keys("testings") end - test "foreign keys can be created in one query" do + test "foreign keys can be created in one query when index is not added" do assert_queries(1) do @connection.create_table :testings do |t| - t.references :testing_parent, foreign_key: true + t.references :testing_parent, foreign_key: true, index: false end end end @@ -144,6 +144,22 @@ module ActiveRecord @connection.drop_table "testing", if_exists: true end end + + test "multiple foreign keys can be added to the same table" do + @connection.create_table :testings do |t| + t.integer :col_1 + t.integer :col_2 + + t.foreign_key :testing_parents, column: :col_1 + t.foreign_key :testing_parents, column: :col_2 + end + + fks = @connection.foreign_keys("testings") + + fk_definitions = fks.map {|fk| [fk.from_table, fk.to_table, fk.column] } + assert_equal([["testings", "testing_parents", "col_1"], + ["testings", "testing_parents", "col_2"]], fk_definitions) + end end end end @@ -164,7 +180,7 @@ class ReferencesWithoutForeignKeySupportTest < ActiveRecord::TestCase t.references :testing_parent, foreign_key: true end - assert_includes @connection.tables, "testings" + assert_includes @connection.data_sources, "testings" end end end diff --git a/activerecord/test/cases/migration/references_index_test.rb b/activerecord/test/cases/migration/references_index_test.rb index ad6b828d0b..a9a7f0f4c4 100644 --- a/activerecord/test/cases/migration/references_index_test.rb +++ b/activerecord/test/cases/migration/references_index_test.rb @@ -23,12 +23,12 @@ module ActiveRecord assert connection.index_exists?(table_name, :foo_id, :name => :index_testings_on_foo_id) end - def test_does_not_create_index + def test_creates_index_by_default_even_if_index_option_is_not_passed connection.create_table table_name do |t| t.references :foo end - assert_not connection.index_exists?(table_name, :foo_id, :name => :index_testings_on_foo_id) + assert connection.index_exists?(table_name, :foo_id, :name => :index_testings_on_foo_id) end def test_does_not_create_index_explicit @@ -68,13 +68,13 @@ module ActiveRecord assert connection.index_exists?(table_name, :foo_id, :name => :index_testings_on_foo_id) end - def test_does_not_create_index_for_existing_table + def test_creates_index_for_existing_table_even_if_index_option_is_not_passed connection.create_table table_name connection.change_table table_name do |t| t.references :foo end - assert_not connection.index_exists?(table_name, :foo_id, :name => :index_testings_on_foo_id) + assert connection.index_exists?(table_name, :foo_id, :name => :index_testings_on_foo_id) end def test_does_not_create_index_for_existing_table_explicit diff --git a/activerecord/test/cases/migration/references_statements_test.rb b/activerecord/test/cases/migration/references_statements_test.rb index f613fd66c3..b9ce6bbc55 100644 --- a/activerecord/test/cases/migration/references_statements_test.rb +++ b/activerecord/test/cases/migration/references_statements_test.rb @@ -30,14 +30,14 @@ module ActiveRecord assert column_exists?(table_name, :taggable_type, :string) end - def test_creates_reference_id_index - add_reference table_name, :user, index: true - assert index_exists?(table_name, :user_id) + def test_does_not_create_reference_id_index_if_index_is_false + add_reference table_name, :user, index: false + assert_not index_exists?(table_name, :user_id) end - def test_does_not_create_reference_id_index + def test_create_reference_id_index_even_if_index_option_is_passed add_reference table_name, :user - assert_not index_exists?(table_name, :user_id) + assert index_exists?(table_name, :user_id) end def test_creates_polymorphic_index diff --git a/activerecord/test/cases/migration/rename_table_test.rb b/activerecord/test/cases/migration/rename_table_test.rb index 6d742d3f2f..b926a92849 100644 --- a/activerecord/test/cases/migration/rename_table_test.rb +++ b/activerecord/test/cases/migration/rename_table_test.rb @@ -15,7 +15,7 @@ module ActiveRecord end def teardown - rename_table :octopi, :test_models if connection.table_exists? :octopi + ActiveSupport::Deprecation.silence { rename_table :octopi, :test_models if connection.table_exists? :octopi } super end @@ -80,12 +80,10 @@ module ActiveRecord end def test_renaming_table_doesnt_attempt_to_rename_non_existent_sequences - enable_extension!('uuid-ossp', connection) connection.create_table :cats, id: :uuid assert_nothing_raised { rename_table :cats, :felines } - assert connection.table_exists? :felines + ActiveSupport::Deprecation.silence { assert connection.table_exists? :felines } ensure - disable_extension!('uuid-ossp', connection) connection.drop_table :cats, if_exists: true connection.drop_table :felines, if_exists: true end diff --git a/activerecord/test/cases/migration/table_and_index_test.rb b/activerecord/test/cases/migration/table_and_index_test.rb deleted file mode 100644 index 24cba84a09..0000000000 --- a/activerecord/test/cases/migration/table_and_index_test.rb +++ /dev/null @@ -1,24 +0,0 @@ -require "cases/helper" - -module ActiveRecord - class Migration - class TableAndIndexTest < ActiveRecord::TestCase - def test_add_schema_info_respects_prefix_and_suffix - conn = ActiveRecord::Base.connection - - conn.drop_table(ActiveRecord::Migrator.schema_migrations_table_name, if_exists: true) - # Use shorter prefix and suffix as in Oracle database identifier cannot be larger than 30 characters - ActiveRecord::Base.table_name_prefix = 'p_' - ActiveRecord::Base.table_name_suffix = '_s' - conn.drop_table(ActiveRecord::Migrator.schema_migrations_table_name, if_exists: true) - - conn.initialize_schema_migrations_table - - assert_equal "p_unique_schema_migrations_s", conn.indexes(ActiveRecord::Migrator.schema_migrations_table_name)[0][:name] - ensure - ActiveRecord::Base.table_name_prefix = "" - ActiveRecord::Base.table_name_suffix = "" - end - end - end -end diff --git a/activerecord/test/cases/migration_test.rb b/activerecord/test/cases/migration_test.rb index 741bec6017..6a6250eec3 100644 --- a/activerecord/test/cases/migration_test.rb +++ b/activerecord/test/cases/migration_test.rb @@ -1,6 +1,7 @@ -require "cases/helper" -require "cases/migration/helper" +require 'cases/helper' +require 'cases/migration/helper' require 'bigdecimal/util' +require 'concurrent/atomic/count_down_latch' require 'models/person' require 'models/topic' @@ -131,12 +132,12 @@ class MigrationTest < ActiveRecord::TestCase end def test_migration_instance_has_connection - migration = Class.new(ActiveRecord::Migration).new + migration = Class.new(ActiveRecord::Migration::Current).new assert_equal ActiveRecord::Base.connection, migration.connection end def test_method_missing_delegates_to_connection - migration = Class.new(ActiveRecord::Migration) { + migration = Class.new(ActiveRecord::Migration::Current) { def connection Class.new { def create_table; "hi mom!"; end @@ -191,8 +192,6 @@ class MigrationTest < ActiveRecord::TestCase # of 0, they take on the compile-time limit for precision and scale, # so the following should succeed unless you have used really wacky # compilation options - # - SQLite2 has the default behavior of preserving all data sent in, - # so this happens there too assert_kind_of BigDecimal, b.value_of_e assert_equal BigDecimal("2.7182818284590452353602875"), b.value_of_e elsif current_adapter?(:SQLite3Adapter) @@ -225,7 +224,7 @@ class MigrationTest < ActiveRecord::TestCase assert_raise(ActiveRecord::StatementInvalid) { Reminder.first } end - class MockMigration < ActiveRecord::Migration + class MockMigration < ActiveRecord::Migration::Current attr_reader :went_up, :went_down def initialize @went_up = false @@ -267,7 +266,7 @@ class MigrationTest < ActiveRecord::TestCase def test_migrator_one_up_with_exception_and_rollback assert_no_column Person, :last_name - migration = Class.new(ActiveRecord::Migration) { + migration = Class.new(ActiveRecord::Migration::Current) { def version; 100 end def migrate(x) add_column "people", "last_name", :string @@ -288,7 +287,7 @@ class MigrationTest < ActiveRecord::TestCase def test_migrator_one_up_with_exception_and_rollback_using_run assert_no_column Person, :last_name - migration = Class.new(ActiveRecord::Migration) { + migration = Class.new(ActiveRecord::Migration::Current) { def version; 100 end def migrate(x) add_column "people", "last_name", :string @@ -300,7 +299,7 @@ class MigrationTest < ActiveRecord::TestCase e = assert_raise(StandardError) { migrator.run } - assert_equal "An error has occurred, this migration was canceled:\n\nSomething broke", e.message + assert_equal "An error has occurred, this and all later migrations canceled:\n\nSomething broke", e.message assert_no_column Person, :last_name, "On error, the Migrator should revert schema changes but it did not." @@ -309,7 +308,7 @@ class MigrationTest < ActiveRecord::TestCase def test_migration_without_transaction assert_no_column Person, :last_name - migration = Class.new(ActiveRecord::Migration) { + migration = Class.new(ActiveRecord::Migration::Current) { self.disable_ddl_transaction! def version; 101 end @@ -353,6 +352,93 @@ class MigrationTest < ActiveRecord::TestCase Reminder.reset_table_name end + def test_internal_metadata_table_name + original_internal_metadata_table_name = ActiveRecord::Base.internal_metadata_table_name + + assert_equal "ar_internal_metadata", ActiveRecord::InternalMetadata.table_name + ActiveRecord::Base.table_name_prefix = "p_" + ActiveRecord::Base.table_name_suffix = "_s" + Reminder.reset_table_name + assert_equal "p_ar_internal_metadata_s", ActiveRecord::InternalMetadata.table_name + ActiveRecord::Base.internal_metadata_table_name = "changed" + Reminder.reset_table_name + assert_equal "p_changed_s", ActiveRecord::InternalMetadata.table_name + ActiveRecord::Base.table_name_prefix = "" + ActiveRecord::Base.table_name_suffix = "" + Reminder.reset_table_name + assert_equal "changed", ActiveRecord::InternalMetadata.table_name + ensure + ActiveRecord::Base.internal_metadata_table_name = original_internal_metadata_table_name + Reminder.reset_table_name + end + + def test_internal_metadata_stores_environment + current_env = ActiveRecord::ConnectionHandling::DEFAULT_ENV.call + migrations_path = MIGRATIONS_ROOT + "/valid" + old_path = ActiveRecord::Migrator.migrations_paths + ActiveRecord::Migrator.migrations_paths = migrations_path + + ActiveRecord::Migrator.up(migrations_path) + assert_equal current_env, ActiveRecord::InternalMetadata[:environment] + + original_rails_env = ENV["RAILS_ENV"] + original_rack_env = ENV["RACK_ENV"] + ENV["RAILS_ENV"] = ENV["RACK_ENV"] = "foofoo" + new_env = ActiveRecord::ConnectionHandling::DEFAULT_ENV.call + + refute_equal current_env, new_env + + sleep 1 # mysql by default does not store fractional seconds in the database + ActiveRecord::Migrator.up(migrations_path) + assert_equal new_env, ActiveRecord::InternalMetadata[:environment] + ensure + ActiveRecord::Migrator.migrations_paths = old_path + ENV["RAILS_ENV"] = original_rails_env + ENV["RACK_ENV"] = original_rack_env + end + + + def test_migration_sets_internal_metadata_even_when_fully_migrated + current_env = ActiveRecord::ConnectionHandling::DEFAULT_ENV.call + migrations_path = MIGRATIONS_ROOT + "/valid" + old_path = ActiveRecord::Migrator.migrations_paths + ActiveRecord::Migrator.migrations_paths = migrations_path + + ActiveRecord::Migrator.up(migrations_path) + assert_equal current_env, ActiveRecord::InternalMetadata[:environment] + + original_rails_env = ENV["RAILS_ENV"] + original_rack_env = ENV["RACK_ENV"] + ENV["RAILS_ENV"] = ENV["RACK_ENV"] = "foofoo" + new_env = ActiveRecord::ConnectionHandling::DEFAULT_ENV.call + + refute_equal current_env, new_env + + sleep 1 # mysql by default does not store fractional seconds in the database + + ActiveRecord::Migrator.up(migrations_path) + assert_equal new_env, ActiveRecord::InternalMetadata[:environment] + ensure + ActiveRecord::Migrator.migrations_paths = old_path + ENV["RAILS_ENV"] = original_rails_env + ENV["RACK_ENV"] = original_rack_env + end + + def test_rename_internal_metadata_table + original_internal_metadata_table_name = ActiveRecord::Base.internal_metadata_table_name + + ActiveRecord::Base.internal_metadata_table_name = "active_record_internal_metadatas" + Reminder.reset_table_name + + ActiveRecord::Base.internal_metadata_table_name = original_internal_metadata_table_name + Reminder.reset_table_name + + assert_equal "ar_internal_metadata", ActiveRecord::InternalMetadata.table_name + ensure + ActiveRecord::Base.internal_metadata_table_name = original_internal_metadata_table_name + Reminder.reset_table_name + end + def test_proper_table_name_on_migration reminder_class = new_isolated_reminder_class migration = ActiveRecord::Migration.new @@ -430,13 +516,12 @@ class MigrationTest < ActiveRecord::TestCase data_column = columns.detect { |c| c.name == "data" } assert_nil data_column.default - + ensure Person.connection.drop_table :binary_testings, if_exists: true end unless mysql_enforcing_gtid_consistency? def test_create_table_with_query - Person.connection.drop_table :table_from_query_testings rescue nil Person.connection.create_table(:person, force: true) Person.connection.create_table :table_from_query_testings, as: "SELECT id FROM person" @@ -444,12 +529,11 @@ class MigrationTest < ActiveRecord::TestCase columns = Person.connection.columns(:table_from_query_testings) assert_equal 1, columns.length assert_equal "id", columns.first.name - + ensure Person.connection.drop_table :table_from_query_testings rescue nil end def test_create_table_with_query_from_relation - Person.connection.drop_table :table_from_query_testings rescue nil Person.connection.create_table(:person, force: true) Person.connection.create_table :table_from_query_testings, as: Person.select(:id) @@ -457,7 +541,7 @@ class MigrationTest < ActiveRecord::TestCase columns = Person.connection.columns(:table_from_query_testings) assert_equal 1, columns.length assert_equal "id", columns.first.name - + ensure Person.connection.drop_table :table_from_query_testings rescue nil end end @@ -499,9 +583,8 @@ class MigrationTest < ActiveRecord::TestCase end end - if current_adapter?(:MysqlAdapter, :Mysql2Adapter, :PostgreSQLAdapter) - def test_out_of_range_limit_should_raise - Person.connection.drop_table :test_limits rescue nil + if current_adapter?(:Mysql2Adapter, :PostgreSQLAdapter) + def test_out_of_range_integer_limit_should_raise e = assert_raise(ActiveRecord::ActiveRecordError, "integer limit didn't raise") do Person.connection.create_table :test_integer_limits, :force => true do |t| t.column :bigone, :integer, :limit => 10 @@ -509,53 +592,58 @@ class MigrationTest < ActiveRecord::TestCase end assert_match(/No integer type has byte size 10/, e.message) + ensure + Person.connection.drop_table :test_integer_limits, if_exists: true + end + end - unless current_adapter?(:PostgreSQLAdapter) - assert_raise(ActiveRecord::ActiveRecordError, "text limit didn't raise") do - Person.connection.create_table :test_text_limits, :force => true do |t| - t.column :bigtext, :text, :limit => 0xfffffffff - end + if current_adapter?(:Mysql2Adapter) + def test_out_of_range_text_limit_should_raise + e = assert_raise(ActiveRecord::ActiveRecordError, "text limit didn't raise") do + Person.connection.create_table :test_text_limits, force: true do |t| + t.text :bigtext, limit: 0xfffffffff end end - Person.connection.drop_table :test_limits rescue nil + assert_match(/No text type has byte length #{0xfffffffff}/, e.message) + ensure + Person.connection.drop_table :test_text_limits, if_exists: true end end if ActiveRecord::Base.connection.supports_advisory_locks? - def test_migrator_generates_valid_lock_key - migration = Class.new(ActiveRecord::Migration).new + def test_migrator_generates_valid_lock_id + migration = Class.new(ActiveRecord::Migration::Current).new migrator = ActiveRecord::Migrator.new(:up, [migration], 100) - lock_key = migrator.send(:generate_migrator_advisory_lock_key) + lock_id = migrator.send(:generate_migrator_advisory_lock_id) - assert ActiveRecord::Base.connection.get_advisory_lock(lock_key), - "the Migrator should have generated a valid lock key, but it didn't" - assert ActiveRecord::Base.connection.release_advisory_lock(lock_key), - "the Migrator should have generated a valid lock key, but it didn't" + assert ActiveRecord::Base.connection.get_advisory_lock(lock_id), + "the Migrator should have generated a valid lock id, but it didn't" + assert ActiveRecord::Base.connection.release_advisory_lock(lock_id), + "the Migrator should have generated a valid lock id, but it didn't" end - def test_generate_migrator_advisory_lock_key + def test_generate_migrator_advisory_lock_id # It is important we are consistent with how we generate this so that # exclusive locking works across migrator versions - migration = Class.new(ActiveRecord::Migration).new + migration = Class.new(ActiveRecord::Migration::Current).new migrator = ActiveRecord::Migrator.new(:up, [migration], 100) - lock_key = migrator.send(:generate_migrator_advisory_lock_key) + lock_id = migrator.send(:generate_migrator_advisory_lock_id) current_database = ActiveRecord::Base.connection.current_database salt = ActiveRecord::Migrator::MIGRATOR_SALT - expected_key = Zlib.crc32(current_database) * salt + expected_id = Zlib.crc32(current_database) * salt - assert lock_key == expected_key, "expected lock key generated by the migrator to be #{expected_key}, but it was #{lock_key} instead" - assert lock_key.is_a?(Fixnum), "expected lock key to be a Fixnum, but it wasn't" - assert lock_key.bit_length <= 63, "lock key must be a signed integer of max 63 bits magnitude" + assert lock_id == expected_id, "expected lock id generated by the migrator to be #{expected_id}, but it was #{lock_id} instead" + assert lock_id.bit_length <= 63, "lock id must be a signed integer of max 63 bits magnitude" end def test_migrator_one_up_with_unavailable_lock assert_no_column Person, :last_name - migration = Class.new(ActiveRecord::Migration) { + migration = Class.new(ActiveRecord::Migration::Current) { def version; 100 end def migrate(x) add_column "people", "last_name", :string @@ -563,9 +651,9 @@ class MigrationTest < ActiveRecord::TestCase }.new migrator = ActiveRecord::Migrator.new(:up, [migration], 100) - lock_key = migrator.send(:generate_migrator_advisory_lock_key) + lock_id = migrator.send(:generate_migrator_advisory_lock_id) - with_another_process_holding_lock(lock_key) do + with_another_process_holding_lock(lock_id) do assert_raise(ActiveRecord::ConcurrentMigrationError) { migrator.migrate } end @@ -576,7 +664,7 @@ class MigrationTest < ActiveRecord::TestCase def test_migrator_one_up_with_unavailable_lock_using_run assert_no_column Person, :last_name - migration = Class.new(ActiveRecord::Migration) { + migration = Class.new(ActiveRecord::Migration::Current) { def version; 100 end def migrate(x) add_column "people", "last_name", :string @@ -584,9 +672,9 @@ class MigrationTest < ActiveRecord::TestCase }.new migrator = ActiveRecord::Migrator.new(:up, [migration], 100) - lock_key = migrator.send(:generate_migrator_advisory_lock_key) + lock_id = migrator.send(:generate_migrator_advisory_lock_id) - with_another_process_holding_lock(lock_key) do + with_another_process_holding_lock(lock_id) do assert_raise(ActiveRecord::ConcurrentMigrationError) { migrator.run } end @@ -605,18 +693,18 @@ class MigrationTest < ActiveRecord::TestCase } end - def with_another_process_holding_lock(lock_key) + def with_another_process_holding_lock(lock_id) thread_lock = Concurrent::CountDownLatch.new test_terminated = Concurrent::CountDownLatch.new other_process = Thread.new do begin conn = ActiveRecord::Base.connection_pool.checkout - conn.get_advisory_lock(lock_key) + conn.get_advisory_lock(lock_id) thread_lock.count_down test_terminated.wait # hold the lock open until we tested everything ensure - conn.release_advisory_lock(lock_key) + conn.release_advisory_lock(lock_id) ActiveRecord::Base.connection_pool.checkin(conn) end end @@ -641,7 +729,7 @@ class ReservedWordsMigrationTest < ActiveRecord::TestCase connection.add_index :values, :value connection.remove_index :values, :column => :value end - + ensure connection.drop_table :values rescue nil end end @@ -653,11 +741,11 @@ class ExplicitlyNamedIndexMigrationTest < ActiveRecord::TestCase t.integer :value end - assert_nothing_raised ArgumentError do + assert_nothing_raised do connection.add_index :values, :value, name: 'a_different_name' connection.remove_index :values, column: :value, name: 'a_different_name' end - + ensure connection.drop_table :values rescue nil end end diff --git a/activerecord/test/cases/migrator_test.rb b/activerecord/test/cases/migrator_test.rb index 2ff6938e7b..86eca53141 100644 --- a/activerecord/test/cases/migrator_test.rb +++ b/activerecord/test/cases/migrator_test.rb @@ -6,7 +6,7 @@ class MigratorTest < ActiveRecord::TestCase # Use this class to sense if migrations have gone # up or down. - class Sensor < ActiveRecord::Migration + class Sensor < ActiveRecord::Migration::Current attr_reader :went_up, :went_down def initialize name = self.class.name, version = nil @@ -313,9 +313,9 @@ class MigratorTest < ActiveRecord::TestCase _, migrator = migrator_class(3) ActiveRecord::Base.connection.drop_table "schema_migrations", if_exists: true - assert_not ActiveRecord::Base.connection.table_exists?('schema_migrations') + ActiveSupport::Deprecation.silence { assert_not ActiveRecord::Base.connection.table_exists?('schema_migrations') } migrator.migrate("valid", 1) - assert ActiveRecord::Base.connection.table_exists?('schema_migrations') + ActiveSupport::Deprecation.silence { assert ActiveRecord::Base.connection.table_exists?('schema_migrations') } end def test_migrator_forward diff --git a/activerecord/test/cases/modules_test.rb b/activerecord/test/cases/modules_test.rb index 7f31325f47..486bcc22df 100644 --- a/activerecord/test/cases/modules_test.rb +++ b/activerecord/test/cases/modules_test.rb @@ -63,7 +63,7 @@ class ModulesTest < ActiveRecord::TestCase def test_assign_ids firm = MyApplication::Business::Firm.first - assert_nothing_raised NameError, "Should be able to resolve all class constants via reflection" do + assert_nothing_raised do firm.client_ids = [MyApplication::Business::Client.first.id] end end @@ -72,7 +72,7 @@ class ModulesTest < ActiveRecord::TestCase def test_eager_loading_in_modules clients = [] - assert_nothing_raised NameError, "Should be able to resolve all class constants via reflection" do + assert_nothing_raised do clients << MyApplication::Business::Client.references(:accounts).merge!(:includes => {:firm => :account}, :where => 'accounts.id IS NOT NULL').find(3) clients << MyApplication::Business::Client.includes(:firm => :account).find(3) end diff --git a/activerecord/test/cases/multiple_db_test.rb b/activerecord/test/cases/multiple_db_test.rb index 39cdcf5403..af4183a601 100644 --- a/activerecord/test/cases/multiple_db_test.rb +++ b/activerecord/test/cases/multiple_db_test.rb @@ -104,7 +104,7 @@ class MultipleDbTest < ActiveRecord::TestCase def test_associations_should_work_when_model_has_no_connection begin ActiveRecord::Base.remove_connection - assert_nothing_raised ActiveRecord::ConnectionNotEstablished do + assert_nothing_raised do College.first.courses.first end ensure diff --git a/activerecord/test/cases/nested_attributes_test.rb b/activerecord/test/cases/nested_attributes_test.rb index 93cb631a04..11fb164d50 100644 --- a/activerecord/test/cases/nested_attributes_test.rb +++ b/activerecord/test/cases/nested_attributes_test.rb @@ -61,6 +61,13 @@ class TestNestedAttributesInGeneral < ActiveRecord::TestCase assert_equal "No association found for name `honesty'. Has it been defined yet?", exception.message end + def test_should_raise_an_UnknownAttributeError_for_non_existing_nested_attributes + exception = assert_raise ActiveModel::UnknownAttributeError do + Pirate.new(:ship_attributes => { :sail => true }) + end + assert_equal "unknown attribute 'sail' for Ship.", exception.message + end + def test_should_disable_allow_destroy_by_default Pirate.accepts_nested_attributes_for :ship @@ -69,7 +76,7 @@ class TestNestedAttributesInGeneral < ActiveRecord::TestCase pirate.update(ship_attributes: { '_destroy' => true, :id => ship.id }) - assert_nothing_raised(ActiveRecord::RecordNotFound) { pirate.ship.reload } + assert_nothing_raised { pirate.ship.reload } end def test_a_model_should_respond_to_underscore_destroy_and_return_if_it_is_marked_for_destruction @@ -146,6 +153,19 @@ class TestNestedAttributesInGeneral < ActiveRecord::TestCase assert man.reload.interests.empty? end + def test_reject_if_is_not_short_circuited_if_allow_destroy_is_false + Pirate.accepts_nested_attributes_for :ship, reject_if: ->(a) { a[:name] == "The Golden Hind" }, allow_destroy: false + + pirate = Pirate.create!(catchphrase: "Stop wastin' me time", ship_attributes: { name: "White Pearl", _destroy: "1" }) + assert_equal "White Pearl", pirate.reload.ship.name + + pirate.update!(ship_attributes: { id: pirate.ship.id, name: "The Golden Hind", _destroy: "1" }) + assert_equal "White Pearl", pirate.reload.ship.name + + pirate.update!(ship_attributes: { id: pirate.ship.id, name: "Black Pearl", _destroy: "1" }) + assert_equal "Black Pearl", pirate.reload.ship.name + end + def test_has_many_association_updating_a_single_record Man.accepts_nested_attributes_for(:interests) man = Man.create(name: 'John') @@ -160,7 +180,7 @@ class TestNestedAttributesInGeneral < ActiveRecord::TestCase pirate = Pirate.new(:catchphrase => "Stop wastin' me time") pirate.ship_attributes = { :id => "" } - assert_nothing_raised(ActiveRecord::RecordNotFound) { pirate.save! } + assert_nothing_raised { pirate.save! } end def test_first_and_array_index_zero_methods_return_the_same_value_when_nested_attributes_are_set_to_update_existing_record @@ -491,7 +511,7 @@ class TestNestedAttributesOnABelongsToAssociation < ActiveRecord::TestCase def test_should_not_destroy_an_existing_record_if_destroy_is_not_truthy [nil, '0', 0, 'false', false].each do |not_truth| @ship.update(pirate_attributes: { id: @ship.pirate.id, _destroy: not_truth }) - assert_nothing_raised(ActiveRecord::RecordNotFound) { @ship.pirate.reload } + assert_nothing_raised { @ship.pirate.reload } end end @@ -499,7 +519,7 @@ class TestNestedAttributesOnABelongsToAssociation < ActiveRecord::TestCase Ship.accepts_nested_attributes_for :pirate, :allow_destroy => false, :reject_if => proc(&:empty?) @ship.update(pirate_attributes: { id: @ship.pirate.id, _destroy: '1' }) - assert_nothing_raised(ActiveRecord::RecordNotFound) { @ship.pirate.reload } + assert_nothing_raised { @ship.pirate.reload } ensure Ship.accepts_nested_attributes_for :pirate, :allow_destroy => true, :reject_if => proc(&:empty?) end @@ -516,7 +536,7 @@ class TestNestedAttributesOnABelongsToAssociation < ActiveRecord::TestCase pirate = @ship.pirate @ship.attributes = { :pirate_attributes => { :id => pirate.id, '_destroy' => true } } - assert_nothing_raised(ActiveRecord::RecordNotFound) { Pirate.find(pirate.id) } + assert_nothing_raised { Pirate.find(pirate.id) } @ship.save assert_raise(ActiveRecord::RecordNotFound) { Pirate.find(pirate.id) } end @@ -569,6 +589,13 @@ module NestedAttributesOnACollectionAssociationTests assert_respond_to @pirate, association_setter end + def test_should_raise_an_UnknownAttributeError_for_non_existing_nested_attributes_for_has_many + exception = assert_raise ActiveModel::UnknownAttributeError do + @pirate.parrots_attributes = [{ peg_leg: true }] + end + assert_equal "unknown attribute 'peg_leg' for Parrot.", exception.message + end + def test_should_save_only_one_association_on_create pirate = Pirate.create!({ :catchphrase => 'Arr', @@ -686,7 +713,7 @@ module NestedAttributesOnACollectionAssociationTests end def test_should_not_assign_destroy_key_to_a_record - assert_nothing_raised ActiveRecord::UnknownAttributeError do + assert_nothing_raised do @pirate.send(association_setter, { 'foo' => { '_destroy' => '0' }}) end end @@ -721,8 +748,8 @@ module NestedAttributesOnACollectionAssociationTests end def test_should_raise_an_argument_error_if_something_else_than_a_hash_is_passed - assert_nothing_raised(ArgumentError) { @pirate.send(association_setter, {}) } - assert_nothing_raised(ArgumentError) { @pirate.send(association_setter, Hash.new) } + assert_nothing_raised { @pirate.send(association_setter, {}) } + assert_nothing_raised { @pirate.send(association_setter, Hash.new) } exception = assert_raise ArgumentError do @pirate.send(association_setter, "foo") @@ -797,7 +824,7 @@ module NestedAttributesOnACollectionAssociationTests def test_can_use_symbols_as_object_identifier @pirate.attributes = { :parrots_attributes => { :foo => { :name => 'Lovely Day' }, :bar => { :name => 'Blown Away' } } } - assert_nothing_raised(NoMethodError) { @pirate.save! } + assert_nothing_raised { @pirate.save! } end def test_numeric_column_changes_from_zero_to_no_empty_string @@ -1068,39 +1095,4 @@ class TestHasManyAutosaveAssociationWhichItselfHasAutosaveAssociations < ActiveR assert_not part.valid? assert_equal ["Ship name can't be blank"], part.errors.full_messages end - - class ProtectedParameters - def initialize(hash) - @hash = hash - end - - def permitted? - true - end - - def [](key) - @hash[key] - end - - def to_h - @hash - end - end - - test "strong params style objects can be assigned for singular associations" do - params = { name: "Stern", ship_attributes: - ProtectedParameters.new(name: "The Black Rock") } - part = ShipPart.new(params) - - assert_equal "Stern", part.name - assert_equal "The Black Rock", part.ship.name - end - - test "strong params style objects can be assigned for collection associations" do - params = { trinkets_attributes: ProtectedParameters.new("0" => ProtectedParameters.new(name: "Necklace"), "1" => ProtectedParameters.new(name: "Spoon")) } - part = ShipPart.new(params) - - assert_equal "Necklace", part.trinkets[0].name - assert_equal "Spoon", part.trinkets[1].name - end end diff --git a/activerecord/test/cases/persistence_test.rb b/activerecord/test/cases/persistence_test.rb index acc3103ac6..56092aaa0c 100644 --- a/activerecord/test/cases/persistence_test.rb +++ b/activerecord/test/cases/persistence_test.rb @@ -19,6 +19,8 @@ require 'models/person' require 'models/pet' require 'models/ship' require 'models/toy' +require 'models/admin' +require 'models/admin/user' require 'rexml/document' class PersistenceTest < ActiveRecord::TestCase @@ -161,10 +163,27 @@ class PersistenceTest < ActiveRecord::TestCase assert !company.valid? original_errors = company.errors client = company.becomes(Client) - assert_equal original_errors, client.errors + assert_equal original_errors.keys, client.errors.keys end - def test_dupd_becomes_persists_changes_from_the_original + def test_becomes_errors_base + child_class = Class.new(Admin::User) do + store_accessor :settings, :foo + + def self.name; 'Admin::ChildUser'; end + end + + admin = Admin::User.new + admin.errors.add :token, :invalid + child = admin.becomes(child_class) + + assert_equal [:token], child.errors.keys + assert_nothing_raised do + child.errors.add :foo, :invalid + end + end + + def test_duped_becomes_persists_changes_from_the_original original = topics(:first) copy = original.dup.becomes(Reply) copy.save! @@ -966,4 +985,17 @@ class PersistenceTest < ActiveRecord::TestCase widget.reset_column_information end end + + def test_reset_column_information_resets_children + child = Class.new(Topic) + child.new # force schema to load + + ActiveRecord::Base.connection.add_column(:topics, :foo, :string) + Topic.reset_column_information + + assert_equal "bar", child.new(foo: :bar).foo + ensure + ActiveRecord::Base.connection.remove_column(:topics, :foo) + Topic.reset_column_information + end end diff --git a/activerecord/test/cases/pooled_connections_test.rb b/activerecord/test/cases/pooled_connections_test.rb index daa3271777..bca50dd008 100644 --- a/activerecord/test/cases/pooled_connections_test.rb +++ b/activerecord/test/cases/pooled_connections_test.rb @@ -44,7 +44,7 @@ class PooledConnectionsTest < ActiveRecord::TestCase conn = ActiveRecord::Base.connection_pool.checkout ActiveRecord::Base.connection_pool.checkin conn @connection_count += 1 - ActiveRecord::Base.connection.tables + ActiveRecord::Base.connection.data_sources rescue ActiveRecord::ConnectionTimeoutError @timed_out += 1 end diff --git a/activerecord/test/cases/primary_keys_test.rb b/activerecord/test/cases/primary_keys_test.rb index 344822c883..e27a747730 100644 --- a/activerecord/test/cases/primary_keys_test.rb +++ b/activerecord/test/cases/primary_keys_test.rb @@ -130,27 +130,25 @@ class PrimaryKeysTest < ActiveRecord::TestCase end def test_supports_primary_key - assert_nothing_raised NoMethodError do + assert_nothing_raised do ActiveRecord::Base.connection.supports_primary_key? end end - def test_primary_key_returns_value_if_it_exists - klass = Class.new(ActiveRecord::Base) do - self.table_name = 'developers' - end + if ActiveRecord::Base.connection.supports_primary_key? + def test_primary_key_returns_value_if_it_exists + klass = Class.new(ActiveRecord::Base) do + self.table_name = 'developers' + end - if ActiveRecord::Base.connection.supports_primary_key? assert_equal 'id', klass.primary_key end - end - def test_primary_key_returns_nil_if_it_does_not_exist - klass = Class.new(ActiveRecord::Base) do - self.table_name = 'developers_projects' - end + def test_primary_key_returns_nil_if_it_does_not_exist + klass = Class.new(ActiveRecord::Base) do + self.table_name = 'developers_projects' + end - if ActiveRecord::Base.connection.supports_primary_key? assert_nil klass.primary_key end end @@ -224,7 +222,7 @@ class PrimaryKeyAnyTypeTest < ActiveRecord::TestCase end teardown do - @connection.drop_table(:barcodes) if @connection.table_exists? :barcodes + @connection.drop_table(:barcodes, if_exists: true) end def test_any_type_primary_key @@ -262,13 +260,20 @@ class CompositePrimaryKeyTest < ActiveRecord::TestCase assert_equal ["region", "code"], @connection.primary_keys("barcodes") end + def test_primary_key_issues_warning + warning = capture(:stderr) do + assert_nil @connection.primary_key("barcodes") + end + assert_match(/WARNING: Rails does not support composite primary key\./, warning) + end + def test_collectly_dump_composite_primary_key schema = dump_table_schema "barcodes" assert_match %r{create_table "barcodes", primary_key: \["region", "code"\]}, schema end end -if current_adapter?(:MysqlAdapter, :Mysql2Adapter) +if current_adapter?(:Mysql2Adapter) class PrimaryKeyWithAnsiQuotesTest < ActiveRecord::TestCase self.use_transactional_tests = false @@ -308,7 +313,7 @@ if current_adapter?(:MysqlAdapter, :Mysql2Adapter) end end -if current_adapter?(:PostgreSQLAdapter, :MysqlAdapter, :Mysql2Adapter) +if current_adapter?(:PostgreSQLAdapter, :Mysql2Adapter) class PrimaryKeyBigSerialTest < ActiveRecord::TestCase include SchemaDumpingHelper @@ -351,7 +356,7 @@ if current_adapter?(:PostgreSQLAdapter, :MysqlAdapter, :Mysql2Adapter) end end - if current_adapter?(:MysqlAdapter, :Mysql2Adapter) + if current_adapter?(:Mysql2Adapter) test "primary key column type with options" do @connection.create_table(:widgets, id: :primary_key, limit: 8, unsigned: true, force: true) column = @connection.columns(:widgets).find { |c| c.name == 'id' } diff --git a/activerecord/test/cases/reflection_test.rb b/activerecord/test/cases/reflection_test.rb index 9c04a41e69..710c86b151 100644 --- a/activerecord/test/cases/reflection_test.rb +++ b/activerecord/test/cases/reflection_test.rb @@ -23,6 +23,7 @@ require 'models/chef' require 'models/department' require 'models/cake_designer' require 'models/drink_designer' +require 'models/mocktail_designer' require 'models/recipe' class ReflectionTest < ActiveRecord::TestCase @@ -278,6 +279,15 @@ class ReflectionTest < ActiveRecord::TestCase assert_equal 2, @hotel.chefs.size end + def test_scope_chain_does_not_interfere_with_hmt_with_polymorphic_case_and_sti + @hotel = Hotel.create! + @hotel.mocktail_designers << MocktailDesigner.create! + + assert_equal 1, @hotel.mocktail_designers.size + assert_equal 1, @hotel.mocktail_designers.count + assert_equal 1, @hotel.chef_lists.size + end + def test_scope_chain_of_polymorphic_association_does_not_leak_into_other_hmt_associations hotel = Hotel.create! department = hotel.departments.create! diff --git a/activerecord/test/cases/relation/delegation_test.rb b/activerecord/test/cases/relation/delegation_test.rb index 989f4e1e5d..f0e07e0731 100644 --- a/activerecord/test/cases/relation/delegation_test.rb +++ b/activerecord/test/cases/relation/delegation_test.rb @@ -27,7 +27,7 @@ module ActiveRecord module DelegationWhitelistBlacklistTests ARRAY_DELEGATES = [ - :+, :-, :|, :&, :[], + :+, :-, :|, :&, :[], :shuffle, :all?, :collect, :compact, :detect, :each, :each_cons, :each_with_index, :exclude?, :find_all, :flat_map, :group_by, :include?, :length, :map, :none?, :one?, :partition, :reject, :reverse, @@ -40,12 +40,6 @@ module ActiveRecord assert_respond_to target, method end end - - ActiveRecord::Delegation::BLACKLISTED_ARRAY_METHODS.each do |method| - define_method "test_#{method}_is_not_delegated_to_Array" do - assert_raises(NoMethodError) { call_method(target, method) } - end - end end class DelegationAssociationTest < DelegationTest diff --git a/activerecord/test/cases/relation/merging_test.rb b/activerecord/test/cases/relation/merging_test.rb index 0a2e874e4f..60a806c05a 100644 --- a/activerecord/test/cases/relation/merging_test.rb +++ b/activerecord/test/cases/relation/merging_test.rb @@ -104,6 +104,13 @@ class RelationMergingTest < ActiveRecord::TestCase post = PostThatLoadsCommentsInAnAfterSaveHook.create!(title: "First Post", body: "Blah blah blah.") assert_equal "First comment!", post.comments.where(body: "First comment!").first_or_create.body end + + def test_merging_with_from_clause + relation = Post.all + assert relation.from_clause.empty? + relation = relation.merge(Post.from("posts")) + refute relation.from_clause.empty? + end end class MergingDifferentRelationsTest < ActiveRecord::TestCase diff --git a/activerecord/test/cases/relation/mutation_test.rb b/activerecord/test/cases/relation/mutation_test.rb index d0f60a84b5..ffb2da7a26 100644 --- a/activerecord/test/cases/relation/mutation_test.rb +++ b/activerecord/test/cases/relation/mutation_test.rb @@ -26,6 +26,10 @@ module ActiveRecord def sanitize_sql_for_order(sql) sql end + + def arel_attribute(name, table) + table[name] + end end def relation diff --git a/activerecord/test/cases/relation/or_test.rb b/activerecord/test/cases/relation/or_test.rb index 2006fc9611..ce8c5ca489 100644 --- a/activerecord/test/cases/relation/or_test.rb +++ b/activerecord/test/cases/relation/or_test.rb @@ -52,9 +52,11 @@ module ActiveRecord end def test_or_with_incompatible_relations - assert_raises ArgumentError do + error = assert_raises ArgumentError do Post.order('body asc').where('id = 1').or(Post.order('id desc').where(:id => [2, 3])).to_a end + + assert_equal "Relation passed to #or must be structurally compatible. Incompatible values: [:order]", error.message end def test_or_when_grouping @@ -80,5 +82,11 @@ module ActiveRecord assert_equal p.loaded?, true assert_equal expected, p.or(Post.where('id = 2')).to_a end + + def test_or_with_non_relation_object_raises_error + assert_raises ArgumentError do + Post.where(id: [1, 2, 3]).or(title: 'Rails') + end + end end end diff --git a/activerecord/test/cases/relation/record_fetch_warning_test.rb b/activerecord/test/cases/relation/record_fetch_warning_test.rb index 62f0a7cc49..53daf436e5 100644 --- a/activerecord/test/cases/relation/record_fetch_warning_test.rb +++ b/activerecord/test/cases/relation/record_fetch_warning_test.rb @@ -7,7 +7,7 @@ module ActiveRecord def test_warn_on_records_fetched_greater_than original_logger = ActiveRecord::Base.logger - orginal_warn_on_records_fetched_greater_than = ActiveRecord::Base.warn_on_records_fetched_greater_than + original_warn_on_records_fetched_greater_than = ActiveRecord::Base.warn_on_records_fetched_greater_than log = StringIO.new ActiveRecord::Base.logger = ActiveSupport::Logger.new(log) @@ -22,7 +22,7 @@ module ActiveRecord assert_match(/Query fetched/, log.string) ensure ActiveRecord::Base.logger = original_logger - ActiveRecord::Base.warn_on_records_fetched_greater_than = orginal_warn_on_records_fetched_greater_than + ActiveRecord::Base.warn_on_records_fetched_greater_than = original_warn_on_records_fetched_greater_than end end end diff --git a/activerecord/test/cases/relation/where_test.rb b/activerecord/test/cases/relation/where_test.rb index bc6378b90e..56a2b5b8c6 100644 --- a/activerecord/test/cases/relation/where_test.rb +++ b/activerecord/test/cases/relation/where_test.rb @@ -2,6 +2,7 @@ require "cases/helper" require "models/author" require "models/binary" require "models/cake_designer" +require "models/car" require "models/chef" require "models/comment" require "models/edge" @@ -14,7 +15,7 @@ require "models/vertex" module ActiveRecord class WhereTest < ActiveRecord::TestCase - fixtures :posts, :edges, :authors, :binaries, :essays + fixtures :posts, :edges, :authors, :binaries, :essays, :cars, :treasures, :price_estimates def test_where_copies_bind_params author = authors(:david) @@ -114,6 +115,17 @@ module ActiveRecord assert_equal expected.to_sql, actual.to_sql end + def test_polymorphic_array_where_multiple_types + treasure_1 = treasures(:diamond) + treasure_2 = treasures(:sapphire) + car = cars(:honda) + + expected = [price_estimates(:diamond), price_estimates(:sapphire_1), price_estimates(:sapphire_2), price_estimates(:honda)].sort + actual = PriceEstimate.where(estimate_of: [treasure_1, treasure_2, car]).to_a.sort + + assert_equal expected, actual + end + def test_polymorphic_nested_relation_where expected = PriceEstimate.where(estimate_of_type: 'Treasure', estimate_of_id: Treasure.where(id: [1,2])) actual = PriceEstimate.where(estimate_of: Treasure.where(id: [1,2])) diff --git a/activerecord/test/cases/relation_test.rb b/activerecord/test/cases/relation_test.rb index 8794bc8043..03583344a8 100644 --- a/activerecord/test/cases/relation_test.rb +++ b/activerecord/test/cases/relation_test.rb @@ -43,13 +43,17 @@ module ActiveRecord (Relation::SINGLE_VALUE_METHODS - [:create_with]).each do |method| assert_nil relation.send("#{method}_value"), method.to_s end - assert_equal({}, relation.create_with_value) + value = relation.create_with_value + assert_equal({}, value) + assert_predicate value, :frozen? end def test_multi_value_initialize relation = Relation.new(FakeKlass, :b, nil) Relation::MULTI_VALUE_METHODS.each do |method| - assert_equal [], relation.send("#{method}_values"), method.to_s + values = relation.send("#{method}_values") + assert_equal [], values, method.to_s + assert_predicate values, :frozen?, method.to_s end end @@ -77,7 +81,6 @@ module ActiveRecord def test_tree_is_not_traversed relation = Relation.new(Post, Post.arel_table, Post.predicate_builder) - # FIXME: Remove the Arel::Nodes::Quoted in Rails 5.1 left = relation.table[:id].eq(10) right = relation.table[:id].eq(10) combine = left.and right @@ -104,7 +107,6 @@ module ActiveRecord def test_create_with_value_with_wheres relation = Relation.new(Post, Post.arel_table, Post.predicate_builder) - # FIXME: Remove the Arel::Nodes::Quoted in Rails 5.1 relation.where! relation.table[:id].eq(10) relation.create_with_value = {:hello => 'world'} assert_equal({:hello => 'world', :id => 10}, relation.scope_for_create) @@ -115,7 +117,6 @@ module ActiveRecord relation = Relation.new(Post, Post.arel_table, Post.predicate_builder) assert_equal({}, relation.scope_for_create) - # FIXME: Remove the Arel::Nodes::Quoted in Rails 5.1 relation.where! relation.table[:id].eq(10) assert_equal({}, relation.scope_for_create) @@ -235,6 +236,13 @@ module ActiveRecord assert_equal 3, relation.where(id: post.id).pluck(:id).size end + def test_merge_raises_with_invalid_argument + assert_raises ArgumentError do + relation = Relation.new(FakeKlass, :b, nil) + relation.merge(true) + end + end + def test_respond_to_for_non_selected_element post = Post.select(:title).first assert_equal false, post.respond_to?(:body), "post should not respond_to?(:body) since invoking it raises exception" diff --git a/activerecord/test/cases/relations_test.rb b/activerecord/test/cases/relations_test.rb index cd23c1b3e1..95e4230a58 100644 --- a/activerecord/test/cases/relations_test.rb +++ b/activerecord/test/cases/relations_test.rb @@ -18,6 +18,8 @@ require 'models/minivan' require 'models/aircraft' require "models/possession" require "models/reader" +require "models/categorization" +require "models/edge" class RelationTest < ActiveRecord::TestCase fixtures :authors, :topics, :entrants, :developers, :companies, :developers_projects, :accounts, :categories, :categorizations, :posts, :comments, @@ -110,15 +112,38 @@ class RelationTest < ActiveRecord::TestCase def test_loaded_first topics = Topic.all.order('id ASC') + topics.to_a # force load - assert_queries(1) do - topics.to_a # force load - 2.times { assert_equal "The First Topic", topics.first.title } + assert_no_queries do + assert_equal "The First Topic", topics.first.title + end + + assert topics.loaded? + end + + def test_loaded_first_with_limit + topics = Topic.all.order('id ASC') + topics.to_a # force load + + assert_no_queries do + assert_equal ["The First Topic", + "The Second Topic of the day"], topics.first(2).map(&:title) end assert topics.loaded? end + def test_first_get_more_than_available + topics = Topic.all.order('id ASC') + unloaded_first = topics.first(10) + topics.to_a # force load + + assert_no_queries do + loaded_first = topics.first(10) + assert_equal unloaded_first, loaded_first + end + end + def test_reload topics = Topic.all @@ -199,6 +224,39 @@ class RelationTest < ActiveRecord::TestCase 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_order_with_function_other_predicates + topics = Topic.order("author_name, length(title), id").reverse_order + assert_equal topics(:second).title, topics.first.title + topics = Topic.order("length(author_name), id, length(title)").reverse_order + assert_equal topics(:fifth).title, topics.first.title + end + + def test_reverse_order_with_multiargument_function + assert_raises(ActiveRecord::IrreversibleOrderError) do + Topic.order("concat(author_name, title)").reverse_order + end + end + + def test_reverse_order_with_nulls_first_or_last + assert_raises(ActiveRecord::IrreversibleOrderError) do + Topic.order("title NULLS FIRST").reverse_order + end + assert_raises(ActiveRecord::IrreversibleOrderError) do + Topic.order("title nulls last").reverse_order + end + end + + def test_default_reverse_order_on_table_without_primary_key + assert_raises(ActiveRecord::IrreversibleOrderError) do + Edge.all.reverse_order + end + end + def test_order_with_hash_and_symbol_generates_the_same_sql assert_equal Topic.order(:id).to_sql, Topic.order(:id => :asc).to_sql end @@ -300,6 +358,12 @@ class RelationTest < ActiveRecord::TestCase def test_finding_with_sanitized_order query = Tag.order(["field(id, ?)", [1,3,2]]).to_sql assert_match(/field\(id, 1,3,2\)/, query) + + query = Tag.order(["field(id, ?)", []]).to_sql + assert_match(/field\(id, NULL\)/, query) + + query = Tag.order(["field(id, ?)", nil]).to_sql + assert_match(/field\(id, NULL\)/, query) end def test_finding_with_order_limit_and_offset @@ -918,6 +982,12 @@ class RelationTest < ActiveRecord::TestCase assert authors.exists?(authors(:david).id) end + def test_any_with_scope_on_hash_includes + post = authors(:david).posts.first + categories = Categorization.includes(author: :posts).where(posts: { id: post.id }) + assert categories.exists? + end + def test_last authors = Author.all assert_equal authors(:bob), authors.last @@ -1209,6 +1279,16 @@ class RelationTest < ActiveRecord::TestCase assert posts.loaded? end + def test_to_a_should_dup_target + posts = Post.all + + original_size = posts.size + removed = posts.to_a.pop + + assert_equal original_size, posts.size + assert_includes posts.to_a, removed + end + def test_build posts = Post.all diff --git a/activerecord/test/cases/sanitize_test.rb b/activerecord/test/cases/sanitize_test.rb index 14e392ac30..239f63d27b 100644 --- a/activerecord/test/cases/sanitize_test.rb +++ b/activerecord/test/cases/sanitize_test.rb @@ -25,6 +25,16 @@ class SanitizeTest < ActiveRecord::TestCase assert_equal "name=#{quoted_bambi_and_thumper}", Binary.send(:sanitize_sql_array, ["name=?", "Bambi\nand\nThumper".mb_chars]) end + def test_sanitize_sql_array_handles_named_bind_variables + quoted_bambi = ActiveRecord::Base.connection.quote("Bambi") + assert_equal "name=#{quoted_bambi}", Binary.send(:sanitize_sql_array, ["name=:name", name: "Bambi"]) + assert_equal "name=#{quoted_bambi} AND id=1", Binary.send(:sanitize_sql_array, ["name=:name AND id=:id", name: "Bambi", id: 1]) + + quoted_bambi_and_thumper = ActiveRecord::Base.connection.quote("Bambi\nand\nThumper") + assert_equal "name=#{quoted_bambi_and_thumper}", Binary.send(:sanitize_sql_array, ["name=:name", name: "Bambi\nand\nThumper"]) + assert_equal "name=#{quoted_bambi_and_thumper} AND name2=#{quoted_bambi_and_thumper}", Binary.send(:sanitize_sql_array, ["name=:name AND name2=:name", name: "Bambi\nand\nThumper"]) + end + def test_sanitize_sql_array_handles_relations david = Author.create!(name: 'David') david_posts = david.posts.select(:id) @@ -69,4 +79,98 @@ class SanitizeTest < ActiveRecord::TestCase searchable_post.search("20% _reduction_!").to_a end end + + def test_bind_arity + assert_nothing_raised { bind '' } + assert_raise(ActiveRecord::PreparedStatementInvalid) { bind '', 1 } + + assert_raise(ActiveRecord::PreparedStatementInvalid) { bind '?' } + assert_nothing_raised { bind '?', 1 } + assert_raise(ActiveRecord::PreparedStatementInvalid) { bind '?', 1, 1 } + end + + def test_named_bind_variables + assert_equal '1', bind(':a', :a => 1) # ' ruby-mode + assert_equal '1 1', bind(':a :a', :a => 1) # ' ruby-mode + + assert_nothing_raised { bind("'+00:00'", :foo => "bar") } + end + + def test_named_bind_arity + assert_nothing_raised { bind "name = :name", { name: "37signals" } } + assert_nothing_raised { bind "name = :name", { name: "37signals", id: 1 } } + assert_raise(ActiveRecord::PreparedStatementInvalid) { bind "name = :name", { id: 1 } } + end + + class SimpleEnumerable + include Enumerable + + def initialize(ary) + @ary = ary + end + + def each(&b) + @ary.each(&b) + end + end + + def test_bind_enumerable + quoted_abc = %(#{ActiveRecord::Base.connection.quote('a')},#{ActiveRecord::Base.connection.quote('b')},#{ActiveRecord::Base.connection.quote('c')}) + + assert_equal '1,2,3', bind('?', [1, 2, 3]) + assert_equal quoted_abc, bind('?', %w(a b c)) + + assert_equal '1,2,3', bind(':a', :a => [1, 2, 3]) + assert_equal quoted_abc, bind(':a', :a => %w(a b c)) # ' + + assert_equal '1,2,3', bind('?', SimpleEnumerable.new([1, 2, 3])) + assert_equal quoted_abc, bind('?', SimpleEnumerable.new(%w(a b c))) + + assert_equal '1,2,3', bind(':a', :a => SimpleEnumerable.new([1, 2, 3])) + assert_equal quoted_abc, bind(':a', :a => SimpleEnumerable.new(%w(a b c))) # ' + end + + def test_bind_empty_enumerable + quoted_nil = ActiveRecord::Base.connection.quote(nil) + assert_equal quoted_nil, bind('?', []) + assert_equal " in (#{quoted_nil})", bind(' in (?)', []) + assert_equal "foo in (#{quoted_nil})", bind('foo in (?)', []) + end + + def test_bind_empty_string + quoted_empty = ActiveRecord::Base.connection.quote('') + assert_equal quoted_empty, bind('?', '') + end + + def test_bind_chars + quoted_bambi = ActiveRecord::Base.connection.quote("Bambi") + quoted_bambi_and_thumper = ActiveRecord::Base.connection.quote("Bambi\nand\nThumper") + assert_equal "name=#{quoted_bambi}", bind('name=?', "Bambi") + assert_equal "name=#{quoted_bambi_and_thumper}", bind('name=?', "Bambi\nand\nThumper") + assert_equal "name=#{quoted_bambi}", bind('name=?', "Bambi".mb_chars) + assert_equal "name=#{quoted_bambi_and_thumper}", bind('name=?', "Bambi\nand\nThumper".mb_chars) + end + + def test_bind_record + o = Struct.new(:quoted_id).new(1) + assert_equal '1', bind('?', o) + + os = [o] * 3 + assert_equal '1,1,1', bind('?', os) + end + + def test_named_bind_with_postgresql_type_casts + l = Proc.new { bind(":a::integer '2009-01-01'::date", :a => '10') } + assert_nothing_raised(&l) + assert_equal "#{ActiveRecord::Base.connection.quote('10')}::integer '2009-01-01'::date", l.call + end + + private + def bind(statement, *vars) + if vars.first.is_a?(Hash) + ActiveRecord::Base.send(:replace_named_bind_variables, statement, vars.first) + else + ActiveRecord::Base.send(:replace_bind_variables, statement, vars) + end + end end diff --git a/activerecord/test/cases/schema_dumper_test.rb b/activerecord/test/cases/schema_dumper_test.rb index 2a2c2bc8d0..8def74e75b 100644 --- a/activerecord/test/cases/schema_dumper_test.rb +++ b/activerecord/test/cases/schema_dumper_test.rb @@ -38,6 +38,7 @@ class SchemaDumperTest < ActiveRecord::TestCase assert_match %r{create_table "accounts"}, output assert_match %r{create_table "authors"}, output assert_no_match %r{create_table "schema_migrations"}, output + assert_no_match %r{create_table "ar_internal_metadata"}, output end def test_schema_dump_uses_force_cascade_on_create_table @@ -117,8 +118,8 @@ class SchemaDumperTest < ActiveRecord::TestCase assert_match %r{c_int_4.*}, output assert_no_match %r{c_int_4.*limit:}, output - elsif current_adapter?(:MysqlAdapter, :Mysql2Adapter) - assert_match %r{c_int_without_limit.*limit: 4}, output + elsif current_adapter?(:Mysql2Adapter) + assert_match %r{c_int_without_limit"$}, output assert_match %r{c_int_1.*limit: 1}, output assert_match %r{c_int_2.*limit: 2}, output @@ -158,6 +159,7 @@ class SchemaDumperTest < ActiveRecord::TestCase assert_no_match %r{create_table "accounts"}, output assert_match %r{create_table "authors"}, output assert_no_match %r{create_table "schema_migrations"}, output + assert_no_match %r{create_table "ar_internal_metadata"}, output end def test_schema_dump_with_regexp_ignored_table @@ -165,27 +167,28 @@ class SchemaDumperTest < ActiveRecord::TestCase assert_no_match %r{create_table "accounts"}, output assert_match %r{create_table "authors"}, output assert_no_match %r{create_table "schema_migrations"}, output + assert_no_match %r{create_table "ar_internal_metadata"}, output end def test_schema_dumps_index_columns_in_right_order - index_definition = standard_dump.split(/\n/).grep(/t\.index.*company_index/).first.strip - if current_adapter?(:MysqlAdapter, :Mysql2Adapter, :PostgreSQLAdapter) - assert_equal 't.index ["firm_id", "type", "rating"], name: "company_index", using: :btree', index_definition + index_definition = standard_dump.split(/\n/).grep(/add_index.*companies/).first.strip + if current_adapter?(:Mysql2Adapter, :PostgreSQLAdapter) + assert_equal 'add_index "companies", ["firm_id", "type", "rating"], name: "company_index", using: :btree', index_definition else - assert_equal 't.index ["firm_id", "type", "rating"], name: "company_index"', index_definition + assert_equal 'add_index "companies", ["firm_id", "type", "rating"], name: "company_index"', index_definition end end def test_schema_dumps_partial_indices - index_definition = standard_dump.split(/\n/).grep(/t\.index.*company_partial_index/).first.strip + index_definition = standard_dump.split(/\n/).grep(/add_index.*company_partial_index/).first.strip if current_adapter?(:PostgreSQLAdapter) - assert_equal 't.index ["firm_id", "type"], name: "company_partial_index", where: "(rating > 10)", using: :btree', index_definition - elsif current_adapter?(:MysqlAdapter, :Mysql2Adapter) - assert_equal 't.index ["firm_id", "type"], name: "company_partial_index", using: :btree', index_definition + assert_equal 'add_index "companies", ["firm_id", "type"], name: "company_partial_index", where: "(rating > 10)", using: :btree', index_definition + elsif current_adapter?(:Mysql2Adapter) + assert_equal 'add_index "companies", ["firm_id", "type"], name: "company_partial_index", using: :btree', index_definition elsif current_adapter?(:SQLite3Adapter) && ActiveRecord::Base.connection.supports_partial_index? - assert_equal 't.index ["firm_id", "type"], name: "company_partial_index", where: "rating > 10"', index_definition + assert_equal 'add_index "companies", ["firm_id", "type"], name: "company_partial_index", where: "rating > 10"', index_definition else - assert_equal 't.index ["firm_id", "type"], name: "company_partial_index"', index_definition + assert_equal 'add_index "companies", ["firm_id", "type"], name: "company_partial_index"', index_definition end end @@ -201,7 +204,7 @@ class SchemaDumperTest < ActiveRecord::TestCase assert_match %r{t\.boolean\s+"has_fun",.+default: false}, output end - if current_adapter?(:MysqlAdapter, :Mysql2Adapter) + if current_adapter?(:Mysql2Adapter) def test_schema_dump_should_add_default_value_for_mysql_text_field output = standard_dump assert_match %r{t\.text\s+"body",\s+limit: 65535,\s+null: false$}, output @@ -232,8 +235,8 @@ class SchemaDumperTest < ActiveRecord::TestCase def test_schema_dumps_index_type output = standard_dump - assert_match %r{t\.index \["awesome"\], name: "index_key_tests_on_awesome", type: :fulltext}, output - assert_match %r{t\.index \["pizza"\], name: "index_key_tests_on_pizza", using: :btree}, output + assert_match %r{add_index "key_tests", \["awesome"\], name: "index_key_tests_on_awesome", type: :fulltext}, output + assert_match %r{add_index "key_tests", \["pizza"\], name: "index_key_tests_on_pizza", using: :btree}, output end end @@ -312,7 +315,7 @@ class SchemaDumperTest < ActiveRecord::TestCase end end - class CreateDogMigration < ActiveRecord::Migration + class CreateDogMigration < ActiveRecord::Migration::Current def up create_table("dog_owners") do |t| end @@ -342,6 +345,7 @@ class SchemaDumperTest < ActiveRecord::TestCase assert_no_match %r{create_table "foo_.+_bar"}, output assert_no_match %r{add_index "foo_.+_bar"}, output assert_no_match %r{create_table "schema_migrations"}, output + assert_no_match %r{create_table "ar_internal_metadata"}, output if ActiveRecord::Base.connection.supports_foreign_keys? assert_no_match %r{add_foreign_key "foo_.+_bar"}, output @@ -353,6 +357,38 @@ class SchemaDumperTest < ActiveRecord::TestCase ActiveRecord::Base.table_name_suffix = ActiveRecord::Base.table_name_prefix = '' $stdout = original end + + def test_schema_dump_with_table_name_prefix_and_ignoring_tables + original, $stdout = $stdout, StringIO.new + + create_cat_migration = Class.new(ActiveRecord::Migration::Current) do + def change + create_table("cats") do |t| + end + create_table("omg_cats") do |t| + end + end + end + + original_table_name_prefix = ActiveRecord::Base.table_name_prefix + original_schema_dumper_ignore_tables = ActiveRecord::SchemaDumper.ignore_tables + ActiveRecord::Base.table_name_prefix = 'omg_' + ActiveRecord::SchemaDumper.ignore_tables = ["cats"] + migration = create_cat_migration.new + migration.migrate(:up) + + stream = StringIO.new + output = ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, stream).string + + assert_match %r{create_table "omg_cats"}, output + refute_match %r{create_table "cats"}, output + ensure + migration.migrate(:down) + ActiveRecord::Base.table_name_prefix = original_table_name_prefix + ActiveRecord::SchemaDumper.ignore_tables = original_schema_dumper_ignore_tables + + $stdout = original + end end class SchemaDumperDefaultsTest < ActiveRecord::TestCase diff --git a/activerecord/test/cases/scoping/default_scoping_test.rb b/activerecord/test/cases/scoping/default_scoping_test.rb index 86316ab476..c918cbdef5 100644 --- a/activerecord/test/cases/scoping/default_scoping_test.rb +++ b/activerecord/test/cases/scoping/default_scoping_test.rb @@ -374,6 +374,18 @@ class DefaultScopingTest < ActiveRecord::TestCase assert_equal 10, DeveloperCalledJamis.unscoped { DeveloperCalledJamis.poor }.length end + def test_default_scope_with_joins + assert_equal Comment.where(post_id: SpecialPostWithDefaultScope.pluck(:id)).count, + Comment.joins(:special_post_with_default_scope).count + assert_equal Comment.where(post_id: Post.pluck(:id)).count, + Comment.joins(:post).count + end + + def test_unscoped_with_joins_should_not_have_default_scope + assert_equal SpecialPostWithDefaultScope.unscoped { Comment.joins(:special_post_with_default_scope).to_a }, + Comment.joins(:post).to_a + end + def test_default_scope_select_ignored_by_aggregations assert_equal DeveloperWithSelect.all.to_a.count, DeveloperWithSelect.count end @@ -459,4 +471,18 @@ class DefaultScopingTest < ActiveRecord::TestCase scope = Bus.all assert_equal scope.where_clause.ast.children.length, 1 end + + def test_sti_conditions_are_not_carried_in_default_scope + ConditionalStiPost.create! body: '' + SubConditionalStiPost.create! body: '' + SubConditionalStiPost.create! title: 'Hello world', body: '' + + assert_equal 2, ConditionalStiPost.count + assert_equal 2, ConditionalStiPost.all.to_a.size + assert_equal 3, ConditionalStiPost.unscope(where: :title).to_a.size + + assert_equal 1, SubConditionalStiPost.count + assert_equal 1, SubConditionalStiPost.all.to_a.size + assert_equal 2, SubConditionalStiPost.unscope(where: :title).to_a.size + end end diff --git a/activerecord/test/cases/scoping/named_scoping_test.rb b/activerecord/test/cases/scoping/named_scoping_test.rb index 7a8eaeccb7..acba97bbb8 100644 --- a/activerecord/test/cases/scoping/named_scoping_test.rb +++ b/activerecord/test/cases/scoping/named_scoping_test.rb @@ -440,6 +440,25 @@ class NamedScopingTest < ActiveRecord::TestCase end end + def test_scopes_with_reserved_names + class << Topic + def public_method; end + public :public_method + + def protected_method; end + protected :protected_method + + def private_method; end + private :private_method + end + + [:public_method, :protected_method, :private_method].each do |reserved_method| + assert Topic.respond_to?(reserved_method, true) + ActiveRecord::Base.logger.expects(:warn) + silence_warnings { Topic.scope(reserved_method, -> { }) } + end + end + def test_scopes_on_relations # Topic.replied approved_topics = Topic.all.approved.order('id DESC') diff --git a/activerecord/test/cases/scoping/relation_scoping_test.rb b/activerecord/test/cases/scoping/relation_scoping_test.rb index 4bfffbe9c6..c15d57460b 100644 --- a/activerecord/test/cases/scoping/relation_scoping_test.rb +++ b/activerecord/test/cases/scoping/relation_scoping_test.rb @@ -209,9 +209,23 @@ class RelationScopingTest < ActiveRecord::TestCase assert_not_equal [], Developer.all end - def test_current_scope_does_not_pollute_other_subclasses - Post.none.scoping do - assert StiPost.all.any? + def test_current_scope_does_not_pollute_sibling_subclasses + Comment.none.scoping do + assert_not SpecialComment.all.any? + assert_not VerySpecialComment.all.any? + assert_not SubSpecialComment.all.any? + end + + SpecialComment.none.scoping do + assert Comment.all.any? + assert VerySpecialComment.all.any? + assert_not SubSpecialComment.all.any? + end + + SubSpecialComment.none.scoping do + assert Comment.all.any? + assert VerySpecialComment.all.any? + assert SpecialComment.all.any? end end end diff --git a/activerecord/test/cases/store_test.rb b/activerecord/test/cases/store_test.rb index e9cdf94c99..bce86875e1 100644 --- a/activerecord/test/cases/store_test.rb +++ b/activerecord/test/cases/store_test.rb @@ -104,7 +104,7 @@ class StoreTest < ActiveRecord::TestCase assert_equal true, user.settings.instance_of?(ActiveSupport::HashWithIndifferentAccess) end - test "convert store attributes from any format other than Hash or HashWithIndifferent access losing the data" do + test "convert store attributes from any format other than Hash or HashWithIndifferentAccess losing the data" do @john.json_data = "somedata" @john.height = 'low' assert_equal true, @john.json_data.instance_of?(ActiveSupport::HashWithIndifferentAccess) @@ -177,6 +177,7 @@ class StoreTest < ActiveRecord::TestCase assert_equal [:color], first_model.stored_attributes[:data] assert_equal [:color, :width, :height], second_model.stored_attributes[:data] assert_equal [:color, :area, :volume], third_model.stored_attributes[:data] + assert_equal [:color], first_model.stored_attributes[:data] end test "YAML coder initializes the store when a Nil value is given" do diff --git a/activerecord/test/cases/suppressor_test.rb b/activerecord/test/cases/suppressor_test.rb index 72c5c16555..7d44e36419 100644 --- a/activerecord/test/cases/suppressor_test.rb +++ b/activerecord/test/cases/suppressor_test.rb @@ -46,7 +46,18 @@ class SuppressorTest < ActiveRecord::TestCase Notification.suppress { UserWithNotification.create! } assert_difference -> { Notification.count } do - Notification.create! + Notification.create!(message: "New Comment") + end + end + + def test_suppresses_validations_on_create + assert_no_difference -> { Notification.count } do + Notification.suppress do + User.create + User.create! + User.new.save + User.new.save! + end end end end diff --git a/activerecord/test/cases/tasks/database_tasks_test.rb b/activerecord/test/cases/tasks/database_tasks_test.rb index c8f4179313..49df6628eb 100644 --- a/activerecord/test/cases/tasks/database_tasks_test.rb +++ b/activerecord/test/cases/tasks/database_tasks_test.rb @@ -12,12 +12,39 @@ module ActiveRecord end ADAPTERS_TASKS = { - mysql: :mysql_tasks, mysql2: :mysql_tasks, postgresql: :postgresql_tasks, sqlite3: :sqlite_tasks } + class DatabaseTasksUtilsTask< ActiveRecord::TestCase + def test_raises_an_error_when_called_with_protected_environment + ActiveRecord::Migrator.stubs(:current_version).returns(1) + + protected_environments = ActiveRecord::Base.protected_environments.dup + current_env = ActiveRecord::Migrator.current_environment + assert !protected_environments.include?(current_env) + # Assert no error + ActiveRecord::Tasks::DatabaseTasks.check_protected_environments! + + ActiveRecord::Base.protected_environments << current_env + assert_raise(ActiveRecord::ProtectedEnvironmentError) do + ActiveRecord::Tasks::DatabaseTasks.check_protected_environments! + end + ensure + ActiveRecord::Base.protected_environments = protected_environments + end + + def test_raises_an_error_if_no_migrations_have_been_made + ActiveRecord::InternalMetadata.stubs(:table_exists?).returns(false) + ActiveRecord::Migrator.stubs(:current_version).returns(1) + + assert_raise(ActiveRecord::NoEnvironmentInSchemaError) do + ActiveRecord::Tasks::DatabaseTasks.check_protected_environments! + end + end + end + class DatabaseTasksRegisterTask < ActiveRecord::TestCase def test_register_task klazz = Class.new do diff --git a/activerecord/test/cases/tasks/mysql_rake_test.rb b/activerecord/test/cases/tasks/mysql_rake_test.rb index 723a7618ba..1632f04854 100644 --- a/activerecord/test/cases/tasks/mysql_rake_test.rb +++ b/activerecord/test/cases/tasks/mysql_rake_test.rb @@ -1,12 +1,12 @@ require 'cases/helper' -if current_adapter?(:MysqlAdapter, :Mysql2Adapter) +if current_adapter?(:Mysql2Adapter) module ActiveRecord class MysqlDBCreateTest < ActiveRecord::TestCase def setup @connection = stub(:create_database => true) @configuration = { - 'adapter' => 'mysql', + 'adapter' => 'mysql2', 'database' => 'my-app-db' } @@ -16,7 +16,7 @@ module ActiveRecord def test_establishes_connection_without_database ActiveRecord::Base.expects(:establish_connection). - with('adapter' => 'mysql', 'database' => nil) + with('adapter' => 'mysql2', 'database' => nil) ActiveRecord::Tasks::DatabaseTasks.create @configuration end @@ -59,97 +59,94 @@ module ActiveRecord end end - if current_adapter?(:MysqlAdapter) - class MysqlDBCreateAsRootTest < ActiveRecord::TestCase - def setup - @connection = stub("Connection", create_database: true) - @error = Mysql::Error.new "Invalid permissions" - @configuration = { - 'adapter' => 'mysql', - 'database' => 'my-app-db', - 'username' => 'pat', - 'password' => 'wossname' - } - - $stdin.stubs(:gets).returns("secret\n") - $stdout.stubs(:print).returns(nil) - @error.stubs(:errno).returns(1045) - ActiveRecord::Base.stubs(:connection).returns(@connection) - ActiveRecord::Base.stubs(:establish_connection). - raises(@error). - then.returns(true) - end + class MysqlDBCreateAsRootTest < ActiveRecord::TestCase + def setup + @connection = stub("Connection", create_database: true) + @error = Mysql2::Error.new("Invalid permissions") + @configuration = { + 'adapter' => 'mysql2', + 'database' => 'my-app-db', + 'username' => 'pat', + 'password' => 'wossname' + } - if defined?(::Mysql) - def test_root_password_is_requested - assert_permissions_granted_for "pat" - $stdin.expects(:gets).returns("secret\n") + $stdin.stubs(:gets).returns("secret\n") + $stdout.stubs(:print).returns(nil) + @error.stubs(:errno).returns(1045) + ActiveRecord::Base.stubs(:connection).returns(@connection) + ActiveRecord::Base.stubs(:establish_connection). + raises(@error). + then.returns(true) + end - ActiveRecord::Tasks::DatabaseTasks.create @configuration - end - end + def test_root_password_is_requested + assert_permissions_granted_for("pat") + $stdin.expects(:gets).returns("secret\n") - def test_connection_established_as_root - assert_permissions_granted_for "pat" - ActiveRecord::Base.expects(:establish_connection).with( - 'adapter' => 'mysql', - 'database' => nil, - 'username' => 'root', - 'password' => 'secret' - ) + ActiveRecord::Tasks::DatabaseTasks.create @configuration + end - ActiveRecord::Tasks::DatabaseTasks.create @configuration - end + def test_connection_established_as_root + assert_permissions_granted_for("pat") + ActiveRecord::Base.expects(:establish_connection).with( + 'adapter' => 'mysql2', + 'database' => nil, + 'username' => 'root', + 'password' => 'secret' + ) - def test_database_created_by_root - assert_permissions_granted_for "pat" - @connection.expects(:create_database). - with('my-app-db', {}) + ActiveRecord::Tasks::DatabaseTasks.create @configuration + end - ActiveRecord::Tasks::DatabaseTasks.create @configuration - end + def test_database_created_by_root + assert_permissions_granted_for("pat") + @connection.expects(:create_database). + with('my-app-db', {}) - def test_grant_privileges_for_normal_user - assert_permissions_granted_for "pat" - ActiveRecord::Tasks::DatabaseTasks.create @configuration - end + ActiveRecord::Tasks::DatabaseTasks.create @configuration + end - def test_do_not_grant_privileges_for_root_user - @configuration['username'] = 'root' - @configuration['password'] = '' - ActiveRecord::Tasks::DatabaseTasks.create @configuration - end + def test_grant_privileges_for_normal_user + assert_permissions_granted_for("pat") + ActiveRecord::Tasks::DatabaseTasks.create @configuration + end - def test_connection_established_as_normal_user - assert_permissions_granted_for "pat" - ActiveRecord::Base.expects(:establish_connection).returns do - ActiveRecord::Base.expects(:establish_connection).with( - 'adapter' => 'mysql', - 'database' => 'my-app-db', - 'username' => 'pat', - 'password' => 'secret' - ) + def test_do_not_grant_privileges_for_root_user + @configuration['username'] = 'root' + @configuration['password'] = '' + ActiveRecord::Tasks::DatabaseTasks.create @configuration + end - raise @error - end + def test_connection_established_as_normal_user + assert_permissions_granted_for("pat") + ActiveRecord::Base.expects(:establish_connection).returns do + ActiveRecord::Base.expects(:establish_connection).with( + 'adapter' => 'mysql2', + 'database' => 'my-app-db', + 'username' => 'pat', + 'password' => 'secret' + ) - ActiveRecord::Tasks::DatabaseTasks.create @configuration + raise @error end - def test_sends_output_to_stderr_when_other_errors - @error.stubs(:errno).returns(42) + ActiveRecord::Tasks::DatabaseTasks.create @configuration + end + + def test_sends_output_to_stderr_when_other_errors + @error.stubs(:errno).returns(42) - $stderr.expects(:puts).at_least_once.returns(nil) + $stderr.expects(:puts).at_least_once.returns(nil) - ActiveRecord::Tasks::DatabaseTasks.create @configuration - end + ActiveRecord::Tasks::DatabaseTasks.create @configuration + end + + private - private - def assert_permissions_granted_for(db_user) - db_name = @configuration['database'] - db_password = @configuration['password'] - @connection.expects(:execute).with("GRANT ALL PRIVILEGES ON #{db_name}.* TO '#{db_user}'@'localhost' IDENTIFIED BY '#{db_password}' WITH GRANT OPTION;") - end + def assert_permissions_granted_for(db_user) + db_name = @configuration['database'] + db_password = @configuration['password'] + @connection.expects(:execute).with("GRANT ALL PRIVILEGES ON #{db_name}.* TO '#{db_user}'@'localhost' IDENTIFIED BY '#{db_password}' WITH GRANT OPTION;") end end @@ -157,7 +154,7 @@ module ActiveRecord def setup @connection = stub(:drop_database => true) @configuration = { - 'adapter' => 'mysql', + 'adapter' => 'mysql2', 'database' => 'my-app-db' } @@ -182,7 +179,7 @@ module ActiveRecord def setup @connection = stub(:recreate_database => true) @configuration = { - 'adapter' => 'mysql', + 'adapter' => 'mysql2', 'database' => 'test-db' } @@ -216,7 +213,7 @@ module ActiveRecord def setup @connection = stub(:create_database => true) @configuration = { - 'adapter' => 'mysql', + 'adapter' => 'mysql2', 'database' => 'my-app-db' } @@ -234,7 +231,7 @@ module ActiveRecord def setup @connection = stub(:create_database => true) @configuration = { - 'adapter' => 'mysql', + 'adapter' => 'mysql2', 'database' => 'my-app-db' } @@ -251,7 +248,7 @@ module ActiveRecord class MySQLStructureDumpTest < ActiveRecord::TestCase def setup @configuration = { - 'adapter' => 'mysql', + 'adapter' => 'mysql2', 'database' => 'test-db' } end @@ -297,7 +294,7 @@ module ActiveRecord class MySQLStructureLoadTest < ActiveRecord::TestCase def setup @configuration = { - 'adapter' => 'mysql', + 'adapter' => 'mysql2', 'database' => 'test-db' } end diff --git a/activerecord/test/cases/tasks/postgresql_rake_test.rb b/activerecord/test/cases/tasks/postgresql_rake_test.rb index c31f94b2f2..ba53f340ae 100644 --- a/activerecord/test/cases/tasks/postgresql_rake_test.rb +++ b/activerecord/test/cases/tasks/postgresql_rake_test.rb @@ -212,7 +212,7 @@ module ActiveRecord def test_structure_dump_with_schema_search_path @configuration['schema_search_path'] = 'foo,bar' - Kernel.expects(:system).with('pg_dump', '-s', '-x', '-O', '-f', @filename, '--schema=foo --schema=bar', 'my-app-db').returns(true) + Kernel.expects(:system).with('pg_dump', '-s', '-x', '-O', '-f', @filename, '--schema=foo', '--schema=bar', 'my-app-db').returns(true) ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, @filename) end @@ -228,7 +228,7 @@ module ActiveRecord end def test_structure_dump_with_dump_schemas_string - Kernel.expects(:system).with("pg_dump", '-s', '-x', '-O', '-f', @filename, '--schema=foo --schema=bar', "my-app-db").returns(true) + Kernel.expects(:system).with("pg_dump", '-s', '-x', '-O', '-f', @filename, '--schema=foo', '--schema=bar', "my-app-db").returns(true) with_dump_schemas('foo,bar') do ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, @filename) diff --git a/activerecord/test/cases/test_case.rb b/activerecord/test/cases/test_case.rb index 47e664f4e7..87299c0dab 100644 --- a/activerecord/test/cases/test_case.rb +++ b/activerecord/test/cases/test_case.rb @@ -77,12 +77,6 @@ module ActiveRecord end end - class MysqlTestCase < TestCase - def self.run(*args) - super if current_adapter?(:MysqlAdapter) - end - end - class SQLite3TestCase < TestCase def self.run(*args) super if current_adapter?(:SQLite3Adapter) diff --git a/activerecord/test/cases/time_precision_test.rb b/activerecord/test/cases/time_precision_test.rb index ff7a81fe60..3b6e4dcc2b 100644 --- a/activerecord/test/cases/time_precision_test.rb +++ b/activerecord/test/cases/time_precision_test.rb @@ -10,6 +10,7 @@ class TimePrecisionTest < ActiveRecord::TestCase setup do @connection = ActiveRecord::Base.connection + Foo.reset_column_information end teardown do @@ -20,8 +21,8 @@ class TimePrecisionTest < ActiveRecord::TestCase @connection.create_table(:foos, force: true) @connection.add_column :foos, :start, :time, precision: 3 @connection.add_column :foos, :finish, :time, precision: 6 - assert_equal 3, activerecord_column_option('foos', 'start', 'precision') - assert_equal 6, activerecord_column_option('foos', 'finish', 'precision') + assert_equal 3, Foo.columns_hash['start'].precision + assert_equal 6, Foo.columns_hash['finish'].precision end def test_passing_precision_to_time_does_not_set_limit @@ -29,8 +30,8 @@ class TimePrecisionTest < ActiveRecord::TestCase t.time :start, precision: 3 t.time :finish, precision: 6 end - assert_nil activerecord_column_option('foos', 'start', 'limit') - assert_nil activerecord_column_option('foos', 'finish', 'limit') + assert_nil Foo.columns_hash['start'].limit + assert_nil Foo.columns_hash['finish'].limit end def test_invalid_time_precision_raises_error @@ -42,15 +43,6 @@ class TimePrecisionTest < ActiveRecord::TestCase end end - def test_database_agrees_with_activerecord_about_precision - @connection.create_table(:foos, force: true) do |t| - t.time :start, precision: 2 - t.time :finish, precision: 4 - end - assert_equal 2, database_datetime_precision('foos', 'start') - assert_equal 4, database_datetime_precision('foos', 'finish') - end - def test_formatting_time_according_to_precision @connection.create_table(:foos, force: true) do |t| t.time :start, precision: 0 @@ -88,21 +80,5 @@ class TimePrecisionTest < ActiveRecord::TestCase end end - private - - def database_datetime_precision(table_name, column_name) - results = @connection.exec_query("SELECT column_name, datetime_precision FROM information_schema.columns WHERE table_name = '#{table_name}'") - result = results.find do |result_hash| - result_hash["column_name"] == column_name - end - result && result["datetime_precision"].to_i - end - - def activerecord_column_option(tablename, column_name, option) - result = @connection.columns(tablename).find do |column| - column.name == column_name - end - result && result.send(option) - end end end diff --git a/activerecord/test/cases/timestamp_test.rb b/activerecord/test/cases/timestamp_test.rb index 970f6bcf4a..937b84bccc 100644 --- a/activerecord/test/cases/timestamp_test.rb +++ b/activerecord/test/cases/timestamp_test.rb @@ -98,8 +98,11 @@ class TimestampTest < ActiveRecord::TestCase task = Task.first previous_value = task.ending task.touch(:ending) + + now = Time.now.change(usec: 0) + assert_not_equal previous_value, task.ending - assert_in_delta Time.now, task.ending, 1 + assert_in_delta now, task.ending, 1 end def test_touching_an_attribute_updates_timestamp_with_given_time @@ -120,10 +123,12 @@ class TimestampTest < ActiveRecord::TestCase previous_ending = task.ending task.touch(:starting, :ending) + now = Time.now.change(usec: 0) + assert_not_equal previous_starting, task.starting assert_not_equal previous_ending, task.ending - assert_in_delta Time.now, task.starting, 1 - assert_in_delta Time.now, task.ending, 1 + assert_in_delta now, task.starting, 1 + assert_in_delta now, task.ending, 1 end def test_touching_a_record_without_timestamps_is_unexceptional diff --git a/activerecord/test/cases/touch_later_test.rb b/activerecord/test/cases/touch_later_test.rb index 7058f4fbe2..b47769eed7 100644 --- a/activerecord/test/cases/touch_later_test.rb +++ b/activerecord/test/cases/touch_later_test.rb @@ -95,16 +95,14 @@ class TouchLaterTest < ActiveRecord::TestCase end def test_touching_three_deep - skip "Pending from #19324" - previous_tree_updated_at = trees(:root).updated_at previous_grandparent_updated_at = nodes(:grandparent).updated_at previous_parent_updated_at = nodes(:parent_a).updated_at previous_child_updated_at = nodes(:child_one_of_a).updated_at - travel 5.seconds - - Node.create! parent: nodes(:child_one_of_a), tree: trees(:root) + travel 5.seconds do + Node.create! parent: nodes(:child_one_of_a), tree: trees(:root) + end assert_not_equal nodes(:child_one_of_a).reload.updated_at, previous_child_updated_at assert_not_equal nodes(:parent_a).reload.updated_at, previous_parent_updated_at diff --git a/activerecord/test/cases/transaction_callbacks_test.rb b/activerecord/test/cases/transaction_callbacks_test.rb index f2229939c8..8a7f19293d 100644 --- a/activerecord/test/cases/transaction_callbacks_test.rb +++ b/activerecord/test/cases/transaction_callbacks_test.rb @@ -34,10 +34,11 @@ class TransactionCallbacksTest < ActiveRecord::TestCase has_many :replies, class_name: "ReplyWithCallbacks", foreign_key: "parent_id" + before_commit { |record| record.do_before_commit(nil) } after_commit { |record| record.do_after_commit(nil) } - after_commit(on: :create) { |record| record.do_after_commit(:create) } - after_commit(on: :update) { |record| record.do_after_commit(:update) } - after_commit(on: :destroy) { |record| record.do_after_commit(:destroy) } + after_create_commit { |record| record.do_after_commit(:create) } + after_update_commit { |record| record.do_after_commit(:update) } + after_destroy_commit { |record| record.do_after_commit(:destroy) } after_rollback { |record| record.do_after_rollback(nil) } after_rollback(on: :create) { |record| record.do_after_rollback(:create) } after_rollback(on: :update) { |record| record.do_after_rollback(:update) } @@ -47,6 +48,12 @@ class TransactionCallbacksTest < ActiveRecord::TestCase @history ||= [] end + def before_commit_block(on = nil, &block) + @before_commit ||= {} + @before_commit[on] ||= [] + @before_commit[on] << block + end + def after_commit_block(on = nil, &block) @after_commit ||= {} @after_commit[on] ||= [] @@ -59,6 +66,11 @@ class TransactionCallbacksTest < ActiveRecord::TestCase @after_rollback[on] << block end + def do_before_commit(on) + blocks = @before_commit[on] if defined?(@before_commit) + blocks.each{|b| b.call(self)} if blocks + end + def do_after_commit(on) blocks = @after_commit[on] if defined?(@after_commit) blocks.each{|b| b.call(self)} if blocks @@ -74,6 +86,20 @@ class TransactionCallbacksTest < ActiveRecord::TestCase @first = TopicWithCallbacks.find(1) end + # FIXME: Test behavior, not implementation. + def test_before_commit_exception_should_pop_transaction_stack + @first.before_commit_block { raise 'better pop this txn from the stack!' } + + original_txn = @first.class.connection.current_transaction + + begin + @first.save! + fail + rescue + assert_equal original_txn, @first.class.connection.current_transaction + end + end + def test_call_after_commit_after_transaction_commits @first.after_commit_block{|r| r.history << :after_commit} @first.after_rollback_block{|r| r.history << :after_rollback} diff --git a/activerecord/test/cases/transactions_test.rb b/activerecord/test/cases/transactions_test.rb index ec5bdfd725..791b895d02 100644 --- a/activerecord/test/cases/transactions_test.rb +++ b/activerecord/test/cases/transactions_test.rb @@ -58,6 +58,11 @@ class TransactionTest < ActiveRecord::TestCase end end + def test_add_to_null_transaction + topic = Topic.new + topic.add_to_transaction + end + def test_successful_with_return committed = false diff --git a/activerecord/test/cases/validations/absence_validation_test.rb b/activerecord/test/cases/validations/absence_validation_test.rb index dd43ee358c..c0b3750bcc 100644 --- a/activerecord/test/cases/validations/absence_validation_test.rb +++ b/activerecord/test/cases/validations/absence_validation_test.rb @@ -57,19 +57,17 @@ class AbsenceValidationTest < ActiveRecord::TestCase assert_nothing_raised { boy_klass.new(face: face_with_to_a).valid? } end - def test_does_not_validate_if_parent_record_is_validate_false + def test_validates_absence_of_virtual_attribute_on_model repair_validations(Interest) do - Interest.validates_absence_of(:topic) - interest = Interest.new(topic: Topic.new(title: "Math")) - interest.save!(validate: false) - assert interest.persisted? + Interest.send(:attr_accessor, :token) + Interest.validates_absence_of(:token) - man = Man.new(interest_ids: [interest.id]) - man.save! - - assert_equal man.interests.size, 1 + interest = Interest.create!(topic: 'Thought Leadering') assert interest.valid? - assert man.valid? + + interest.token = 'tl' + + assert interest.invalid? end end end diff --git a/activerecord/test/cases/validations/association_validation_test.rb b/activerecord/test/cases/validations/association_validation_test.rb index bff5ffa65e..584a3dc0d8 100644 --- a/activerecord/test/cases/validations/association_validation_test.rb +++ b/activerecord/test/cases/validations/association_validation_test.rb @@ -45,6 +45,18 @@ class AssociationValidationTest < ActiveRecord::TestCase assert t.valid? end + def test_validates_associated_without_marked_for_destruction + reply = Class.new do + def valid? + true + end + end + Topic.validates_associated(:replies) + t = Topic.new + t.define_singleton_method(:replies) { [reply.new] } + assert t.valid? + end + def test_validates_associated_with_custom_message_using_quotes Reply.validates_associated :topic, :message=> "This string contains 'single' and \"double\" quotes" Topic.validates_presence_of :content diff --git a/activerecord/test/cases/validations/length_validation_test.rb b/activerecord/test/cases/validations/length_validation_test.rb index c5d8f8895c..78263fd955 100644 --- a/activerecord/test/cases/validations/length_validation_test.rb +++ b/activerecord/test/cases/validations/length_validation_test.rb @@ -61,17 +61,19 @@ class LengthValidationTest < ActiveRecord::TestCase assert_equal pet_count, Pet.count end - def test_does_not_validate_length_of_if_parent_record_is_validate_false - @owner.validates_length_of :name, minimum: 1 - owner = @owner.new - owner.save!(validate: false) - assert owner.persisted? + def test_validates_length_of_virtual_attribute_on_model + repair_validations(Pet) do + Pet.send(:attr_accessor, :nickname) + Pet.validates_length_of(:name, minimum: 1) + Pet.validates_length_of(:nickname, minimum: 1) - pet = Pet.new(owner_id: owner.id) - pet.save! + pet = Pet.create!(name: 'Fancy Pants', nickname: 'Fancy') - assert_equal owner.pets.size, 1 - assert owner.valid? - assert pet.valid? + assert pet.valid? + + pet.nickname = '' + + assert pet.invalid? + end end end diff --git a/activerecord/test/cases/validations/presence_validation_test.rb b/activerecord/test/cases/validations/presence_validation_test.rb index 6f8ad06ab6..868d111b8c 100644 --- a/activerecord/test/cases/validations/presence_validation_test.rb +++ b/activerecord/test/cases/validations/presence_validation_test.rb @@ -65,19 +65,39 @@ class PresenceValidationTest < ActiveRecord::TestCase assert_nothing_raised { s.valid? } end - def test_does_not_validate_presence_of_if_parent_record_is_validate_false + def test_validates_presence_of_virtual_attribute_on_model repair_validations(Interest) do + Interest.send(:attr_accessor, :abbreviation) Interest.validates_presence_of(:topic) + Interest.validates_presence_of(:abbreviation) + + interest = Interest.create!(topic: 'Thought Leadering', abbreviation: 'tl') + assert interest.valid? + + interest.abbreviation = '' + + assert interest.invalid? + end + end + + def test_validations_run_on_persisted_record + repair_validations(Interest) do interest = Interest.new - interest.save!(validate: false) - assert interest.persisted? + interest.save! + assert_predicate interest, :valid? - man = Man.new(interest_ids: [interest.id]) - man.save! + Interest.validates_presence_of(:topic) - assert_equal man.interests.size, 1 - assert interest.valid? - assert man.valid? + assert_not_predicate interest, :valid? + end + end + + def test_validates_presence_with_on_context + repair_validations(Interest) do + Interest.validates_presence_of(:topic, on: :required_name) + interest = Interest.new + interest.save! + assert_not interest.valid?(:required_name) end end end diff --git a/activerecord/test/cases/validations/uniqueness_validation_test.rb b/activerecord/test/cases/validations/uniqueness_validation_test.rb index 7502a55391..4c14d93c66 100644 --- a/activerecord/test/cases/validations/uniqueness_validation_test.rb +++ b/activerecord/test/cases/validations/uniqueness_validation_test.rb @@ -5,6 +5,7 @@ require 'models/warehouse_thing' require 'models/guid' require 'models/event' require 'models/dashboard' +require 'models/uuid_item' class Wizard < ActiveRecord::Base self.abstract_class = true @@ -48,6 +49,18 @@ class BigIntReverseTest < ActiveRecord::Base validates :engines_count, uniqueness: true end +class CoolTopic < Topic + validates_uniqueness_of :id +end + +class TopicWithAfterCreate < Topic + after_create :set_author + + def set_author + update_attributes!(:author_name => "#{title} #{id}") + end +end + class UniquenessValidationTest < ActiveRecord::TestCase INT_MAX_VALUE = 2147483647 @@ -412,23 +425,6 @@ class UniquenessValidationTest < ActiveRecord::TestCase assert topic.valid? end - def test_does_not_validate_uniqueness_of_if_parent_record_is_validate_false - Reply.validates_uniqueness_of(:content) - - Reply.create!(content: "Topic Title") - - reply = Reply.new(content: "Topic Title") - reply.save!(validate: false) - assert reply.persisted? - - topic = Topic.new(reply_ids: [reply.id]) - topic.save! - - assert_equal topic.replies.size, 1 - assert reply.valid? - assert topic.valid? - end - def test_validate_uniqueness_of_custom_primary_key klass = Class.new(ActiveRecord::Base) do self.table_name = "keyboards" @@ -469,4 +465,46 @@ class UniquenessValidationTest < ActiveRecord::TestCase assert_match(/\AUnknown primary key for table dashboards in model/, e.message) assert_match(/Can not validate uniqueness for persisted record without primary key.\z/, e.message) end + + def test_validate_uniqueness_ignores_itself_when_primary_key_changed + Topic.validates_uniqueness_of(:title) + + t = Topic.new("title" => "This is a unique title") + assert t.save, "Should save t as unique" + + t.id += 1 + assert t.valid?, "Should be valid" + assert t.save, "Should still save t as unique" + end + + def test_validate_uniqueness_with_after_create_performing_save + TopicWithAfterCreate.validates_uniqueness_of(:title) + topic = TopicWithAfterCreate.create!(:title => "Title1") + assert topic.author_name.start_with?("Title1") + + topic2 = TopicWithAfterCreate.new(:title => "Title1") + refute topic2.valid? + assert_equal(["has already been taken"], topic2.errors[:title]) + end + + def test_validate_uniqueness_uuid + skip unless current_adapter?(:PostgreSQLAdapter) + item = UuidItem.create!(uuid: SecureRandom.uuid, title: 'item1') + item.update(title: 'item1-title2') + assert_empty item.errors + + item2 = UuidValidatingItem.create!(uuid: SecureRandom.uuid, title: 'item2') + item2.update(title: 'item2-title2') + assert_empty item2.errors + end + + def test_validate_uniqueness_regular_id + item = CoolTopic.create!(title: 'MyItem') + assert_empty item.errors + + item2 = CoolTopic.new(id: item.id, title: 'MyItem2') + refute item2.valid? + + assert_equal(["has already been taken"], item2.errors[:id]) + end end diff --git a/activerecord/test/cases/validations_test.rb b/activerecord/test/cases/validations_test.rb index d04f4f7ce7..85e33d2218 100644 --- a/activerecord/test/cases/validations_test.rb +++ b/activerecord/test/cases/validations_test.rb @@ -130,7 +130,7 @@ class ValidationsTest < ActiveRecord::TestCase def test_validates_acceptance_of_with_non_existent_table Object.const_set :IncorporealModel, Class.new(ActiveRecord::Base) - assert_nothing_raised ActiveRecord::StatementInvalid do + assert_nothing_raised do IncorporealModel.validates_acceptance_of(:incorporeal_column) end end diff --git a/activerecord/test/cases/view_test.rb b/activerecord/test/cases/view_test.rb index e80d8bd584..f3c2d2f30e 100644 --- a/activerecord/test/cases/view_test.rb +++ b/activerecord/test/cases/view_test.rb @@ -45,7 +45,7 @@ module ViewBehavior def test_table_exists view_name = Ebook.table_name # TODO: switch this assertion around once we changed #tables to not return views. - assert @connection.table_exists?(view_name), "'#{view_name}' table should exist" + ActiveSupport::Deprecation.silence { assert @connection.table_exists?(view_name), "'#{view_name}' table should exist" } end def test_views_ara_valid_data_sources @@ -87,7 +87,7 @@ class ViewWithPrimaryKeyTest < ActiveRecord::TestCase end def drop_view(name) - @connection.execute "DROP VIEW #{name}" if @connection.table_exists? name + @connection.execute "DROP VIEW #{name}" if @connection.view_exists? name end end @@ -106,7 +106,7 @@ class ViewWithoutPrimaryKeyTest < ActiveRecord::TestCase end teardown do - @connection.execute "DROP VIEW paperbacks" if @connection.table_exists? "paperbacks" + @connection.execute "DROP VIEW paperbacks" if @connection.view_exists? "paperbacks" end def test_reading @@ -125,7 +125,8 @@ class ViewWithoutPrimaryKeyTest < ActiveRecord::TestCase def test_table_exists view_name = Paperback.table_name - assert @connection.table_exists?(view_name), "'#{view_name}' table should exist" + # TODO: switch this assertion around once we changed #tables to not return views. + ActiveSupport::Deprecation.silence { assert @connection.table_exists?(view_name), "'#{view_name}' table should exist" } end def test_column_definitions @@ -149,7 +150,7 @@ class ViewWithoutPrimaryKeyTest < ActiveRecord::TestCase end # sqlite dose not support CREATE, INSERT, and DELETE for VIEW -if current_adapter?(:MysqlAdapter, :Mysql2Adapter, :PostgreSQLAdapter) +if current_adapter?(:Mysql2Adapter, :PostgreSQLAdapter) class UpdateableViewTest < ActiveRecord::TestCase self.use_transactional_tests = false fixtures :books @@ -167,7 +168,7 @@ class UpdateableViewTest < ActiveRecord::TestCase end teardown do - @connection.execute "DROP VIEW printed_books" if @connection.table_exists? "printed_books" + @connection.execute "DROP VIEW printed_books" if @connection.view_exists? "printed_books" end def test_update_record @@ -195,7 +196,7 @@ class UpdateableViewTest < ActiveRecord::TestCase end end end -end # end fo `if current_adapter?(:MysqlAdapter, :Mysql2Adapter, :PostgreSQLAdapter)` +end # end fo `if current_adapter?(:Mysql2Adapter, :PostgreSQLAdapter)` end # end fo `if ActiveRecord::Base.connection.supports_views?` if ActiveRecord::Base.connection.respond_to?(:supports_materialized_views?) && @@ -209,8 +210,7 @@ class MaterializedViewTest < ActiveRecord::PostgreSQLTestCase end def drop_view(name) - @connection.execute "DROP MATERIALIZED VIEW #{name}" if @connection.table_exists? name - + @connection.execute "DROP MATERIALIZED VIEW #{name}" if @connection.view_exists? name end end end diff --git a/activerecord/test/config.example.yml b/activerecord/test/config.example.yml index e3b55d640e..58e2d45748 100644 --- a/activerecord/test/config.example.yml +++ b/activerecord/test/config.example.yml @@ -51,15 +51,6 @@ connections: password: arunit database: arunit2 - mysql: - arunit: - username: rails - encoding: utf8 - collation: utf8_unicode_ci - arunit2: - username: rails - encoding: utf8 - mysql2: arunit: username: rails diff --git a/activerecord/test/fixtures/author_addresses.yml b/activerecord/test/fixtures/author_addresses.yml index 7b90572187..cf75e5998d 100644 --- a/activerecord/test/fixtures/author_addresses.yml +++ b/activerecord/test/fixtures/author_addresses.yml @@ -3,3 +3,9 @@ david_address: david_address_extra: id: 2 + +mary_address: + id: 3 + +bob_address: + id: 4 diff --git a/activerecord/test/fixtures/authors.yml b/activerecord/test/fixtures/authors.yml index 832236a486..41c124179e 100644 --- a/activerecord/test/fixtures/authors.yml +++ b/activerecord/test/fixtures/authors.yml @@ -9,7 +9,9 @@ david: mary: id: 2 name: Mary + author_address_id: 3 bob: id: 3 name: Bob + author_address_id: 4 diff --git a/activerecord/test/fixtures/naked/yml/trees.yml b/activerecord/test/fixtures/naked/yml/trees.yml new file mode 100644 index 0000000000..d163b98f21 --- /dev/null +++ b/activerecord/test/fixtures/naked/yml/trees.yml @@ -0,0 +1,3 @@ +root: + :id: 1 + :name: The Root diff --git a/activerecord/test/fixtures/price_estimates.yml b/activerecord/test/fixtures/price_estimates.yml index 1149ab17a2..406d65a142 100644 --- a/activerecord/test/fixtures/price_estimates.yml +++ b/activerecord/test/fixtures/price_estimates.yml @@ -1,7 +1,16 @@ -saphire_1: +sapphire_1: price: 10 estimate_of: sapphire (Treasure) sapphire_2: price: 20 estimate_of: sapphire (Treasure) + +diamond: + price: 30 + estimate_of: diamond (Treasure) + +honda: + price: 40 + estimate_of_type: Car + estimate_of_id: 1 diff --git a/activerecord/test/migrations/10_urban/9_add_expressions.rb b/activerecord/test/migrations/10_urban/9_add_expressions.rb index 79a342e574..e908c9eabc 100644 --- a/activerecord/test/migrations/10_urban/9_add_expressions.rb +++ b/activerecord/test/migrations/10_urban/9_add_expressions.rb @@ -1,4 +1,4 @@ -class AddExpressions < ActiveRecord::Migration +class AddExpressions < ActiveRecord::Migration::Current def self.up create_table("expressions") do |t| t.column :expression, :string diff --git a/activerecord/test/migrations/decimal/1_give_me_big_numbers.rb b/activerecord/test/migrations/decimal/1_give_me_big_numbers.rb index 0aed7cbd84..549647de86 100644 --- a/activerecord/test/migrations/decimal/1_give_me_big_numbers.rb +++ b/activerecord/test/migrations/decimal/1_give_me_big_numbers.rb @@ -1,4 +1,4 @@ -class GiveMeBigNumbers < ActiveRecord::Migration +class GiveMeBigNumbers < ActiveRecord::Migration::Current def self.up create_table :big_numbers do |table| table.column :bank_balance, :decimal, :precision => 10, :scale => 2 diff --git a/activerecord/test/migrations/magic/1_currencies_have_symbols.rb b/activerecord/test/migrations/magic/1_currencies_have_symbols.rb index c066c068c2..53b263bf55 100644 --- a/activerecord/test/migrations/magic/1_currencies_have_symbols.rb +++ b/activerecord/test/migrations/magic/1_currencies_have_symbols.rb @@ -1,6 +1,6 @@ # coding: ISO-8859-15 -class CurrenciesHaveSymbols < ActiveRecord::Migration +class CurrenciesHaveSymbols < ActiveRecord::Migration::Current def self.up # We use ¤ for default currency symbol add_column "currencies", "symbol", :string, :default => "¤" diff --git a/activerecord/test/migrations/missing/1000_people_have_middle_names.rb b/activerecord/test/migrations/missing/1000_people_have_middle_names.rb index 4b83d61beb..e046944e31 100644 --- a/activerecord/test/migrations/missing/1000_people_have_middle_names.rb +++ b/activerecord/test/migrations/missing/1000_people_have_middle_names.rb @@ -1,4 +1,4 @@ -class PeopleHaveMiddleNames < ActiveRecord::Migration +class PeopleHaveMiddleNames < ActiveRecord::Migration::Current def self.up add_column "people", "middle_name", :string end diff --git a/activerecord/test/migrations/missing/1_people_have_last_names.rb b/activerecord/test/migrations/missing/1_people_have_last_names.rb index 68209f3ce9..50fe2a9c8e 100644 --- a/activerecord/test/migrations/missing/1_people_have_last_names.rb +++ b/activerecord/test/migrations/missing/1_people_have_last_names.rb @@ -1,4 +1,4 @@ -class PeopleHaveLastNames < ActiveRecord::Migration +class PeopleHaveLastNames < ActiveRecord::Migration::Current def self.up add_column "people", "last_name", :string end diff --git a/activerecord/test/migrations/missing/3_we_need_reminders.rb b/activerecord/test/migrations/missing/3_we_need_reminders.rb index 25bb49cb32..d7c63ac892 100644 --- a/activerecord/test/migrations/missing/3_we_need_reminders.rb +++ b/activerecord/test/migrations/missing/3_we_need_reminders.rb @@ -1,4 +1,4 @@ -class WeNeedReminders < ActiveRecord::Migration +class WeNeedReminders < ActiveRecord::Migration::Current def self.up create_table("reminders") do |t| t.column :content, :text diff --git a/activerecord/test/migrations/missing/4_innocent_jointable.rb b/activerecord/test/migrations/missing/4_innocent_jointable.rb index 002a1bf2a6..20fe183777 100644 --- a/activerecord/test/migrations/missing/4_innocent_jointable.rb +++ b/activerecord/test/migrations/missing/4_innocent_jointable.rb @@ -1,4 +1,4 @@ -class InnocentJointable < ActiveRecord::Migration +class InnocentJointable < ActiveRecord::Migration::Current def self.up create_table("people_reminders", :id => false) do |t| t.column :reminder_id, :integer diff --git a/activerecord/test/migrations/rename/1_we_need_things.rb b/activerecord/test/migrations/rename/1_we_need_things.rb index f5484ac54f..9dce01acfd 100644 --- a/activerecord/test/migrations/rename/1_we_need_things.rb +++ b/activerecord/test/migrations/rename/1_we_need_things.rb @@ -1,4 +1,4 @@ -class WeNeedThings < ActiveRecord::Migration +class WeNeedThings < ActiveRecord::Migration::Current def self.up create_table("things") do |t| t.column :content, :text diff --git a/activerecord/test/migrations/rename/2_rename_things.rb b/activerecord/test/migrations/rename/2_rename_things.rb index 533a113ea8..cb8484e7dc 100644 --- a/activerecord/test/migrations/rename/2_rename_things.rb +++ b/activerecord/test/migrations/rename/2_rename_things.rb @@ -1,4 +1,4 @@ -class RenameThings < ActiveRecord::Migration +class RenameThings < ActiveRecord::Migration::Current def self.up rename_table "things", "awesome_things" end diff --git a/activerecord/test/migrations/to_copy/1_people_have_hobbies.rb b/activerecord/test/migrations/to_copy/1_people_have_hobbies.rb index 639841f663..607113b091 100644 --- a/activerecord/test/migrations/to_copy/1_people_have_hobbies.rb +++ b/activerecord/test/migrations/to_copy/1_people_have_hobbies.rb @@ -1,4 +1,4 @@ -class PeopleHaveLastNames < ActiveRecord::Migration +class PeopleHaveLastNames < ActiveRecord::Migration::Current def self.up add_column "people", "hobbies", :text end diff --git a/activerecord/test/migrations/to_copy/2_people_have_descriptions.rb b/activerecord/test/migrations/to_copy/2_people_have_descriptions.rb index b3d0b30640..d4cbddab50 100644 --- a/activerecord/test/migrations/to_copy/2_people_have_descriptions.rb +++ b/activerecord/test/migrations/to_copy/2_people_have_descriptions.rb @@ -1,4 +1,4 @@ -class PeopleHaveLastNames < ActiveRecord::Migration +class PeopleHaveLastNames < ActiveRecord::Migration::Current def self.up add_column "people", "description", :text end diff --git a/activerecord/test/migrations/to_copy2/1_create_articles.rb b/activerecord/test/migrations/to_copy2/1_create_articles.rb index 0f048d90f7..2e9f5ec6bc 100644 --- a/activerecord/test/migrations/to_copy2/1_create_articles.rb +++ b/activerecord/test/migrations/to_copy2/1_create_articles.rb @@ -1,4 +1,4 @@ -class CreateArticles < ActiveRecord::Migration +class CreateArticles < ActiveRecord::Migration::Current def self.up end diff --git a/activerecord/test/migrations/to_copy2/2_create_comments.rb b/activerecord/test/migrations/to_copy2/2_create_comments.rb index 0f048d90f7..2e9f5ec6bc 100644 --- a/activerecord/test/migrations/to_copy2/2_create_comments.rb +++ b/activerecord/test/migrations/to_copy2/2_create_comments.rb @@ -1,4 +1,4 @@ -class CreateArticles < ActiveRecord::Migration +class CreateArticles < ActiveRecord::Migration::Current def self.up end diff --git a/activerecord/test/migrations/to_copy_with_name_collision/1_people_have_hobbies.rb b/activerecord/test/migrations/to_copy_with_name_collision/1_people_have_hobbies.rb index e438cf5999..8f81805fe1 100644 --- a/activerecord/test/migrations/to_copy_with_name_collision/1_people_have_hobbies.rb +++ b/activerecord/test/migrations/to_copy_with_name_collision/1_people_have_hobbies.rb @@ -1,4 +1,4 @@ -class PeopleHaveLastNames < ActiveRecord::Migration +class PeopleHaveLastNames < ActiveRecord::Migration::Current def self.up add_column "people", "hobbies", :string end diff --git a/activerecord/test/migrations/to_copy_with_timestamps/20090101010101_people_have_hobbies.rb b/activerecord/test/migrations/to_copy_with_timestamps/20090101010101_people_have_hobbies.rb index 639841f663..607113b091 100644 --- a/activerecord/test/migrations/to_copy_with_timestamps/20090101010101_people_have_hobbies.rb +++ b/activerecord/test/migrations/to_copy_with_timestamps/20090101010101_people_have_hobbies.rb @@ -1,4 +1,4 @@ -class PeopleHaveLastNames < ActiveRecord::Migration +class PeopleHaveLastNames < ActiveRecord::Migration::Current def self.up add_column "people", "hobbies", :text end diff --git a/activerecord/test/migrations/to_copy_with_timestamps/20090101010202_people_have_descriptions.rb b/activerecord/test/migrations/to_copy_with_timestamps/20090101010202_people_have_descriptions.rb index b3d0b30640..d4cbddab50 100644 --- a/activerecord/test/migrations/to_copy_with_timestamps/20090101010202_people_have_descriptions.rb +++ b/activerecord/test/migrations/to_copy_with_timestamps/20090101010202_people_have_descriptions.rb @@ -1,4 +1,4 @@ -class PeopleHaveLastNames < ActiveRecord::Migration +class PeopleHaveLastNames < ActiveRecord::Migration::Current def self.up add_column "people", "description", :text end diff --git a/activerecord/test/migrations/to_copy_with_timestamps2/20090101010101_create_articles.rb b/activerecord/test/migrations/to_copy_with_timestamps2/20090101010101_create_articles.rb index 0f048d90f7..2e9f5ec6bc 100644 --- a/activerecord/test/migrations/to_copy_with_timestamps2/20090101010101_create_articles.rb +++ b/activerecord/test/migrations/to_copy_with_timestamps2/20090101010101_create_articles.rb @@ -1,4 +1,4 @@ -class CreateArticles < ActiveRecord::Migration +class CreateArticles < ActiveRecord::Migration::Current def self.up end diff --git a/activerecord/test/migrations/to_copy_with_timestamps2/20090101010202_create_comments.rb b/activerecord/test/migrations/to_copy_with_timestamps2/20090101010202_create_comments.rb index 2b048edbb5..d361847d4b 100644 --- a/activerecord/test/migrations/to_copy_with_timestamps2/20090101010202_create_comments.rb +++ b/activerecord/test/migrations/to_copy_with_timestamps2/20090101010202_create_comments.rb @@ -1,4 +1,4 @@ -class CreateComments < ActiveRecord::Migration +class CreateComments < ActiveRecord::Migration::Current def self.up end diff --git a/activerecord/test/migrations/valid/1_valid_people_have_last_names.rb b/activerecord/test/migrations/valid/1_valid_people_have_last_names.rb index 06cb911117..c450211d8c 100644 --- a/activerecord/test/migrations/valid/1_valid_people_have_last_names.rb +++ b/activerecord/test/migrations/valid/1_valid_people_have_last_names.rb @@ -1,4 +1,4 @@ -class ValidPeopleHaveLastNames < ActiveRecord::Migration +class ValidPeopleHaveLastNames < ActiveRecord::Migration::Current def self.up add_column "people", "last_name", :string end diff --git a/activerecord/test/migrations/valid/2_we_need_reminders.rb b/activerecord/test/migrations/valid/2_we_need_reminders.rb index 25bb49cb32..d7c63ac892 100644 --- a/activerecord/test/migrations/valid/2_we_need_reminders.rb +++ b/activerecord/test/migrations/valid/2_we_need_reminders.rb @@ -1,4 +1,4 @@ -class WeNeedReminders < ActiveRecord::Migration +class WeNeedReminders < ActiveRecord::Migration::Current def self.up create_table("reminders") do |t| t.column :content, :text diff --git a/activerecord/test/migrations/valid/3_innocent_jointable.rb b/activerecord/test/migrations/valid/3_innocent_jointable.rb index 002a1bf2a6..20fe183777 100644 --- a/activerecord/test/migrations/valid/3_innocent_jointable.rb +++ b/activerecord/test/migrations/valid/3_innocent_jointable.rb @@ -1,4 +1,4 @@ -class InnocentJointable < ActiveRecord::Migration +class InnocentJointable < ActiveRecord::Migration::Current def self.up create_table("people_reminders", :id => false) do |t| t.column :reminder_id, :integer diff --git a/activerecord/test/migrations/valid_with_subdirectories/1_valid_people_have_last_names.rb b/activerecord/test/migrations/valid_with_subdirectories/1_valid_people_have_last_names.rb index 06cb911117..c450211d8c 100644 --- a/activerecord/test/migrations/valid_with_subdirectories/1_valid_people_have_last_names.rb +++ b/activerecord/test/migrations/valid_with_subdirectories/1_valid_people_have_last_names.rb @@ -1,4 +1,4 @@ -class ValidPeopleHaveLastNames < ActiveRecord::Migration +class ValidPeopleHaveLastNames < ActiveRecord::Migration::Current def self.up add_column "people", "last_name", :string end diff --git a/activerecord/test/migrations/valid_with_subdirectories/sub/2_we_need_reminders.rb b/activerecord/test/migrations/valid_with_subdirectories/sub/2_we_need_reminders.rb index 25bb49cb32..d7c63ac892 100644 --- a/activerecord/test/migrations/valid_with_subdirectories/sub/2_we_need_reminders.rb +++ b/activerecord/test/migrations/valid_with_subdirectories/sub/2_we_need_reminders.rb @@ -1,4 +1,4 @@ -class WeNeedReminders < ActiveRecord::Migration +class WeNeedReminders < ActiveRecord::Migration::Current def self.up create_table("reminders") do |t| t.column :content, :text diff --git a/activerecord/test/migrations/valid_with_subdirectories/sub1/3_innocent_jointable.rb b/activerecord/test/migrations/valid_with_subdirectories/sub1/3_innocent_jointable.rb index 002a1bf2a6..20fe183777 100644 --- a/activerecord/test/migrations/valid_with_subdirectories/sub1/3_innocent_jointable.rb +++ b/activerecord/test/migrations/valid_with_subdirectories/sub1/3_innocent_jointable.rb @@ -1,4 +1,4 @@ -class InnocentJointable < ActiveRecord::Migration +class InnocentJointable < ActiveRecord::Migration::Current def self.up create_table("people_reminders", :id => false) do |t| t.column :reminder_id, :integer diff --git a/activerecord/test/migrations/valid_with_timestamps/20100101010101_valid_with_timestamps_people_have_last_names.rb b/activerecord/test/migrations/valid_with_timestamps/20100101010101_valid_with_timestamps_people_have_last_names.rb index 1da99ceaba..9fd27593f0 100644 --- a/activerecord/test/migrations/valid_with_timestamps/20100101010101_valid_with_timestamps_people_have_last_names.rb +++ b/activerecord/test/migrations/valid_with_timestamps/20100101010101_valid_with_timestamps_people_have_last_names.rb @@ -1,4 +1,4 @@ -class ValidWithTimestampsPeopleHaveLastNames < ActiveRecord::Migration +class ValidWithTimestampsPeopleHaveLastNames < ActiveRecord::Migration::Current def self.up add_column "people", "last_name", :string end diff --git a/activerecord/test/migrations/valid_with_timestamps/20100201010101_valid_with_timestamps_we_need_reminders.rb b/activerecord/test/migrations/valid_with_timestamps/20100201010101_valid_with_timestamps_we_need_reminders.rb index cb6d735c8b..4a59921136 100644 --- a/activerecord/test/migrations/valid_with_timestamps/20100201010101_valid_with_timestamps_we_need_reminders.rb +++ b/activerecord/test/migrations/valid_with_timestamps/20100201010101_valid_with_timestamps_we_need_reminders.rb @@ -1,4 +1,4 @@ -class ValidWithTimestampsWeNeedReminders < ActiveRecord::Migration +class ValidWithTimestampsWeNeedReminders < ActiveRecord::Migration::Current def self.up create_table("reminders") do |t| t.column :content, :text diff --git a/activerecord/test/migrations/valid_with_timestamps/20100301010101_valid_with_timestamps_innocent_jointable.rb b/activerecord/test/migrations/valid_with_timestamps/20100301010101_valid_with_timestamps_innocent_jointable.rb index 4bd4b4714d..bf934576c9 100644 --- a/activerecord/test/migrations/valid_with_timestamps/20100301010101_valid_with_timestamps_innocent_jointable.rb +++ b/activerecord/test/migrations/valid_with_timestamps/20100301010101_valid_with_timestamps_innocent_jointable.rb @@ -1,4 +1,4 @@ -class ValidWithTimestampsInnocentJointable < ActiveRecord::Migration +class ValidWithTimestampsInnocentJointable < ActiveRecord::Migration::Current def self.up create_table("people_reminders", :id => false) do |t| t.column :reminder_id, :integer diff --git a/activerecord/test/migrations/version_check/20131219224947_migration_version_check.rb b/activerecord/test/migrations/version_check/20131219224947_migration_version_check.rb index 9d46485a31..6f314c881c 100644 --- a/activerecord/test/migrations/version_check/20131219224947_migration_version_check.rb +++ b/activerecord/test/migrations/version_check/20131219224947_migration_version_check.rb @@ -1,4 +1,4 @@ -class MigrationVersionCheck < ActiveRecord::Migration +class MigrationVersionCheck < ActiveRecord::Migration::Current def self.up raise "incorrect migration version" unless version == 20131219224947 end diff --git a/activerecord/test/models/author.rb b/activerecord/test/models/author.rb index 0d90cbb110..f25e31b13d 100644 --- a/activerecord/test/models/author.rb +++ b/activerecord/test/models/author.rb @@ -75,7 +75,7 @@ class Author < ActiveRecord::Base has_many :posts_with_multiple_callbacks, :class_name => "Post", :before_add => [:log_before_adding, Proc.new {|o, r| o.post_log << "before_adding_proc#{r.id || '<new>'}"}], :after_add => [:log_after_adding, Proc.new {|o, r| o.post_log << "after_adding_proc#{r.id || '<new>'}"}] - has_many :unchangable_posts, :class_name => "Post", :before_add => :raise_exception, :after_add => :log_after_adding + has_many :unchangeable_posts, :class_name => "Post", :before_add => :raise_exception, :after_add => :log_after_adding has_many :categorizations has_many :categories, :through => :categorizations diff --git a/activerecord/test/models/book.rb b/activerecord/test/models/book.rb index 1927191393..e43e5c3901 100644 --- a/activerecord/test/models/book.rb +++ b/activerecord/test/models/book.rb @@ -14,6 +14,7 @@ class Book < ActiveRecord::Base enum author_visibility: [:visible, :invisible], _prefix: true enum illustrator_visibility: [:visible, :invisible], _prefix: true enum font_size: [:small, :medium, :large], _prefix: :with, _suffix: true + enum cover: { hard: 'hard', soft: 'soft' } def published! super diff --git a/activerecord/test/models/bulb.rb b/activerecord/test/models/bulb.rb index a6e83fe353..dc0296305a 100644 --- a/activerecord/test/models/bulb.rb +++ b/activerecord/test/models/bulb.rb @@ -1,6 +1,7 @@ class Bulb < ActiveRecord::Base default_scope { where(:name => 'defaulty') } belongs_to :car, :touch => true + scope :awesome, -> { where(frickinawesome: true) } attr_reader :scope_after_initialize, :attributes_after_initialize @@ -49,3 +50,9 @@ class FailedBulb < Bulb throw(:abort) end end + +class TrickyBulb < Bulb + after_create do |record| + record.car.bulbs.to_a + end +end diff --git a/activerecord/test/models/car.rb b/activerecord/test/models/car.rb index 81263b79d1..0f37e9a289 100644 --- a/activerecord/test/models/car.rb +++ b/activerecord/test/models/car.rb @@ -4,6 +4,7 @@ class Car < ActiveRecord::Base has_many :funky_bulbs, class_name: 'FunkyBulb', dependent: :destroy has_many :failed_bulbs, class_name: 'FailedBulb', dependent: :destroy has_many :foo_bulbs, -> { where(:name => 'foo') }, :class_name => "Bulb" + has_many :awesome_bulbs, -> { awesome }, class_name: "Bulb" has_one :bulb @@ -11,6 +12,8 @@ class Car < ActiveRecord::Base has_many :engines, :dependent => :destroy, inverse_of: :my_car has_many :wheels, :as => :wheelable, :dependent => :destroy + has_many :price_estimates, :as => :estimate_of + scope :incl_tyres, -> { includes(:tyres) } scope :incl_engines, -> { includes(:engines) } diff --git a/activerecord/test/models/chef.rb b/activerecord/test/models/chef.rb index 698a52e045..9d3dd01016 100644 --- a/activerecord/test/models/chef.rb +++ b/activerecord/test/models/chef.rb @@ -2,3 +2,7 @@ class Chef < ActiveRecord::Base belongs_to :employable, polymorphic: true has_many :recipes end + +class ChefList < Chef + belongs_to :employable_list, polymorphic: true +end diff --git a/activerecord/test/models/comment.rb b/activerecord/test/models/comment.rb index b38b17e90e..dcc5c5a310 100644 --- a/activerecord/test/models/comment.rb +++ b/activerecord/test/models/comment.rb @@ -14,6 +14,7 @@ class Comment < ActiveRecord::Base has_many :ratings belongs_to :first_post, :foreign_key => :post_id + belongs_to :special_post_with_default_scope, foreign_key: :post_id has_many :children, :class_name => 'Comment', :foreign_key => :parent_id belongs_to :parent, :class_name => 'Comment', :counter_cache => :children_count diff --git a/activerecord/test/models/hotel.rb b/activerecord/test/models/hotel.rb index 491f8dfde3..9c90ffcff4 100644 --- a/activerecord/test/models/hotel.rb +++ b/activerecord/test/models/hotel.rb @@ -3,5 +3,9 @@ class Hotel < ActiveRecord::Base has_many :chefs, through: :departments has_many :cake_designers, source_type: 'CakeDesigner', source: :employable, through: :chefs has_many :drink_designers, source_type: 'DrinkDesigner', source: :employable, through: :chefs + + has_many :chef_lists, as: :employable_list + has_many :mocktail_designers, through: :chef_lists, source: :employable, :source_type => "MocktailDesigner" + has_many :recipes, through: :chefs end diff --git a/activerecord/test/models/mocktail_designer.rb b/activerecord/test/models/mocktail_designer.rb new file mode 100644 index 0000000000..77b44651a3 --- /dev/null +++ b/activerecord/test/models/mocktail_designer.rb @@ -0,0 +1,2 @@ +class MocktailDesigner < DrinkDesigner +end diff --git a/activerecord/test/models/notification.rb b/activerecord/test/models/notification.rb index b4b4b8f1b6..82edc64b68 100644 --- a/activerecord/test/models/notification.rb +++ b/activerecord/test/models/notification.rb @@ -1,2 +1,3 @@ class Notification < ActiveRecord::Base + validates_presence_of :message end diff --git a/activerecord/test/models/post.rb b/activerecord/test/models/post.rb index 23cebe2602..bf3079a1df 100644 --- a/activerecord/test/models/post.rb +++ b/activerecord/test/models/post.rb @@ -263,3 +263,10 @@ end class SerializedPost < ActiveRecord::Base serialize :title end + +class ConditionalStiPost < Post + default_scope { where(title: 'Untitled') } +end + +class SubConditionalStiPost < ConditionalStiPost +end diff --git a/activerecord/test/models/tag.rb b/activerecord/test/models/tag.rb index 80d4725f7e..b48b9a2155 100644 --- a/activerecord/test/models/tag.rb +++ b/activerecord/test/models/tag.rb @@ -5,3 +5,9 @@ class Tag < ActiveRecord::Base has_many :tagged_posts, :through => :taggings, :source => 'taggable', :source_type => 'Post' end + +class OrderedTag < Tag + self.table_name = "tags" + + has_many :taggings, -> { order('taggings.id DESC') }, foreign_key: 'tag_id' +end diff --git a/activerecord/test/models/uuid_item.rb b/activerecord/test/models/uuid_item.rb new file mode 100644 index 0000000000..2353e40213 --- /dev/null +++ b/activerecord/test/models/uuid_item.rb @@ -0,0 +1,6 @@ +class UuidItem < ActiveRecord::Base +end + +class UuidValidatingItem < UuidItem + validates_uniqueness_of :uuid +end diff --git a/activerecord/test/schema/mysql2_specific_schema.rb b/activerecord/test/schema/mysql2_specific_schema.rb index 92e0b197a7..101e657982 100644 --- a/activerecord/test/schema/mysql2_specific_schema.rb +++ b/activerecord/test/schema/mysql2_specific_schema.rb @@ -1,29 +1,35 @@ ActiveRecord::Schema.define do + + if ActiveRecord::Base.connection.version >= '5.6.0' + create_table :datetime_defaults, force: true do |t| + t.datetime :modified_datetime, default: -> { 'CURRENT_TIMESTAMP' } + end + end + create_table :binary_fields, force: true do |t| t.binary :var_binary, limit: 255 t.binary :var_binary_large, limit: 4095 - t.blob :tiny_blob, limit: 255 - t.binary :normal_blob, limit: 65535 - t.binary :medium_blob, limit: 16777215 - t.binary :long_blob, limit: 2147483647 - t.text :tiny_text, limit: 255 - t.text :normal_text, limit: 65535 - t.text :medium_text, limit: 16777215 - t.text :long_text, limit: 2147483647 - end + t.tinyblob :tiny_blob + t.blob :normal_blob + t.mediumblob :medium_blob + t.longblob :long_blob + t.tinytext :tiny_text + t.text :normal_text + t.mediumtext :medium_text + t.longtext :long_text - add_index :binary_fields, :var_binary + t.index :var_binary + end - create_table :key_tests, force: true, :options => 'ENGINE=MyISAM' do |t| + create_table :key_tests, force: true, options: 'ENGINE=MyISAM' do |t| t.string :awesome t.string :pizza t.string :snacks + t.index :awesome, type: :fulltext, name: 'index_key_tests_on_awesome' + t.index :pizza, using: :btree, name: 'index_key_tests_on_pizza' + t.index :snacks, name: 'index_key_tests_on_snack' end - add_index :key_tests, :awesome, :type => :fulltext, :name => 'index_key_tests_on_awesome' - add_index :key_tests, :pizza, :using => :btree, :name => 'index_key_tests_on_pizza' - add_index :key_tests, :snacks, :name => 'index_key_tests_on_snack' - create_table :collation_tests, id: false, force: true do |t| t.string :string_cs_column, limit: 1, collation: 'utf8_bin' t.string :string_ci_column, limit: 1, collation: 'utf8_general_ci' @@ -55,7 +61,7 @@ SQL ActiveRecord::Base.connection.execute <<-SQL CREATE TABLE enum_tests ( - enum_column ENUM('text','blob','tiny','medium','long') + enum_column ENUM('text','blob','tiny','medium','long','unsigned','bigint') ) SQL end diff --git a/activerecord/test/schema/mysql_specific_schema.rb b/activerecord/test/schema/mysql_specific_schema.rb deleted file mode 100644 index 553cb56103..0000000000 --- a/activerecord/test/schema/mysql_specific_schema.rb +++ /dev/null @@ -1,62 +0,0 @@ -ActiveRecord::Schema.define do - create_table :binary_fields, force: true do |t| - t.binary :var_binary, limit: 255 - t.binary :var_binary_large, limit: 4095 - t.blob :tiny_blob, limit: 255 - t.binary :normal_blob, limit: 65535 - t.binary :medium_blob, limit: 16777215 - t.binary :long_blob, limit: 2147483647 - t.text :tiny_text, limit: 255 - t.text :normal_text, limit: 65535 - t.text :medium_text, limit: 16777215 - t.text :long_text, limit: 2147483647 - end - - add_index :binary_fields, :var_binary - - create_table :key_tests, force: true, :options => 'ENGINE=MyISAM' do |t| - t.string :awesome - t.string :pizza - t.string :snacks - end - - add_index :key_tests, :awesome, :type => :fulltext, :name => 'index_key_tests_on_awesome' - add_index :key_tests, :pizza, :using => :btree, :name => 'index_key_tests_on_pizza' - add_index :key_tests, :snacks, :name => 'index_key_tests_on_snack' - - create_table :collation_tests, id: false, force: true do |t| - t.string :string_cs_column, limit: 1, collation: 'utf8_bin' - t.string :string_ci_column, limit: 1, collation: 'utf8_general_ci' - end - - ActiveRecord::Base.connection.execute <<-SQL -DROP PROCEDURE IF EXISTS ten; -SQL - - ActiveRecord::Base.connection.execute <<-SQL -CREATE PROCEDURE ten() SQL SECURITY INVOKER -BEGIN - select 10; -END -SQL - - ActiveRecord::Base.connection.execute <<-SQL -DROP PROCEDURE IF EXISTS topics; -SQL - - ActiveRecord::Base.connection.execute <<-SQL -CREATE PROCEDURE topics(IN num INT) SQL SECURITY INVOKER -BEGIN - select * from topics limit num; -END -SQL - - ActiveRecord::Base.connection.drop_table "enum_tests", if_exists: true - - ActiveRecord::Base.connection.execute <<-SQL -CREATE TABLE enum_tests ( - enum_column ENUM('text','blob','tiny','medium','long') -) -SQL - -end diff --git a/activerecord/test/schema/postgresql_specific_schema.rb b/activerecord/test/schema/postgresql_specific_schema.rb index df0362573b..24713f722a 100644 --- a/activerecord/test/schema/postgresql_specific_schema.rb +++ b/activerecord/test/schema/postgresql_specific_schema.rb @@ -11,7 +11,23 @@ ActiveRecord::Schema.define do t.uuid :uuid_parent_id end - %w(postgresql_times postgresql_oids defaults postgresql_timestamp_with_zones + create_table :defaults, force: true do |t| + t.date :modified_date, default: -> { 'CURRENT_DATE' } + t.date :modified_date_function, default: -> { 'now()' } + t.date :fixed_date, default: '2004-01-01' + t.datetime :modified_time, default: -> { 'CURRENT_TIMESTAMP' } + t.datetime :modified_time_function, default: -> { 'now()' } + t.datetime :fixed_time, default: '2004-01-01 00:00:00.000000-00' + t.column :char1, 'char(1)', default: 'Y' + t.string :char2, limit: 50, default: 'a varchar field' + t.text :char3, default: 'a text field' + t.bigint :bigint_default, default: -> { '0::bigint' } + t.text :multiline_default, default: '--- [] + +' + end + + %w(postgresql_times postgresql_oids postgresql_timestamp_with_zones postgresql_partitioned_table postgresql_partitioned_table_parent).each do |table_name| drop_table table_name, if_exists: true end @@ -28,25 +44,6 @@ ActiveRecord::Schema.define do end execute <<_SQL - CREATE TABLE defaults ( - id serial primary key, - modified_date date default CURRENT_DATE, - modified_date_function date default now(), - fixed_date date default '2004-01-01', - modified_time timestamp default CURRENT_TIMESTAMP, - modified_time_function timestamp default now(), - fixed_time timestamp default '2004-01-01 00:00:00.000000-00', - char1 char(1) default 'Y', - char2 character varying(50) default 'a varchar field', - char3 text default 'a text field', - bigint_default bigint default 0::bigint, - multiline_default text DEFAULT '--- [] - -'::text -); -_SQL - - execute <<_SQL CREATE TABLE postgresql_times ( id SERIAL PRIMARY KEY, time_interval INTERVAL, @@ -109,4 +106,9 @@ _SQL t.integer :big_int_data_points, limit: 8, array: true t.decimal :decimal_array_default, array: true, default: [1.23, 3.45] end + + create_table :uuid_items, force: true, id: false do |t| + t.uuid :uuid, primary_key: true + t.string :title + end end diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb index 66a1f5aa8a..2a8996f35c 100644 --- a/activerecord/test/schema/schema.rb +++ b/activerecord/test/schema/schema.rb @@ -104,6 +104,7 @@ ActiveRecord::Schema.define do t.column :author_visibility, :integer, default: 0 t.column :illustrator_visibility, :integer, default: 0 t.column :font_size, :integer, default: 0 + t.column :cover, :string, default: 'hard' end create_table :booleans, force: true do |t| @@ -114,7 +115,7 @@ ActiveRecord::Schema.define do create_table :bulbs, force: true do |t| t.integer :car_id t.string :name - t.boolean :frickinawesome + t.boolean :frickinawesome, default: false t.string :color end @@ -201,12 +202,11 @@ ActiveRecord::Schema.define do t.integer :rating, default: 1 t.integer :account_id t.string :description, default: "" + t.index [:firm_id, :type, :rating], name: "company_index" + t.index [:firm_id, :type], name: "company_partial_index", where: "rating > 10" + t.index :name, name: 'company_name_index', using: :btree end - add_index :companies, [:firm_id, :type, :rating], name: "company_index" - add_index :companies, [:firm_id, :type], name: "company_partial_index", where: "rating > 10" - add_index :companies, :name, name: 'company_name_index', using: :btree - create_table :content, force: true do |t| t.string :title end @@ -303,8 +303,8 @@ ActiveRecord::Schema.define do create_table :edges, force: true, id: false do |t| t.column :source_id, :integer, null: false t.column :sink_id, :integer, null: false + t.index [:source_id, :sink_id], unique: true, name: 'unique_edge_index' end - add_index :edges, [:source_id, :sink_id], unique: true, name: 'unique_edge_index' create_table :engines, force: true do |t| t.integer :car_id @@ -356,7 +356,7 @@ ActiveRecord::Schema.define do t.column :key, :string end - create_table :guitar, force: true do |t| + create_table :guitars, force: true do |t| t.string :color end @@ -781,8 +781,8 @@ ActiveRecord::Schema.define do t.string :nick, null: false t.string :name t.column :books_count, :integer, null: false, default: 0 + t.index :nick, unique: true end - add_index :subscribers, :nick, unique: true create_table :subscriptions, force: true do |t| t.string :subscriber_id @@ -929,7 +929,7 @@ ActiveRecord::Schema.define do t.string :treaty_id t.string :name end - create_table :countries_treaties, force: true, id: false do |t| + create_table :countries_treaties, force: true, primary_key: [:country_id, :treaty_id] do |t| t.string :country_id, null: false t.string :treaty_id, null: false end @@ -975,6 +975,8 @@ ActiveRecord::Schema.define do t.integer :employable_id t.string :employable_type t.integer :department_id + t.string :employable_list_type + t.integer :employable_list_id end create_table :recipes, force: true do |t| t.integer :chef_id diff --git a/activerecord/test/support/schema_dumping_helper.rb b/activerecord/test/support/schema_dumping_helper.rb index 2d1651454d..666c1b6a14 100644 --- a/activerecord/test/support/schema_dumping_helper.rb +++ b/activerecord/test/support/schema_dumping_helper.rb @@ -1,7 +1,7 @@ module SchemaDumpingHelper def dump_table_schema(table, connection = ActiveRecord::Base.connection) old_ignore_tables = ActiveRecord::SchemaDumper.ignore_tables - ActiveRecord::SchemaDumper.ignore_tables = connection.tables - [table] + ActiveRecord::SchemaDumper.ignore_tables = connection.data_sources - [table] stream = StringIO.new ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, stream) stream.string |