diff options
Diffstat (limited to 'activerecord')
386 files changed, 9904 insertions, 4817 deletions
diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index 263164a8e9..d213d6afcf 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -2,771 +2,170 @@ *Andrew McCloud* -* Deprecate `serialized_attributes` without replacement. You can access its - behavior by going through the column's type object. +* Change `ActiveRecord::Relation#update` behavior so that it can + be called without passing ids of the records to be updated. - *Sean Griffin* - -* Correctly extract IPv6 addresses from `DATABASE_URI`: the square brackets - are part of the URI structure, not the actual host. - - Fixes #15705. - - *Andy Bakun*, *Aaron Stone* - -* Ensure both parent IDs are set on join records when both sides of a - through association are new. + This change allows to update multiple records returned by + `ActiveRecord::Relation` with callbacks and validations. - *Sean Griffin* - -* `ActiveRecord::Dirty` now detects in-place changes to mutable values. - Serialized attributes on ActiveRecord models will no longer save when - unchanged. Fixes #8328. + # Before + # ArgumentError: wrong number of arguments (1 for 2) + Comment.where(group: 'expert').update(body: "Group of Rails Experts") - Sean Griffin + # After + # Comments with group expert updated with body "Group of Rails Experts" + Comment.where(group: 'expert').update(body: "Group of Rails Experts") -* Fixed automatic maintaining test schema to properly handle sql structure - schema format. + *Prathamesh Sonpatki* - Fixes #15394. +* Fix `reaping_frequency` option when the value is a string. - *Wojciech Wnętrzak* + This usually happens when it is configured using `DATABASE_URL`. -* Pluck now works when selecting columns from different tables with the same - name. + *korbin* - Fixes #15649 +* Fix error message when trying to create an associated record and the foreign + key is missing. - *Sean Griffin* + Before this fix the following exception was being raised: -* Remove `cache_attributes` and friends. All attributes are cached. - - *Sean Griffin* + NoMethodError: undefined method `val' for #<Arel::Nodes::BindParam:0x007fc64d19c218> -* Remove deprecated method `ActiveRecord::Base.quoted_locking_column`. + Now the message is: - *Akshay Vishnoi* + ActiveRecord::UnknownAttributeError: unknown attribute 'foreign_key' for Model. -* `ActiveRecord::FinderMethods.find` with block can handle proc parameter as - `Enumerable#find` does. + *Rafael Mendonça França* - Fixes #15382. +* When a table has a composite primary key, the `primary_key` method for + SQLite3 and PostgreSQL adapters was only returning the first field of the key. + Ensures that it will return nil instead, as Active Record doesn't support + composite primary keys. - *James Yang* + Fixes #18070. -* Make timezone aware attributes work with PostgreSQL array columns. - - Fixes #13402. - - *Kuldeep Aggarwal*, *Sean Griffin* - -* `ActiveRecord::SchemaMigration` has no primary key regardless of the - `primary_key_prefix_type` configuration. - - Fixes #15051. - - *JoseLuis Torres*, *Yves Senn* + *arthurnn* -* `rake db:migrate:status` works with legacy migration numbers like `00018_xyz.rb`. +* `validates_size_of` / `validates_length_of` do not count records, + which are `marked_for_destruction?`. - Fixes #15538. + Fixes #7247. *Yves Senn* -* Baseclass becomes! subclass. - - Before this change, a record which changed its STI type, could not be - updated. - - Fixes #14785. - - *Matthew Draper*, *Earl St Sauver*, *Edo Balvers* +* Ensure `first!` and friends work on loaded associations. -* Remove deprecated `ActiveRecord::Migrator.proper_table_name`. Use the - `proper_table_name` instance method on `ActiveRecord::Migration` instead. - - *Akshay Vishnoi* - -* Fix regression on eager loading association based on SQL query rather than - existing column. - - Fixes #15480. - - *Lauro Caetano*, *Carlos Antonio da Silva* - -* Return a null column from `column_for_attribute` when no column exists. - - *Sean Griffin* - -* Implemented ActiveRecord::Base#pretty_print to work with PP. - - *Ethan* - -* Preserve type when dumping PostgreSQL point, bit, bit varying and money - columns. - - *Yves Senn* - -* New records remain new after YAML serialization. + Fixes #18237. *Sean Griffin* -* PostgreSQL support default values for enum types. Fixes #7814. - - *Yves Senn* - -* PostgreSQL `default_sequence_name` respects schema. Fixes #7516. - - *Yves Senn* - -* Fixed `columns_for_distinct` of postgresql adapter to work correctly - with orders without sort direction modifiers. - - *Nikolay Kondratyev* - -* PostgreSQL `reset_pk_sequence!` respects schemas. Fixes #14719. - - *Yves Senn* - -* Keep PostgreSQL `hstore` and `json` attributes as `Hash` in `@attributes`. - Fixes duplication in combination with `store_accessor`. - - Fixes #15369. - - *Yves Senn* - -* `rake railties:install:migrations` respects the order of railties. - - *Arun Agrawal* - -* Fix redefine a has_and_belongs_to_many inside inherited class - Fixing regression case, where redefining the same has_an_belongs_to_many - definition into a subclass would raise. - - Fixes #14983. - - *arthurnn* - -* Fix has_and_belongs_to_many public reflection. - When defining a has_and_belongs_to_many, internally we convert that to two has_many. - But as `reflections` is a public API, people expect to see the right macro. - - Fixes #14682. - - *arthurnn* +* `eager_load` preserves readonly flag for associations. -* Fixed serialization for records with an attribute named `format`. + Closes #15853. - Fixes #15188. + *Takashi Kokubun* - *Godfrey Chan* +* Provide `:touch` option to `save()` to accommodate saving without updating + timestamps. -* When a `group` is set, `sum`, `size`, `average`, `minimum` and `maximum` - on a NullRelation should return a Hash. + Fixes #18202. - *Kuldeep Aggarwal* + *Dan Olson* -* Fixed serialized fields returning serialized data after being updated with - `update_column`. +* Provide a more helpful error message when an unsupported class is passed to + `serialize`. - *Simon Hørup Eskildsen* - -* Fixed polymorphic eager loading when using a String as foreign key. - - Fixes #14734. - - *Lauro Caetano* - -* Change belongs_to touch to be consistent with timestamp updates - - If a model is set up with a belongs_to: touch relationship the parent - record will only be touched if the record was modified. This makes it - consistent with timestamp updating on the record itself. - - *Brock Trappitt* - -* Fixed the inferred table name of a has_and_belongs_to_many auxiliar - table inside a schema. - - Fixes #14824 - - *Eric Chahin* - -* Remove unused `:timestamp` type. Transparently alias it to `:datetime` - in all cases. Fixes inconsistencies when column types are sent outside of - `ActiveRecord`, such as for XML Serialization. + Fixes #18224. *Sean Griffin* -* Fix bug that added `table_name_prefix` and `table_name_suffix` to - extension names in PostgreSQL when migrating. - - *Joao Carlos* - -* The `:index` option in migrations, which previously was only available for - `references`, now works with any column types. - - *Marc Schütz* - -* Add support for counter name to be passed as parameter on `CounterCache::ClassMethods#reset_counters`. - - *jnormore* - -* Restrict deletion of record when using `delete_all` with `uniq`, `group`, `having` - or `offset`. - - In these cases the generated query ignored them and that caused unintended - records to be deleted. - - Fixes #11985. - - *Leandro Facchinetti* - -* Floats with limit >= 25 that get turned into doubles in MySQL no longer have - their limit dropped from the schema. - - Fixes #14135. - - *Aaron Nelson* - -* Fix how to calculate associated class name when using namespaced has_and_belongs_to_many - association. - - Fixes #14709. - - *Kassio Borges* - -* `ActiveRecord::Relation::Merger#filter_binds` now compares equivalent symbols and - strings in column names as equal. - - This fixes a rare case in which more bind values are passed than there are - placeholders for them in the generated SQL statement, which can make PostgreSQL - throw a `StatementInvalid` exception. - - *Nat Budin* - -* Fix `stored_attributes` to correctly merge the details of stored - attributes defined in parent classes. - - Fixes #14672. - - *Brad Bennett*, *Jessica Yao*, *Lakshmi Parthasarathy* - -* `change_column_default` allows `[]` as argument to `change_column_default`. - - Fixes #11586. - - *Yves Senn* - -* Handle `name` and `"char"` column types in the PostgreSQL adapter. - - `name` and `"char"` are special character types used internally by - PostgreSQL and are used by internal system catalogs. These field types - can sometimes show up in structure-sniffing queries that feature internal system - structures or with certain PostgreSQL extensions. - - *J Smith*, *Yves Senn* - -* Fix `PostgreSQLAdapter::OID::Float#type_cast` to convert Infinity and - NaN PostgreSQL values into a native Ruby `Float::INFINITY` and `Float::NAN` - - Before: - - Point.create(value: 1.0/0) - Point.last.value # => 0.0 - - After: - - Point.create(value: 1.0/0) - Point.last.value # => Infinity - - *Innokenty Mikhailov* - -* Allow the PostgreSQL adapter to handle bigserial primary key types again. - - Fixes #10410. - - *Patrick Robertson* - -* Deprecate joining, eager loading and preloading of instance dependent - associations without replacement. These operations happen before instances - are created. The current behavior is unexpected and can result in broken - behavior. - - Fixes #15024. - - *Yves Senn* - -* Fixed has_and_belongs_to_many's CollectionAssociation size calculation. - - has_and_belongs_to_many should fall back to using the normal CollectionAssociation's - size calculation if the collection is not cached or loaded. - - Fixes #14913, #14914. - - *Fred Wu* - -* Return a non zero status when running `rake db:migrate:status` and migration table does - not exist. - - *Paul B.* - -* Add support for module-level `table_name_suffix` in models. - - This makes `table_name_suffix` work the same way as `table_name_prefix` when - using namespaced models. - - *Jenner LaFave* - -* Revert the behaviour of `ActiveRecord::Relation#join` changed through 4.0 => 4.1 to 4.0. - - In 4.1.0 `Relation#join` is delegated to `Arel#SelectManager`. - In 4.0 series it is delegated to `Array#join`. - - *Bogdan Gusiev* - -* Log nil binary column values correctly. - - When an object with a binary column is updated with a nil value - in that column, the SQL logger would throw an exception when trying - to log that nil value. This only occurs when updating a record - that already has a non-nil value in that column since an initial nil - value isn't included in the SQL anyway (at least, when dirty checking - is enabled.) The column's new value will now be logged as `<NULL binary data>` - to parallel the existing `<N bytes of binary data>` for non-nil values. - - *James Coleman* - -* Rails will now pass a custom validation context through to autosave associations - in order to validate child associations with the same context. - - Fixes #13854. - - *Eric Chahin*, *Aaron Nelson*, *Kevin Casey* - -* Stringify all variables keys of MySQL connection configuration. - - When `sql_mode` variable for MySQL adapters set in configuration as `String` - was ignored and overwritten by strict mode option. - - Fixes #14895. - - *Paul Nikitochkin* - -* Ensure SQLite3 statements are closed on errors. - - Fixes #13631. - - *Timur Alperovich* - -* Give ActiveRecord::PredicateBuilder private methods the privacy they deserve. - - *Hector Satre* - -* When using a custom `join_table` name on a `habtm`, rails was not saving it - on Reflections. This causes a problem when rails loads fixtures, because it - uses the reflections to set database with fixtures. - - Fixes #14845. - - *Kassio Borges* - -* Reset the cache when modifying a Relation with cached Arel. - Additionally display a warning message to make the user aware. - - *Yves Senn* - -* PostgreSQL should internally use `:datetime` consistently for TimeStamp. Assures - different spellings of timestamps are treated the same. +* Add bigint primary key support for MySQL. Example: - mytimestamp.simplified_type('timestamp without time zone') - # => :datetime - mytimestamp.simplified_type('timestamp(6) without time zone') - # => also :datetime (previously would be :timestamp) - - See #14513. - - *Jefferson Lai* - -* `ActiveRecord::Base.no_touching` no longer triggers callbacks or start empty transactions. - - Fixes #14841. - - *Lucas Mazza* - -* Fix name collision with `Array#select!` with `Relation#select!`. - - Fixes #14752. - - *Earl St Sauver* - -* Fixed unexpected behavior for `has_many :through` associations going through a scoped `has_many`. - - If a `has_many` association is adjusted using a scope, and another `has_many :through` - uses this association, then the scope adjustment is unexpectedly neglected. - - Fixes #14537. - - *Jan Habermann* - -* `@destroyed` should always be set to `false` when an object is duped. - - *Kuldeep Aggarwal* - -* Fixed has_many association to make it support irregular inflections. - - Fixes #8928. - - *arthurnn*, *Javier Goizueta* - -* Fixed a problem where count used with a grouping was not returning a Hash. - - Fixes #14721. - - *Eric Chahin* - -* `sanitize_sql_like` helper method to escape a string for safe use in a SQL - LIKE statement. - - Example: - - class Article - def self.search(term) - where("title LIKE ?", sanitize_sql_like(term)) - end + create_table :foos, id: :bigint do |t| end - Article.search("20% _reduction_") - # => Query looks like "... title LIKE '20\% \_reduction\_' ..." - - *Rob Gilson*, *Yves Senn* - -* Do not quote uuid default value on `change_column`. - - Fixes #14604. - - *Eric Chahin* - -* The comparison between `Relation` and `CollectionProxy` should be consistent. - - Example: - - author.posts == Post.where(author_id: author.id) - # => true - Post.where(author_id: author.id) == author.posts - # => true - - Fixes #13506. - - *Lauro Caetano* - -* Calling `delete_all` on an unloaded `CollectionProxy` no longer - generates a SQL statement containing each id of the collection: - - Before: - - DELETE FROM `model` WHERE `model`.`parent_id` = 1 - AND `model`.`id` IN (1, 2, 3...) - - After: - - DELETE FROM `model` WHERE `model`.`parent_id` = 1 - - *Eileen M. Uchitelle*, *Aaron Patterson* - -* Fixed error for aggregate methods (`empty?`, `any?`, `count`) with `select` - which created invalid SQL. - - Fixes #13648. - - *Simon Woker* - -* PostgreSQL adapter only warns once for every missing OID per connection. - - Fixes #14275. - - *Matthew Draper*, *Yves Senn* - -* PostgreSQL adapter automatically reloads it's type map when encountering - unknown OIDs. - - Fixes #14678. - - *Matthew Draper*, *Yves Senn* - -* Fix insertion of records via `has_many :through` association with scope. - - Fixes #3548. - - *Ivan Antropov* - -* Auto-generate stable fixture UUIDs on PostgreSQL. - - Fixes #11524. - - *Roderick van Domburg* - -* Fixed a problem where an enum would overwrite values of another enum - with the same name in an unrelated class. - - Fixes #14607. - - *Evan Whalen* - -* PostgreSQL and SQLite string columns no longer have a default limit of 255. - - Fixes #13435, #9153. - - *Vladimir Sazhin*, *Toms Mikoss*, *Yves Senn* - -* Make possible to have an association called `records`. - - Fixes #11645. - - *prathamesh-sonpatki* - -* `to_sql` on an association now matches the query that is actually executed, where it - could previously have incorrectly accrued additional conditions (e.g. as a result of - a previous query). CollectionProxy now always defers to the association scope's - `arel` method so the (incorrect) inherited one should be entirely concealed. - - Fixes #14003. - - *Jefferson Lai* - -* Block a few default Class methods as scope name. - - For instance, this will raise: - - scope :public, -> { where(status: 1) } - - *arthurnn* - -* Fixed error when using `with_options` with lambda. - - Fixes #9805. - - *Lauro Caetano* - -* Switch `sqlite3:///` URLs (which were temporarily - deprecated in 4.1) from relative to absolute. - - If you still want the previous interpretation, you should replace - `sqlite3:///my/path` with `sqlite3:my/path`. - - *Matthew Draper* + *Ryuta Kamizono* -* Treat blank UUID values as `nil`. +* Support for any type primary key. - Example: + Fixes #14194. - Sample.new(uuid_field: '') #=> <Sample id: nil, uuid_field: nil> + *Ryuta Kamizono* - *Dmitry Lavrov* +* Dump the default `nil` for PostgreSQL UUID primary key. -* Enable support for materialized views on PostgreSQL >= 9.3. + *Ryuta Kamizono* - *Dave Lee* +* Add a `:foreign_key` option to `references` and associated migration + methods. The model and migration generators now use this option, rather than + the `add_foreign_key` form. -* The PostgreSQL adapter supports custom domains. Fixes #14305. + *Sean Griffin* - *Yves Senn* +* Don't raise when writing an attribute with an out-of-range datetime passed + by the user. -* PostgreSQL `Column#type` is now determined through the corresponding OID. - The column types stay the same except for enum columns. They no longer have - `nil` as type but `enum`. + *Grey Baker* - See #7814. +* Replace deprecated `ActiveRecord::Tasks::DatabaseTasks#load_schema` with + `ActiveRecord::Tasks::DatabaseTasks#load_schema_for`. *Yves Senn* -* Fixed error when specifying a non-empty default value on a PostgreSQL array column. - - Fixes #10613. - - *Luke Steensen* - -* Fixed error where .persisted? throws SystemStackError for an unsaved model with a - custom primary key that didn't save due to validation error. +* Fixes bug with 'ActiveRecord::Type::Numeric' that causes negative values to + be marked as having changed when set to the same negative value. - Fixes #14393. + Closes #18161. - *Chris Finne* + *Daniel Fox* -* Introduce `validate` as an alias for `valid?`. - - This is more intuitive when you want to run validations but don't care about the return value. - - *Henrik Nyh* - -* Create indexes inline in CREATE TABLE for MySQL. - - This is important, because adding an index on a temporary table after it has been created - would commit the transaction. - - It also allows creating and dropping indexed tables with fewer queries and fewer permissions - required. - - Example: - - create_table :temp, temporary: true, as: "SELECT id, name, zip FROM a_really_complicated_query" do |t| - t.index :zip - end - # => CREATE TEMPORARY TABLE temp (INDEX (zip)) AS SELECT id, name, zip FROM a_really_complicated_query +* Introduce `force: :cascade` option for `create_table`. Using this option + will recreate tables even if they have dependent objects (like foreign keys). + `db/schema.rb` now uses `force: :cascade`. This makes it possible to + reload the schema when foreign keys are in place. - *Cody Cutrer*, *Steve Rice*, *Rafael Mendonça Franca* - -* Use singular table name in generated migrations when - `ActiveRecord::Base.pluralize_table_names` is `false`. - - Fixes #13426. - - *Kuldeep Aggarwal* - -* `touch` accepts many attributes to be touched at once. - - Example: - - # touches :signed_at, :sealed_at, and :updated_at/on attributes. - Photo.last.touch(:signed_at, :sealed_at) - - *James Pinto* - -* `rake db:structure:dump` only dumps schema information if the schema - migration table exists. - - Fixes #14217. - - *Yves Senn* - -* Reap connections that were checked out by now-dead threads, instead - of waiting until they disconnect by themselves. Before this change, - a suitably constructed series of short-lived threads could starve - the connection pool, without ever having more than a couple alive at - the same time. - - *Matthew Draper* - -* `pk_and_sequence_for` now ensures that only the pg_depend entries - pointing to pg_class, and thus only sequence objects, are considered. - - *Josh Williams* + *Matthew Draper*, *Yves Senn* -* `where.not` adds `references` for `includes` like normal `where` calls do. +* `db:schema:load` and `db:structure:load` no longer purge the database + before loading the schema. This is left for the user to do. + `db:test:prepare` will still purge the database. - Fixes #14406. + Closes #17945. *Yves Senn* -* Extend fixture `$LABEL` replacement to allow string interpolation. - - Example: - - martin: - email: $LABEL@email.com - - users(:martin).email # => martin@email.com - - *Eric Steele* - -* Add support for `Relation` be passed as parameter on `QueryCache#select_all`. - - Fixes #14361. - - *arthurnn* - -* Passing an Active Record object to `find` or `exists?` is now deprecated. - Call `.id` on the object first. - - *Aaron Patterson* - -* Only use BINARY for MySQL case sensitive uniqueness check when column has a case insensitive collation. +* Fix undesirable RangeError by `Type::Integer`. Add `Type::UnsignedInteger`. *Ryuta Kamizono* -* Support for MySQL 5.6 fractional seconds. - - *arthurnn*, *Tatsuhiko Miyagawa* - -* Support for Postgres `citext` data type enabling case-insensitive where - values without needing to wrap in UPPER/LOWER sql functions. - - *Troy Kruthoff*, *Lachlan Sylvester* - -* Only save has_one associations if record has changes. - Previously after save related callbacks, such as `#after_commit`, were triggered when the has_one - object did not get saved to the db. - - *Alan Kennedy* - -* Allow strings to specify the `#order` value. - - Example: - - Model.order(id: 'asc').to_sql == Model.order(id: :asc).to_sql - - *Marcelo Casiraghi*, *Robin Dupret* - -* Dynamically register PostgreSQL enum OIDs. This prevents "unknown OID" - warnings on enum columns. - - *Dieter Komendera* - -* `includes` is able to detect the right preloading strategy when string - joins are involved. - - Fixes #14109. - - *Aaron Patterson*, *Yves Senn* - -* Fixed error with validation with enum fields for records where the - value for any enum attribute is always evaluated as 0 during - uniqueness validation. +* Add `foreign_type` option to `has_one` and `has_many` association macros. - Fixes #14172. + This option enables to define the column name of associated object's type for polymorphic associations. - *Vilius Luneckas* *Ahmed AbouElhamayed* + *Ulisses Almeida, Kassio Borges* -* `before_add` callbacks are fired before the record is saved on - `has_and_belongs_to_many` assocations *and* on `has_many :through` - associations. Before this change, `before_add` callbacks would be fired - before the record was saved on `has_and_belongs_to_many` associations, but - *not* on `has_many :through` associations. +* Remove deprecated behavior allowing nested arrays to be passed as query + values. - Fixes #14144. + *Melanie Gilman* -* Fixed STI classes not defining an attribute method if there is a - conflicting private method defined on its ancestors. +* Deprecate passing a class as a value in a query. Users should pass strings + instead. - Fixes #11569. + *Melanie Gilman* - *Godfrey Chan* +* `add_timestamps` and `remove_timestamps` now properly reversible with + options. -* Coerce strings when reading attributes. Fixes #10485. - - Example: - - book = Book.new(title: 12345) - book.save! - book.title # => "12345" - - *Yves Senn* - -* Deprecate half-baked support for PostgreSQL range values with excluding beginnings. - We currently map PostgreSQL ranges to Ruby ranges. This conversion is not fully - possible because the Ruby range does not support excluded beginnings. - - The current solution of incrementing the beginning is not correct and is now - deprecated. For subtypes where we don't know how to increment (e.g. `#succ` - is not defined) it will raise an ArgumentException for ranges with excluding - beginnings. - - *Yves Senn* + *Noam Gagliardi-Rabinovich* -* Support for user created range types in PostgreSQL. +* `ActiveRecord::ConnectionAdapters::ColumnDumper#column_spec` and + `ActiveRecord::ConnectionAdapters::ColumnDumper#prepare_column_options` no + longer have a `types` argument. They should access + `connection#native_database_types` directly. *Yves Senn* -Please check [4-1-stable](https://github.com/rails/rails/blob/4-1-stable/activerecord/CHANGELOG.md) for previous changes. +Please check [4-2-stable](https://github.com/rails/rails/blob/4-2-stable/activerecord/CHANGELOG.md) for previous changes. diff --git a/activerecord/MIT-LICENSE b/activerecord/MIT-LICENSE index 2950f05b11..7c2197229d 100644 --- a/activerecord/MIT-LICENSE +++ b/activerecord/MIT-LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2004-2014 David Heinemeier Hansson +Copyright (c) 2004-2015 David Heinemeier Hansson Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the diff --git a/activerecord/RUNNING_UNIT_TESTS.rdoc b/activerecord/RUNNING_UNIT_TESTS.rdoc index ca1f2fd665..7e3460365b 100644 --- a/activerecord/RUNNING_UNIT_TESTS.rdoc +++ b/activerecord/RUNNING_UNIT_TESTS.rdoc @@ -20,11 +20,11 @@ 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 - $ bundle exec rake test_sqlite3_mem + $ bundle exec rake test:mysql + $ bundle exec rake test:mysql2 + $ bundle exec rake test:postgresql + $ bundle exec rake test:sqlite3 + $ bundle exec rake test:sqlite3_mem There should be tests available for each database backend listed in the {Config File}[rdoc-label:label-Config+File]. (the exact set of available tests is @@ -32,8 +32,8 @@ defined in +Rakefile+) == Config File -If +test/config.yml+ is present, it's parameters are obeyed. Otherwise, the -parameters in +test/config.example.yml+ are obeyed. +If +test/config.yml+ is present, then its parameters are obeyed; otherwise, the +parameters in +test/config.example.yml+ are. You can override the +connections:+ parameter in either file using the +ARCONN+ (Active Record CONNection) environment variable: diff --git a/activerecord/Rakefile b/activerecord/Rakefile index 7769966a22..976b559da9 100644 --- a/activerecord/Rakefile +++ b/activerecord/Rakefile @@ -55,6 +55,7 @@ end t.warning = true t.verbose = true + t.ruby_opts = ["--dev"] if defined?(JRUBY_VERSION) } namespace :isolated do @@ -146,27 +147,9 @@ task :drop_postgresql_databases => 'db:postgresql:drop' task :rebuild_postgresql_databases => 'db:postgresql:rebuild' task :lines do - lines, codelines, total_lines, total_codelines = 0, 0, 0, 0 - - FileList["lib/active_record/**/*.rb"].each do |file_name| - next if file_name =~ /vendor/ - File.open(file_name, 'r') do |f| - while line = f.gets - lines += 1 - next if line =~ /^\s*$/ - next if line =~ /^\s*#/ - codelines += 1 - end - end - puts "L: #{sprintf("%4d", lines)}, LOC #{sprintf("%4d", codelines)} | #{file_name}" - - total_lines += lines - total_codelines += codelines - - lines, codelines = 0, 0 - end - - puts "Total: Lines #{total_lines}, LOC #{total_codelines}" + load File.expand_path('..', File.dirname(__FILE__)) + '/tools/line_statistics' + files = FileList["lib/active_record/**/*.rb"] + CodeTools::LineStatistics.new(files).print_loc end spec = eval(File.read('activerecord.gemspec')) diff --git a/activerecord/activerecord.gemspec b/activerecord/activerecord.gemspec index 8075008574..c5cd0c89f7 100644 --- a/activerecord/activerecord.gemspec +++ b/activerecord/activerecord.gemspec @@ -7,7 +7,7 @@ Gem::Specification.new do |s| s.summary = 'Object-relational mapper framework (part of Rails).' s.description = 'Databases on Rails. Build a persistent domain model by mapping database tables to Ruby classes. Strong conventions for associations, validations, aggregations, migrations, and testing come baked-in.' - s.required_ruby_version = '>= 1.9.3' + s.required_ruby_version = '>= 2.2.0' s.license = 'MIT' @@ -24,5 +24,5 @@ Gem::Specification.new do |s| s.add_dependency 'activesupport', version s.add_dependency 'activemodel', version - s.add_dependency 'arel', '~> 6.0.0' + s.add_dependency 'arel', '7.0.0.alpha' end diff --git a/activerecord/examples/performance.rb b/activerecord/examples/performance.rb index d3546ce948..a5a1f284a0 100644 --- a/activerecord/examples/performance.rb +++ b/activerecord/examples/performance.rb @@ -39,8 +39,8 @@ class Exhibit < ActiveRecord::Base where("notes IS NOT NULL") end - def self.look(exhibits) exhibits.each { |e| e.look } end - def self.feel(exhibits) exhibits.each { |e| e.feel } end + def self.look(exhibits) exhibits.each(&:look) end + def self.feel(exhibits) exhibits.each(&:feel) end end def progress_bar(int); print "." if (int%100).zero? ; end diff --git a/activerecord/lib/active_record.rb b/activerecord/lib/active_record.rb index 53fa132219..2eec62846b 100644 --- a/activerecord/lib/active_record.rb +++ b/activerecord/lib/active_record.rb @@ -1,5 +1,5 @@ #-- -# Copyright (c) 2004-2014 David Heinemeier Hansson +# Copyright (c) 2004-2015 David Heinemeier Hansson # # Permission is hereby granted, free of charge, to any person obtaining # a copy of this software and associated documentation files (the @@ -27,6 +27,7 @@ require 'active_model' require 'arel' require 'active_record/version' +require 'active_record/attribute_set' module ActiveRecord extend ActiveSupport::Autoload @@ -61,6 +62,7 @@ module ActiveRecord autoload :Serialization autoload :StatementCache autoload :Store + autoload :TableMetadata autoload :Timestamp autoload :Transactions autoload :Translation @@ -96,6 +98,7 @@ module ActiveRecord module Coders autoload :YAMLColumn, 'active_record/coders/yaml_column' + autoload :JSON, 'active_record/coders/json' end module AttributeMethods diff --git a/activerecord/lib/active_record/aggregations.rb b/activerecord/lib/active_record/aggregations.rb index e576ec4d40..1040e6e3bb 100644 --- a/activerecord/lib/active_record/aggregations.rb +++ b/activerecord/lib/active_record/aggregations.rb @@ -230,8 +230,8 @@ module ActiveRecord private def reader_method(name, class_name, mapping, allow_nil, constructor) define_method(name) do - if @aggregation_cache[name].nil? && (!allow_nil || mapping.any? {|key, _| !read_attribute(key).nil? }) - attrs = mapping.collect {|key, _| read_attribute(key)} + if @aggregation_cache[name].nil? && (!allow_nil || mapping.any? {|key, _| !_read_attribute(key).nil? }) + attrs = mapping.collect {|key, _| _read_attribute(key)} object = constructor.respond_to?(:call) ? constructor.call(*attrs) : class_name.constantize.send(constructor, *attrs) diff --git a/activerecord/lib/active_record/association_relation.rb b/activerecord/lib/active_record/association_relation.rb index 5a84792f45..f2b44913db 100644 --- a/activerecord/lib/active_record/association_relation.rb +++ b/activerecord/lib/active_record/association_relation.rb @@ -1,7 +1,7 @@ module ActiveRecord class AssociationRelation < Relation - def initialize(klass, table, association) - super(klass, table) + def initialize(klass, table, predicate_builder, association) + super(klass, table, predicate_builder) @association = association end diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index 6222bfe903..14af55f327 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -46,12 +46,18 @@ module ActiveRecord end end + class HasOneAssociationPolymorphicThroughError < ActiveRecordError #:nodoc: + def initialize(owner_class_name, reflection) + super("Cannot have a has_one :through association '#{owner_class_name}##{reflection.name}' which goes through the polymorphic association '#{owner_class_name}##{reflection.through_reflection.name}'.") + end + end + class HasManyThroughSourceAssociationNotFoundError < ActiveRecordError #:nodoc: def initialize(reflection) through_reflection = reflection.through_reflection source_reflection_names = reflection.source_reflection_names source_associations = reflection.through_reflection.klass._reflections.keys - super("Could not find the source association(s) #{source_reflection_names.collect{ |a| a.inspect }.to_sentence(:two_words_connector => ' or ', :last_word_connector => ', or ', :locale => :en)} in model #{through_reflection.klass}. Try 'has_many #{reflection.name.inspect}, :through => #{through_reflection.name.inspect}, :source => <name>'. Is it one of #{source_associations.to_sentence(:two_words_connector => ' or ', :last_word_connector => ', or ', :locale => :en)}?") + super("Could not find the source association(s) #{source_reflection_names.collect(&:inspect).to_sentence(:two_words_connector => ' or ', :last_word_connector => ', or ', :locale => :en)} in model #{through_reflection.klass}. Try 'has_many #{reflection.name.inspect}, :through => #{through_reflection.name.inspect}, :source => <name>'. Is it one of #{source_associations.to_sentence(:two_words_connector => ' or ', :last_word_connector => ', or ', :locale => :en)}?") end end @@ -110,6 +116,7 @@ module ActiveRecord autoload :Association, 'active_record/associations/association' autoload :SingularAssociation, 'active_record/associations/singular_association' autoload :CollectionAssociation, 'active_record/associations/collection_association' + autoload :ForeignAssociation, 'active_record/associations/foreign_association' autoload :CollectionProxy, 'active_record/associations/collection_proxy' autoload :BelongsToAssociation, 'active_record/associations/belongs_to_association' @@ -447,9 +454,11 @@ module ActiveRecord # # Possible callbacks are: +before_add+, +after_add+, +before_remove+ and +after_remove+. # - # Should any of the +before_add+ callbacks throw an exception, the object does not get - # added to the collection. Same with the +before_remove+ callbacks; if an exception is - # thrown the object doesn't get removed. + # If any of the +before_add+ callbacks throw an exception, the object will not be + # added to the collection. + # + # Similarly, if any of the +before_remove+ callbacks throw an exception, the object + # will not be removed from the collection. # # == Association extensions # @@ -647,7 +656,7 @@ module ActiveRecord # belongs_to :commenter # end # - # When using nested association, you will not be able to modify the association because there + # When using a nested association, you will not be able to modify the association because there # is not enough information to know what modification to make. For example, if you tried to # add a <tt>Commenter</tt> in the example above, there would be no way to tell how to set up the # intermediate <tt>Post</tt> and <tt>Comment</tt> objects. @@ -717,9 +726,9 @@ module ActiveRecord # == Eager loading of associations # # Eager loading is a way to find objects of a certain class and a number of named associations. - # This is one of the easiest ways of to prevent the dreaded 1+N problem in which fetching 100 + # It is one of the easiest ways to prevent the dreaded N+1 problem in which fetching 100 # posts that each need to display their author triggers 101 database queries. Through the - # use of eager loading, the 101 queries can be reduced to 2. + # use of eager loading, the number of queries will be reduced from 101 to 2. # # class Post < ActiveRecord::Base # belongs_to :author @@ -749,16 +758,16 @@ module ActiveRecord # Post.includes(:author, :comments).each do |post| # # This will load all comments with a single query. This reduces the total number of queries - # to 3. More generally the number of queries will be 1 plus the number of associations + # to 3. In general, the number of queries will be 1 plus the number of associations # named (except if some of the associations are polymorphic +belongs_to+ - see below). # # To include a deep hierarchy of associations, use a hash: # - # Post.includes(:author, {comments: {author: :gravatar}}).each do |post| + # Post.includes(:author, { comments: { author: :gravatar } }).each do |post| # - # That'll grab not only all the comments but all their authors and gravatar pictures. - # You can mix and match symbols, arrays and hashes in any combination to describe the - # associations you want to load. + # The above code will load all the comments and all of their associated + # authors and gravatars. You can mix and match any combination of symbols, + # arrays, and hashes to retrieve the associations you want to load. # # All of this power shouldn't fool you into thinking that you can pull out huge amounts # of data with no performance penalty just because you've reduced the number of queries. @@ -767,8 +776,8 @@ module ActiveRecord # cut down on the number of queries in a situation as the one described above. # # Since only one table is loaded at a time, conditions or orders cannot reference tables - # other than the main one. If this is the case Active Record falls back to the previously - # used LEFT OUTER JOIN based strategy. For example + # other than the main one. If this is the case, Active Record falls back to the previously + # used LEFT OUTER JOIN based strategy. For example: # # Post.includes([:author, :comments]).where(['comments.approved = ?', true]) # @@ -1052,7 +1061,7 @@ module ActiveRecord # Specifies a one-to-many association. The following methods for retrieval and query of # collections of associated objects will be added: # - # +collection+ is a placeholder for the symbol passed as the first argument, so + # +collection+ is a placeholder for the symbol passed as the +name+ argument, so # <tt>has_many :clients</tt> would add among others <tt>clients.empty?</tt>. # # [collection(force_reload = false)] @@ -1131,7 +1140,32 @@ module ActiveRecord # * <tt>Firm#clients.build</tt> (similar to <tt>Client.new("firm_id" => id)</tt>) # * <tt>Firm#clients.create</tt> (similar to <tt>c = Client.new("firm_id" => id); c.save; c</tt>) # * <tt>Firm#clients.create!</tt> (similar to <tt>c = Client.new("firm_id" => id); c.save!</tt>) - # The declaration can also include an options hash to specialize the behavior of the association. + # The declaration can also include an +options+ hash to specialize the behavior of the association. + # + # === Scopes + # + # You can pass a second argument +scope+ as a callable (i.e. proc or + # lambda) to retrieve a specific set of records or customize the generated + # query when you access the associated collection. + # + # Scope examples: + # has_many :comments, -> { where(author_id: 1) } + # has_many :employees, -> { joins(:address) } + # has_many :posts, ->(post) { where("max_post_length > ?", post.length) } + # + # === Extensions + # + # The +extension+ argument allows you to pass a block into a has_many + # association. This is useful for adding new finders, creators and other + # factory-type methods to be used as part of the association. + # + # Extension examples: + # has_many :employees do + # def find_or_create_by_name(name) + # first_name, last_name = name.split(" ", 2) + # find_or_create_by(first_name: first_name, last_name: last_name) + # end + # end # # === Options # [:class_name] @@ -1143,8 +1177,14 @@ module ActiveRecord # Specify the foreign key used for the association. By default this is guessed to be the name # of this class in lower-case and "_id" suffixed. So a Person class that makes a +has_many+ # association will use "person_id" as the default <tt>:foreign_key</tt>. + # [:foreign_type] + # Specify the column used to store the associated object's type, if this is a polymorphic + # association. By default this is guessed to be the name of the polymorphic association + # specified on "as" option with a "_type" suffix. So a class that defines a + # <tt>has_many :tags, as: :taggable</tt> association will use "taggable_type" as the + # default <tt>:foreign_type</tt>. # [:primary_key] - # Specify the method that returns the primary key used for the association. By default this is +id+. + # Specify the name of the column to use as the primary key for the association. By default this is +id+. # [:dependent] # Controls what happens to the associated objects when # their owner is destroyed. Note that these are implemented as @@ -1209,7 +1249,7 @@ module ActiveRecord # Option examples: # has_many :comments, -> { order "posted_on" } # has_many :comments, -> { includes :author } - # has_many :people, -> { where("deleted = 0").order("name") }, class_name: "Person" + # has_many :people, -> { where(deleted: false).order("name") }, class_name: "Person" # has_many :tracks, -> { order "position" }, dependent: :destroy # has_many :comments, dependent: :nullify # has_many :tags, as: :taggable @@ -1227,7 +1267,7 @@ module ActiveRecord # # The following methods for retrieval and query of a single associated object will be added: # - # +association+ is a placeholder for the symbol passed as the first argument, so + # +association+ is a placeholder for the symbol passed as the +name+ argument, so # <tt>has_one :manager</tt> would add among others <tt>manager.nil?</tt>. # # [association(force_reload = false)] @@ -1257,9 +1297,20 @@ module ActiveRecord # * <tt>Account#create_beneficiary</tt> (similar to <tt>b = Beneficiary.new("account_id" => id); b.save; b</tt>) # * <tt>Account#create_beneficiary!</tt> (similar to <tt>b = Beneficiary.new("account_id" => id); b.save!; b</tt>) # + # === Scopes + # + # You can pass a second argument +scope+ as a callable (i.e. proc or + # lambda) to retrieve a specific record or customize the generated query + # when you access the associated object. + # + # Scope examples: + # has_one :author, -> { where(comment_id: 1) } + # has_one :employer, -> { joins(:company) } + # has_one :dob, ->(dob) { where("Date.new(2000, 01, 01) > ?", dob) } + # # === Options # - # The declaration can also include an options hash to specialize the behavior of the association. + # The declaration can also include an +options+ hash to specialize the behavior of the association. # # Options are: # [:class_name] @@ -1279,6 +1330,12 @@ module ActiveRecord # Specify the foreign key used for the association. By default this is guessed to be the name # of this class in lower-case and "_id" suffixed. So a Person class that makes a +has_one+ association # will use "person_id" as the default <tt>:foreign_key</tt>. + # [:foreign_type] + # Specify the column used to store the associated object's type, if this is a polymorphic + # association. By default this is guessed to be the name of the polymorphic association + # specified on "as" option with a "_type" suffix. So a class that defines a + # <tt>has_one :tag, as: :taggable</tt> association will use "taggable_type" as the + # default <tt>:foreign_type</tt>. # [:primary_key] # Specify the method that returns the primary key used for the association. By default this is +id+. # [:as] @@ -1309,6 +1366,10 @@ module ActiveRecord # that is the inverse of this <tt>has_one</tt> association. Does not work in combination # with <tt>:through</tt> or <tt>:as</tt> options. # See ActiveRecord::Associations::ClassMethods's overview on Bi-directional associations for more detail. + # [:required] + # When set to +true+, the association will also have its presence validated. + # This will validate the association itself, not the id. You can use + # +:inverse_of+ to avoid an extra query during validation. # # Option examples: # has_one :credit_card, dependent: :destroy # destroys the associated credit card @@ -1320,6 +1381,7 @@ module ActiveRecord # has_one :boss, readonly: :true # has_one :club, through: :membership # has_one :primary_address, -> { where primary: true }, through: :addressables, source: :addressable + # has_one :credit_card, required: true def has_one(name, scope = nil, options = {}) reflection = Builder::HasOne.build(self, name, scope, options) Reflection.add_reflection self, name, reflection @@ -1333,7 +1395,7 @@ module ActiveRecord # Methods will be added for retrieval and query for a single associated object, for which # this object holds an id: # - # +association+ is a placeholder for the symbol passed as the first argument, so + # +association+ is a placeholder for the symbol passed as the +name+ argument, so # <tt>belongs_to :author</tt> would add among others <tt>author.nil?</tt>. # # [association(force_reload = false)] @@ -1359,7 +1421,18 @@ module ActiveRecord # * <tt>Post#build_author</tt> (similar to <tt>post.author = Author.new</tt>) # * <tt>Post#create_author</tt> (similar to <tt>post.author = Author.new; post.author.save; post.author</tt>) # * <tt>Post#create_author!</tt> (similar to <tt>post.author = Author.new; post.author.save!; post.author</tt>) - # The declaration can also include an options hash to specialize the behavior of the association. + # The declaration can also include an +options+ hash to specialize the behavior of the association. + # + # === Scopes + # + # You can pass a second argument +scope+ as a callable (i.e. proc or + # lambda) to retrieve a specific record or customize the generated query + # when you access the associated object. + # + # Scope examples: + # belongs_to :user, -> { where(id: 2) } + # belongs_to :user, -> { joins(:friends) } + # belongs_to :level, ->(level) { where("game_level > ?", level.current) } # # === Options # @@ -1413,7 +1486,7 @@ module ActiveRecord # # Note that <tt>accepts_nested_attributes_for</tt> sets <tt>:autosave</tt> to <tt>true</tt>. # [:touch] - # If true, the associated object will be touched (the updated_at/on attributes set to now) + # 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. # [:inverse_of] @@ -1421,18 +1494,23 @@ module ActiveRecord # object that is the inverse of this <tt>belongs_to</tt> association. Does not work in # combination with the <tt>:polymorphic</tt> options. # See ActiveRecord::Associations::ClassMethods's overview on Bi-directional associations for more detail. + # [:required] + # When set to +true+, the association will also have its presence validated. + # This will validate the association itself, not the id. You can use + # +:inverse_of+ to avoid an extra query during validation. # # Option examples: # belongs_to :firm, foreign_key: "client_of" # belongs_to :person, primary_key: "name", foreign_key: "person_name" # belongs_to :author, class_name: "Person", foreign_key: "author_id" - # belongs_to :valid_coupon, ->(o) { where "discounts > #{o.payments_count}" }, + # belongs_to :valid_coupon, ->(o) { where "discounts > ?", o.payments_count }, # class_name: "Coupon", foreign_key: "coupon_id" # belongs_to :attachable, polymorphic: true # belongs_to :project, readonly: true # belongs_to :post, counter_cache: true # belongs_to :company, touch: true # belongs_to :company, touch: :employees_last_updated_at + # belongs_to :company, required: true def belongs_to(name, scope = nil, options = {}) reflection = Builder::BelongsTo.build(self, name, scope, options) Reflection.add_reflection self, name, reflection @@ -1470,7 +1548,7 @@ module ActiveRecord # # Adds the following methods for retrieval and query: # - # +collection+ is a placeholder for the symbol passed as the first argument, so + # +collection+ is a placeholder for the symbol passed as the +name+ argument, so # <tt>has_and_belongs_to_many :categories</tt> would add among others <tt>categories.empty?</tt>. # # [collection(force_reload = false)] @@ -1531,7 +1609,34 @@ module ActiveRecord # * <tt>Developer#projects.exists?(...)</tt> # * <tt>Developer#projects.build</tt> (similar to <tt>Project.new("developer_id" => id)</tt>) # * <tt>Developer#projects.create</tt> (similar to <tt>c = Project.new("developer_id" => id); c.save; c</tt>) - # The declaration may include an options hash to specialize the behavior of the association. + # The declaration may include an +options+ hash to specialize the behavior of the association. + # + # === Scopes + # + # You can pass a second argument +scope+ as a callable (i.e. proc or + # lambda) to retrieve a specific set of records or customize the generated + # query when you access the associated collection. + # + # Scope examples: + # has_and_belongs_to_many :projects, -> { includes :milestones, :manager } + # has_and_belongs_to_many :categories, ->(category) { + # where("default_category = ?", category.name) + # } + # + # === Extensions + # + # The +extension+ argument allows you to pass a block into a + # has_and_belongs_to_many association. This is useful for adding new + # finders, creators and other factory-type methods to be used as part of + # the association. + # + # Extension examples: + # has_and_belongs_to_many :contractors do + # def find_or_create_by_name(name) + # first_name, last_name = name.split(" ", 2) + # find_or_create_by(first_name: first_name, last_name: last_name) + # end + # end # # === Options # @@ -1577,7 +1682,7 @@ module ActiveRecord scope = nil end - habtm_reflection = ActiveRecord::Reflection::HasAndBelongsToManyReflection.new(:has_and_belongs_to_many, name, scope, options, self) + habtm_reflection = ActiveRecord::Reflection::HasAndBelongsToManyReflection.new(name, scope, options, self) builder = Builder::HasAndBelongsToMany.new name, self, options @@ -1608,7 +1713,7 @@ module ActiveRecord hm_options[:through] = middle_reflection.name hm_options[:source] = join_model.right_reflection.name - [:before_add, :after_add, :before_remove, :after_remove, :autosave, :validate, :join_table].each do |k| + [:before_add, :after_add, :before_remove, :after_remove, :autosave, :validate, :join_table, :class_name].each do |k| hm_options[k] = options[k] if options.key? k end diff --git a/activerecord/lib/active_record/associations/alias_tracker.rb b/activerecord/lib/active_record/associations/alias_tracker.rb index a6a1947148..f9c9f8afda 100644 --- a/activerecord/lib/active_record/associations/alias_tracker.rb +++ b/activerecord/lib/active_record/associations/alias_tracker.rb @@ -56,21 +56,11 @@ module ActiveRecord @connection = connection end - def aliased_table_for(table_name, aliased_name) - table_alias = aliased_name_for(table_name, aliased_name) - - if table_alias == table_name - Arel::Table.new(table_name) - else - Arel::Table.new(table_name).alias(table_alias) - end - end - - def aliased_name_for(table_name, aliased_name) + def aliased_table_for(table_name, aliased_name, **table_options) if aliases[table_name].zero? # If it's zero, we can have our table_name aliases[table_name] = 1 - table_name + Arel::Table.new(table_name, table_options) else # Otherwise, we need to use an alias aliased_name = connection.table_alias_for(aliased_name) @@ -78,11 +68,12 @@ module ActiveRecord # Update the count aliases[aliased_name] += 1 - if aliases[aliased_name] > 1 + table_alias = if aliases[aliased_name] > 1 "#{truncate(aliased_name)}_#{aliases[aliased_name]}" else aliased_name end + Arel::Table.new(table_name, table_options).alias(table_alias) end end diff --git a/activerecord/lib/active_record/associations/association.rb b/activerecord/lib/active_record/associations/association.rb index f1c36cd047..0d8e4ba870 100644 --- a/activerecord/lib/active_record/associations/association.rb +++ b/activerecord/lib/active_record/associations/association.rb @@ -121,7 +121,7 @@ module ActiveRecord # Can be overridden (i.e. in ThroughAssociation) to merge in other scopes (i.e. the # through association's scope) def target_scope - AssociationRelation.create(klass, klass.arel_table, self).merge!(klass.all) + AssociationRelation.create(klass, klass.arel_table, klass.predicate_builder, self).merge!(klass.all) end # Loads the \target if needed and returns it. diff --git a/activerecord/lib/active_record/associations/association_scope.rb b/activerecord/lib/active_record/associations/association_scope.rb index 519d4d8651..53f65920e1 100644 --- a/activerecord/lib/active_record/associations/association_scope.rb +++ b/activerecord/lib/active_record/associations/association_scope.rb @@ -10,9 +10,8 @@ module ActiveRecord @block = block end - def bind_value(scope, column, value, alias_tracker) - substitute = alias_tracker.connection.substitute_at( - column, scope.bind_values.length) + def bind_value(scope, column, value, connection) + substitute = connection.substitute_at(column) scope.bind_values += [[column, @block.call(value)]] substitute end @@ -45,20 +44,20 @@ module ActiveRecord end def self.get_bind_values(owner, chain) - bvs = [] - chain.each_with_index do |reflection, i| - if reflection == chain.last - bvs << reflection.join_id_for(owner) - if reflection.type - bvs << owner.class.base_class.name - end - else - if reflection.type - bvs << chain[i + 1].klass.base_class.name - end + binds = [] + last_reflection = chain.last + + binds << last_reflection.join_id_for(owner) + if last_reflection.type + binds << owner.class.base_class.name + end + + chain.each_cons(2).each do |reflection, next_reflection| + if reflection.type + binds << next_reflection.klass.base_class.name end end - bvs + binds end private @@ -67,7 +66,8 @@ module ActiveRecord chain.map do |reflection| alias_tracker.aliased_table_for( table_name_for(reflection, klass, refl), - table_alias_for(reflection, refl, reflection != refl) + table_alias_for(reflection, refl, reflection != refl), + type_caster: klass.type_caster, ) end end @@ -82,52 +82,70 @@ module ActiveRecord table.create_join(table, table.create_on(constraint), join_type) end - def column_for(table_name, column_name, alias_tracker) - columns = alias_tracker.connection.schema_cache.columns_hash(table_name) + def column_for(table_name, column_name, connection) + columns = connection.schema_cache.columns_hash(table_name) columns[column_name] end - def bind_value(scope, column, value, alias_tracker) - @bind_substitution.bind_value scope, column, value, alias_tracker + def bind_value(scope, column, value, connection) + @bind_substitution.bind_value scope, column, value, connection end - def bind(scope, table_name, column_name, value, tracker) - column = column_for table_name, column_name, tracker - bind_value scope, column, value, tracker + def bind(scope, table_name, column_name, value, connection) + column = column_for table_name, column_name, connection + bind_value scope, column, value, connection + end + + def last_chain_scope(scope, table, reflection, owner, connection, assoc_klass) + join_keys = reflection.join_keys(assoc_klass) + key = join_keys.key + foreign_key = join_keys.foreign_key + + bind_val = bind scope, table.table_name, key.to_s, owner[foreign_key], connection + scope = scope.where(table[key].eq(bind_val)) + + if reflection.type + value = owner.class.base_class.name + bind_val = bind scope, table.table_name, reflection.type, value, connection + scope = scope.where(table[reflection.type].eq(bind_val)) + else + scope + end + end + + def next_chain_scope(scope, table, reflection, connection, assoc_klass, foreign_table, next_reflection) + join_keys = reflection.join_keys(assoc_klass) + key = join_keys.key + foreign_key = join_keys.foreign_key + + constraint = table[key].eq(foreign_table[foreign_key]) + + if reflection.type + value = next_reflection.klass.base_class.name + bind_val = bind scope, table.table_name, reflection.type, value, connection + scope = scope.where(table[reflection.type].eq(bind_val)) + end + + scope = scope.joins(join(foreign_table, constraint)) end def add_constraints(scope, owner, assoc_klass, refl, tracker) chain = refl.chain scope_chain = refl.scope_chain + connection = tracker.connection tables = construct_tables(chain, assoc_klass, refl, tracker) + owner_reflection = chain.last + table = tables.last + scope = last_chain_scope(scope, table, owner_reflection, owner, connection, assoc_klass) + chain.each_with_index do |reflection, i| table, foreign_table = tables.shift, tables.first - join_keys = reflection.join_keys(assoc_klass) - key = join_keys.key - foreign_key = join_keys.foreign_key - - if reflection == chain.last - bind_val = bind scope, table.table_name, key.to_s, owner[foreign_key], tracker - scope = scope.where(table[key].eq(bind_val)) - - if reflection.type - value = owner.class.base_class.name - bind_val = bind scope, table.table_name, reflection.type.to_s, value, tracker - scope = scope.where(table[reflection.type].eq(bind_val)) - end - else - constraint = table[key].eq(foreign_table[foreign_key]) - - if reflection.type - value = chain[i + 1].klass.base_class.name - bind_val = bind scope, table.table_name, reflection.type.to_s, value, tracker - scope = scope.where(table[reflection.type].eq(bind_val)) - end - - scope = scope.joins(join(foreign_table, constraint)) + unless reflection == chain.last + next_reflection = chain[i + 1] + scope = next_chain_scope(scope, table, reflection, connection, assoc_klass, foreign_table, next_reflection) end is_first_chain = i == 0 @@ -171,11 +189,7 @@ module ActiveRecord end def eval_scope(klass, scope, owner) - if scope.is_a?(Relation) - scope - else - klass.unscoped.instance_exec(owner, &scope) - end + klass.unscoped.instance_exec(owner, &scope) end end end diff --git a/activerecord/lib/active_record/associations/belongs_to_association.rb b/activerecord/lib/active_record/associations/belongs_to_association.rb index 81fdd681de..c63b42e2a0 100644 --- a/activerecord/lib/active_record/associations/belongs_to_association.rb +++ b/activerecord/lib/active_record/associations/belongs_to_association.rb @@ -73,11 +73,11 @@ module ActiveRecord # Checks whether record is different to the current target, without loading it def different_target?(record) - record.id != owner[reflection.foreign_key] + record.id != owner._read_attribute(reflection.foreign_key) end def replace_keys(record) - owner[reflection.foreign_key] = record[reflection.association_primary_key(record.class)] + owner[reflection.foreign_key] = record._read_attribute(reflection.association_primary_key(record.class)) end def remove_keys @@ -85,7 +85,7 @@ module ActiveRecord end def foreign_key_present? - owner[reflection.foreign_key] + owner._read_attribute(reflection.foreign_key) end # NOTE - for now, we're only supporting inverse setting from belongs_to back onto @@ -99,12 +99,13 @@ module ActiveRecord if options[:primary_key] owner.send(reflection.name).try(:id) else - owner[reflection.foreign_key] + owner._read_attribute(reflection.foreign_key) end end def stale_state - owner[reflection.foreign_key] && owner[reflection.foreign_key].to_s + result = owner._read_attribute(reflection.foreign_key) + result && result.to_s end end end diff --git a/activerecord/lib/active_record/associations/builder/association.rb b/activerecord/lib/active_record/associations/builder/association.rb index f085fd1cfd..947d61ee7b 100644 --- a/activerecord/lib/active_record/associations/builder/association.rb +++ b/activerecord/lib/active_record/associations/builder/association.rb @@ -36,6 +36,7 @@ module ActiveRecord::Associations::Builder reflection = builder.build(model) define_accessors model, reflection define_callbacks model, reflection + define_validations model, reflection builder.define_extensions model reflection end @@ -85,7 +86,11 @@ module ActiveRecord::Associations::Builder end def self.define_callbacks(model, reflection) - add_before_destroy_callbacks(model, reflection) if reflection.options[:dependent] + if dependent = reflection.options[:dependent] + check_dependent_options(dependent) + add_destroy_callbacks(model, reflection) + end + Association.extensions.each do |extension| extension.build model, reflection end @@ -120,17 +125,23 @@ module ActiveRecord::Associations::Builder CODE end + def self.define_validations(model, reflection) + # noop + end + def self.valid_dependent_options raise NotImplementedError end private - def self.add_before_destroy_callbacks(model, reflection) - unless valid_dependent_options.include? reflection.options[:dependent] - raise ArgumentError, "The :dependent option must be one of #{valid_dependent_options}, but is :#{reflection.options[:dependent]}" + def self.check_dependent_options(dependent) + unless valid_dependent_options.include? dependent + raise ArgumentError, "The :dependent option must be one of #{valid_dependent_options}, but is :#{dependent}" end + end + def self.add_destroy_callbacks(model, reflection) name = reflection.name model.before_destroy lambda { |o| o.association(name).handle_dependency } end diff --git a/activerecord/lib/active_record/associations/builder/belongs_to.rb b/activerecord/lib/active_record/associations/builder/belongs_to.rb index 3998aca23e..954ea3878a 100644 --- a/activerecord/lib/active_record/associations/builder/belongs_to.rb +++ b/activerecord/lib/active_record/associations/builder/belongs_to.rb @@ -107,5 +107,10 @@ module ActiveRecord::Associations::Builder model.after_touch callback model.after_destroy callback end + + def self.add_destroy_callbacks(model, reflection) + name = reflection.name + model.after_destroy lambda { |o| o.association(name).handle_dependency } + end end end 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 0ad5206980..092b4ebd2f 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 @@ -11,11 +11,14 @@ module ActiveRecord::Associations::Builder end def join_table - @join_table ||= [@lhs_class.table_name, klass.table_name].sort.join("\0").gsub(/^(.*[._])(.+)\0\1(.+)/, '\1\2_\3').gsub("\0", "_") + @join_table ||= [@lhs_class.table_name, klass.table_name].sort.join("\0").gsub(/^(.*[._])(.+)\0\1(.+)/, '\1\2_\3').tr("\0", "_") end private - def klass; @rhs_class_name.constantize; end + + def klass + @lhs_class.send(:compute_type, @rhs_class_name) + end end def self.build(lhs_class, name, options) @@ -23,13 +26,7 @@ module ActiveRecord::Associations::Builder KnownTable.new options[:join_table].to_s else class_name = options.fetch(:class_name) { - model_name = name.to_s.camelize.singularize - - if lhs_class.parent_name - model_name.prepend("#{lhs_class.parent_name}::") - end - - model_name + name.to_s.camelize.singularize } KnownClass.new lhs_class, class_name end @@ -101,7 +98,7 @@ module ActiveRecord::Associations::Builder def middle_options(join_model) middle_options = {} - middle_options[:class] = join_model + middle_options[:class_name] = "#{lhs_model.name}::#{join_model.name}" middle_options[:source] = join_model.left_reflection.name if options.key? :foreign_key middle_options[:foreign_key] = options[:foreign_key] @@ -113,7 +110,7 @@ module ActiveRecord::Associations::Builder rhs_options = {} if options.key? :class_name - rhs_options[:foreign_key] = options[:class_name].foreign_key + rhs_options[:foreign_key] = options[:class_name].to_s.foreign_key rhs_options[:class_name] = options[:class_name] end diff --git a/activerecord/lib/active_record/associations/builder/has_many.rb b/activerecord/lib/active_record/associations/builder/has_many.rb index 4c8c826f76..1b87f92170 100644 --- a/activerecord/lib/active_record/associations/builder/has_many.rb +++ b/activerecord/lib/active_record/associations/builder/has_many.rb @@ -5,7 +5,7 @@ module ActiveRecord::Associations::Builder end def valid_options - super + [:primary_key, :dependent, :as, :through, :source, :source_type, :inverse_of, :counter_cache, :join_table] + super + [:primary_key, :dependent, :as, :through, :source, :source_type, :inverse_of, :counter_cache, :join_table, :foreign_type] end def self.valid_dependent_options diff --git a/activerecord/lib/active_record/associations/builder/has_one.rb b/activerecord/lib/active_record/associations/builder/has_one.rb index a1f4f51664..1387717396 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 end def valid_options - valid = super + [:as] + valid = super + [:as, :foreign_type] valid += [:through, :source, :source_type] if options[:through] valid end @@ -16,7 +16,7 @@ module ActiveRecord::Associations::Builder private - def self.add_before_destroy_callbacks(model, reflection) + def self.add_destroy_callbacks(model, reflection) super unless reflection.options[:through] end end diff --git a/activerecord/lib/active_record/associations/builder/singular_association.rb b/activerecord/lib/active_record/associations/builder/singular_association.rb index e655c389a6..6e6dd7204c 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 class SingularAssociation < Association #:nodoc: def valid_options - super + [:remote, :dependent, :primary_key, :inverse_of] + super + [:dependent, :primary_key, :inverse_of, :required] end def self.define_accessors(model, reflection) @@ -27,5 +27,12 @@ module ActiveRecord::Associations::Builder end CODE end + + def self.define_validations(model, reflection) + super + if reflection.options[:required] + model.validates_presence_of reflection.name + end + end end end diff --git a/activerecord/lib/active_record/associations/collection_association.rb b/activerecord/lib/active_record/associations/collection_association.rb index 306588ac66..16b1228b8a 100644 --- a/activerecord/lib/active_record/associations/collection_association.rb +++ b/activerecord/lib/active_record/associations/collection_association.rb @@ -33,7 +33,13 @@ module ActiveRecord reload end - @proxy ||= CollectionProxy.create(klass, self) + if owner.new_record? + # Cache the proxy separately before the owner has an id + # or else a post-save proxy will still lack the id + @new_record_proxy ||= CollectionProxy.create(klass, self) + else + @proxy ||= CollectionProxy.create(klass, self) + end end # Implements the writer method, e.g. foo.items= for Foo.has_many :items @@ -55,10 +61,10 @@ module ActiveRecord # Implements the ids writer method, e.g. foo.item_ids= for Foo.has_many :items def ids_writer(ids) - pk_column = reflection.primary_key_column - ids = Array(ids).reject { |id| id.blank? } - ids.map! { |i| pk_column.type_cast_from_user(i) } - replace(klass.find(ids).index_by { |r| r.id }.values_at(*ids)) + pk_type = reflection.primary_key_type + ids = Array(ids).reject(&:blank?) + ids.map! { |i| pk_type.type_cast_from_user(i) } + replace(klass.find(ids).index_by(&:id).values_at(*ids)) end def reset @@ -169,8 +175,8 @@ module ActiveRecord end # Removes all records from the association without calling callbacks - # on the associated records. It honors the `:dependent` option. However - # if the `:dependent` value is `:destroy` then in that case the `:delete_all` + # on the associated records. It honors the +:dependent+ option. However + # if the +:dependent+ value is +:destroy+ then in that case the +:delete_all+ # deletion strategy for the association is applied. # # You can force a particular deletion strategy by passing a parameter. @@ -283,7 +289,7 @@ module ActiveRecord elsif !loaded? && !association_scope.group_values.empty? load_target.size elsif !loaded? && !association_scope.distinct_value && target.is_a?(Array) - unsaved_records = target.select { |r| r.new_record? } + unsaved_records = target.select(&:new_record?) unsaved_records.size + count_records else count_records @@ -352,6 +358,7 @@ module ActiveRecord if owner.new_record? replace_records(other_array, original_target) else + replace_common_records_in_memory(other_array, original_target) if other_array != original_target transaction { replace_records(other_array, original_target) } end @@ -379,11 +386,18 @@ module ActiveRecord target end - def add_to_target(record, skip_callbacks = false) + def add_to_target(record, skip_callbacks = false, &block) + if association_scope.distinct_value + index = @target.index(record) + end + replace_on_target(record, index, skip_callbacks, &block) + end + + def replace_on_target(record, index, skip_callbacks) callback(:before_add, record) unless skip_callbacks yield(record) if block_given? - if association_scope.distinct_value && index = @target.index(record) + if index @target[index] = record else @target << record @@ -407,7 +421,13 @@ module ActiveRecord private def get_records - return scope.to_a if reflection.scope_chain.any?(&:any?) + if reflection.scope_chain.any?(&:any?) || + scope.eager_loading? || + klass.current_scope || + klass.default_scopes.any? + + return scope.to_a + end conn = klass.connection sc = reflection.association_scope_cache(conn, owner) do @@ -486,7 +506,7 @@ module ActiveRecord def delete_or_destroy(records, method) records = records.flatten records.each { |record| raise_on_type_mismatch!(record) } - existing_records = records.reject { |r| r.new_record? } + existing_records = records.reject(&:new_record?) if existing_records.empty? remove_records(existing_records, records, method) @@ -522,6 +542,14 @@ module ActiveRecord target end + def replace_common_records_in_memory(new_target, original_target) + common_records = new_target & original_target + common_records.each do |record| + skip_callbacks = true + replace_on_target(record, @target.index(record), skip_callbacks) + end + end + def concat_records(records, should_raise = false) result = true @@ -569,8 +597,8 @@ module ActiveRecord if reflection.is_a?(ActiveRecord::Reflection::ThroughReflection) assoc = owner.association(reflection.through_reflection.name) assoc.reader.any? { |source| - target = source.send(reflection.source_reflection.name) - target.respond_to?(:include?) ? target.include?(record) : target == record + target_reflection = source.send(reflection.source_reflection.name) + target_reflection.respond_to?(:include?) ? target_reflection.include?(record) : target_reflection == record } || target.include?(record) else target.include?(record) @@ -581,7 +609,7 @@ module ActiveRecord # specified, then #find scans the entire collection. def find_by_scan(*args) expects_array = args.first.kind_of?(Array) - ids = args.flatten.compact.map{ |arg| arg.to_s }.uniq + ids = args.flatten.compact.map(&:to_s).uniq if ids.size == 1 id = ids.first diff --git a/activerecord/lib/active_record/associations/collection_proxy.rb b/activerecord/lib/active_record/associations/collection_proxy.rb index 84c8cfe72b..dc42b19a83 100644 --- a/activerecord/lib/active_record/associations/collection_proxy.rb +++ b/activerecord/lib/active_record/associations/collection_proxy.rb @@ -29,10 +29,11 @@ module ActiveRecord # instantiation of the actual post records. class CollectionProxy < Relation delegate(*(ActiveRecord::Calculations.public_instance_methods - [:count]), to: :scope) + delegate :find_nth, to: :scope def initialize(klass, association) #:nodoc: @association = association - super klass, klass.arel_table + super klass, klass.arel_table, klass.predicate_builder merge! association.scope(nullify: false) end @@ -355,14 +356,15 @@ module ActiveRecord @association.replace(other_array) end - # Deletes all the records from the collection. For +has_many+ associations, - # the deletion is done according to the strategy specified by the <tt>:dependent</tt> - # option. + # Deletes all the records from the collection according to the strategy + # specified by the +:dependent+ option. If no +:dependent+ option is given, + # then it will follow the default strategy. # - # If no <tt>:dependent</tt> option is given, then it will follow the - # default strategy. The default strategy is <tt>:nullify</tt>. This - # sets the foreign keys to <tt>NULL</tt>. For, +has_many+ <tt>:through</tt>, - # the default strategy is +delete_all+. + # For +has_many :through+ associations, the default deletion strategy is + # +:delete_all+. + # + # For +has_many+ associations, the default deletion strategy is +:nullify+. + # This sets the foreign keys to +NULL+. # # class Person < ActiveRecord::Base # has_many :pets # dependent: :nullify option by default @@ -393,9 +395,9 @@ module ActiveRecord # # #<Pet id: 3, name: "Choo-Choo", person_id: nil> # # ] # - # If it is set to <tt>:destroy</tt> all the objects from the collection - # are removed by calling their +destroy+ method. See +destroy+ for more - # information. + # Both +has_many+ and +has_many :through+ dependencies default to the + # +:delete_all+ strategy if the +:dependent+ option is set to +:destroy+. + # Records are not instantiated and callbacks will not be fired. # # class Person < ActiveRecord::Base # has_many :pets, dependent: :destroy @@ -410,11 +412,6 @@ module ActiveRecord # # ] # # person.pets.delete_all - # # => [ - # # #<Pet id: 1, name: "Fancy-Fancy", person_id: 1>, - # # #<Pet id: 2, name: "Spook", person_id: 1>, - # # #<Pet id: 3, name: "Choo-Choo", person_id: 1> - # # ] # # Pet.find(1, 2, 3) # # => ActiveRecord::RecordNotFound @@ -443,8 +440,9 @@ module ActiveRecord end # Deletes the records of the collection directly from the database - # ignoring the +:dependent+ option. It invokes +before_remove+, - # +after_remove+ , +before_destroy+ and +after_destroy+ callbacks. + # ignoring the +:dependent+ option. Records are instantiated and it + # invokes +before_remove+, +after_remove+ , +before_destroy+ and + # +after_destroy+ callbacks. # # class Person < ActiveRecord::Base # has_many :pets @@ -783,7 +781,7 @@ module ActiveRecord # person.pets.count # => 0 # person.pets.any? # => true # - # You can also pass a block to define criteria. The behavior + # You can also pass a +block+ to define criteria. The behavior # is the same, it returns true if the collection based on the # criteria is not empty. # @@ -817,7 +815,7 @@ module ActiveRecord # person.pets.count # => 2 # person.pets.many? # => true # - # You can also pass a block to define criteria. The + # You can also pass a +block+ to define criteria. The # behavior is the same, it returns true if the collection # based on the criteria has more than one record. # @@ -841,7 +839,7 @@ module ActiveRecord @association.many?(&block) end - # Returns +true+ if the given object is present in the collection. + # Returns +true+ if the given +record+ is present in the collection. # # class Person < ActiveRecord::Base # has_many :pets @@ -879,7 +877,7 @@ module ActiveRecord # Equivalent to <tt>Array#==</tt>. Returns +true+ if the two arrays # contain the same number of elements and if each element is equal - # to the corresponding element in the other array, otherwise returns + # to the corresponding element in the +other+ array, otherwise returns # +false+. # # class Person < ActiveRecord::Base diff --git a/activerecord/lib/active_record/associations/foreign_association.rb b/activerecord/lib/active_record/associations/foreign_association.rb new file mode 100644 index 0000000000..fe48ecec29 --- /dev/null +++ b/activerecord/lib/active_record/associations/foreign_association.rb @@ -0,0 +1,11 @@ +module ActiveRecord::Associations + module ForeignAssociation + def foreign_key_present? + if reflection.klass.primary_key + owner.attribute_present?(reflection.active_record_primary_key) + else + false + end + end + end +end diff --git a/activerecord/lib/active_record/associations/has_many_association.rb b/activerecord/lib/active_record/associations/has_many_association.rb index 2727e23870..d7f655d00c 100644 --- a/activerecord/lib/active_record/associations/has_many_association.rb +++ b/activerecord/lib/active_record/associations/has_many_association.rb @@ -6,6 +6,7 @@ module ActiveRecord # If the association has a <tt>:through</tt> option further specialization # is provided by its child HasManyThroughAssociation. class HasManyAssociation < CollectionAssociation #:nodoc: + include ForeignAssociation def handle_dependency case options[:dependent] @@ -41,6 +42,14 @@ module ActiveRecord end end + def empty? + if has_cached_counter? + size.zero? + else + super + end + end + private # Returns the number of records in this collection. @@ -58,7 +67,7 @@ module ActiveRecord # the loaded flag is set to true as well. def count_records count = if has_cached_counter? - owner.read_attribute cached_counter_attribute_name + owner._read_attribute cached_counter_attribute_name else scope.count end @@ -80,11 +89,22 @@ module ActiveRecord end def update_counter(difference, reflection = reflection()) + update_counter_in_database(difference, reflection) + update_counter_in_memory(difference, reflection) + end + + def update_counter_in_database(difference, reflection = reflection()) if has_cached_counter?(reflection) counter = cached_counter_attribute_name(reflection) owner.class.update_counters(owner.id, counter => difference) + end + end + + def update_counter_in_memory(difference, reflection = reflection()) + if has_cached_counter?(reflection) + counter = cached_counter_attribute_name(reflection) owner[counter] += difference - owner.changed_attributes.delete(counter) # eww + owner.send(:clear_attribute_changes, counter) # eww end end @@ -100,8 +120,12 @@ module ActiveRecord # Hence this method. def inverse_updates_counter_cache?(reflection = reflection()) counter_name = cached_counter_attribute_name(reflection) + inverse_updates_counter_named?(counter_name, reflection) + end + + def inverse_updates_counter_named?(counter_name, reflection = reflection()) reflection.klass._reflections.values.any? { |inverse_reflection| - :belongs_to == inverse_reflection.macro && + inverse_reflection.belongs_to? && inverse_reflection.counter_cache_column == counter_name } end @@ -130,12 +154,23 @@ module ActiveRecord end end - def foreign_key_present? - if reflection.klass.primary_key - owner.attribute_present?(reflection.association_primary_key) + def concat_records(records, *) + update_counter_if_success(super, records.length) + end + + def _create_record(attributes, *) + if attributes.is_a?(Array) + super else - false + update_counter_if_success(super, 1) + end + end + + def update_counter_if_success(saved_successfully, difference) + if saved_successfully + update_counter_in_memory(difference) end + saved_successfully end end end diff --git a/activerecord/lib/active_record/associations/has_many_through_association.rb b/activerecord/lib/active_record/associations/has_many_through_association.rb index 175019a72b..7a050ca224 100644 --- a/activerecord/lib/active_record/associations/has_many_through_association.rb +++ b/activerecord/lib/active_record/associations/has_many_through_association.rb @@ -1,3 +1,4 @@ +require 'active_support/core_ext/string/filters' module ActiveRecord # = Active Record Has Many Through Association @@ -12,21 +13,6 @@ module ActiveRecord @through_association = nil end - # Returns the size of the collection by executing a SELECT COUNT(*) query - # if the collection hasn't been loaded, and by calling collection.size if - # it has. If the collection will likely have a size greater than zero, - # and if fetching the collection will be needed afterwards, one less - # SELECT query will be generated by using #length instead. - def size - if has_cached_counter? - owner.read_attribute cached_counter_attribute_name(reflection) - elsif loaded? - target.size - else - super - end - end - def concat(*records) unless owner.new_record? records.flatten.each do |record| @@ -63,7 +49,16 @@ module ActiveRecord end save_through_record(record) - update_counter(1) + if has_cached_counter? && !through_reflection_updates_counter_cache? + ActiveSupport::Deprecation.warn(<<-MSG.squish) + Automatic updating of counter caches on through associations has been + deprecated, and will be removed in Rails 5. Instead, please set the + appropriate `counter_cache` options on the `has_many` and `belongs_to` + for your associations to #{through_reflection.name}. + MSG + + update_counter_in_database(1) + end record end @@ -93,7 +88,9 @@ module ActiveRecord end def through_scope_attributes - scope.where_values_hash(through_association.reflection.name.to_s).except!(through_association.reflection.foreign_key) + scope.where_values_hash(through_association.reflection.name.to_s). + except!(through_association.reflection.foreign_key, + through_association.reflection.klass.inheritance_column) end def save_through_record(record) @@ -149,13 +146,11 @@ module ActiveRecord if scope.klass.primary_key count = scope.destroy_all.length else - scope.to_a.each do |record| - record.run_callbacks :destroy - end + scope.each(&:_run_destroy_callbacks) arel = scope.arel - stmt = Arel::DeleteManager.new arel.engine + stmt = Arel::DeleteManager.new stmt.from scope.klass.arel_table stmt.wheres = arel.constraints @@ -216,6 +211,11 @@ module ActiveRecord def invertible_for?(record) false end + + def through_reflection_updates_counter_cache? + counter_name = cached_counter_attribute_name + inverse_updates_counter_named?(counter_name, through_reflection) + end end end end diff --git a/activerecord/lib/active_record/associations/has_one_association.rb b/activerecord/lib/active_record/associations/has_one_association.rb index 944caacab6..74b8c53758 100644 --- a/activerecord/lib/active_record/associations/has_one_association.rb +++ b/activerecord/lib/active_record/associations/has_one_association.rb @@ -1,8 +1,8 @@ - module ActiveRecord # = Active Record Belongs To Has One Association module Associations class HasOneAssociation < SingularAssociation #:nodoc: + include ForeignAssociation def handle_dependency case options[:dependent] diff --git a/activerecord/lib/active_record/associations/join_dependency.rb b/activerecord/lib/active_record/associations/join_dependency.rb index fbb4551b22..66e997c3c8 100644 --- a/activerecord/lib/active_record/associations/join_dependency.rb +++ b/activerecord/lib/active_record/associations/join_dependency.rb @@ -20,7 +20,7 @@ module ActiveRecord end def columns - @tables.flat_map { |t| t.column_aliases } + @tables.flat_map(&:column_aliases) end # An array of [column_name, alias] pairs for the table @@ -94,7 +94,7 @@ module ActiveRecord # def initialize(base, associations, joins) @alias_tracker = AliasTracker.create(base.connection, joins) - @alias_tracker.aliased_name_for(base.table_name, base.table_name) # Updates the count for base.table_name to 1 + @alias_tracker.aliased_table_for(base.table_name, base.table_name, type_caster: base.type_caster) # Updates the count for base.table_name to 1 tree = self.class.make_tree associations @join_root = JoinBase.new base, build(tree, base) @join_root.children.each { |child| construct_tables! @join_root, child } @@ -131,7 +131,6 @@ module ActiveRecord def instantiate(result_set, aliases) primary_key = aliases.column_alias(join_root, join_root.primary_key) - type_caster = result_set.column_type primary_key seen = Hash.new { |h,parent_klass| h[parent_klass] = Hash.new { |i,parent_id| @@ -143,12 +142,20 @@ module ActiveRecord parents = model_cache[join_root] column_aliases = aliases.column_aliases join_root - result_set.each { |row_hash| - primary_id = type_caster.type_cast_from_database row_hash[primary_key] - parent = parents[primary_id] ||= join_root.instantiate(row_hash, column_aliases) - construct(parent, join_root, row_hash, result_set, seen, model_cache, aliases) + message_bus = ActiveSupport::Notifications.instrumenter + + payload = { + record_count: result_set.length, + class_name: join_root.base_klass.name } + message_bus.instrument('instantiation.active_record', payload) do + result_set.each { |row_hash| + parent = parents[row_hash[primary_key]] ||= join_root.instantiate(row_hash, column_aliases) + construct(parent, join_root, row_hash, result_set, seen, model_cache, aliases) + } + end + parents.values end @@ -179,9 +186,13 @@ module ActiveRecord def table_aliases_for(parent, node) node.reflection.chain.map { |reflection| + if reflection.klass + type_caster = reflection.klass.type_caster + end alias_tracker.aliased_table_for( reflection.table_name, - table_alias_for(reflection, parent, reflection != node.reflection) + table_alias_for(reflection, parent, reflection != node.reflection), + type_caster: type_caster, ) } end @@ -250,6 +261,7 @@ module ActiveRecord construct(model, node, row, rs, seen, model_cache, aliases) else model = construct_model(ar_parent, node, row, model_cache, id, aliases) + model.readonly! seen[parent.base_klass][primary_id][node.base_klass][id] = model construct(model, node, row, rs, seen, model_cache, aliases) 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 a0e83c0a02..c1ef86a95b 100644 --- a/activerecord/lib/active_record/associations/join_dependency/join_association.rb +++ b/activerecord/lib/active_record/associations/join_dependency/join_association.rb @@ -37,27 +37,29 @@ module ActiveRecord table = tables.shift klass = reflection.klass - case reflection.source_macro - when :belongs_to - key = reflection.association_primary_key - foreign_key = reflection.foreign_key - else - key = reflection.foreign_key - foreign_key = reflection.active_record_primary_key - end + join_keys = reflection.join_keys(klass) + key = join_keys.key + foreign_key = join_keys.foreign_key constraint = build_constraint(klass, table, key, foreign_table, foreign_key) + predicate_builder = PredicateBuilder.new(TableMetadata.new(klass, table)) scope_chain_items = scope_chain[scope_chain_index].map do |item| if item.is_a?(Relation) item else - ActiveRecord::Relation.create(klass, table).instance_exec(node, &item) + ActiveRecord::Relation.create(klass, table, predicate_builder) + .instance_exec(node, &item) end end scope_chain_index += 1 - scope_chain_items.concat [klass.send(:build_default_scope, ActiveRecord::Relation.create(klass, table))].compact + relation = ActiveRecord::Relation.create( + klass, + table, + predicate_builder, + ) + scope_chain_items.concat [klass.send(:build_default_scope, relation)].compact rel = scope_chain_items.inject(scope_chain_items.shift) do |left, right| left.merge right @@ -70,9 +72,9 @@ module ActiveRecord if reflection.type value = foreign_klass.base_class.name - column = klass.columns_hash[column.to_s] + column = klass.columns_hash[reflection.type.to_s] - substitute = klass.connection.substitute_at(column, bind_values.length) + substitute = klass.connection.substitute_at(column) bind_values.push [column, value] constraint = constraint.and table[reflection.type].eq substitute end @@ -95,7 +97,7 @@ module ActiveRecord # end # # If I execute `Physician.joins(:appointments).to_a` then - # reflection # => #<ActiveRecord::Reflection::AssociationReflection @macro=:has_many ...> + # klass # => Physician # table # => #<Arel::Table @name="appointments" ...> # key # => physician_id # foreign_table # => #<Arel::Table @name="physicians" ...> diff --git a/activerecord/lib/active_record/associations/join_dependency/join_part.rb b/activerecord/lib/active_record/associations/join_dependency/join_part.rb index 91e1c6a9d7..9c6573f913 100644 --- a/activerecord/lib/active_record/associations/join_dependency/join_part.rb +++ b/activerecord/lib/active_record/associations/join_dependency/join_part.rb @@ -19,7 +19,6 @@ module ActiveRecord def initialize(base_klass, children) @base_klass = base_klass - @column_names_with_alias = nil @children = children end diff --git a/activerecord/lib/active_record/associations/preloader.rb b/activerecord/lib/active_record/associations/preloader.rb index 7519fec10a..4358f3b581 100644 --- a/activerecord/lib/active_record/associations/preloader.rb +++ b/activerecord/lib/active_record/associations/preloader.rb @@ -2,33 +2,42 @@ module ActiveRecord module Associations # Implements the details of eager loading of Active Record associations. # - # Note that 'eager loading' and 'preloading' are actually the same thing. - # However, there are two different eager loading strategies. + # Suppose that you have the following two Active Record models: # - # The first one is by using table joins. This was only strategy available - # prior to Rails 2.1. Suppose that you have an Author model with columns - # 'name' and 'age', and a Book model with columns 'name' and 'sales'. Using - # this strategy, Active Record would try to retrieve all data for an author - # and all of its books via a single query: + # class Author < ActiveRecord::Base + # # columns: name, age + # has_many :books + # end # - # SELECT * FROM authors - # LEFT OUTER JOIN books ON authors.id = books.author_id - # WHERE authors.name = 'Ken Akamatsu' + # class Book < ActiveRecord::Base + # # columns: title, sales + # end # - # However, this could result in many rows that contain redundant data. After - # having received the first row, we already have enough data to instantiate - # the Author object. In all subsequent rows, only the data for the joined - # 'books' table is useful; the joined 'authors' data is just redundant, and - # processing this redundant data takes memory and CPU time. The problem - # quickly becomes worse and worse as the level of eager loading increases - # (i.e. if Active Record is to eager load the associations' associations as - # well). + # When you load an author with all associated books Active Record will make + # multiple queries like this: + # + # Author.includes(:books).where(:name => ['bell hooks', 'Homer').to_a + # + # => SELECT `authors`.* FROM `authors` WHERE `name` IN ('bell hooks', 'Homer') + # => SELECT `books`.* FROM `books` WHERE `author_id` IN (2, 5) + # + # Active Record saves the ids of the records from the first query to use in + # the second. Depending on the number of associations involved there can be + # arbitrarily many SQL queries made. + # + # However, if there is a WHERE clause that spans across tables Active + # Record will fall back to a slightly more resource-intensive single query: + # + # Author.includes(:books).where(books: {title: 'Illiad'}).to_a + # => SELECT `authors`.`id` AS t0_r0, `authors`.`name` AS t0_r1, `authors`.`age` AS t0_r2, + # `books`.`id` AS t1_r0, `books`.`title` AS t1_r1, `books`.`sales` AS t1_r2 + # FROM `authors` + # LEFT OUTER JOIN `books` ON `authors`.`id` = `books`.`author_id` + # WHERE `books`.`title` = 'Illiad' + # + # This could result in many rows that contain redundant data and it performs poorly at scale + # and is therefore only used when necessary. # - # The second strategy is to use multiple database queries, one for each - # level of association. Since Rails 2.1, this is the default strategy. In - # situations where a table join is necessary (e.g. when the +:conditions+ - # option references an association's column), it will fallback to the table - # join strategy. class Preloader #:nodoc: extend ActiveSupport::Autoload @@ -169,6 +178,7 @@ module ActiveRecord class NullPreloader def self.new(klass, owners, reflection, preload_scope); self; end def self.run(preloader); end + def self.preloaded_records; []; end end def preloader_for(reflection, owners, rhs_klass) diff --git a/activerecord/lib/active_record/associations/preloader/association.rb b/activerecord/lib/active_record/associations/preloader/association.rb index 33c8619359..a6e1a24360 100644 --- a/activerecord/lib/active_record/associations/preloader/association.rb +++ b/activerecord/lib/active_record/associations/preloader/association.rb @@ -33,7 +33,7 @@ module ActiveRecord end def query_scope(ids) - scope.where(association_key.in(ids)) + scope.where(association_key_name => ids) end def table @@ -104,11 +104,11 @@ module ActiveRecord end def association_key_type - @klass.column_for_attribute(association_key_name).type + @klass.type_for_attribute(association_key_name.to_s).type end def owner_key_type - @model.column_for_attribute(owner_key_name).type + @model.type_for_attribute(owner_key_name.to_s).type end def load_slices(slices) @@ -142,19 +142,18 @@ module ActiveRecord scope._select! preload_values[:select] || values[:select] || table[Arel.star] scope.includes! preload_values[:includes] || values[:includes] + scope.joins! preload_values[:joins] || values[:joins] + scope.order! preload_values[:order] || values[:order] - if preload_values.key? :order - scope.order! preload_values[:order] - else - if values.key? :order - scope.order! values[:order] - end + if preload_values[:readonly] || values[:readonly] + scope.readonly! end if options[:as] scope.where!(klass.table_name => { reflection.type => model.base_class.sti_name }) end + scope.unscope_values = Array(values[:unscope]) klass.default_scoped.merge(scope) end end diff --git a/activerecord/lib/active_record/associations/preloader/has_many_through.rb b/activerecord/lib/active_record/associations/preloader/has_many_through.rb index 7b37b5942d..2029871f39 100644 --- a/activerecord/lib/active_record/associations/preloader/has_many_through.rb +++ b/activerecord/lib/active_record/associations/preloader/has_many_through.rb @@ -8,7 +8,7 @@ module ActiveRecord records_by_owner = super if reflection_scope.distinct_value - records_by_owner.each_value { |records| records.uniq! } + records_by_owner.each_value(&:uniq!) end records_by_owner diff --git a/activerecord/lib/active_record/associations/preloader/through_association.rb b/activerecord/lib/active_record/associations/preloader/through_association.rb index 1fed7f74e7..12bf3ef138 100644 --- a/activerecord/lib/active_record/associations/preloader/through_association.rb +++ b/activerecord/lib/active_record/associations/preloader/through_association.rb @@ -63,7 +63,7 @@ module ActiveRecord should_reset = (through_scope != through_reflection.klass.unscoped) || (reflection.options[:source_type] && through_reflection.collection?) - # Dont cache the association - we would only be caching a subset + # Don't cache the association - we would only be caching a subset if should_reset owners.each { |owner| owner.association(association_name).reset @@ -81,6 +81,7 @@ module ActiveRecord unless reflection_scope.where_values.empty? scope.includes_values = Array(reflection_scope.values[:includes] || options[:source]) scope.where_values = reflection_scope.values[:where] + scope.bind_values = reflection_scope.bind_values end scope.references! reflection_scope.values[:references] diff --git a/activerecord/lib/active_record/associations/singular_association.rb b/activerecord/lib/active_record/associations/singular_association.rb index f2e3a4e40f..c44242a0f0 100644 --- a/activerecord/lib/active_record/associations/singular_association.rb +++ b/activerecord/lib/active_record/associations/singular_association.rb @@ -39,7 +39,13 @@ module ActiveRecord end def get_records - return scope.limit(1).to_a if reflection.scope_chain.any?(&:any?) + if reflection.scope_chain.any?(&:any?) || + scope.eager_loading? || + klass.current_scope || + klass.default_scopes.any? + + return scope.limit(1).to_a + end conn = klass.connection sc = reflection.association_scope_cache(conn, owner) do diff --git a/activerecord/lib/active_record/associations/through_association.rb b/activerecord/lib/active_record/associations/through_association.rb index f00fef8b9e..e47e81aa0f 100644 --- a/activerecord/lib/active_record/associations/through_association.rb +++ b/activerecord/lib/active_record/associations/through_association.rb @@ -3,7 +3,7 @@ module ActiveRecord module Associations module ThroughAssociation #:nodoc: - delegate :source_reflection, :through_reflection, :chain, :to => :reflection + delegate :source_reflection, :through_reflection, :to => :reflection protected @@ -13,9 +13,13 @@ module ActiveRecord # 2. To get the type conditions for any STI models in the chain def target_scope scope = super - chain.drop(1).each do |reflection| + reflection.chain.drop(1).each do |reflection| relation = reflection.klass.all - relation.merge!(reflection.scope) if reflection.scope + + reflection_scope = reflection.scope + if reflection_scope && reflection_scope.arity.zero? + relation.merge!(reflection_scope) + end scope.merge!( relation.except(:select, :create_with, :includes, :preload, :joins, :eager_load) @@ -77,7 +81,7 @@ module ActiveRecord end def ensure_mutable - if source_reflection.macro != :belongs_to + unless source_reflection.belongs_to? raise HasManyThroughCantAssociateThroughHasOneOrManyReflection.new(owner, reflection) end end diff --git a/activerecord/lib/active_record/attribute.rb b/activerecord/lib/active_record/attribute.rb index f78cde23c5..88536eaac0 100644 --- a/activerecord/lib/active_record/attribute.rb +++ b/activerecord/lib/active_record/attribute.rb @@ -1,38 +1,86 @@ module ActiveRecord class Attribute # :nodoc: class << self - def from_database(value, type) - FromDatabase.new(value, type) + def from_database(name, value, type) + FromDatabase.new(name, value, type) end - def from_user(value, type) - FromUser.new(value, type) + def from_user(name, value, type) + FromUser.new(name, value, type) + end + + def with_cast_value(name, value, type) + WithCastValue.new(name, value, type) + end + + def null(name) + Null.new(name) + end + + def uninitialized(name, type) + Uninitialized.new(name, type) end end - attr_reader :value_before_type_cast, :type + attr_reader :name, :value_before_type_cast, :type # This method should not be called directly. # Use #from_database or #from_user - def initialize(value_before_type_cast, type) + def initialize(name, value_before_type_cast, type) + @name = name @value_before_type_cast = value_before_type_cast @type = type end def value # `defined?` is cheaper than `||=` when we get back falsy values - @value = type_cast(value_before_type_cast) unless defined?(@value) + @value = original_value unless defined?(@value) @value end + def original_value + type_cast(value_before_type_cast) + end + def value_for_database type.type_cast_for_database(value) end - def type_cast + def changed_from?(old_value) + type.changed?(old_value, value, value_before_type_cast) + end + + def changed_in_place_from?(old_value) + type.changed_in_place?(old_value, value) + end + + def with_value_from_user(value) + self.class.from_user(name, value, type) + end + + def with_value_from_database(value) + self.class.from_database(name, value, type) + end + + def with_cast_value(value) + self.class.with_cast_value(name, value, type) + end + + def type_cast(*) raise NotImplementedError end + def initialized? + true + end + + def ==(other) + self.class == other.class && + name == other.name && + value_before_type_cast == other.value_before_type_cast && + type == other.type + end + protected def initialize_dup(other) @@ -41,16 +89,61 @@ module ActiveRecord end end - class FromDatabase < Attribute + class FromDatabase < Attribute # :nodoc: def type_cast(value) type.type_cast_from_database(value) end end - class FromUser < Attribute + class FromUser < Attribute # :nodoc: def type_cast(value) type.type_cast_from_user(value) end end + + class WithCastValue < Attribute # :nodoc: + def type_cast(value) + value + end + + def changed_in_place_from?(old_value) + false + end + end + + class Null < Attribute # :nodoc: + def initialize(name) + super(name, nil, Type::Value.new) + end + + def value + nil + end + + def with_value_from_database(value) + raise ActiveModel::MissingAttributeError, "can't write unknown attribute `#{name}`" + end + alias_method :with_value_from_user, :with_value_from_database + end + + class Uninitialized < Attribute # :nodoc: + def initialize(name, type) + super(name, nil, type) + end + + def value + if block_given? + yield name + end + end + + def value_for_database + end + + def initialized? + false + end + end + private_constant :FromDatabase, :FromUser, :Null, :Uninitialized end end diff --git a/activerecord/lib/active_record/attribute_assignment.rb b/activerecord/lib/active_record/attribute_assignment.rb index 40e2918777..bf64830417 100644 --- a/activerecord/lib/active_record/attribute_assignment.rb +++ b/activerecord/lib/active_record/attribute_assignment.rb @@ -87,7 +87,7 @@ module ActiveRecord end end unless errors.empty? - error_descriptions = errors.map { |ex| ex.message }.join(",") + error_descriptions = errors.map(&:message).join(",") raise MultiparameterAssignmentErrors.new(errors), "#{errors.size} error(s) on assignment of multiparameter attributes [#{error_descriptions}]" end end @@ -115,7 +115,7 @@ module ActiveRecord end class MultiparameterAttribute #:nodoc: - attr_reader :object, :name, :values, :column + attr_reader :object, :name, :values, :cast_type def initialize(object, name, values) @object = object @@ -126,22 +126,22 @@ module ActiveRecord def read_value return if values.values.compact.empty? - @column = object.column_for_attribute(name) - klass = column ? column.klass : nil + @cast_type = object.type_for_attribute(name) + klass = cast_type.klass if klass == Time read_time elsif klass == Date read_date else - read_other(klass) + read_other end end private def instantiate_time_object(set_values) - if object.class.send(:create_time_zone_conversion_attribute?, name, column) + if object.class.send(:create_time_zone_conversion_attribute?, name, cast_type) Time.zone.local(*set_values) else Time.send(object.class.default_timezone, *set_values) @@ -151,7 +151,7 @@ module ActiveRecord def read_time # If column is a :time (and not :date or :datetime) there is no need to validate if # there are year/month/day fields - if column.type == :time + if cast_type.type == :time # if the column is a time set the values to their defaults as January 1, 1970, but only if they're nil { 1 => 1970, 2 => 1, 3 => 1 }.each do |key,value| values[key] ||= value @@ -181,7 +181,7 @@ module ActiveRecord end end - def read_other(klass) + def read_other max_position = extract_max_param positions = (1..max_position) validate_required_parameters!(positions) diff --git a/activerecord/lib/active_record/attribute_decorators.rb b/activerecord/lib/active_record/attribute_decorators.rb index 596161f81d..5b96623b6e 100644 --- a/activerecord/lib/active_record/attribute_decorators.rb +++ b/activerecord/lib/active_record/attribute_decorators.rb @@ -4,31 +4,63 @@ module ActiveRecord included do class_attribute :attribute_type_decorations, instance_accessor: false # :internal: - self.attribute_type_decorations = Hash.new({}) + self.attribute_type_decorations = TypeDecorator.new end - module ClassMethods + module ClassMethods # :nodoc: def decorate_attribute_type(column_name, decorator_name, &block) + matcher = ->(name, _) { name == column_name.to_s } + key = "_#{column_name}_#{decorator_name}" + decorate_matching_attribute_types(matcher, key, &block) + end + + def decorate_matching_attribute_types(matcher, decorator_name, &block) clear_caches_calculated_from_columns - column_name = column_name.to_s + decorator_name = decorator_name.to_s # Create new hashes so we don't modify parent classes - decorations_for_column = attribute_type_decorations[column_name] - new_decorations = decorations_for_column.merge(decorator_name.to_s => block) - self.attribute_type_decorations = attribute_type_decorations.merge(column_name => new_decorations) + self.attribute_type_decorations = attribute_type_decorations.merge(decorator_name => [matcher, block]) end private def add_user_provided_columns(*) super.map do |column| - decorations = attribute_type_decorations[column.name].values - decorated_type = decorations.inject(column.cast_type) do |type, block| - block.call(type) - end + decorated_type = attribute_type_decorations.apply(column.name, column.cast_type) column.with_type(decorated_type) end end end + + class TypeDecorator # :nodoc: + delegate :clear, to: :@decorations + + def initialize(decorations = {}) + @decorations = decorations + end + + def merge(*args) + TypeDecorator.new(@decorations.merge(*args)) + end + + def apply(name, type) + decorations = decorators_for(name, type) + decorations.inject(type) do |new_type, block| + block.call(new_type) + end + end + + private + + def decorators_for(name, type) + matching(name, type).map(&:last) + end + + def matching(name, type) + @decorations.values.select do |(matcher, _)| + matcher.call(name, type) + end + end + end end end diff --git a/activerecord/lib/active_record/attribute_methods.rb b/activerecord/lib/active_record/attribute_methods.rb index b4d75d6556..b7edac791e 100644 --- a/activerecord/lib/active_record/attribute_methods.rb +++ b/activerecord/lib/active_record/attribute_methods.rb @@ -1,4 +1,5 @@ require 'active_support/core_ext/enumerable' +require 'active_support/core_ext/string/filters' require 'mutex_m' require 'thread_safe' @@ -31,7 +32,7 @@ module ActiveRecord end } - BLACKLISTED_CLASS_METHODS = %w(private public protected) + BLACKLISTED_CLASS_METHODS = %w(private public protected allocate new name parent superclass) class AttributeMethodCache def initialize @@ -57,6 +58,8 @@ module ActiveRecord end end + class GeneratedAttributeMethods < Module; end # :nodoc: + module ClassMethods def inherited(child_class) #:nodoc: child_class.initialize_generated_modules @@ -64,16 +67,18 @@ module ActiveRecord end def initialize_generated_modules # :nodoc: - @generated_attribute_methods = Module.new { extend Mutex_m } + @generated_attribute_methods = GeneratedAttributeMethods.new { extend Mutex_m } @attribute_methods_generated = false include @generated_attribute_methods + + super end # Generates all the attribute related methods for columns in the database # accessors, mutators and query methods. def define_attribute_methods # :nodoc: return false if @attribute_methods_generated - # Use a mutex; we don't want two thread simultaneously trying to define + # Use a mutex; we don't want two threads simultaneously trying to define # attribute methods. generated_attribute_methods.synchronize do return false if @attribute_methods_generated @@ -86,7 +91,7 @@ module ActiveRecord def undefine_attribute_methods # :nodoc: generated_attribute_methods.synchronize do - super if @attribute_methods_generated + super if defined?(@attribute_methods_generated) && @attribute_methods_generated @attribute_methods_generated = false end end @@ -107,16 +112,17 @@ module ActiveRecord # # => false def instance_method_already_implemented?(method_name) if dangerous_attribute_method?(method_name) - raise DangerousAttributeError, "#{method_name} is defined by Active Record" + raise DangerousAttributeError, "#{method_name} is defined by Active Record. Check to make sure that you don't have an attribute or method with the same name." end if superclass == Base super else - # If B < A and A defines its own attribute method, then we don't want to overwrite that. - defined = method_defined_within?(method_name, superclass, superclass.generated_attribute_methods) - base_defined = Base.method_defined?(method_name) || Base.private_method_defined?(method_name) - defined && !base_defined || super + # If ThisClass < ... < SomeSuperClass < ... < Base and SomeSuperClass + # defines its own attribute method, then we don't want to overwrite that. + defined = method_defined_within?(method_name, superclass, Base) && + ! superclass.instance_method(method_name).owner.is_a?(GeneratedAttributeMethods) + defined || super end end @@ -144,7 +150,7 @@ module ActiveRecord BLACKLISTED_CLASS_METHODS.include?(method_name.to_s) || class_method_defined_within?(method_name, Base) end - def class_method_defined_within?(name, klass, superklass = klass.superclass) # :nodoc + def class_method_defined_within?(name, klass, superklass = klass.superclass) # :nodoc: if klass.respond_to?(name, true) if superklass.respond_to?(name, true) klass.method(name).owner != superklass.method(name).owner @@ -186,29 +192,33 @@ module ActiveRecord end # Returns the column object for the named attribute. - # Returns a +ActiveRecord::ConnectionAdapters::NullColumn+ if the - # named attribute does not exist. + # Returns nil if the named attribute does not exist. # # class Person < ActiveRecord::Base # end # # person = Person.new # person.column_for_attribute(:name) # the result depends on the ConnectionAdapter - # # => #<ActiveRecord::ConnectionAdapters::SQLite3Column:0x007ff4ab083980 @name="name", @sql_type="varchar(255)", @null=true, ...> + # # => #<ActiveRecord::ConnectionAdapters::Column:0x007ff4ab083980 @name="name", @sql_type="varchar(255)", @null=true, ...> # # person.column_for_attribute(:nothing) - # # => #<ActiveRecord::ConnectionAdapters::NullColumn:0xXXX @name=nil, @sql_type=nil, @cast_type=#<Type::Value>, ...> + # # => nil def column_for_attribute(name) - name = name.to_s - columns_hash.fetch(name) do - ConnectionAdapters::NullColumn.new(name) + column = columns_hash[name.to_s] + if column.nil? + ActiveSupport::Deprecation.warn(<<-MSG.squish) + `#column_for_attribute` will return a null object for non-existent + columns in Rails 5. Use `#has_attribute?` if you need to check for + an attribute's existence. + MSG end + column end end # A Person object with a name attribute can ask <tt>person.respond_to?(:name)</tt>, # <tt>person.respond_to?(:name=)</tt>, and <tt>person.respond_to?(:name?)</tt> - # which will all return +true+. It also define the attribute methods if they have + # which will all return +true+. It also defines the attribute methods if they have # not been generated. # # class Person < ActiveRecord::Base @@ -230,7 +240,7 @@ module ActiveRecord # For queries selecting a subset of columns, return false for unselected columns. # We check defined?(@attributes) not to issue warnings if called on objects that # have been allocated but not yet initialized. - if defined?(@attributes) && @attributes.any? && self.class.column_names.include?(name) + if defined?(@attributes) && self.class.column_names.include?(name) return has_attribute?(name) end @@ -247,7 +257,7 @@ module ActiveRecord # person.has_attribute?('age') # => true # person.has_attribute?(:nothing) # => false def has_attribute?(attr_name) - @attributes.has_key?(attr_name.to_s) + @attributes.key?(attr_name.to_s) end # Returns an array of names for the attributes available on this object. @@ -271,15 +281,13 @@ module ActiveRecord # person.attributes # # => {"id"=>3, "created_at"=>Sun, 21 Oct 2012 04:53:04, "updated_at"=>Sun, 21 Oct 2012 04:53:04, "name"=>"Francesco", "age"=>22} def attributes - attribute_names.each_with_object({}) { |name, attrs| - attrs[name] = read_attribute(name) - } + @attributes.to_hash end # Returns an <tt>#inspect</tt>-like string for the value of the - # attribute +attr_name+. String attributes are truncated upto 50 + # attribute +attr_name+. String attributes are truncated up to 50 # characters, Date and Time attributes are returned in the - # <tt>:db</tt> format, Array attributes are truncated upto 10 values. + # <tt>:db</tt> format, Array attributes are truncated up to 10 values. # Other attributes return the value of <tt>#inspect</tt> without # modification. # @@ -324,7 +332,7 @@ module ActiveRecord # task.attribute_present?(:title) # => true # task.attribute_present?(:is_done) # => true def attribute_present?(attribute) - value = read_attribute(attribute) + value = _read_attribute(attribute) !value.nil? && !(value.respond_to?(:empty?) && value.empty?) end @@ -367,12 +375,6 @@ module ActiveRecord protected - def clone_attributes # :nodoc: - @attributes.each_with_object({}) do |(name, attr), h| - h[name] = attr.dup - end - end - def clone_attribute_value(reader_method, attribute_name) # :nodoc: value = send(reader_method, attribute_name) value.duplicable? ? value.clone : value @@ -390,7 +392,7 @@ module ActiveRecord def attribute_method?(attr_name) # :nodoc: # We check defined? because Syck calls respond_to? before actually calling initialize. - defined?(@attributes) && @attributes.include?(attr_name) + defined?(@attributes) && @attributes.key?(attr_name) end private @@ -431,7 +433,7 @@ module ActiveRecord end def typecasted_attribute_value(name) - read_attribute(name) + _read_attribute(name) end end end diff --git a/activerecord/lib/active_record/attribute_methods/before_type_cast.rb b/activerecord/lib/active_record/attribute_methods/before_type_cast.rb index d057f0941a..fd61febd57 100644 --- a/activerecord/lib/active_record/attribute_methods/before_type_cast.rb +++ b/activerecord/lib/active_record/attribute_methods/before_type_cast.rb @@ -43,9 +43,7 @@ module ActiveRecord # task.read_attribute_before_type_cast('completed_on') # => "2012-10-21" # task.read_attribute_before_type_cast(:completed_on) # => "2012-10-21" def read_attribute_before_type_cast(attr_name) - if attr = @attributes[attr_name.to_s] - attr.value_before_type_cast - end + @attributes[attr_name.to_s].value_before_type_cast end # Returns a hash of attributes before typecasting and deserialization. @@ -59,7 +57,7 @@ module ActiveRecord # task.attributes_before_type_cast # # => {"id"=>nil, "title"=>nil, "is_done"=>true, "completed_on"=>"2012-10-21", "created_at"=>nil, "updated_at"=>nil} def attributes_before_type_cast - @attributes.each_with_object({}) { |(k, v), h| h[k] = v.value_before_type_cast } + @attributes.values_before_type_cast end private diff --git a/activerecord/lib/active_record/attribute_methods/dirty.rb b/activerecord/lib/active_record/attribute_methods/dirty.rb index 6a5c057384..d5702accaf 100644 --- a/activerecord/lib/active_record/attribute_methods/dirty.rb +++ b/activerecord/lib/active_record/attribute_methods/dirty.rb @@ -34,55 +34,56 @@ module ActiveRecord # <tt>reload</tt> the record and clears changed attributes. def reload(*) super.tap do - reset_changes + clear_changes_information end end def initialize_dup(other) # :nodoc: super - init_changed_attributes + calculate_changes_from_defaults end - def changed? - super || changed_in_place.any? + def changes_applied + super + store_original_raw_attributes end - def changed - super | changed_in_place + def clear_changes_information + super + original_raw_attributes.clear end - def attribute_changed?(attr_name, options = {}) - result = super - # We can't change "from" something in place. Only setters can define - # "from" and "to" - result ||= changed_in_place?(attr_name) unless options.key?(:from) - result + def changed_attributes + # This should only be set by methods which will call changed_attributes + # multiple times when it is known that the computed value cannot change. + if defined?(@cached_changed_attributes) + @cached_changed_attributes + else + super.reverse_merge(attributes_changed_in_place).freeze + end end - def changes_applied - super - store_original_raw_attributes + def changes + cache_changed_attributes do + super + end end - def reset_changes - super - original_raw_attributes.clear + def attribute_changed_in_place?(attr_name) + old_value = original_raw_attribute(attr_name) + @attributes[attr_name].changed_in_place_from?(old_value) end private - def initialize_internals_callback - super - init_changed_attributes + def changes_include?(attr_name) + super || attribute_changed_in_place?(attr_name) end - def init_changed_attributes + def calculate_changes_from_defaults @changed_attributes = nil - # Intentionally avoid using #column_defaults since overridden defaults (as is done in - # optimistic locking) won't get written unless they get marked as changed - self.class.columns.each do |c| - attr, orig_value = c.name, c.default - changed_attributes[attr] = orig_value if _field_changed?(attr, orig_value) + self.class.column_defaults.each do |attr, orig_value| + set_attribute_was(attr, orig_value) if _field_changed?(attr, orig_value) end end @@ -108,9 +109,9 @@ module ActiveRecord def save_changed_attribute(attr, old_value) if attribute_changed?(attr) - changed_attributes.delete(attr) unless _field_changed?(attr, old_value) + clear_attribute_changes(attr) unless _field_changed?(attr, old_value) else - changed_attributes[attr] = old_value if _field_changed?(attr, old_value) + set_attribute_was(attr, old_value) if _field_changed?(attr, old_value) end end @@ -118,7 +119,7 @@ module ActiveRecord if attribute_changed?(attr) changed_attributes[attr] else - clone_attribute_value(:read_attribute, attr) + clone_attribute_value(:_read_attribute, attr) end end @@ -137,22 +138,20 @@ module ActiveRecord end def _field_changed?(attr, old_value) - new_value = read_attribute(attr) - raw_value = read_attribute_before_type_cast(attr) - column_for_attribute(attr).changed?(old_value, new_value, raw_value) + @attributes[attr].changed_from?(old_value) end - def changed_in_place - self.class.attribute_names.select do |attr_name| - changed_in_place?(attr_name) + def attributes_changed_in_place + changed_in_place.each_with_object({}) do |attr_name, h| + orig = @attributes[attr_name].original_value + h[attr_name] = orig end end - def changed_in_place?(attr_name) - type = type_for_attribute(attr_name) - old_value = original_raw_attribute(attr_name) - value = read_attribute(attr_name) - type.changed_in_place?(old_value, value) + def changed_in_place + self.class.attribute_names.select do |attr_name| + attribute_changed_in_place?(attr_name) + end end def original_raw_attribute(attr_name) @@ -166,9 +165,7 @@ module ActiveRecord end def store_original_raw_attribute(attr_name) - type = type_for_attribute(attr_name) - value = type.type_cast_for_database(read_attribute(attr_name)) - original_raw_attributes[attr_name] = value + original_raw_attributes[attr_name] = @attributes[attr_name].value_for_database end def store_original_raw_attributes @@ -176,6 +173,13 @@ module ActiveRecord store_original_raw_attribute(attr) end end + + def cache_changed_attributes + @cached_changed_attributes = changed_attributes + yield + ensure + remove_instance_variable(:@cached_changed_attributes) + end end end end diff --git a/activerecord/lib/active_record/attribute_methods/primary_key.rb b/activerecord/lib/active_record/attribute_methods/primary_key.rb index 1c81a5b71b..c28374e4ab 100644 --- a/activerecord/lib/active_record/attribute_methods/primary_key.rb +++ b/activerecord/lib/active_record/attribute_methods/primary_key.rb @@ -17,7 +17,7 @@ module ActiveRecord def id if pk = self.class.primary_key sync_with_transaction_state - read_attribute(pk) + _read_attribute(pk) end end @@ -39,6 +39,12 @@ module ActiveRecord read_attribute_before_type_cast(self.class.primary_key) end + # Returns the primary key previous value. + def id_was + sync_with_transaction_state + attribute_was(self.class.primary_key) + end + protected def attribute_method?(attr_name) @@ -54,7 +60,7 @@ module ActiveRecord end end - ID_ATTRIBUTE_METHODS = %w(id id= id? id_before_type_cast).to_set + ID_ATTRIBUTE_METHODS = %w(id id= id? id_before_type_cast id_was).to_set def dangerous_attribute_method?(method_name) super && !ID_ATTRIBUTE_METHODS.include?(method_name) @@ -83,12 +89,9 @@ module ActiveRecord end def get_primary_key(base_name) #:nodoc: - return 'id' if base_name.blank? - - case primary_key_prefix_type - when :table_name + if base_name && primary_key_prefix_type == :table_name base_name.foreign_key(false) - when :table_name_with_underscore + elsif base_name && primary_key_prefix_type == :table_name_with_underscore base_name.foreign_key else if ActiveRecord::Base != self && table_exists? @@ -117,6 +120,7 @@ module ActiveRecord def primary_key=(value) @primary_key = value && value.to_s @quoted_primary_key = nil + @attributes_builder = nil end end end diff --git a/activerecord/lib/active_record/attribute_methods/query.rb b/activerecord/lib/active_record/attribute_methods/query.rb index 0f9723febb..dc689f399a 100644 --- a/activerecord/lib/active_record/attribute_methods/query.rb +++ b/activerecord/lib/active_record/attribute_methods/query.rb @@ -8,7 +8,7 @@ module ActiveRecord end def query_attribute(attr_name) - value = read_attribute(attr_name) { |n| missing_attribute(n, caller) } + value = self[attr_name] case value when true then true diff --git a/activerecord/lib/active_record/attribute_methods/read.rb b/activerecord/lib/active_record/attribute_methods/read.rb index 8c1cc128f7..20f0936e52 100644 --- a/activerecord/lib/active_record/attribute_methods/read.rb +++ b/activerecord/lib/active_record/attribute_methods/read.rb @@ -27,7 +27,7 @@ module ActiveRecord <<-EOMETHOD def #{method_name} name = ::ActiveRecord::AttributeMethods::AttrNames::ATTR_#{const_name} - read_attribute(name) { |n| missing_attribute(n, caller) } + _read_attribute(name) { |n| missing_attribute(n, caller) } end EOMETHOD end @@ -46,9 +46,7 @@ module ActiveRecord protected def cached_attributes_deprecation_warning(method_name) - ActiveSupport::Deprecation.warn(<<-MESSAGE.strip_heredoc) - Calling `#{method_name}` is no longer necessary. All attributes are cached. - MESSAGE + ActiveSupport::Deprecation.warn "Calling `#{method_name}` is no longer necessary. All attributes are cached." end if Module.methods_transplantable? @@ -66,7 +64,7 @@ module ActiveRecord generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ + 1 def #{temp_method} name = ::ActiveRecord::AttributeMethods::AttrNames::ATTR_#{safe_name} - read_attribute(name) { |n| missing_attribute(n, caller) } + _read_attribute(name) { |n| missing_attribute(n, caller) } end STR @@ -78,26 +76,27 @@ module ActiveRecord end end + ID = 'id'.freeze + # Returns the value of the attribute identified by <tt>attr_name</tt> after # it has been typecast (for example, "2004-12-12" in a date column is cast # to a date object, like Date.new(2004, 12, 12)). - def read_attribute(attr_name) + def read_attribute(attr_name, &block) name = attr_name.to_s - @attributes.fetch(name) { - if name == 'id' - return read_attribute(self.class.primary_key) - elsif block_given? && self.class.columns_hash.key?(name) - return yield(name) - else - return nil - end - }.value + name = self.class.primary_key if name == ID + _read_attribute(name, &block) + end + + # This method exists to avoid the expensive primary_key check internally, without + # breaking compatibility with the read_attribute API + def _read_attribute(attr_name) # :nodoc: + @attributes.fetch_value(attr_name.to_s) { |n| yield n if block_given? } end private def attribute(attribute_name) - read_attribute(attribute_name) + _read_attribute(attribute_name) end end end diff --git a/activerecord/lib/active_record/attribute_methods/serialization.rb b/activerecord/lib/active_record/attribute_methods/serialization.rb index 734d94865a..e5ec5ddca5 100644 --- a/activerecord/lib/active_record/attribute_methods/serialization.rb +++ b/activerecord/lib/active_record/attribute_methods/serialization.rb @@ -1,3 +1,5 @@ +require 'active_support/core_ext/string/filters' + module ActiveRecord module AttributeMethods module Serialization @@ -8,11 +10,8 @@ module ActiveRecord # object, and retrieved as the same object, then specify the name of that # attribute using this method and it will be handled automatically. The # serialization is done through YAML. If +class_name+ is specified, the - # serialized object must be of that class on retrieval or - # <tt>SerializationTypeMismatch</tt> will be raised. - # - # A notable side effect of serialized attributes is that the model will - # be updated on every save, even if it is not dirty. + # serialized object must be of that class on assignment and retrieval. + # Otherwise <tt>SerializationTypeMismatch</tt> will be raised. # # ==== Parameters # @@ -37,7 +36,12 @@ module ActiveRecord # serialize :preferences, Hash # end def serialize(attr_name, class_name_or_coder = Object) - coder = if [:load, :dump].all? { |x| class_name_or_coder.respond_to?(x) } + # When ::JSON is used, force it to go through the Active Support JSON encoder + # to ensure special objects (e.g. Active Record models) are dumped correctly + # using the #as_json hook. + coder = if class_name_or_coder == ::JSON + Coders::JSON + elsif [:load, :dump].all? { |x| class_name_or_coder.respond_to?(x) } class_name_or_coder else Coders::YAMLColumn.new(class_name_or_coder) @@ -49,10 +53,11 @@ module ActiveRecord end def serialized_attributes - ActiveSupport::Deprecation.warn(<<-WARNING.strip_heredoc) + ActiveSupport::Deprecation.warn(<<-MSG.squish) `serialized_attributes` is deprecated without replacement, and will be removed in Rails 5.0. - WARNING + MSG + @serialized_attributes ||= Hash[ columns.select { |t| t.cast_type.is_a?(Type::Serialized) }.map { |c| [c.name, c.cast_type.coder] 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 abad949ef4..777f7ab4d7 100644 --- a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb +++ b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb @@ -1,7 +1,9 @@ module ActiveRecord module AttributeMethods module TimeZoneConversion - class Type < SimpleDelegator # :nodoc: + class TimeZoneConverter < DelegateClass(Type::Value) # :nodoc: + include Type::Decorator + def type_cast_from_database(value) convert_time_to_time_zone(super) end @@ -10,7 +12,11 @@ module ActiveRecord if value.is_a?(Array) value.map { |v| type_cast_from_user(v) } elsif value.respond_to?(:in_time_zone) - value.in_time_zone + begin + value.in_time_zone || super + rescue ArgumentError + nil + end end end @@ -37,10 +43,25 @@ module ActiveRecord module ClassMethods private - def create_time_zone_conversion_attribute?(name, column) + + def inherited(subclass) + # We need to apply this decorator here, rather than on module inclusion. The closure + # created by the matcher would otherwise evaluate for `ActiveRecord::Base`, not the + # sub class being decorated. As such, changes to `time_zone_aware_attributes`, or + # `skip_time_zone_conversion_for_attributes` would not be picked up. + subclass.class_eval do + matcher = ->(name, type) { create_time_zone_conversion_attribute?(name, type) } + decorate_matching_attribute_types(matcher, :_time_zone_conversion) do |type| + TimeZoneConverter.new(type) + end + end + super + end + + def create_time_zone_conversion_attribute?(name, cast_type) time_zone_aware_attributes && !self.skip_time_zone_conversion_for_attributes.include?(name.to_sym) && - (:datetime == column.type) + (:datetime == cast_type.type) end end end diff --git a/activerecord/lib/active_record/attribute_methods/write.rb b/activerecord/lib/active_record/attribute_methods/write.rb index 246a2cd8ba..16804f86bf 100644 --- a/activerecord/lib/active_record/attribute_methods/write.rb +++ b/activerecord/lib/active_record/attribute_methods/write.rb @@ -69,16 +69,11 @@ module ActiveRecord def write_attribute_with_type_cast(attr_name, value, should_type_cast) attr_name = attr_name.to_s attr_name = self.class.primary_key if attr_name == 'id' && self.class.primary_key - type = type_for_attribute(attr_name) - - unless has_attribute?(attr_name) || self.class.columns_hash.key?(attr_name) - raise ActiveModel::MissingAttributeError, "can't write unknown attribute `#{attr_name}'" - end if should_type_cast - @attributes[attr_name] = Attribute.from_user(value, type) + @attributes.write_from_user(attr_name, value) else - @attributes[attr_name] = Attribute.from_database(value, type) + @attributes.write_cast_value(attr_name, value) end value diff --git a/activerecord/lib/active_record/attribute_set.rb b/activerecord/lib/active_record/attribute_set.rb new file mode 100644 index 0000000000..66fcaf6945 --- /dev/null +++ b/activerecord/lib/active_record/attribute_set.rb @@ -0,0 +1,77 @@ +require 'active_record/attribute_set/builder' + +module ActiveRecord + class AttributeSet # :nodoc: + def initialize(attributes) + @attributes = attributes + end + + def [](name) + attributes[name] || Attribute.null(name) + end + + def values_before_type_cast + attributes.transform_values(&:value_before_type_cast) + end + + def to_hash + initialized_attributes.transform_values(&:value) + end + alias_method :to_h, :to_hash + + def key?(name) + attributes.key?(name) && self[name].initialized? + end + + def keys + attributes.initialized_keys + end + + def fetch_value(name) + self[name].value { |n| yield n if block_given? } + end + + def write_from_database(name, value) + attributes[name] = self[name].with_value_from_database(value) + end + + def write_from_user(name, value) + attributes[name] = self[name].with_value_from_user(value) + end + + def write_cast_value(name, value) + attributes[name] = self[name].with_cast_value(value) + end + + def freeze + @attributes.freeze + super + end + + def initialize_dup(_) + @attributes = attributes.dup + super + end + + def initialize_clone(_) + @attributes = attributes.clone + super + end + + def reset(key) + if key?(key) + write_from_database(key, nil) + end + end + + protected + + attr_reader :attributes + + private + + def initialized_attributes + attributes.select { |_, attr| attr.initialized? } + end + end +end diff --git a/activerecord/lib/active_record/attribute_set/builder.rb b/activerecord/lib/active_record/attribute_set/builder.rb new file mode 100644 index 0000000000..3a76f5262d --- /dev/null +++ b/activerecord/lib/active_record/attribute_set/builder.rb @@ -0,0 +1,86 @@ +module ActiveRecord + class AttributeSet # :nodoc: + class Builder # :nodoc: + attr_reader :types, :always_initialized + + def initialize(types, always_initialized = nil) + @types = types + @always_initialized = always_initialized + end + + def build_from_database(values = {}, additional_types = {}) + if always_initialized && !values.key?(always_initialized) + values[always_initialized] = nil + end + + attributes = LazyAttributeHash.new(types, values, additional_types) + AttributeSet.new(attributes) + end + end + end + + class LazyAttributeHash # :nodoc: + delegate :select, :transform_values, to: :materialize + + def initialize(types, values, additional_types) + @types = types + @values = values + @additional_types = additional_types + @materialized = false + @delegate_hash = {} + end + + def key?(key) + delegate_hash.key?(key) || values.key?(key) || types.key?(key) + end + + def [](key) + delegate_hash[key] || assign_default_value(key) + end + + def []=(key, value) + if frozen? + raise RuntimeError, "Can't modify frozen hash" + end + delegate_hash[key] = value + end + + def initialized_keys + delegate_hash.keys | values.keys + end + + def initialize_dup(_) + @delegate_hash = delegate_hash.transform_values(&:dup) + super + end + + protected + + attr_reader :types, :values, :additional_types, :delegate_hash + + private + + def assign_default_value(name) + type = additional_types.fetch(name, types[name]) + value_present = true + value = values.fetch(name) { value_present = false } + + if value_present + delegate_hash[name] = Attribute.from_database(name, value, type) + elsif types.key?(name) + delegate_hash[name] = Attribute.uninitialized(name, type) + end + end + + def materialize + unless @materialized + values.each_key { |key| self[key] } + types.each_key { |key| self[key] } + unless frozen? + @materialized = true + end + end + delegate_hash + end + end +end diff --git a/activerecord/lib/active_record/attributes.rb b/activerecord/lib/active_record/attributes.rb index 9c80121d70..aafb990bc1 100644 --- a/activerecord/lib/active_record/attributes.rb +++ b/activerecord/lib/active_record/attributes.rb @@ -6,10 +6,12 @@ module ActiveRecord included do class_attribute :user_provided_columns, instance_accessor: false # :internal: + class_attribute :user_provided_defaults, instance_accessor: false # :internal: self.user_provided_columns = {} + self.user_provided_defaults = {} end - module ClassMethods + module ClassMethods # :nodoc: # Defines or overrides a attribute on this model. This allows customization of # Active Record's type casting behavior, as well as adding support for user defined # types. @@ -27,7 +29,7 @@ module ActiveRecord # # ==== Examples # - # The type detected by Active Record can be overriden. + # The type detected by Active Record can be overridden. # # # db/schema.rb # create_table :store_listings, force: true do |t| @@ -51,9 +53,9 @@ module ActiveRecord # store_listing.price_in_cents # => 10 # # Users may also define their own custom types, as long as they respond to the methods - # defined on the value type. The `type_cast` method on your type object will be called + # defined on the value type. The +type_cast+ method on your type object will be called # with values both from the database, and from your controllers. See - # `ActiveRecord::Attributes::Type::Value` for the expected API. It is recommended that your + # +ActiveRecord::Attributes::Type::Value+ for the expected API. It is recommended that your # type objects inherit from an existing type, or the base value type. # # class MoneyType < ActiveRecord::Type::Integer @@ -77,7 +79,11 @@ module ActiveRecord name = name.to_s clear_caches_calculated_from_columns # Assign a new hash to ensure that subclasses do not share a hash - self.user_provided_columns = user_provided_columns.merge(name => connection.new_column(name, options[:default], cast_type)) + self.user_provided_columns = user_provided_columns.merge(name => cast_type) + + if options.key?(:default) + self.user_provided_defaults = user_provided_defaults.merge(name => options[:default]) + end end # Returns an array of column objects for the table associated with this class. @@ -99,23 +105,35 @@ module ActiveRecord def add_user_provided_columns(schema_columns) existing_columns = schema_columns.map do |column| - user_provided_columns[column.name] || column + new_type = user_provided_columns[column.name] + if new_type + column.with_type(new_type) + else + column + end end existing_column_names = existing_columns.map(&:name) - new_columns = user_provided_columns.except(*existing_column_names).values + new_columns = user_provided_columns.except(*existing_column_names).map do |(name, type)| + connection.new_column(name, nil, type) + end existing_columns + new_columns end def clear_caches_calculated_from_columns + @arel_table = nil + @attributes_builder = nil + @column_names = nil + @column_types = nil @columns = nil @columns_hash = nil - @column_types = nil - @column_defaults = nil - @raw_column_defaults = nil - @column_names = nil @content_columns = nil + @default_attributes = nil + end + + def raw_default_values + super.merge(user_provided_defaults) end end end diff --git a/activerecord/lib/active_record/autosave_association.rb b/activerecord/lib/active_record/autosave_association.rb index b3c3e26c9f..c39b045a5e 100644 --- a/activerecord/lib/active_record/autosave_association.rb +++ b/activerecord/lib/active_record/autosave_association.rb @@ -263,9 +263,9 @@ module ActiveRecord if new_record association && association.target elsif autosave - association.target.find_all { |record| record.changed_for_autosave? } + association.target.find_all(&:changed_for_autosave?) else - association.target.find_all { |record| record.new_record? } + association.target.find_all(&:new_record?) end end @@ -275,7 +275,7 @@ module ActiveRecord self.class._reflections.values.any? do |reflection| if reflection.options[:autosave] association = association_instance_get(reflection.name) - association && Array.wrap(association.target).any? { |a| a.changed_for_autosave? } + association && Array.wrap(association.target).any?(&:changed_for_autosave?) end end end @@ -340,7 +340,6 @@ module ActiveRecord autosave = reflection.options[:autosave] if records = associated_records_to_validate_or_save(association, @new_record_before_save, autosave) - if autosave records_to_destroy = records.select(&:marked_for_destruction?) records_to_destroy.each { |record| association.destroy(record) } @@ -406,7 +405,9 @@ module ActiveRecord # If the record is new or it has changed, returns true. def record_changed?(reflection, record, key) - record.new_record? || record[reflection.foreign_key] != key || record.attribute_changed?(reflection.foreign_key) + record.new_record? || + (record.has_attribute?(reflection.foreign_key) && record[reflection.foreign_key] != key) || + record.attribute_changed?(reflection.foreign_key) end # Saves the associated record if it's new or <tt>:autosave</tt> is enabled. diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index e4d0abb8ef..bb01231bca 100644 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -9,6 +9,7 @@ require 'active_support/core_ext/class/delegating_attributes' require 'active_support/core_ext/array/extract_options' require 'active_support/core_ext/hash/deep_merge' require 'active_support/core_ext/hash/slice' +require 'active_support/core_ext/hash/transform_values' require 'active_support/core_ext/string/behavior' require 'active_support/core_ext/kernel/singleton_class' require 'active_support/core_ext/module/introspection' @@ -21,6 +22,7 @@ require 'active_record/log_subscriber' require 'active_record/explain_subscriber' require 'active_record/relation/delegation' require 'active_record/attributes' +require 'active_record/type_caster' module ActiveRecord #:nodoc: # = Active Record @@ -140,6 +142,7 @@ module ActiveRecord #:nodoc: # # In addition to the basic accessors, query methods are also automatically available on the Active Record object. # Query methods allow you to test whether an attribute value is present. + # Additionally, when dealing with numeric values, a query method will return false if the value is zero. # # For example, an Active Record User with the <tt>name</tt> attribute has a <tt>name?</tt> method that you can call # to determine whether the user has a name: @@ -219,25 +222,9 @@ module ActiveRecord #:nodoc: # # == Single table inheritance # - # Active Record allows inheritance by storing the name of the class in a column that by - # default is named "type" (can be changed by overwriting <tt>Base.inheritance_column</tt>). - # This means that an inheritance looking like this: - # - # class Company < ActiveRecord::Base; end - # class Firm < Company; end - # class Client < Company; end - # class PriorityClient < Client; end - # - # When you do <tt>Firm.create(name: "37signals")</tt>, this record will be saved in - # the companies table with type = "Firm". You can then fetch this row again using - # <tt>Company.where(name: '37signals').first</tt> and it will return a Firm object. - # - # If you don't have a type column defined in your table, single-table inheritance won't - # be triggered. In that case, it'll work just like normal subclasses with no special magic - # for differentiating between them or reloading the right type with find. - # - # Note, all the attributes for all the cases are kept in the same table. Read more: - # http://www.martinfowler.com/eaaCatalog/singleTableInheritance.html + # Active Record allows inheritance by storing the name of the class in a + # column that is named "type" by default. See ActiveRecord::Inheritance for + # more details. # # == Connection to multiple databases in different models # @@ -308,6 +295,8 @@ module ActiveRecord #:nodoc: include Integration include Validations include CounterCache + include Attributes + include AttributeDecorators include Locking::Optimistic include Locking::Pessimistic include AttributeMethods @@ -323,8 +312,6 @@ module ActiveRecord #:nodoc: include Reflection include Serialization include Store - include Attributes - include AttributeDecorators end ActiveSupport.run_load_hooks(:active_record, Base) diff --git a/activerecord/lib/active_record/callbacks.rb b/activerecord/lib/active_record/callbacks.rb index 5955673b42..497ce8c15c 100644 --- a/activerecord/lib/active_record/callbacks.rb +++ b/activerecord/lib/active_record/callbacks.rb @@ -289,25 +289,25 @@ module ActiveRecord end def destroy #:nodoc: - run_callbacks(:destroy) { super } + _run_destroy_callbacks { super } end def touch(*) #:nodoc: - run_callbacks(:touch) { super } + _run_touch_callbacks { super } end private - def create_or_update #:nodoc: - run_callbacks(:save) { super } + def create_or_update(*) #:nodoc: + _run_save_callbacks { super } end def _create_record #:nodoc: - run_callbacks(:create) { super } + _run_create_callbacks { super } end def _update_record(*) #:nodoc: - run_callbacks(:update) { super } + _run_update_callbacks { super } end end end diff --git a/activerecord/lib/active_record/coders/json.rb b/activerecord/lib/active_record/coders/json.rb new file mode 100644 index 0000000000..75d3bfe625 --- /dev/null +++ b/activerecord/lib/active_record/coders/json.rb @@ -0,0 +1,13 @@ +module ActiveRecord + module Coders # :nodoc: + class JSON # :nodoc: + def self.dump(obj) + ActiveSupport::JSON.encode(obj) + end + + def self.load(json) + ActiveSupport::JSON.decode(json) unless json.nil? + end + end + end +end diff --git a/activerecord/lib/active_record/coders/yaml_column.rb b/activerecord/lib/active_record/coders/yaml_column.rb index d3d7396c91..9ea22ed798 100644 --- a/activerecord/lib/active_record/coders/yaml_column.rb +++ b/activerecord/lib/active_record/coders/yaml_column.rb @@ -8,6 +8,7 @@ module ActiveRecord def initialize(object_class = Object) @object_class = object_class + check_arity_of_constructor end def dump(obj) @@ -33,6 +34,16 @@ module ActiveRecord obj end + + private + + def check_arity_of_constructor + begin + load(nil) + rescue ArgumentError + raise ArgumentError, "Cannot serialize #{object_class}. Classes passed to `serialize` must have a 0 argument constructor." + end + end end end end 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 db80c0faee..6235745fb2 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb @@ -2,6 +2,7 @@ require 'thread' require 'thread_safe' require 'monitor' require 'set' +require 'active_support/core_ext/string/filters' module ActiveRecord # Raised when a connection could not be obtained within the connection @@ -121,13 +122,13 @@ module ActiveRecord # greater than the number of threads currently waiting (that # is, don't jump ahead in line). Otherwise, return nil. # - # If +timeout+ is given, block if it there is no element + # If +timeout+ is given, block if there is no element # available, waiting up to +timeout+ seconds for an element to # become available. # # Raises: # - ConnectionTimeoutError if +timeout+ is given and no element - # becomes available after +timeout+ seconds, + # becomes available within +timeout+ seconds, def poll(timeout = nil) synchronize do if timeout @@ -150,7 +151,7 @@ module ActiveRecord end # A thread can remove an element from the queue without - # waiting if an only if the number of currently available + # waiting if and only if the number of currently available # connections is strictly greater than the number of waiting # threads. def can_remove_no_wait? @@ -234,8 +235,8 @@ module ActiveRecord @spec = spec - @checkout_timeout = spec.config[:checkout_timeout] || 5 - @reaper = Reaper.new self, spec.config[:reaping_frequency] + @checkout_timeout = (spec.config[:checkout_timeout] && spec.config[:checkout_timeout].to_f) || 5 + @reaper = Reaper.new(self, (spec.config[:reaping_frequency] && spec.config[:reaping_frequency].to_f)) @reaper.run # default max pool size to 5 @@ -319,9 +320,7 @@ module ActiveRecord checkin conn conn.disconnect! if conn.requires_reloading? end - @connections.delete_if do |conn| - conn.requires_reloading? - end + @connections.delete_if(&:requires_reloading?) @available.clear @connections.each do |conn| @available.add conn @@ -360,7 +359,7 @@ module ActiveRecord synchronize do owner = conn.owner - conn.run_callbacks :checkin do + conn._run_checkin_callbacks do conn.expire end @@ -428,7 +427,9 @@ module ActiveRecord def release(conn, owner) thread_id = owner.object_id - @reserved_connections.delete thread_id + if @reserved_connections[thread_id] == conn + @reserved_connections.delete thread_id + end end def new_connection @@ -449,7 +450,7 @@ module ActiveRecord end def checkout_and_verify(c) - c.run_callbacks :checkout do + c._run_checkout_callbacks do c.verify! end c @@ -462,23 +463,44 @@ module ActiveRecord # # For example, suppose that you have 5 models, with the following hierarchy: # - # | - # +-- Book - # | | - # | +-- ScaryBook - # | +-- GoodBook - # +-- Author - # +-- BankAccount + # class Author < ActiveRecord::Base + # end + # + # class BankAccount < ActiveRecord::Base + # end + # + # class Book < ActiveRecord::Base + # establish_connection "library_db" + # end + # + # class ScaryBook < Book + # end + # + # class GoodBook < Book + # end # - # Suppose that Book is to connect to a separate database (i.e. one other - # than the default database). Then Book, ScaryBook and GoodBook will all use - # the same connection pool. Likewise, Author and BankAccount will use the - # same connection pool. However, the connection pool used by Author/BankAccount - # is not the same as the one used by Book/ScaryBook/GoodBook. + # And a database.yml that looked like this: # - # Normally there is only a single ConnectionHandler instance, accessible via - # ActiveRecord::Base.connection_handler. Active Record models use this to - # determine the connection pool that they should use. + # development: + # database: my_application + # host: localhost + # + # library_db: + # database: library + # host: some.library.org + # + # Your primary database in the development environment is "my_application" + # but the Book model connects to a separate database called "library_db" + # (this can even be a database on a different machine). + # + # Book, ScaryBook and GoodBook will all use the same connection pool to + # "library_db" while Author, BankAccount, and any other models you create + # will use the default connection pool to "my_application". + # + # The various connection pools are managed by a single instance of + # ConnectionHandler accessible via ActiveRecord::Base.connection_handler. + # All Active Record models use this handler to determine the connection pool that they + # should use. class ConnectionHandler def initialize # These caches are keyed by klass.name, NOT klass. Keying them by klass @@ -497,10 +519,11 @@ module ActiveRecord end def connection_pools - ActiveSupport::Deprecation.warn( - "In the next release, this will return the same as #connection_pool_list. " \ - "(An array of pools, rather than a hash mapping specs to pools.)" - ) + ActiveSupport::Deprecation.warn(<<-MSG.squish) + In the next release, this will return the same as `#connection_pool_list`. + (An array of pools, rather than a hash mapping specs to pools.) + MSG + Hash[connection_pool_list.map { |pool| [pool.spec, pool] }] end @@ -619,7 +642,7 @@ module ActiveRecord end def call(env) - testing = env.key?('rack.test') + testing = env['rack.test'] response = @app.call(env) response[2] = ::Rack::BodyProxy.new(response[2]) do 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 7ff5001796..12b16b2473 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb @@ -83,6 +83,11 @@ module ActiveRecord exec_query(sql, name, binds) end + # Executes the truncate statement. + def truncate(table_name, name = nil) + raise NotImplementedError + end + # Executes update +sql+ statement in the context of this connection using # +binds+ as the bind substitutes. +name+ is logged along with # the executed +sql+ statement. @@ -193,7 +198,7 @@ module ActiveRecord # * You are creating a nested (savepoint) transaction # # The mysql, mysql2 and postgresql adapters support setting the transaction - # isolation level. However, support is disabled for mysql versions below 5, + # 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. def transaction(options = {}) @@ -203,62 +208,30 @@ module ActiveRecord if options[:isolation] raise ActiveRecord::TransactionIsolationError, "cannot set isolation when joining a transaction" end - yield else - within_new_transaction(options) { yield } + transaction_manager.within_new_transaction(options) { yield } end rescue ActiveRecord::Rollback # rollbacks are silently swallowed end - def within_new_transaction(options = {}) #:nodoc: - transaction = begin_transaction(options) - yield - rescue Exception => error - rollback_transaction if transaction - raise - ensure - begin - commit_transaction unless error - rescue Exception - rollback_transaction - raise - end - end + attr_reader :transaction_manager #:nodoc: - def open_transactions - @transaction.number - end - - def current_transaction #:nodoc: - @transaction - end + delegate :within_new_transaction, :open_transactions, :current_transaction, :begin_transaction, :commit_transaction, :rollback_transaction, to: :transaction_manager def transaction_open? - @transaction.open? - end - - def begin_transaction(options = {}) #:nodoc: - @transaction = @transaction.begin(options) - end - - def commit_transaction #:nodoc: - @transaction = @transaction.commit - end - - def rollback_transaction #:nodoc: - @transaction = @transaction.rollback + current_transaction.open? end def reset_transaction #:nodoc: - @transaction = ClosedTransaction.new(self) + @transaction_manager = TransactionManager.new(self) end # Register a record with the current transaction so that its after_commit and after_rollback callbacks # can be called. def add_transaction_record(record) - @transaction.add_record(record) + current_transaction.add_record(record) end # Begins the transaction (and turns off auto-committing). @@ -314,10 +287,6 @@ module ActiveRecord "DEFAULT VALUES" end - def limited_update_conditions(where_sql, quoted_table_name, quoted_primary_key) - "WHERE #{quoted_primary_key} IN (SELECT #{quoted_primary_key} FROM #{quoted_table_name} #{where_sql})" - end - # Sanitizes the given LIMIT parameter in order to prevent SQL injection. # # The +limit+ may be anything that can evaluate to a string via #to_s. It @@ -338,8 +307,8 @@ module ActiveRecord end # 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. + # 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 subselect = subquery_for(key, select) @@ -364,8 +333,9 @@ module ActiveRecord # Returns an ActiveRecord::Result instance. def select(sql, name = nil, binds = []) + exec_query(sql, name, binds) end - undef_method :select + # Returns the last auto-generated ID from the affected table. def insert_sql(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) diff --git a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb index 04ae67234f..143d7d9574 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb @@ -9,13 +9,8 @@ module ActiveRecord # records are quoted as their primary key return value.quoted_id if value.respond_to?(:quoted_id) - # FIXME: The only case we get an object other than nil or a real column - # is `SchemaStatements#add_column` with a PG array that has a non-empty default - # value. Is this really the only case? Are we missing tests for other types? - # We should have a real column object passed (or nil) here, and check for that - # instead - if column.respond_to?(:type_cast_for_database) - value = column.type_cast_for_database(value) + if column + value = column.cast_type.type_cast_for_database(value) end _quote(value) @@ -24,18 +19,13 @@ module ActiveRecord # Cast a +value+ to a type that the database understands. For example, # SQLite does not understand dates, so this method will convert a Date # to a String. - def type_cast(value, column) + def type_cast(value, column = nil) if value.respond_to?(:quoted_id) && value.respond_to?(:id) return value.id end - # FIXME: The only case we get an object other than nil or a real column - # is `SchemaStatements#add_column` with a PG array that has a non-empty default - # value. Is this really the only case? Are we missing tests for other types? - # We should have a real column object passed (or nil) here, and check for that - # instead - if column.respond_to?(:type_cast_for_database) - value = column.type_cast_for_database(value) + if column + value = column.cast_type.type_cast_for_database(value) end _type_cast(value) @@ -66,7 +56,7 @@ module ActiveRecord # This works for mysql and mysql2 where table.column can be used to # resolve ambiguity. # - # We override this in the sqlite and postgresql adapters to use only + # We override this in the sqlite3 and postgresql adapters to use only # the column name (as per syntax requirements). def quote_table_name_for_assignment(table, attr) quote_table_name("#{table}.#{attr}") @@ -118,7 +108,7 @@ module ActiveRecord when Numeric, ActiveSupport::Duration then value.to_s when Date, Time then "'#{quoted_date(value)}'" when Symbol then "'#{quote_string(value.to_s)}'" - when Class then "'#{value.to_s}'" + when Class then "'#{value}'" else "'#{quote_string(YAML.dump(value))}'" end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb index 47fe501752..18ff869ea6 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb @@ -1,3 +1,5 @@ +require 'active_support/core_ext/string/strip' + module ActiveRecord module ConnectionAdapters class AbstractAdapter @@ -13,9 +15,7 @@ module ActiveRecord end def visit_AddColumn(o) - sql_type = type_to_sql(o.type, o.limit, o.precision, o.scale) - sql = "ADD #{quote_column_name(o.name)} #{sql_type}" - add_column_options!(sql, column_options(o)) + "ADD #{accept(o)}" end private @@ -23,12 +23,14 @@ module ActiveRecord def visit_AlterTable(o) sql = "ALTER TABLE #{quote_table_name(o.name)} " sql << o.adds.map { |col| visit_AddColumn col }.join(' ') + sql << o.foreign_key_adds.map { |fk| visit_AddForeignKey fk }.join(' ') + sql << o.foreign_key_drops.map { |fk| visit_DropForeignKey fk }.join(' ') end def visit_ColumnDefinition(o) sql_type = type_to_sql(o.type, o.limit, o.precision, o.scale) column_sql = "#{quote_column_name(o.name)} #{sql_type}" - add_column_options!(column_sql, column_options(o)) unless o.primary_key? + add_column_options!(column_sql, column_options(o)) unless o.type == :primary_key column_sql end @@ -41,6 +43,21 @@ module ActiveRecord create_sql end + def visit_AddForeignKey(o) + sql = <<-SQL.strip_heredoc + ADD CONSTRAINT #{quote_column_name(o.name)} + FOREIGN KEY (#{quote_column_name(o.column)}) + REFERENCES #{quote_table_name(o.to_table)} (#{quote_column_name(o.primary_key)}) + SQL + sql << " #{action_sql('DELETE', o.on_delete)}" if o.on_delete + sql << " #{action_sql('UPDATE', o.on_update)}" if o.on_update + sql + end + + def visit_DropForeignKey(name) + "DROP CONSTRAINT #{quote_column_name(name)}" + end + def column_options(o) column_options = {} column_options[:null] = o.null unless o.null.nil? @@ -48,6 +65,8 @@ module ActiveRecord column_options[:column] = o column_options[:first] = o.first column_options[:after] = o.after + column_options[:auto_increment] = o.auto_increment + column_options[:primary_key] = o.primary_key column_options end @@ -64,7 +83,7 @@ module ActiveRecord end def add_column_options!(sql, options) - sql << " DEFAULT #{quote_value(options[:default], options[:column])}" if options_include_default?(options) + sql << " DEFAULT #{quote_default_expression(options[:default], options[:column])}" if options_include_default?(options) # must explicitly check for :null to allow change_column to work on migrations if options[:null] == false sql << " NOT NULL" @@ -72,18 +91,39 @@ module ActiveRecord if options[:auto_increment] == true sql << " AUTO_INCREMENT" end + if options[:primary_key] == true + sql << " PRIMARY KEY" + end sql end - def quote_value(value, column) + def quote_default_expression(value, column) column.sql_type ||= type_to_sql(column.type, column.limit, column.precision, column.scale) + value = type_for_column(column).type_cast_for_database(value) - @conn.quote(value, column) + @conn.quote(value) end def options_include_default?(options) options.include?(:default) && !(options[:null] == false && options[:default].nil?) end + + def action_sql(action, dependency) + case dependency + when :nullify then "ON #{action} SET NULL" + when :cascade then "ON #{action} CASCADE" + when :restrict then "ON #{action} RESTRICT" + else + raise ArgumentError, <<-MSG.strip_heredoc + '#{dependency}' is not supported for :on_update or :on_delete. + Supported values are: :nullify, :cascade, :restrict + MSG + end + end + + def type_for_column(column) + @conn.lookup_cast_type(column.sql_type) + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb index a9b3e9cfb9..1cf1600d81 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb @@ -15,7 +15,7 @@ module ActiveRecord # are typically created by methods in TableDefinition, and added to the # +columns+ attribute of said TableDefinition object, in order to be used # for generating a number of table creation or table changing SQL statements. - class ColumnDefinition < Struct.new(:name, :type, :limit, :precision, :scale, :default, :null, :first, :after, :primary_key, :sql_type) #:nodoc: + class ColumnDefinition < Struct.new(:name, :type, :limit, :precision, :scale, :default, :null, :first, :after, :auto_increment, :primary_key, :sql_type, :cast_type) #:nodoc: def primary_key? primary_key || type.to_sym == :primary_key @@ -25,6 +25,127 @@ module ActiveRecord class ChangeColumnDefinition < Struct.new(:column, :type, :options) #:nodoc: end + class ForeignKeyDefinition < Struct.new(:from_table, :to_table, :options) #:nodoc: + def name + options[:name] + end + + def column + options[:column] + end + + def primary_key + options[:primary_key] || default_primary_key + end + + def on_delete + options[:on_delete] + end + + def on_update + options[:on_update] + end + + def custom_primary_key? + options[:primary_key] != default_primary_key + end + + private + def default_primary_key + "id" + end + end + + module TimestampDefaultDeprecation # :nodoc: + def emit_warning_if_null_unspecified(options) + return if options.key?(:null) + + ActiveSupport::Deprecation.warn(<<-MSG.squish) + `#timestamp` was called without specifying an option for `null`. In Rails 5, + this behavior will change to `null: false`. You should manually specify + `null: true` to prevent the behavior of your existing migrations from changing. + MSG + end + end + + class ReferenceDefinition # :nodoc: + def initialize( + name, + polymorphic: false, + index: false, + foreign_key: false, + type: :integer, + **options + ) + @name = name + @polymorphic = polymorphic + @index = index + @foreign_key = foreign_key + @type = type + @options = options + + if polymorphic && foreign_key + raise ArgumentError, "Cannot add a foreign key to a polymorphic relation" + end + end + + def add_to(table) + columns.each do |column_options| + table.column(*column_options) + end + + if index + table.index(column_names, index_options) + end + + if foreign_key + table.foreign_key(foreign_table_name, foreign_key_options) + end + end + + protected + + attr_reader :name, :polymorphic, :index, :foreign_key, :type, :options + + private + + def as_options(value, default = {}) + if value.is_a?(Hash) + value + else + default + end + end + + def polymorphic_options + as_options(polymorphic, options) + end + + def index_options + as_options(index) + end + + def foreign_key_options + as_options(foreign_key) + end + + def columns + result = [["#{name}_id", type, options]] + if polymorphic + result.unshift(["#{name}_type", :string, polymorphic_options]) + end + result + end + + def column_names + columns.map(&:first) + end + + def foreign_table_name + name.to_s.pluralize + end + end + # Represents the schema of an SQL table in an abstract way. This class # provides methods for manipulating the schema representation. # @@ -46,14 +167,17 @@ module ActiveRecord # The table definitions # The Columns are stored as a ColumnDefinition in the +columns+ attribute. class TableDefinition + include TimestampDefaultDeprecation + # An array of ColumnDefinition objects, representing the column changes # that have been defined. attr_accessor :indexes - attr_reader :name, :temporary, :options, :as + attr_reader :name, :temporary, :options, :as, :foreign_keys def initialize(types, name, temporary, options, as = nil) @columns_hash = {} @indexes = {} + @foreign_keys = {} @native = types @temporary = temporary @options = options @@ -79,8 +203,8 @@ module ActiveRecord # which is one of the following: # <tt>:primary_key</tt>, <tt>:string</tt>, <tt>:text</tt>, # <tt>:integer</tt>, <tt>:float</tt>, <tt>:decimal</tt>, - # <tt>:datetime</tt>, <tt>:timestamp</tt>, <tt>:time</tt>, - # <tt>:date</tt>, <tt>:binary</tt>, <tt>:boolean</tt>. + # <tt>:datetime</tt>, <tt>:time</tt>, <tt>:date</tt>, + # <tt>:binary</tt>, <tt>:boolean</tt>. # # You may use a type not in this list as long as it is supported by your # database (for example, "polygon" in MySQL), but this will not be database @@ -181,7 +305,7 @@ module ActiveRecord # t.integer :shop_id, :creator_id # t.string :item_number, index: true # t.string :name, :value, default: "Untitled" - # t.timestamps + # t.timestamps null: false # end # # There's a short-hand method for each of the type values declared at the top. And then there's @@ -211,7 +335,7 @@ module ActiveRecord name = name.to_s type = type.to_sym - if primary_key_column_name == name + if @columns_hash[name] && @columns_hash[name].primary_key? raise ArgumentError, "you can't redefine the primary key column '#{name}'. To define a custom primary key, pass { id: false } to create_table." end @@ -225,7 +349,7 @@ module ActiveRecord @columns_hash.delete name.to_s end - [:string, :text, :integer, :float, :decimal, :datetime, :timestamp, :time, :date, :binary, :boolean].each do |column_type| + [:string, :text, :integer, :bigint, :float, :decimal, :datetime, :timestamp, :time, :date, :binary, :boolean].each do |column_type| define_method column_type do |*args| options = args.extract_options! column_names = args @@ -241,41 +365,54 @@ module ActiveRecord indexes[column_name] = options end + def foreign_key(table_name, options = {}) # :nodoc: + foreign_keys[table_name] = options + end + # Appends <tt>:datetime</tt> columns <tt>:created_at</tt> and - # <tt>:updated_at</tt> to the table. + # <tt>:updated_at</tt> to the table. See SchemaStatements#add_timestamps + # + # t.timestamps null: false def timestamps(*args) options = args.extract_options! + emit_warning_if_null_unspecified(options) column(:created_at, :datetime, options) column(:updated_at, :datetime, options) end - def references(*args) - options = args.extract_options! - polymorphic = options.delete(:polymorphic) - index_options = options.delete(:index) + # Adds a reference. Optionally adds a +type+ column, if the + # +:polymorphic+ option is provided. +references+ and +belongs_to+ + # are acceptable. The reference column will be an +integer+ by default, + # the +:type+ option can be used to specify a different type. A foreign + # key will be created if the +:foreign_key+ option is passed. + # + # t.references(:user) + # t.references(:user, type: "string") + # t.belongs_to(:supplier, polymorphic: true) + # + # See SchemaStatements#add_reference + def references(*args, **options) args.each do |col| - column("#{col}_id", :integer, options) - column("#{col}_type", :string, polymorphic.is_a?(Hash) ? polymorphic : options) if polymorphic - index(polymorphic ? %w(id type).map { |t| "#{col}_#{t}" } : "#{col}_id", index_options.is_a?(Hash) ? index_options : {}) if index_options + ReferenceDefinition.new(col, **options).add_to(self) end end alias :belongs_to :references def new_column_definition(name, type, options) # :nodoc: - type = aliased_types[type] || type + 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.array = options[:array] if column.respond_to?(:array) column.precision = options[:precision] column.scale = options[:scale] column.default = options[:default] column.null = options[:null] column.first = options[:first] column.after = options[:after] + column.auto_increment = options[:auto_increment] column.primary_key = type == :primary_key || options[:primary_key] column end @@ -285,32 +422,37 @@ module ActiveRecord ColumnDefinition.new name, type end - def primary_key_column_name - primary_key_column = columns.detect { |c| c.primary_key? } - primary_key_column && primary_key_column.name - end - def native @native end - def aliased_types - HashWithIndifferentAccess.new( - timestamp: :datetime, - ) + def aliased_types(name, fallback) + 'timestamp' == name ? :datetime : fallback end end class AlterTable # :nodoc: attr_reader :adds + attr_reader :foreign_key_adds + attr_reader :foreign_key_drops def initialize(td) @td = td @adds = [] + @foreign_key_adds = [] + @foreign_key_drops = [] end def name; @td.name; end + def add_foreign_key(to_table, options) + @foreign_key_adds << ForeignKeyDefinition.new(name, to_table, options) + end + + def drop_foreign_key(name) + @foreign_key_drops << name + end + def add_column(name, type, options) name = name.to_s type = type.to_sym @@ -352,156 +494,179 @@ module ActiveRecord # end # class Table + attr_reader :name + def initialize(table_name, base) - @table_name = table_name + @name = table_name @base = base end # Adds a new column to the named table. - # See TableDefinition#column for details of the options you can use. # - # ====== Creating a simple column # t.column(:name, :string) + # + # See TableDefinition#column for details of the options you can use. def column(column_name, type, options = {}) - @base.add_column(@table_name, column_name, type, options) + @base.add_column(name, column_name, type, options) end - # Checks to see if a column exists. See SchemaStatements#column_exists? + # Checks to see if a column exists. + # + # See SchemaStatements#column_exists? def column_exists?(column_name, type = nil, options = {}) - @base.column_exists?(@table_name, column_name, type, options) + @base.column_exists?(name, column_name, type, options) end # Adds a new index to the table. +column_name+ can be a single Symbol, or - # an Array of Symbols. See SchemaStatements#add_index + # an Array of Symbols. # - # ====== Creating a simple index # t.index(:name) - # ====== Creating a unique index # t.index([:branch_id, :party_id], unique: true) - # ====== Creating a named index # t.index([:branch_id, :party_id], unique: true, name: 'by_branch_party') + # + # See SchemaStatements#add_index for details of the options you can use. def index(column_name, options = {}) - @base.add_index(@table_name, column_name, options) + @base.add_index(name, column_name, options) end - # Checks to see if an index exists. See SchemaStatements#index_exists? + # Checks to see if an index exists. + # + # See SchemaStatements#index_exists? def index_exists?(column_name, options = {}) - @base.index_exists?(@table_name, column_name, options) + @base.index_exists?(name, column_name, options) end # Renames the given index on the table. # # t.rename_index(:user_id, :account_id) + # + # See SchemaStatements#rename_index def rename_index(index_name, new_index_name) - @base.rename_index(@table_name, index_name, new_index_name) + @base.rename_index(name, index_name, new_index_name) end - # Adds timestamps (+created_at+ and +updated_at+) columns to the table. See SchemaStatements#add_timestamps + # Adds timestamps (+created_at+ and +updated_at+) columns to the table. + # + # t.timestamps(null: false) # - # t.timestamps - def timestamps - @base.add_timestamps(@table_name) + # See SchemaStatements#add_timestamps + def timestamps(options = {}) + @base.add_timestamps(name, options) end # Changes the column's definition according to the new options. - # See TableDefinition#column for details of the options you can use. # # t.change(:name, :string, limit: 80) # t.change(:description, :text) + # + # See TableDefinition#column for details of the options you can use. def change(column_name, type, options = {}) - @base.change_column(@table_name, column_name, type, options) + @base.change_column(name, column_name, type, options) end - # Sets a new default value for a column. See SchemaStatements#change_column_default + # Sets a new default value for a column. # # t.change_default(:qualification, 'new') # t.change_default(:authorized, 1) + # + # See SchemaStatements#change_column_default def change_default(column_name, default) - @base.change_column_default(@table_name, column_name, default) + @base.change_column_default(name, column_name, default) end # Removes the column(s) from the table definition. # # t.remove(:qualification) # t.remove(:qualification, :experience) + # + # See SchemaStatements#remove_columns def remove(*column_names) - @base.remove_columns(@table_name, *column_names) + @base.remove_columns(name, *column_names) end # Removes the given index from the table. # - # ====== Remove the index_table_name_on_column in the table_name table - # t.remove_index :column - # ====== Remove the index named index_table_name_on_branch_id in the table_name table - # t.remove_index column: :branch_id - # ====== Remove the index named index_table_name_on_branch_id_and_party_id in the table_name table - # t.remove_index column: [:branch_id, :party_id] - # ====== Remove the index named by_branch_party in the table_name table - # t.remove_index name: :by_branch_party + # t.remove_index(:branch_id) + # t.remove_index(column: [:branch_id, :party_id]) + # t.remove_index(name: :by_branch_party) + # + # See SchemaStatements#remove_index def remove_index(options = {}) - @base.remove_index(@table_name, options) + @base.remove_index(name, options) end # Removes the timestamp columns (+created_at+ and +updated_at+) from the table. # # t.remove_timestamps - def remove_timestamps - @base.remove_timestamps(@table_name) + # + # See SchemaStatements#remove_timestamps + def remove_timestamps(options = {}) + @base.remove_timestamps(name, options) end # Renames a column. # # t.rename(:description, :name) + # + # See SchemaStatements#rename_column def rename(column_name, new_column_name) - @base.rename_column(@table_name, column_name, new_column_name) + @base.rename_column(name, column_name, new_column_name) end - # Adds a reference. Optionally adds a +type+ column, if <tt>:polymorphic</tt> option is provided. - # <tt>references</tt> and <tt>belongs_to</tt> are acceptable. + # Adds a reference. Optionally adds a +type+ column, if + # <tt>:polymorphic</tt> option is provided. # # t.references(:user) + # t.references(:user, type: "string") # t.belongs_to(:supplier, polymorphic: true) + # t.belongs_to(:supplier, foreign_key: true) # + # See SchemaStatements#add_reference def references(*args) options = args.extract_options! args.each do |ref_name| - @base.add_reference(@table_name, ref_name, options) + @base.add_reference(name, ref_name, options) end end alias :belongs_to :references # Removes a reference. Optionally removes a +type+ column. - # <tt>remove_references</tt> and <tt>remove_belongs_to</tt> are acceptable. # # t.remove_references(:user) # t.remove_belongs_to(:supplier, polymorphic: true) # + # See SchemaStatements#remove_reference def remove_references(*args) options = args.extract_options! args.each do |ref_name| - @base.remove_reference(@table_name, ref_name, options) + @base.remove_reference(name, ref_name, options) end end alias :remove_belongs_to :remove_references - # Adds a column or columns of a specified type + # Adds a column or columns of a specified type. # # t.string(:goat) # t.string(:goat, :sheep) + # + # See SchemaStatements#add_column [:string, :text, :integer, :float, :decimal, :datetime, :timestamp, :time, :date, :binary, :boolean].each do |column_type| define_method column_type do |*args| options = args.extract_options! - args.each do |name| - @base.add_column(@table_name, name, column_type, options) + args.each do |column_name| + @base.add_column(name, column_name, column_type, options) end end end + def foreign_key(*args) # :nodoc: + @base.add_foreign_key(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 5a0efe49c7..0834105079 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb @@ -6,24 +6,29 @@ module ActiveRecord # We can then redefine how certain data types may be handled in the schema dumper on the # Adapter level by over-writing this code inside the database specific adapters module ColumnDumper - def column_spec(column, types) - spec = prepare_column_options(column, types) - (spec.keys - [:name, :type]).each{ |k| spec[k].insert(0, "#{k.to_s}: ")} + def column_spec(column) + spec = prepare_column_options(column) + (spec.keys - [:name, :type]).each{ |k| spec[k].insert(0, "#{k}: ")} spec end # This can be overridden on a Adapter level basis to support other # extended datatypes (Example: Adding an array option in the # PostgreSQLAdapter) - def prepare_column_options(column, types) + def prepare_column_options(column) spec = {} spec[:name] = column.name.inspect spec[:type] = column.type.to_s - spec[:limit] = column.limit.inspect if column.limit != types[column.type][:limit] + spec[:null] = 'false' unless column.null + + limit = column.limit || native_database_types[column.type][:limit] + spec[:limit] = limit.inspect if limit spec[:precision] = column.precision.inspect if column.precision spec[:scale] = column.scale.inspect if column.scale - spec[:null] = 'false' unless column.null - spec[:default] = column.type_cast_for_schema(column.default) if column.has_default? + + default = schema_default(column) if column.has_default? + spec[:default] = default unless default.nil? + spec end @@ -31,6 +36,15 @@ module ActiveRecord def migration_keys [:name, :limit, :precision, :scale, :default, :null] end + + private + + def schema_default(column) + default = column.type_cast_from_database(column.default) + unless default.nil? + column.type_cast_for_schema(default) + end + end end end 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 ffa6af6d99..24afd9c5da 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb @@ -43,13 +43,14 @@ module ActiveRecord # index_exists?(:suppliers, :company_id, name: "idx_company_id") # def index_exists?(table_name, column_name, options = {}) - column_names = Array(column_name) - index_name = options.key?(:name) ? options[:name].to_s : index_name(table_name, :column => column_names) - if options[:unique] - indexes(table_name).any?{ |i| i.unique && i.name == index_name } - else - indexes(table_name).any?{ |i| i.name == index_name } - end + 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] + + indexes(table_name).any? { |i| checks.all? { |check| check[i] } } end # Returns an array of Column objects for the table specified by +table_name+. @@ -131,6 +132,7 @@ module ActiveRecord # Make a temporary table. # [<tt>:force</tt>] # Set to true to drop the table before creating it. + # Set to +:cascade+ to drop dependent objects as well. # Defaults to false. # [<tt>:as</tt>] # SQL to use to generate the table. When this option is used, the block is @@ -202,7 +204,17 @@ module ActiveRecord end result = execute schema_creation.accept td - td.indexes.each_pair { |c, o| add_index(table_name, c, o) } unless supports_indexes_in_create? + + unless supports_indexes_in_create? + td.indexes.each_pair do |column_name, index_options| + add_index(table_name, column_name, index_options) + end + end + + td.foreign_keys.each_pair do |other_table_name, foreign_key_options| + add_foreign_key(table_name, other_table_name, foreign_key_options) + end + result end @@ -360,8 +372,12 @@ module ActiveRecord # Drops a table from the database. # - # Although this command ignores +options+ and the block if one is given, it can be helpful - # to provide these in a migration's +change+ method so it can be reverted. + # [<tt>:force</tt>] + # Set to +:cascade+ to drop dependent objects as well. + # Defaults to false. + # + # Although this command ignores most +options+ and the block if one is given, + # 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 = {}) execute "DROP TABLE #{quote_table_name(table_name)}" @@ -570,6 +586,8 @@ module ActiveRecord # rename_index :people, 'index_people_on_last_name', 'index_users_on_last_name' # def rename_index(table_name, old_name, new_name) + validate_index_length!(table_name, new_name) + # this is a naive implementation; some DBs may support this more efficiently (Postgres, for instance) old_index_def = indexes(table_name).detect { |i| i.name == old_name } return unless old_index_def @@ -602,26 +620,32 @@ module ActiveRecord end # Adds a reference. Optionally adds a +type+ column, if <tt>:polymorphic</tt> option is provided. + # The reference column is an +integer+ by default, the <tt>:type</tt> option can be used to specify + # a different type. # <tt>add_reference</tt> and <tt>add_belongs_to</tt> are acceptable. # - # ====== Create a user_id column + # ====== Create a user_id integer column # # add_reference(:products, :user) # + # ====== Create a user_id string column + # + # add_reference(:products, :user, type: :string) + # # ====== Create a supplier_id and supplier_type columns # # add_belongs_to(:products, :supplier, polymorphic: true) # - # ====== Create a supplier_id, supplier_type columns and appropriate index + # ====== Create supplier_id, supplier_type columns and appropriate index # # add_reference(:products, :supplier, polymorphic: true, index: true) # - def add_reference(table_name, ref_name, options = {}) - polymorphic = options.delete(:polymorphic) - index_options = options.delete(:index) - add_column(table_name, "#{ref_name}_id", :integer, options) - add_column(table_name, "#{ref_name}_type", :string, polymorphic.is_a?(Hash) ? polymorphic : options) if polymorphic - add_index(table_name, polymorphic ? %w[id type].map{ |t| "#{ref_name}_#{t}" } : "#{ref_name}_id", index_options.is_a?(Hash) ? index_options : {}) if index_options + # ====== Create a supplier_id column and appropriate foreign key + # + # add_reference(:products, :supplier, foreign_key: true) + # + def add_reference(table_name, *args) + ReferenceDefinition.new(*args).add_to(update_table_definition(table_name, self)) end alias :add_belongs_to :add_reference @@ -642,6 +666,115 @@ module ActiveRecord end alias :remove_belongs_to :remove_reference + # Returns an array of foreign keys for the given table. + # The foreign keys are represented as +ForeignKeyDefinition+ objects. + def foreign_keys(table_name) + raise NotImplementedError, "foreign_keys is not implemented" + end + + # Adds a new foreign key. +from_table+ is the table with the key column, + # +to_table+ contains the referenced primary key. + # + # The foreign key will be named after the following pattern: <tt>fk_rails_<identifier></tt>. + # +identifier+ is a 10 character long random string. A custom name can be specified with + # the <tt>:name</tt> option. + # + # ====== Creating a simple foreign key + # + # add_foreign_key :articles, :authors + # + # generates: + # + # ALTER TABLE "articles" ADD CONSTRAINT articles_author_id_fk FOREIGN KEY ("author_id") REFERENCES "authors" ("id") + # + # ====== Creating a foreign key on a specific column + # + # add_foreign_key :articles, :users, column: :author_id, primary_key: "lng_id" + # + # generates: + # + # ALTER TABLE "articles" ADD CONSTRAINT fk_rails_58ca3d3a82 FOREIGN KEY ("author_id") REFERENCES "users" ("lng_id") + # + # ====== Creating a cascading foreign key + # + # add_foreign_key :articles, :authors, on_delete: :cascade + # + # generates: + # + # ALTER TABLE "articles" ADD CONSTRAINT articles_author_id_fk FOREIGN KEY ("author_id") REFERENCES "authors" ("id") ON DELETE CASCADE + # + # The +options+ hash can include the following keys: + # [<tt>:column</tt>] + # The foreign key column name on +from_table+. Defaults to <tt>to_table.singularize + "_id"</tt> + # [<tt>:primary_key</tt>] + # The primary key column name on +to_table+. Defaults to +id+. + # [<tt>:name</tt>] + # The constraint name. Defaults to <tt>fk_rails_<identifier></tt>. + # [<tt>:on_delete</tt>] + # Action that happens <tt>ON DELETE</tt>. Valid values are +:nullify+, +:cascade:+ and +:restrict+ + # [<tt>:on_update</tt>] + # Action that happens <tt>ON UPDATE</tt>. Valid values are +:nullify+, +:cascade:+ and +:restrict+ + def add_foreign_key(from_table, to_table, options = {}) + return unless supports_foreign_keys? + + options[:column] ||= foreign_key_column_for(to_table) + + options = { + column: options[:column], + primary_key: options[:primary_key], + name: foreign_key_name(from_table, options), + on_delete: options[:on_delete], + on_update: options[:on_update] + } + at = create_alter_table from_table + at.add_foreign_key to_table, options + + execute schema_creation.accept(at) + end + + # Removes the given foreign key from the table. + # + # Removes the foreign key on +accounts.branch_id+. + # + # remove_foreign_key :accounts, :branches + # + # Removes the foreign key on +accounts.owner_id+. + # + # remove_foreign_key :accounts, column: :owner_id + # + # Removes the foreign key named +special_fk_name+ on the +accounts+ table. + # + # remove_foreign_key :accounts, name: :special_fk_name + # + def remove_foreign_key(from_table, options_or_to_table = {}) + return unless supports_foreign_keys? + + if options_or_to_table.is_a?(Hash) + options = options_or_to_table + else + options = { column: foreign_key_column_for(options_or_to_table) } + end + + fk_name_to_delete = options.fetch(:name) do + fk_to_delete = foreign_keys(from_table).detect {|fk| fk.column == options[:column].to_s } + + if fk_to_delete + fk_to_delete.name + else + raise ArgumentError, "Table '#{from_table}' has no foreign key on column '#{options[:column]}'" + end + end + + at = create_alter_table from_table + at.drop_foreign_key fk_name_to_delete + + execute schema_creation.accept(at) + end + + def foreign_key_column_for(table_name) # :nodoc: + "#{table_name.to_s.singularize}_id" + end + def dump_schema_information #:nodoc: sm_table = ActiveRecord::Migrator.schema_migrations_table_name @@ -661,7 +794,7 @@ module ActiveRecord version = version.to_i sm_table = quote_table_name(ActiveRecord::Migrator.schema_migrations_table_name) - migrated = select_values("SELECT version FROM #{sm_table}").map { |v| v.to_i } + migrated = select_values("SELECT version FROM #{sm_table}").map(&:to_i) paths = migrations_paths.map {|p| "#{p}/[0-9]*_*.rb" } versions = Dir[*paths].map do |filename| filename.split('/').last.split('_').first.to_i @@ -718,20 +851,23 @@ module ActiveRecord columns end - # Adds timestamps (+created_at+ and +updated_at+) columns to the named table. + include TimestampDefaultDeprecation + # Adds timestamps (+created_at+ and +updated_at+) columns to +table_name+. + # Additional options (like <tt>null: false</tt>) are forwarded to #add_column. # - # add_timestamps(:suppliers) + # add_timestamps(:suppliers, null: false) # - def add_timestamps(table_name) - add_column table_name, :created_at, :datetime - add_column table_name, :updated_at, :datetime + def add_timestamps(table_name, options = {}) + emit_warning_if_null_unspecified(options) + add_column table_name, :created_at, :datetime, options + add_column table_name, :updated_at, :datetime, options end # Removes the timestamp columns (+created_at+ and +updated_at+) from the table definition. # # remove_timestamps(:suppliers) # - def remove_timestamps(table_name) + def remove_timestamps(table_name, options = {}) remove_column table_name, :updated_at remove_column table_name, :created_at end @@ -788,7 +924,7 @@ module ActiveRecord return option_strings end - # Overridden by the mysql adapter for supporting index lengths + # Overridden by the MySQL adapter for supporting index lengths def quoted_columns_for_index(column_names, options = {}) option_strings = Hash[column_names.map {|name| [name, '']}] @@ -852,6 +988,18 @@ module ActiveRecord def create_alter_table(name) AlterTable.new create_table_definition(name, false, {}) end + + def foreign_key_name(table_name, options) # :nodoc: + options.fetch(:name) do + "fk_rails_#{SecureRandom.hex(5)}" + end + end + + def validate_index_length!(table_name, new_name) + if new_name.length > allowed_index_name_length + raise ArgumentError, "Index name '#{new_name}' on table '#{table_name}' is too long; the limit is #{allowed_index_name_length} characters" + end + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb b/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb index bc4884b538..fd666c8c39 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb @@ -1,20 +1,7 @@ module ActiveRecord module ConnectionAdapters - class Transaction #:nodoc: - attr_reader :connection - - def initialize(connection) - @connection = connection - @state = TransactionState.new - end - - def state - @state - end - end - class TransactionState - attr_accessor :parent + attr_reader :parent VALID_STATES = Set.new([:committed, :rolledback, nil]) @@ -35,6 +22,10 @@ module ActiveRecord @state == :rolledback end + def completed? + committed? || rolledback? + end + def set_state(state) if !VALID_STATES.include?(state) raise ArgumentError, "Invalid transaction state: #{state}" @@ -43,82 +34,24 @@ module ActiveRecord end end - class ClosedTransaction < Transaction #:nodoc: - def number - 0 - end - - def begin(options = {}) - RealTransaction.new(connection, self, options) - end - - def closed? - true - end - - def open? - false - end - - def joinable? - false - end - - # This is a noop when there are no open transactions - def add_record(record) - end + class NullTransaction #:nodoc: + def initialize; end + def closed?; true; end + def open?; false; end + def joinable?; false; end + def add_record(record); end end - class OpenTransaction < Transaction #:nodoc: - attr_reader :parent, :records - attr_writer :joinable - - def initialize(connection, parent, options = {}) - super connection - - @parent = parent - @records = [] - @finishing = false - @joinable = options.fetch(:joinable, true) - end - - # This state is necessary so that we correctly handle stuff that might - # happen in a commit/rollback. But it's kinda distasteful. Maybe we can - # find a better way to structure it in the future. - def finishing? - @finishing - end - - def joinable? - @joinable && !finishing? - end - - def number - if finishing? - parent.number - else - parent.number + 1 - end - end - - def begin(options = {}) - if finishing? - parent.begin - else - SavepointTransaction.new(connection, self, options) - end - end + class Transaction #:nodoc: - def rollback - @finishing = true - perform_rollback - parent - end + attr_reader :connection, :state, :records, :savepoint_name + attr_writer :joinable - def commit - @finishing = true - perform_commit - parent + def initialize(connection, options) + @connection = connection + @state = TransactionState.new + @records = [] + @joinable = options.fetch(:joinable, true) end def add_record(record) @@ -129,41 +62,82 @@ module ActiveRecord end end - def rollback_records + def rollback @state.set_state(:rolledback) - records.uniq.each do |record| + end + + def rollback_records + ite = records.uniq + while record = ite.shift begin - record.rolledback!(parent.closed?) + record.rolledback! full_rollback? rescue => e + raise if ActiveRecord::Base.raise_in_transactional_callbacks record.logger.error(e) if record.respond_to?(:logger) && record.logger end end + ensure + ite.each do |i| + i.rolledback!(full_rollback?, false) + end end - def commit_records + def commit @state.set_state(:committed) - records.uniq.each do |record| + end + + def commit_records + ite = records.uniq + while record = ite.shift begin record.committed! rescue => e + raise if ActiveRecord::Base.raise_in_transactional_callbacks record.logger.error(e) if record.respond_to?(:logger) && record.logger end end + ensure + ite.each do |i| + i.committed!(false) + end end - def closed? - false + def full_rollback?; true; end + def joinable?; @joinable; end + def closed?; false; end + def open?; !closed?; end + end + + class SavepointTransaction < Transaction + + def initialize(connection, savepoint_name, options) + super(connection, options) + if options[:isolation] + raise ActiveRecord::TransactionIsolationError, "cannot set transaction isolation in a nested transaction" + end + connection.create_savepoint(@savepoint_name = savepoint_name) end - def open? - true + def rollback + connection.rollback_to_savepoint(savepoint_name) + super + rollback_records end - end - class RealTransaction < OpenTransaction #:nodoc: - def initialize(connection, parent, options = {}) + def commit + connection.release_savepoint(savepoint_name) super + parent = connection.transaction_manager.current_transaction + records.each { |r| parent.add_record(r) } + end + + def full_rollback?; false; end + end + + class RealTransaction < Transaction + def initialize(connection, options) + super if options[:isolation] connection.begin_isolated_db_transaction(options[:isolation]) else @@ -171,37 +145,75 @@ module ActiveRecord end end - def perform_rollback + def rollback connection.rollback_db_transaction + super rollback_records end - def perform_commit + def commit connection.commit_db_transaction + super commit_records end end - class SavepointTransaction < OpenTransaction #:nodoc: - def initialize(connection, parent, options = {}) - if options[:isolation] - raise ActiveRecord::TransactionIsolationError, "cannot set transaction isolation in a nested transaction" - end + class TransactionManager #:nodoc: + def initialize(connection) + @stack = [] + @connection = connection + end - super - connection.create_savepoint + def begin_transaction(options = {}) + transaction = + if @stack.empty? + RealTransaction.new(@connection, options) + else + SavepointTransaction.new(@connection, "active_record_#{@stack.size}", options) + end + @stack.push(transaction) + transaction + end + + def commit_transaction + @stack.pop.commit + end + + def rollback_transaction + @stack.pop.rollback + end + + def within_new_transaction(options = {}) + transaction = begin_transaction options + yield + rescue Exception => error + rollback_transaction if transaction + raise + ensure + unless error + if Thread.current.status == 'aborting' + rollback_transaction + else + begin + commit_transaction + rescue Exception + transaction.rollback unless transaction.state.completed? + raise + end + end + end end - def perform_rollback - connection.rollback_to_savepoint - rollback_records + def open_transactions + @stack.size end - def perform_commit - @state.set_state(:committed) - @state.parent = parent.state - connection.release_savepoint + def current_transaction + @stack.last || NULL_TRANSACTION end + + private + NULL_TRANSACTION = NullTransaction.new end end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb index cc494a7f40..308e4c77bd 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb @@ -14,19 +14,18 @@ module ActiveRecord module ConnectionAdapters # :nodoc: extend ActiveSupport::Autoload - autoload_at 'active_record/connection_adapters/column' do - autoload :Column - autoload :NullColumn - end + autoload :Column autoload :ConnectionSpecification autoload_at 'active_record/connection_adapters/abstract/schema_definitions' do autoload :IndexDefinition autoload :ColumnDefinition autoload :ChangeColumnDefinition + autoload :ForeignKeyDefinition autoload :TableDefinition autoload :Table autoload :AlterTable + autoload :TimestampDefaultDeprecation end autoload_at 'active_record/connection_adapters/abstract/connection_pool' do @@ -45,7 +44,8 @@ module ActiveRecord end autoload_at 'active_record/connection_adapters/abstract/transaction' do - autoload :ClosedTransaction + autoload :TransactionManager + autoload :NullTransaction autoload :RealTransaction autoload :SavepointTransaction autoload :TransactionState @@ -65,6 +65,7 @@ module ActiveRecord # Most of the methods in the adapter are useful during migrations. Most # notably, the instance methods provided by SchemaStatement are very useful. class AbstractAdapter + ADAPTER_NAME = 'Abstract'.freeze include Quoting, DatabaseStatements, SchemaStatements include DatabaseLimits include QueryCache @@ -166,7 +167,7 @@ module ActiveRecord # Returns the human-readable name of the adapter. Use mixed case - one # can always use downcase if needed. def adapter_name - 'Abstract' + self.class::ADAPTER_NAME end # Does this adapter support migrations? @@ -233,6 +234,16 @@ module ActiveRecord false end + # Does this adapter support creating foreign key constraints? + def supports_foreign_keys? + false + end + + # Does this adapter support views? + def supports_views? + false + end + # This is meant to be implemented by the adapters that support extensions def disable_extension(name) end @@ -253,10 +264,20 @@ module ActiveRecord # QUOTING ================================================== - # Returns a bind substitution value given a bind +index+ and +column+ + # Quote date/time values for use in SQL input. Includes microseconds + # if the value is a Time responding to usec. + def quoted_date(value) #:nodoc: + if value.acts_like?(:time) && value.respond_to?(:usec) + "#{super}.#{sprintf("%06d", value.usec)}" + else + super + end + 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, index) - Arel::Nodes::BindParam.new '?' + def substitute_at(column, _unused = 0) + Arel::Nodes::BindParam.new end # REFERENTIAL INTEGRITY ==================================== @@ -352,7 +373,7 @@ module ActiveRecord end def current_savepoint_name - "active_record_#{open_transactions}" + current_transaction.savepoint_name end # Check the connection back in to the connection pool @@ -370,12 +391,16 @@ module ActiveRecord Column.new(name, default, cast_type, sql_type, null) end - protected - def lookup_cast_type(sql_type) # :nodoc: type_map.lookup(sql_type) end + def column_name_for_operation(operation, node) # :nodoc: + node.to_sql + end + + protected + def initialize_type_map(m) # :nodoc: register_class_with_limit m, %r(boolean)i, Type::Boolean register_class_with_limit m, %r(char)i, Type::String 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 200b773172..d083e413a5 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb @@ -1,10 +1,18 @@ require 'arel/visitors/bind_visitor' +require 'active_support/core_ext/string/strip' module ActiveRecord module ConnectionAdapters class AbstractMysqlAdapter < AbstractAdapter include Savepoints + class TableDefinition < ActiveRecord::ConnectionAdapters::TableDefinition + def primary_key(name, type = :primary_key, options = {}) + options[:auto_increment] ||= type == :bigint + super + end + end + class SchemaCreation < AbstractAdapter::SchemaCreation def visit_AddColumn(o) add_column_position!(super, column_options(o)) @@ -12,6 +20,10 @@ module ActiveRecord private + def visit_DropForeignKey(name) + "DROP FOREIGN KEY #{name}" + end + def visit_TableDefinition(o) name = o.name create_sql = "CREATE#{' TEMPORARY' if o.temporary} TABLE #{quote_table_name(name)} " @@ -62,20 +74,19 @@ module ActiveRecord @extra = extra super(name, default, cast_type, sql_type, null) assert_valid_default(default) + extract_default end - def default - @default ||= if blob_or_text_column? - null || strict ? nil : '' - elsif missing_default_forged_as_empty_string?(@original_default) - nil - else - super + 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 + return false if blob_or_text_column? # MySQL forbids defaults on blob and text columns super end @@ -87,6 +98,13 @@ module ActiveRecord collation && !collation.match(/_ci$/) end + def ==(other) + super && + collation == other.collation && + strict == other.strict && + extra == other.extra + end + private # MySQL misreports NOT NULL column default when none is given. @@ -105,6 +123,10 @@ module ActiveRecord raise ArgumentError, "#{type} columns cannot have a default value: #{default.inspect}" end end + + def attributes_for_hash + super + [collation, strict, extra] + end end ## @@ -158,10 +180,6 @@ module ActiveRecord end end - def adapter_name #:nodoc: - self.class::ADAPTER_NAME - end - # Returns true, since this connection adapter supports migrations. def supports_migrations? true @@ -193,6 +211,14 @@ module ActiveRecord true end + def supports_foreign_keys? + true + end + + def supports_views? + version[0] >= 5 + end + def native_database_types NATIVE_DATABASE_TYPES end @@ -213,7 +239,7 @@ module ActiveRecord Column.new(field, default, cast_type, sql_type, null, collation, strict_mode?, extra) end - # Must return the Mysql error number from the exception, if the exception has an + # Must return the MySQL error number from the exception, if the exception has an # error number. def error_number(exception) # :nodoc: raise NotImplementedError @@ -266,7 +292,9 @@ module ActiveRecord end end + #-- # DATABASE STATEMENTS ====================================== + #++ def clear_cache! super @@ -377,12 +405,16 @@ module ActiveRecord sql << "LIKE #{quote(like)}" if like execute_and_free(sql, 'SCHEMA') do |result| - result.collect { |field| field.first } + result.collect(&:first) end end + def truncate(table_name, name = nil) + execute "TRUNCATE TABLE #{quote_table_name(table_name)}", name + end + def table_exists?(name) - return false unless name + return false unless name.present? return true if tables(nil, nil, name).any? name = name.to_s @@ -462,18 +494,20 @@ module ActiveRecord end def drop_table(table_name, options = {}) - execute "DROP#{' TEMPORARY' if options[:temporary]} TABLE #{quote_table_name(table_name)}" + execute "DROP#{' TEMPORARY' if options[:temporary]} TABLE #{quote_table_name(table_name)}#{' CASCADE' if options[:force] == :cascade}" end def rename_index(table_name, old_name, new_name) - if (version[0] == 5 && version[1] >= 7) || version[0] >= 6 + if supports_rename_index? + validate_index_length!(table_name, new_name) + execute "ALTER TABLE #{quote_table_name(table_name)} RENAME INDEX #{quote_table_name(old_name)} TO #{quote_table_name(new_name)}" else super end end - def change_column_default(table_name, column_name, default) + def change_column_default(table_name, column_name, default) #:nodoc: column = column_for(table_name, column_name) change_column table_name, column_name, column.sql_type, :default => default end @@ -502,6 +536,34 @@ module ActiveRecord execute "CREATE #{index_type} INDEX #{quote_column_name(index_name)} #{index_using} ON #{quote_table_name(table_name)} (#{index_columns})#{index_options} #{index_algorithm}" end + def foreign_keys(table_name) + fk_info = select_all <<-SQL.strip_heredoc + SELECT fk.referenced_table_name as 'to_table' + ,fk.referenced_column_name as 'primary_key' + ,fk.column_name as 'column' + ,fk.constraint_name as 'name' + FROM information_schema.key_column_usage fk + WHERE fk.referenced_column_name is not null + AND fk.table_schema = '#{@config[:database]}' + AND fk.table_name = '#{table_name}' + SQL + + create_table_info = select_one("SHOW CREATE TABLE #{quote_table_name(table_name)}")["Create Table"] + + fk_info.map do |row| + options = { + column: row['column'], + name: row['name'], + primary_key: row['primary_key'] + } + + options[:on_update] = extract_foreign_key_action(create_table_info, row['name'], "UPDATE") + options[:on_delete] = extract_foreign_key_action(create_table_info, row['name'], "DELETE") + + ForeignKeyDefinition.new(table_name, row['to_table'], options) + end + end + # Maps logical Rails types to MySQL-specific data types. def type_to_sql(type, limit = nil, precision = nil, scale = nil) case type.to_s @@ -534,14 +596,6 @@ module ActiveRecord end end - def add_column_position!(sql, options) - if options[:first] - sql << " FIRST" - elsif options[:after] - sql << " AFTER #{quote_column_name(options[:after])}" - end - end - # SHOW VARIABLES LIKE 'name' def show_variable(name) variables = select_all("SHOW VARIABLES LIKE '#{name}'", 'SCHEMA') @@ -588,10 +642,6 @@ module ActiveRecord end end - def limited_update_conditions(where_sql, quoted_table_name, quoted_primary_key) - where_sql - end - def strict_mode? self.class.type_cast_config_to_boolean(@config.fetch(:strict, true)) end @@ -604,30 +654,46 @@ module ActiveRecord def initialize_type_map(m) # :nodoc: super - m.register_type(%r(enum)i) do |sql_type| - limit = sql_type[/^enum\((.+)\)/i, 1] - .split(',').map{|enum| enum.strip.length - 2}.max - Type::String.new(limit: limit) - end - - m.register_type %r(tinytext)i, Type::Text.new(limit: 255) - m.register_type %r(tinyblob)i, Type::Binary.new(limit: 255) - m.register_type %r(mediumtext)i, Type::Text.new(limit: 16777215) - m.register_type %r(mediumblob)i, Type::Binary.new(limit: 16777215) - m.register_type %r(longtext)i, Type::Text.new(limit: 2147483647) - m.register_type %r(longblob)i, Type::Binary.new(limit: 2147483647) - m.register_type %r(^bigint)i, Type::Integer.new(limit: 8) - m.register_type %r(^int)i, Type::Integer.new(limit: 4) - m.register_type %r(^mediumint)i, Type::Integer.new(limit: 3) - m.register_type %r(^smallint)i, Type::Integer.new(limit: 2) - m.register_type %r(^tinyint)i, Type::Integer.new(limit: 1) + + register_class_with_limit m, %r(char)i, MysqlString + + m.register_type %r(tinytext)i, Type::Text.new(limit: 2**8 - 1) + m.register_type %r(tinyblob)i, Type::Binary.new(limit: 2**8 - 1) + m.register_type %r(text)i, Type::Text.new(limit: 2**16 - 1) + m.register_type %r(blob)i, Type::Binary.new(limit: 2**16 - 1) + m.register_type %r(mediumtext)i, Type::Text.new(limit: 2**24 - 1) + m.register_type %r(mediumblob)i, Type::Binary.new(limit: 2**24 - 1) + m.register_type %r(longtext)i, Type::Text.new(limit: 2**32 - 1) + m.register_type %r(longblob)i, Type::Binary.new(limit: 2**32 - 1) m.register_type %r(^float)i, Type::Float.new(limit: 24) m.register_type %r(^double)i, Type::Float.new(limit: 53) + register_integer_type m, %r(^bigint)i, limit: 8 + register_integer_type m, %r(^int)i, limit: 4 + register_integer_type m, %r(^mediumint)i, limit: 3 + register_integer_type m, %r(^smallint)i, limit: 2 + register_integer_type m, %r(^tinyint)i, limit: 1 + m.alias_type %r(tinyint\(1\))i, 'boolean' if emulate_booleans m.alias_type %r(set)i, 'varchar' m.alias_type %r(year)i, 'integer' m.alias_type %r(bit)i, 'binary' + + m.register_type(%r(enum)i) do |sql_type| + limit = sql_type[/^enum\((.+)\)/i, 1] + .split(',').map{|enum| enum.strip.length - 2}.max + MysqlString.new(limit: limit) + end + end + + def register_integer_type(mapping, key, options) # :nodoc: + mapping.register_type(key) do |sql_type| + if /unsigned/i =~ sql_type + Type::UnsignedInteger.new(options) + else + Type::Integer.new(options) + end + end end # MySQL is too stupid to create a temporary table for use subquery, so we have @@ -729,18 +795,26 @@ module ActiveRecord "DROP INDEX #{index_name}" end - def add_timestamps_sql(table_name) - [add_column_sql(table_name, :created_at, :datetime), add_column_sql(table_name, :updated_at, :datetime)] + def add_timestamps_sql(table_name, options = {}) + [add_column_sql(table_name, :created_at, :datetime, options), add_column_sql(table_name, :updated_at, :datetime, options)] end - def remove_timestamps_sql(table_name) + def remove_timestamps_sql(table_name, options = {}) [remove_column_sql(table_name, :updated_at), remove_column_sql(table_name, :created_at)] end private - def supports_views? - version[0] >= 5 + def version + @version ||= full_version.scan(/^(\d+)\.(\d+)\.(\d+)/).flatten.map(&:to_i) + end + + def mariadb? + full_version =~ /mariadb/i + end + + def supports_rename_index? + mariadb? ? false : (version[0] == 5 && version[1] >= 7) || version[0] >= 6 end def configure_connection @@ -758,21 +832,25 @@ module ActiveRecord # Make MySQL reject illegal values rather than truncating or blanking them, see # http://dev.mysql.com/doc/refman/5.0/en/server-sql-mode.html#sqlmode_strict_all_tables # If the user has provided another value for sql_mode, don't replace it. - if strict_mode? && !variables.has_key?('sql_mode') - variables['sql_mode'] = 'STRICT_ALL_TABLES' + unless variables.has_key?('sql_mode') + variables['sql_mode'] = strict_mode? ? 'STRICT_ALL_TABLES' : '' end # NAMES does not have an equals sign, see # http://dev.mysql.com/doc/refman/5.0/en/set-statement.html#id944430 # (trailing comma because variable_assignments will always have content) - encoding = "NAMES #{@config[:encoding]}, " if @config[:encoding] + if @config[:encoding] + encoding = "NAMES #{@config[:encoding]}" + encoding << " COLLATE #{@config[:collation]}" if @config[:collation] + encoding << ", " + end # Gather up all of the SET variables... variable_assignments = variables.map do |k, v| if v == ':default' || v == :default - "@@SESSION.#{k.to_s} = DEFAULT" # Sets the value to the global or compile default + "@@SESSION.#{k} = DEFAULT" # Sets the value to the global or compile default elsif !v.nil? - "@@SESSION.#{k.to_s} = #{quote(v)}" + "@@SESSION.#{k} = #{quote(v)}" end # or else nil; compact to clear nils out end.compact.join(', ') @@ -780,6 +858,39 @@ module ActiveRecord # ...and send them all in one query @connection.query "SET #{encoding} #{variable_assignments}" end + + def extract_foreign_key_action(structure, name, action) # :nodoc: + if structure =~ /CONSTRAINT #{quote_column_name(name)} FOREIGN KEY .* REFERENCES .* ON #{action} (CASCADE|SET NULL|RESTRICT)/ + case $1 + when 'CASCADE'; :cascade + when 'SET NULL'; :nullify + end + end + end + + def create_table_definition(name, temporary, options, as = nil) # :nodoc: + TableDefinition.new(native_database_types, name, temporary, options, as) + end + + class MysqlString < Type::String # :nodoc: + def type_cast_for_database(value) + case value + when true then "1" + when false then "0" + else super + end + end + + private + + def cast_value(value) + case value + when true then "1" + when false then "0" + else super + end + end + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/column.rb b/activerecord/lib/active_record/connection_adapters/column.rb index d629fca911..af307b57a4 100644 --- a/activerecord/lib/active_record/connection_adapters/column.rb +++ b/activerecord/lib/active_record/connection_adapters/column.rb @@ -13,7 +13,7 @@ module ActiveRecord ISO_DATETIME = /\A(\d{4})-(\d\d)-(\d\d) (\d\d):(\d\d):(\d\d)(\.\d+)?\z/ end - attr_reader :name, :cast_type, :null, :sql_type, :default_function + attr_reader :name, :cast_type, :null, :sql_type, :default, :default_function delegate :type, :precision, :scale, :limit, :klass, :accessor, :text?, :number?, :binary?, :changed?, @@ -35,7 +35,7 @@ module ActiveRecord @cast_type = cast_type @sql_type = sql_type @null = null - @original_default = default + @default = default @default_function = nil end @@ -51,21 +51,30 @@ module ActiveRecord Base.human_attribute_name(@name) end - def default - @default ||= type_cast_from_database(@original_default) - end - def with_type(type) dup.tap do |clone| - clone.instance_variable_set('@default', nil) clone.instance_variable_set('@cast_type', type) end end - end - class NullColumn < Column - def initialize(name) - super name, nil, Type::Value.new + def ==(other) + other.name == name && + other.default == default && + other.cast_type == cast_type && + other.sql_type == sql_type && + other.null == null && + other.default_function == default_function + end + alias :eql? :== + + def hash + attributes_for_hash.hash + end + + private + + def attributes_for_hash + [self.class, name, default, cast_type, sql_type, null, default_function] end end end diff --git a/activerecord/lib/active_record/connection_adapters/connection_specification.rb b/activerecord/lib/active_record/connection_adapters/connection_specification.rb index 2fcb085ab2..e54e3199ff 100644 --- a/activerecord/lib/active_record/connection_adapters/connection_specification.rb +++ b/activerecord/lib/active_record/connection_adapters/connection_specification.rb @@ -1,4 +1,5 @@ require 'uri' +require 'active_support/core_ext/string/filters' module ActiveRecord module ConnectionAdapters @@ -32,8 +33,8 @@ module ActiveRecord # } def initialize(url) raise "Database URL cannot be empty" if url.blank? - @uri = URI.parse(url) - @adapter = @uri.scheme.gsub('-', '_') + @uri = uri_parser.parse(url) + @adapter = @uri.scheme.tr('-', '_') @adapter = "postgresql" if @adapter == "postgres" if @uri.opaque @@ -160,7 +161,7 @@ module ActiveRecord # config = { "production" => { "host" => "localhost", "database" => "foo", "adapter" => "sqlite3" } } # spec = Resolver.new(config).spec(:production) # spec.adapter_method - # # => "sqlite3" + # # => "sqlite3_connection" # spec.config # # => { "host" => "localhost", "database" => "foo", "adapter" => "sqlite3" } # @@ -221,15 +222,19 @@ module ActiveRecord # this ambiguous behaviour and in the future this function # can be removed in favor of resolve_url_connection. if configurations.key?(spec) || spec !~ /:/ - ActiveSupport::Deprecation.warn "Passing a string to ActiveRecord::Base.establish_connection " \ - "for a configuration lookup is deprecated, please pass a symbol (#{spec.to_sym.inspect}) instead" + ActiveSupport::Deprecation.warn(<<-MSG.squish) + Passing a string to ActiveRecord::Base.establish_connection for a + configuration lookup is deprecated, please pass a symbol + (#{spec.to_sym.inspect}) instead. + MSG + resolve_symbol_connection(spec) else resolve_url_connection(spec) end end - # Takes the environment such as `:production` or `:development`. + # Takes the environment such as +:production+ or +:development+. # This requires that the @configurations was initialized with a key that # matches. # @@ -250,7 +255,7 @@ module ActiveRecord # Connection details inside of the "url" key win any merge conflicts def resolve_hash_connection(spec) if spec["url"] && spec["url"] !~ /^jdbc:/ - connection_hash = resolve_string_connection(spec.delete("url")) + connection_hash = resolve_url_connection(spec.delete("url")) spec.merge!(connection_hash) end spec diff --git a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb index 0a14cdfe89..75f244b3f3 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb @@ -29,7 +29,7 @@ module ActiveRecord module ConnectionAdapters class Mysql2Adapter < AbstractMysqlAdapter - ADAPTER_NAME = 'Mysql2' + ADAPTER_NAME = 'Mysql2'.freeze def initialize(connection, logger, connection_options, config) super @@ -66,21 +66,17 @@ module ActiveRecord exception.error_number if exception.respond_to?(:error_number) end + #-- # QUOTING ================================================== + #++ def quote_string(string) @connection.escape(string) end - def quoted_date(value) - if value.acts_like?(:time) && value.respond_to?(:usec) - "#{super}.#{sprintf("%06d", value.usec)}" - else - super - end - end - + #-- # CONNECTION MANAGEMENT ==================================== + #++ def active? return false unless @connection @@ -104,7 +100,9 @@ module ActiveRecord end end + #-- # DATABASE STATEMENTS ====================================== + #++ def explain(arel, binds = []) sql = "EXPLAIN #{to_sql(arel, binds.dup)}" @@ -232,11 +230,6 @@ module ActiveRecord alias exec_without_stmt exec_query - # Returns an ActiveRecord::Result instance. - def select(sql, name = nil, binds = []) - exec_query(sql, name) - end - def insert_sql(sql, name = nil, pk = nil, id_value = nil, sequence_name = nil) super id_value || @connection.last_id @@ -269,8 +262,8 @@ module ActiveRecord super end - def version - @version ||= @connection.info[:version].scan(/^(\d+)\.(\d+)\.(\d+)/).flatten.map { |v| v.to_i } + def full_version + @full_version ||= @connection.info[:version] end def set_field_encoding field_name diff --git a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb index 909bba8c7d..23d8389abb 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb @@ -58,7 +58,7 @@ module ActiveRecord # * <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.0/en/auto-reconnect.html). # * <tt>:strict</tt> - Defaults to true. Enable STRICT_ALL_TABLES. (See MySQL documentation: http://dev.mysql.com/doc/refman/5.0/en/server-sql-mode.html) - # * <tt>:variables</tt> - (Optional) A hash session variables to send as `SET @@SESSION.key = value` 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.0/en/set-statement.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.0/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. @@ -66,7 +66,7 @@ module ActiveRecord # * <tt>:sslcipher</tt> - Necessary to use MySQL with an SSL connection. # class MysqlAdapter < AbstractMysqlAdapter - ADAPTER_NAME = 'MySQL' + ADAPTER_NAME = 'MySQL'.freeze class StatementPool < ConnectionAdapters::StatementPool def initialize(connection, max = 1000) @@ -88,7 +88,7 @@ module ActiveRecord end def clear - cache.values.each do |hash| + cache.each_value do |hash| hash[:stmt].close end cache.clear @@ -137,7 +137,9 @@ module ActiveRecord @connection.quote(string) end + #-- # CONNECTION MANAGEMENT ==================================== + #++ def active? if @connection.respond_to?(:stat) @@ -178,7 +180,9 @@ module ActiveRecord end end + #-- # DATABASE STATEMENTS ====================================== + #++ def select_rows(sql, name = nil, binds = []) @connection.query_with_result = true @@ -263,7 +267,7 @@ module ActiveRecord end module Fields # :nodoc: - class DateTime < Type::DateTime + class DateTime < Type::DateTime # :nodoc: def cast_value(value) if Mysql::Time === value new_time( @@ -280,7 +284,7 @@ module ActiveRecord end end - class Time < Type::Time + class Time < Type::Time # :nodoc: def cast_value(value) if Mysql::Time === value new_time( @@ -409,7 +413,7 @@ module ActiveRecord stmt.execute(*type_casted_binds.map { |_, val| val }) 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 + # place when an error occurs. To support older MySQL versions, we # need to close the statement and delete the statement from the # cache. stmt.close @@ -419,9 +423,7 @@ module ActiveRecord cols = nil if metadata = stmt.result_metadata - cols = cache[:cols] ||= metadata.fetch_fields.map { |field| - field.name - } + cols = cache[:cols] ||= metadata.fetch_fields.map(&:name) metadata.free end @@ -465,14 +467,14 @@ module ActiveRecord def select(sql, name = nil, binds = []) @connection.query_with_result = true - rows = exec_query(sql, name, binds) + 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 version of the connected MySQL server. - def version - @version ||= @connection.server_info.scan(/^(\d+)\.(\d+)\.(\d+)/).flatten.map { |v| v.to_i } + # Returns the full version of the connected MySQL server. + def full_version + @full_version ||= @connection.server_info end def set_field_encoding field_name diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb b/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb deleted file mode 100644 index bb54de05c8..0000000000 --- a/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb +++ /dev/null @@ -1,105 +0,0 @@ -module ActiveRecord - module ConnectionAdapters - module PostgreSQL - module Cast # :nodoc: - def point_to_string(point) # :nodoc: - "(#{point[0]},#{point[1]})" - end - - def hstore_to_string(object, array_member = false) # :nodoc: - if Hash === object - string = object.map { |k, v| "#{escape_hstore(k)}=>#{escape_hstore(v)}" }.join(',') - string = escape_hstore(string) if array_member - string - else - object - end - end - - def string_to_hstore(string) # :nodoc: - if string.nil? - nil - elsif String === string - Hash[string.scan(HstorePair).map { |k, v| - v = v.upcase == 'NULL' ? nil : v.gsub(/\A"(.*)"\Z/m,'\1').gsub(/\\(.)/, '\1') - k = k.gsub(/\A"(.*)"\Z/m,'\1').gsub(/\\(.)/, '\1') - [k, v] - }] - else - string - end - end - - def json_to_string(object) # :nodoc: - if Hash === object || Array === object - ActiveSupport::JSON.encode(object) - else - object - end - end - - def array_to_string(value, column, adapter) # :nodoc: - casted_values = value.map do |val| - if String === val - if val == "NULL" - "\"#{val}\"" - else - quote_and_escape(adapter.type_cast(val, column, true)) - end - else - adapter.type_cast(val, column, true) - end - end - "{#{casted_values.join(',')}}" - end - - def range_to_string(object) # :nodoc: - from = object.begin.respond_to?(:infinite?) && object.begin.infinite? ? '' : object.begin - to = object.end.respond_to?(:infinite?) && object.end.infinite? ? '' : object.end - "[#{from},#{to}#{object.exclude_end? ? ')' : ']'}" - end - - def string_to_json(string) # :nodoc: - if String === string - ActiveSupport::JSON.decode(string) - else - string - end - end - - private - - HstorePair = begin - quoted_string = /"[^"\\]*(?:\\.[^"\\]*)*"/ - unquoted_string = /(?:\\.|[^\s,])[^\s=,\\]*(?:\\.[^\s=,\\]*|=[^,>])*/ - /(#{quoted_string}|#{unquoted_string})\s*=>\s*(#{quoted_string}|#{unquoted_string})/ - end - - def escape_hstore(value) - if value.nil? - 'NULL' - else - if value == "" - '""' - else - '"%s"' % value.to_s.gsub(/(["\\])/, '\\\\\1') - end - end - end - - ARRAY_ESCAPE = "\\" * 2 * 2 # escape the backslash twice for PG arrays - - def quote_and_escape(value) - case value - when "NULL", Numeric - value - else - value = value.gsub(/\\/, ARRAY_ESCAPE) - value.gsub!(/"/,"\\\"") - "\"#{value}\"" - end - end - end - end - end -end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/column.rb b/activerecord/lib/active_record/connection_adapters/postgresql/column.rb index 847fd4dded..37e5c3859c 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/column.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/column.rb @@ -1,11 +1,7 @@ -require 'active_record/connection_adapters/postgresql/cast' - module ActiveRecord module ConnectionAdapters # PostgreSQL-specific extensions to column definitions in a table. class PostgreSQLColumn < Column #:nodoc: - extend PostgreSQL::Cast - attr_accessor :array def initialize(name, default, cast_type, sql_type = nil, null = true, default_function = nil) 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 89a7257d77..d09468329a 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb @@ -156,10 +156,6 @@ module ActiveRecord end end - def substitute_at(column, index) - Arel::Nodes::BindParam.new "$#{index + 1}" - end - def exec_query(sql, name = 'SQL', binds = []) execute_and_clear(sql, name, binds) do |result| types = {} diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb index 33a98b4fcb..d28a2b4fa0 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb @@ -14,6 +14,7 @@ require 'active_record/connection_adapters/postgresql/oid/hstore' require 'active_record/connection_adapters/postgresql/oid/inet' require 'active_record/connection_adapters/postgresql/oid/integer' require 'active_record/connection_adapters/postgresql/oid/json' +require 'active_record/connection_adapters/postgresql/oid/jsonb' require 'active_record/connection_adapters/postgresql/oid/money' require 'active_record/connection_adapters/postgresql/oid/point' require 'active_record/connection_adapters/postgresql/oid/range' @@ -21,6 +22,7 @@ require 'active_record/connection_adapters/postgresql/oid/specialized_string' require 'active_record/connection_adapters/postgresql/oid/time' require 'active_record/connection_adapters/postgresql/oid/uuid' require 'active_record/connection_adapters/postgresql/oid/vector' +require 'active_record/connection_adapters/postgresql/oid/xml' require 'active_record/connection_adapters/postgresql/oid/type_map_initializer' 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 4e7d472d97..c203e6c604 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb @@ -2,12 +2,27 @@ module ActiveRecord module ConnectionAdapters module PostgreSQL module OID # :nodoc: - class Array < Type::Value - attr_reader :subtype + class Array < Type::Value # :nodoc: + include Type::Mutable + + # Loads pg_array_parser if available. String parsing can be + # performed quicker by a native extension, which will not create + # a large amount of Ruby objects that will need to be garbage + # collected. pg_array_parser has a C and Java extension + begin + require 'pg_array_parser' + include PgArrayParser + rescue LoadError + require 'active_record/connection_adapters/postgresql/array_parser' + include PostgreSQL::ArrayParser + end + + attr_reader :subtype, :delimiter delegate :type, to: :subtype - def initialize(subtype) + def initialize(subtype, delimiter = ',') @subtype = subtype + @delimiter = delimiter end def type_cast_from_database(value) @@ -19,19 +34,18 @@ module ActiveRecord end def type_cast_from_user(value) + if value.is_a?(::String) + value = parse_pg_array(value) + end type_cast_array(value, :type_cast_from_user) end - # Loads pg_array_parser if available. String parsing can be - # performed quicker by a native extension, which will not create - # a large amount of Ruby objects that will need to be garbage - # collected. pg_array_parser has a C and Java extension - begin - require 'pg_array_parser' - include PgArrayParser - rescue LoadError - require 'active_record/connection_adapters/postgresql/array_parser' - include PostgreSQL::ArrayParser + def type_cast_for_database(value) + if value.is_a?(::Array) + cast_value_for_database(value) + else + super + end end private @@ -43,6 +57,41 @@ module ActiveRecord @subtype.public_send(method, value) end end + + def cast_value_for_database(value) + if value.is_a?(::Array) + casted_values = value.map { |item| cast_value_for_database(item) } + "{#{casted_values.join(delimiter)}}" + else + quote_and_escape(subtype.type_cast_for_database(value)) + end + end + + ARRAY_ESCAPE = "\\" * 2 * 2 # escape the backslash twice for PG arrays + + def quote_and_escape(value) + case value + when ::String + if string_requires_quoting?(value) + value = value.gsub(/\\/, ARRAY_ESCAPE) + value.gsub!(/"/,"\\\"") + %("#{value}") + else + value + end + when nil then "NULL" + else value + end + end + + # See http://www.postgresql.org/docs/9.2/static/arrays.html#ARRAYS-IO + # for a list of all cases in which strings will be quoted. + def string_requires_quoting?(string) + string.empty? || + string == "NULL" || + string =~ /[\{\}"\\\s]/ || + string.include?(delimiter) + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit.rb index 3073f8ff30..1dbb40ca1d 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit.rb @@ -2,7 +2,7 @@ module ActiveRecord module ConnectionAdapters module PostgreSQL module OID # :nodoc: - class Bit < Type::Value + class Bit < Type::Value # :nodoc: def type :bit end @@ -19,6 +19,32 @@ module ActiveRecord value end end + + def type_cast_for_database(value) + Data.new(super) if value + end + + class Data + def initialize(value) + @value = value + end + + def to_s + value + end + + def binary? + /\A[01]*\Z/ === value + end + + def hex? + /\A[0-9A-F]*\Z/i === value + end + + protected + + attr_reader :value + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit_varying.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit_varying.rb index 054af285bb..4c21097d48 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit_varying.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/bit_varying.rb @@ -2,7 +2,7 @@ module ActiveRecord module ConnectionAdapters module PostgreSQL module OID # :nodoc: - class BitVarying < OID::Bit + class BitVarying < OID::Bit # :nodoc: def type :bit_varying end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/bytea.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/bytea.rb index 89b203d2b1..6bd1b8ecae 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/bytea.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/bytea.rb @@ -2,9 +2,10 @@ module ActiveRecord module ConnectionAdapters module PostgreSQL module OID # :nodoc: - class Bytea < Type::Binary + class Bytea < Type::Binary # :nodoc: def type_cast_from_database(value) return if value.nil? + return value.to_s if value.is_a?(Type::Binary::Data) PGconn.unescape_bytea(super) end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/cidr.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/cidr.rb index 534961a414..222f10fa8f 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/cidr.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/cidr.rb @@ -2,7 +2,7 @@ module ActiveRecord module ConnectionAdapters module PostgreSQL module OID # :nodoc: - class Cidr < Type::Value + class Cidr < Type::Value # :nodoc: def type :cidr end @@ -12,15 +12,15 @@ module ActiveRecord # If the subnet mask is equal to /32, don't output it if subnet_mask == (2**32 - 1) - "\"#{value.to_s}\"" + "\"#{value}\"" else - "\"#{value.to_s}/#{subnet_mask.to_s(2).count('1')}\"" + "\"#{value}/#{subnet_mask.to_s(2).count('1')}\"" end end def type_cast_for_database(value) if IPAddr === value - "#{value.to_s}/#{value.instance_variable_get(:@mask_addr).to_s(2).count('1')}" + "#{value}/#{value.instance_variable_get(:@mask_addr).to_s(2).count('1')}" else value end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/date.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/date.rb index 3c30ad5fec..1d8d264530 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/date.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/date.rb @@ -2,7 +2,7 @@ module ActiveRecord module ConnectionAdapters module PostgreSQL module OID # :nodoc: - class Date < Type::Date + class Date < Type::Date # :nodoc: include Infinity end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/date_time.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/date_time.rb index 34e2276dd1..b9e7894e5c 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/date_time.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/date_time.rb @@ -2,7 +2,7 @@ module ActiveRecord module ConnectionAdapters module PostgreSQL module OID # :nodoc: - class DateTime < Type::DateTime + class DateTime < Type::DateTime # :nodoc: include Infinity def cast_value(value) diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/decimal.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/decimal.rb index ed4b8911d9..43d22c8daf 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/decimal.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/decimal.rb @@ -2,7 +2,7 @@ module ActiveRecord module ConnectionAdapters module PostgreSQL module OID # :nodoc: - class Decimal < Type::Decimal + class Decimal < Type::Decimal # :nodoc: def infinity(options = {}) BigDecimal.new("Infinity") * (options[:negative] ? -1 : 1) end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/enum.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/enum.rb index 5fed8b0f89..77d5038efd 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/enum.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/enum.rb @@ -2,7 +2,7 @@ module ActiveRecord module ConnectionAdapters module PostgreSQL module OID # :nodoc: - class Enum < Type::Value + class Enum < Type::Value # :nodoc: def type :enum end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/float.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/float.rb index 77dd97e140..78ef94b912 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/float.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/float.rb @@ -2,11 +2,12 @@ module ActiveRecord module ConnectionAdapters module PostgreSQL module OID # :nodoc: - class Float < Type::Float + class Float < Type::Float # :nodoc: include Infinity def cast_value(value) case value + when ::Float then value when 'Infinity' then ::Float::INFINITY when '-Infinity' then -::Float::INFINITY when 'NaN' then ::Float::NAN diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/hstore.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/hstore.rb index 0dd4b65333..be4525c94f 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/hstore.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/hstore.rb @@ -2,7 +2,7 @@ module ActiveRecord module ConnectionAdapters module PostgreSQL module OID # :nodoc: - class Hstore < Type::Value + class Hstore < Type::Value # :nodoc: include Type::Mutable def type @@ -10,16 +10,48 @@ module ActiveRecord end def type_cast_from_database(value) - ConnectionAdapters::PostgreSQLColumn.string_to_hstore(value) + if value.is_a?(::String) + ::Hash[value.scan(HstorePair).map { |k, v| + v = v.upcase == 'NULL' ? nil : v.gsub(/\A"(.*)"\Z/m,'\1').gsub(/\\(.)/, '\1') + k = k.gsub(/\A"(.*)"\Z/m,'\1').gsub(/\\(.)/, '\1') + [k, v] + }] + else + value + end end def type_cast_for_database(value) - ConnectionAdapters::PostgreSQLColumn.hstore_to_string(value) + if value.is_a?(::Hash) + value.map { |k, v| "#{escape_hstore(k)}=>#{escape_hstore(v)}" }.join(', ') + else + value + end end def accessor ActiveRecord::Store::StringKeyedHashAccessor end + + private + + HstorePair = begin + quoted_string = /"[^"\\]*(?:\\.[^"\\]*)*"/ + unquoted_string = /(?:\\.|[^\s,])[^\s=,\\]*(?:\\.[^\s=,\\]*|=[^,>])*/ + /(#{quoted_string}|#{unquoted_string})\s*=>\s*(#{quoted_string}|#{unquoted_string})/ + end + + def escape_hstore(value) + if value.nil? + 'NULL' + else + if value == "" + '""' + else + '"%s"' % value.to_s.gsub(/(["\\])/, '\\\\\1') + end + end + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/inet.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/inet.rb index 7ed8f5f031..96486fa65b 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/inet.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/inet.rb @@ -2,7 +2,7 @@ module ActiveRecord module ConnectionAdapters module PostgreSQL module OID # :nodoc: - class Inet < Cidr + class Inet < Cidr # :nodoc: def type :inet end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/infinity.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/infinity.rb index d438ffa140..e47780399a 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/infinity.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/infinity.rb @@ -2,7 +2,7 @@ module ActiveRecord module ConnectionAdapters module PostgreSQL module OID # :nodoc: - module Infinity + module Infinity # :nodoc: def infinity(options = {}) options[:negative] ? -::Float::INFINITY : ::Float::INFINITY end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/integer.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/integer.rb index 388d3dd9ed..59abdc0009 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/integer.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/integer.rb @@ -2,7 +2,7 @@ module ActiveRecord module ConnectionAdapters module PostgreSQL module OID # :nodoc: - class Integer < Type::Integer + class Integer < Type::Integer # :nodoc: include Infinity end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/json.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/json.rb index d1347c7bb5..e12ddd9901 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/json.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/json.rb @@ -2,7 +2,7 @@ module ActiveRecord module ConnectionAdapters module PostgreSQL module OID # :nodoc: - class Json < Type::Value + class Json < Type::Value # :nodoc: include Type::Mutable def type @@ -10,11 +10,19 @@ module ActiveRecord end def type_cast_from_database(value) - ConnectionAdapters::PostgreSQLColumn.string_to_json(value) + if value.is_a?(::String) + ::ActiveSupport::JSON.decode(value) + else + super + end end def type_cast_for_database(value) - ConnectionAdapters::PostgreSQLColumn.json_to_string(value) + if value.is_a?(::Array) || value.is_a?(::Hash) + ::ActiveSupport::JSON.encode(value) + else + super + end end def accessor diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/jsonb.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/jsonb.rb new file mode 100644 index 0000000000..380c50fc14 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/jsonb.rb @@ -0,0 +1,23 @@ +module ActiveRecord + module ConnectionAdapters + module PostgreSQL + module OID # :nodoc: + class Jsonb < Json # :nodoc: + def type + :jsonb + end + + def changed_in_place?(raw_old_value, new_value) + # Postgres does not preserve insignificant whitespaces when + # roundtripping jsonb columns. This causes some false positives for + # the comparison here. Therefore, we need to parse and re-dump the + # raw value here to ensure the insignificant whitespaces are + # consistent with our encoder's output. + raw_old_value = type_cast_for_database(type_cast_from_database(raw_old_value)) + super(raw_old_value, new_value) + end + end + end + end + end +end 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 d25eb256c2..df890c2ed6 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/money.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/money.rb @@ -2,7 +2,7 @@ module ActiveRecord module ConnectionAdapters module PostgreSQL module OID # :nodoc: - class Money < Type::Decimal + class Money < Type::Decimal # :nodoc: include Infinity class_attribute :precision diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/point.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/point.rb index 9007bfb178..bac8b01d6b 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/point.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/point.rb @@ -2,21 +2,40 @@ module ActiveRecord module ConnectionAdapters module PostgreSQL module OID # :nodoc: - class Point < Type::Value + class Point < Type::Value # :nodoc: + include Type::Mutable + def type :point end def type_cast(value) - if ::String === value + case value + when ::String if value[0] == '(' && value[-1] == ')' value = value[1...-1] end - value.split(',').map{ |v| Float(v) } + type_cast(value.split(',')) + when ::Array + value.map { |v| Float(v) } else value end end + + def type_cast_for_database(value) + if value.is_a?(::Array) + "(#{number_for_point(value[0])},#{number_for_point(value[1])})" + else + super + end + end + + private + + def number_for_point(number) + number.to_s.gsub(/\.0$/, '') + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb index c289ba8980..961e6224c4 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/range.rb @@ -1,8 +1,10 @@ +require 'active_support/core_ext/string/filters' + module ActiveRecord module ConnectionAdapters module PostgreSQL module OID # :nodoc: - class Range < Type::Value + class Range < Type::Value # :nodoc: attr_reader :subtype, :type def initialize(subtype, type) @@ -10,28 +12,10 @@ module ActiveRecord @type = type end - def extract_bounds(value) - from, to = value[1..-2].split(',') - { - from: (value[1] == ',' || from == '-infinity') ? @subtype.infinity(negative: true) : from, - to: (value[-2] == ',' || to == 'infinity') ? @subtype.infinity : to, - exclude_start: (value[0] == '('), - exclude_end: (value[-1] == ')') - } - end - - def infinity?(value) - value.respond_to?(:infinite?) && value.infinite? - end - def type_cast_for_schema(value) value.inspect.gsub('Infinity', '::Float::INFINITY') end - def type_cast_single(value) - infinity?(value) ? value : @subtype.type_cast_from_database(value) - end - def cast_value(value) return if value == 'empty' return value if value.is_a?(::Range) @@ -43,16 +27,51 @@ module ActiveRecord if !infinity?(from) && extracted[:exclude_start] if from.respond_to?(:succ) from = from.succ - ActiveSupport::Deprecation.warn <<-MESSAGE -Excluding the beginning of a Range is only partialy supported through `#succ`. -This is not reliable and will be removed in the future. - MESSAGE + ActiveSupport::Deprecation.warn(<<-MSG.squish) + Excluding the beginning of a Range is only partialy supported + through `#succ`. This is not reliable and will be removed in + the future. + MSG else raise ArgumentError, "The Ruby Range object does not support excluding the beginning of a Range. (unsupported value: '#{value}')" end end ::Range.new(from, to, extracted[:exclude_end]) end + + def type_cast_for_database(value) + if value.is_a?(::Range) + from = type_cast_single_for_database(value.begin) + to = type_cast_single_for_database(value.end) + "[#{from},#{to}#{value.exclude_end? ? ')' : ']'}" + else + super + end + end + + private + + def type_cast_single(value) + infinity?(value) ? value : @subtype.type_cast_from_database(value) + end + + def type_cast_single_for_database(value) + infinity?(value) ? '' : @subtype.type_cast_for_database(value) + end + + def extract_bounds(value) + from, to = value[1..-2].split(',') + { + from: (value[1] == ',' || from == '-infinity') ? @subtype.infinity(negative: true) : from, + to: (value[-2] == ',' || to == 'infinity') ? @subtype.infinity : to, + exclude_start: (value[0] == '('), + exclude_end: (value[-1] == ')') + } + end + + def infinity?(value) + value.respond_to?(:infinite?) && value.infinite? + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/specialized_string.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/specialized_string.rb index 7b1ca16bc4..b2a42e9ebb 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/specialized_string.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/specialized_string.rb @@ -2,7 +2,7 @@ module ActiveRecord module ConnectionAdapters module PostgreSQL module OID # :nodoc: - class SpecializedString < Type::String + class SpecializedString < Type::String # :nodoc: attr_reader :type def initialize(type) diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/time.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/time.rb index ea1f599b0f..8f0246eddb 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/time.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/time.rb @@ -2,7 +2,7 @@ module ActiveRecord module ConnectionAdapters module PostgreSQL module OID # :nodoc: - class Time < Type::Time + class Time < Type::Time # :nodoc: include Infinity end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb index 28f7a4eafb..9b3de41fab 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb @@ -4,7 +4,7 @@ module ActiveRecord module OID # :nodoc: # This class uses the data from PostgreSQL pg_type table to build # the OID -> Type mapping. - # - OID is and integer representing the type. + # - OID is an integer representing the type. # - Type is an OID::Type object. # This class has side effects on the +store+ passed during initialization. class TypeMapInitializer # :nodoc: @@ -39,14 +39,14 @@ module ActiveRecord end def register_array_type(row) - if subtype = @store.lookup(row['typelem'].to_i) - register row['oid'], OID::Array.new(subtype) + register_with_subtype(row['oid'], row['typelem'].to_i) do |subtype| + OID::Array.new(subtype, row['typdelim']) end end def register_range_type(row) - if subtype = @store.lookup(row['rngsubtype'].to_i) - register row['oid'], OID::Range.new(subtype, row['typname'].to_sym) + register_with_subtype(row['oid'], row['rngsubtype'].to_i) do |subtype| + OID::Range.new(subtype, row['typname'].to_sym) end end @@ -64,9 +64,13 @@ module ActiveRecord end end - def register(oid, oid_type) - oid = assert_valid_registration(oid, oid_type) - @store.register_type(oid, oid_type) + def register(oid, oid_type = nil, &block) + oid = assert_valid_registration(oid, oid_type || block) + if block_given? + @store.register_type(oid, &block) + else + @store.register_type(oid, oid_type) + end end def alias_type(oid, target) @@ -74,6 +78,14 @@ module ActiveRecord @store.alias_type(oid, target) end + def register_with_subtype(oid, target_oid) + if @store.key?(target_oid) + register(oid) do |_, *args| + yield @store.lookup(target_oid, *args) + end + end + end + def assert_valid_registration(oid, oid_type) raise ArgumentError, "can't register nil type for OID #{oid}" if oid_type.nil? oid.to_i diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/uuid.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/uuid.rb index 0ed5491887..97b4fd3d08 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/uuid.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/uuid.rb @@ -2,13 +2,17 @@ module ActiveRecord module ConnectionAdapters module PostgreSQL module OID # :nodoc: - class Uuid < Type::Value + class Uuid < Type::Value # :nodoc: + ACCEPTABLE_UUID = %r{\A\{?([a-fA-F0-9]{4}-?){8}\}?\z}x + + alias_method :type_cast_for_database, :type_cast_from_database + def type :uuid end def type_cast(value) - value.presence + value.to_s[ACCEPTABLE_UUID, 0] end end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/vector.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/vector.rb index 2f7d1be197..de4187b028 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/vector.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/vector.rb @@ -2,7 +2,7 @@ module ActiveRecord module ConnectionAdapters module PostgreSQL module OID # :nodoc: - class Vector < Type::Value + class Vector < Type::Value # :nodoc: attr_reader :delim, :subtype # +delim+ corresponds to the `typdelim` column in the pg_types diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/xml.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/xml.rb new file mode 100644 index 0000000000..334af7c598 --- /dev/null +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/xml.rb @@ -0,0 +1,28 @@ +module ActiveRecord + module ConnectionAdapters + module PostgreSQL + module OID # :nodoc: + class Xml < Type::String # :nodoc: + def type + :xml + end + + def type_cast_for_database(value) + return unless value + Data.new(super) + end + + class Data # :nodoc: + def initialize(value) + @value = value + end + + def to_s + @value + end + end + end + end + end + end +end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb index 3cf40e6cd4..607848884b 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb @@ -14,107 +14,6 @@ module ActiveRecord @connection.unescape_bytea(value) if value end - # Quotes PostgreSQL-specific data types for SQL input. - def quote(value, column = nil) #:nodoc: - return super unless column - - sql_type = type_to_sql(column.type, column.limit, column.precision, column.scale) - - case value - when Range - if /range$/ =~ sql_type - "'#{PostgreSQLColumn.range_to_string(value)}'::#{sql_type}" - else - super - end - when Array - case sql_type - when 'point' then super(PostgreSQLColumn.point_to_string(value)) - when 'json' then super(PostgreSQLColumn.json_to_string(value)) - else - if column.array - "'#{PostgreSQLColumn.array_to_string(value, column, self).gsub(/'/, "''")}'" - else - super - end - end - when Hash - case sql_type - when 'hstore' then super(PostgreSQLColumn.hstore_to_string(value), column) - when 'json' then super(PostgreSQLColumn.json_to_string(value), column) - else super - end - when Float - if value.infinite? && column.type == :datetime - "'#{value.to_s.downcase}'" - elsif value.infinite? || value.nan? - "'#{value.to_s}'" - else - super - end - when Numeric - if sql_type == 'money' || [:string, :text].include?(column.type) - # Not truly string input, so doesn't require (or allow) escape string syntax. - "'#{value}'" - else - super - end - when String - case sql_type - when 'xml' then "xml '#{quote_string(value)}'" - when /^bit/ - case value - when /^[01]*$/ then "B'#{value}'" # Bit-string notation - when /^[0-9A-F]*$/i then "X'#{value}'" # Hexadecimal notation - end - else - super - end - else - super - end - end - - def type_cast(value, column, array_member = false) - return super(value, column) unless column - - case value - when Range - if /range$/ =~ column.sql_type - PostgreSQLColumn.range_to_string(value) - else - super(value, column) - end - when NilClass - if column.array && array_member - 'NULL' - elsif column.array - value - else - super(value, column) - end - when Array - case column.sql_type - when 'point' then PostgreSQLColumn.point_to_string(value) - when 'json' then PostgreSQLColumn.json_to_string(value) - else - if column.array - PostgreSQLColumn.array_to_string(value, column, self) - else - super(value, column) - end - end - when Hash - case column.sql_type - when 'hstore' then PostgreSQLColumn.hstore_to_string(value, array_member) - when 'json' then PostgreSQLColumn.json_to_string(value) - else super(value, column) - end - else - super(value, column) - end - end - # Quotes strings for use in SQL input. def quote_string(s) #:nodoc: @connection.escape(s) @@ -144,16 +43,12 @@ module ActiveRecord # Quote date/time values for use in SQL input. Includes microseconds # if the value is a Time responding to usec. def quoted_date(value) #:nodoc: - result = super - if value.acts_like?(:time) && value.respond_to?(:usec) - result = "#{result}.#{sprintf("%06d", value.usec)}" - end - if value.year <= 0 bce_year = format("%04d", -value.year + 1) - result = result.sub(/^-?\d+/, bce_year) + " BC" + super.sub(/^-?\d+/, bce_year) + " BC" + else + super end - result end # Does not quote function default values for UUID columns @@ -168,19 +63,37 @@ module ActiveRecord private def _quote(value) - if value.is_a?(Type::Binary::Data) + case value + when Type::Binary::Data "'#{escape_bytea(value.to_s)}'" + when OID::Xml::Data + "xml '#{quote_string(value.to_s)}'" + when OID::Bit::Data + if value.binary? + "B'#{value}'" + elsif value.hex? + "X'#{value}'" + end + when Float + if value.infinite? || value.nan? + "'#{value}'" + else + super + end else super end end def _type_cast(value) - if value.is_a?(Type::Binary::Data) + case value + when Type::Binary::Data # Return a bind param hash with format as binary. # See http://deveiate.org/code/pg/PGconn.html#method-i-exec_prepared-doc # for more information { value: value.to_s, format: 1 } + when OID::Xml::Data, OID::Bit::Data + value.to_s else super end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb index 0867e5ef54..a9522e152f 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_definitions.rb @@ -64,6 +64,10 @@ module ActiveRecord column(name, :json, options) end + def jsonb(name, options = {}) + column(name, :jsonb, options) + end + def citext(name, options = {}) column(name, :citext, options) end @@ -121,18 +125,14 @@ module ActiveRecord # a record (as primary keys cannot be +nil+). This might be done via the # +SecureRandom.uuid+ method and a +before_save+ callback, for instance. def primary_key(name, type = :primary_key, options = {}) - return super unless type == :uuid - options[:default] = options.fetch(:default, 'uuid_generate_v4()') - options[:primary_key] = true - column name, type, options + options[:default] = options.fetch(:default, 'uuid_generate_v4()') if type == :uuid + super end - def column(name, type = nil, options = {}) - super - column = self[name] + def new_column_definition(name, type, options) # :nodoc: + column = super column.array = options[:array] - - self + column end private 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 b2aeb3a058..a90adcf4aa 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb @@ -4,29 +4,30 @@ module ActiveRecord class SchemaCreation < AbstractAdapter::SchemaCreation private - def visit_AddColumn(o) - sql_type = type_to_sql(o.type, o.limit, o.precision, o.scale) - sql = "ADD COLUMN #{quote_column_name(o.name)} #{sql_type}" - add_column_options!(sql, column_options(o)) + def column_options(o) + column_options = super + column_options[:array] = o.array + column_options end - def visit_ColumnDefinition(o) - sql = super - if o.primary_key? && o.type != :primary_key - sql << " PRIMARY KEY " - add_column_options!(sql, column_options(o)) + def add_column_options!(sql, options) + if options[:array] + sql << '[]' end - sql + super end - def add_column_options!(sql, options) - if options[:array] || options[:column].try(:array) - sql << '[]' + def quote_default_expression(value, column) + if column.type == :uuid && value =~ /\(\)/ + value + else + super end + end - column = options.fetch(:column) { return super } - if column.type == :uuid && options[:default] =~ /\(\)/ - sql << " DEFAULT #{options[:default]}" + def type_for_column(column) + if column.array + @conn.lookup_cast_type("#{column.sql_type}[]") else super end @@ -52,8 +53,8 @@ module ActiveRecord def create_database(name, options = {}) options = { encoding: 'utf8' }.merge!(options.symbolize_keys) - option_string = options.sum do |key, value| - case key + option_string = options.inject("") do |memo, (key, value)| + memo += case key when :owner " OWNER = \"#{value}\"" when :template @@ -110,6 +111,10 @@ module ActiveRecord SQL end + def drop_table(table_name, options = {}) + execute "DROP TABLE #{quote_table_name(table_name)}#{' CASCADE' if options[:force] == :cascade}" + end + # Returns true if schema exists. def schema_exists?(name) exec_query(<<-SQL, 'SCHEMA').rows.first[0].to_i > 0 @@ -273,9 +278,9 @@ module ActiveRecord def default_sequence_name(table_name, pk = nil) #:nodoc: result = serial_sequence(table_name, pk || 'id') return nil unless result - Utils.extract_schema_qualified_name(result) + Utils.extract_schema_qualified_name(result).to_s rescue ActiveRecord::StatementInvalid - PostgreSQL::Name.new(nil, "#{table_name}_#{pk || 'id'}_seq") + PostgreSQL::Name.new(nil, "#{table_name}_#{pk || 'id'}_seq").to_s end def serial_sequence(table, column) @@ -285,6 +290,23 @@ module ActiveRecord result.rows.first.first end + # Sets the sequence of a table's primary key to the specified value. + def set_pk_sequence!(table, value) #:nodoc: + pk, sequence = pk_and_sequence_for(table) + + if pk + if sequence + quoted_sequence = quote_table_name(sequence) + + select_value <<-end_sql, 'SCHEMA' + SELECT setval('#{quoted_sequence}', #{value}) + end_sql + else + @logger.warn "#{table} has primary key #{pk} with no default sequence" if @logger + end + end + end + # Resets the sequence of a table's primary key to the maximum value. def reset_pk_sequence!(table, pk = nil, sequence = nil) #:nodoc: unless pk and sequence @@ -363,20 +385,20 @@ module ActiveRecord # Returns just a table's primary key def primary_key(table) - row = exec_query(<<-end_sql, 'SCHEMA').rows.first + pks = exec_query(<<-end_sql, 'SCHEMA').rows SELECT attr.attname FROM pg_attribute attr - INNER JOIN pg_constraint cons ON attr.attrelid = cons.conrelid AND attr.attnum = cons.conkey[1] + INNER JOIN pg_constraint cons ON attr.attrelid = cons.conrelid AND attr.attnum = any(cons.conkey) WHERE cons.contype = 'p' AND cons.conrelid = '#{quote_table_name(table)}'::regclass end_sql - - row && row.first + return nil unless pks.count == 1 + pks[0][0] end # Renames a table. - # Also renames a table's primary key sequence if the sequence name matches the - # Active Record default. + # Also renames a table's primary key sequence if the sequence name exists and + # matches the Active Record default. # # Example: # rename_table('octopuses', 'octopi') @@ -384,9 +406,12 @@ module ActiveRecord clear_cache! execute "ALTER TABLE #{quote_table_name(table_name)} RENAME TO #{quote_table_name(new_name)}" pk, seq = pk_and_sequence_for(new_name) - if seq.identifier == "#{table_name}_#{pk}_seq" + if seq && seq.identifier == "#{table_name}_#{pk}_seq" new_seq = "#{new_name}_#{pk}_seq" + idx = "#{table_name}_pkey" + new_idx = "#{new_name}_pkey" execute "ALTER TABLE #{quote_table_name(seq)} RENAME TO #{quote_table_name(new_seq)}" + execute "ALTER INDEX #{quote_table_name(idx)} RENAME TO #{quote_table_name(new_idx)}" end rename_table_indexes(table_name, new_name) @@ -405,7 +430,12 @@ module ActiveRecord quoted_table_name = quote_table_name(table_name) sql_type = type_to_sql(type, options[:limit], options[:precision], options[:scale]) sql_type << "[]" if options[:array] - execute "ALTER TABLE #{quoted_table_name} ALTER COLUMN #{quote_column_name(column_name)} TYPE #{sql_type}" + sql = "ALTER TABLE #{quoted_table_name} ALTER COLUMN #{quote_column_name(column_name)} TYPE #{sql_type}" + sql << " USING #{options[:using]}" if options[:using] + if options[:cast_as] + sql << " USING CAST(#{quote_column_name(column_name)} AS #{type_to_sql(options[:cast_as], options[:limit], options[:precision], options[:scale])})" + end + execute sql change_column_default(table_name, column_name, options[:default]) if options_include_default?(options) change_column_null(table_name, column_name, options[:null], options[:default]) if options.key?(:null) @@ -415,8 +445,16 @@ module ActiveRecord def change_column_default(table_name, column_name, default) clear_cache! column = column_for(table_name, column_name) + return unless column - execute "ALTER TABLE #{quote_table_name(table_name)} ALTER COLUMN #{quote_column_name(column_name)} SET DEFAULT #{quote_default_value(default, column)}" if column + alter_column_query = "ALTER TABLE #{quote_table_name(table_name)} ALTER COLUMN #{quote_column_name(column_name)} %s" + if default.nil? + # <tt>DEFAULT NULL</tt> results in the same behavior as <tt>DROP DEFAULT</tt>. However, PostgreSQL will + # cast the default to the columns type, which leaves us with a default like "default NULL::character varying". + execute alter_column_query % "DROP DEFAULT" + else + execute alter_column_query % "SET DEFAULT #{quote_default_value(default, column)}" + end end def change_column_null(table_name, column_name, null, default = nil) @@ -445,9 +483,48 @@ module ActiveRecord end def rename_index(table_name, old_name, new_name) + validate_index_length!(table_name, new_name) + execute "ALTER INDEX #{quote_column_name(old_name)} RENAME TO #{quote_table_name(new_name)}" end + def foreign_keys(table_name) + fk_info = select_all <<-SQL.strip_heredoc + SELECT t2.oid::regclass::text AS to_table, a1.attname AS column, a2.attname AS primary_key, c.conname AS name, c.confupdtype AS on_update, c.confdeltype AS on_delete + FROM pg_constraint c + JOIN pg_class t1 ON c.conrelid = t1.oid + JOIN pg_class t2 ON c.confrelid = t2.oid + JOIN pg_attribute a1 ON a1.attnum = c.conkey[1] AND a1.attrelid = t1.oid + JOIN pg_attribute a2 ON a2.attnum = c.confkey[1] AND a2.attrelid = t2.oid + JOIN pg_namespace t3 ON c.connamespace = t3.oid + WHERE c.contype = 'f' + AND t1.relname = #{quote(table_name)} + AND t3.nspname = ANY (current_schemas(false)) + ORDER BY c.conname + SQL + + fk_info.map do |row| + options = { + column: row['column'], + name: row['name'], + primary_key: row['primary_key'] + } + + options[:on_delete] = extract_foreign_key_action(row['on_delete']) + options[:on_update] = extract_foreign_key_action(row['on_update']) + + ForeignKeyDefinition.new(table_name, row['to_table'], options) + end + end + + def extract_foreign_key_action(specifier) # :nodoc: + case specifier + when 'c'; :cascade + when 'n'; :nullify + when 'r'; :restrict + end + end + def index_name_length 63 end @@ -497,7 +574,8 @@ module ActiveRecord # Convert Arel node to string s = s.to_sql unless s.is_a?(String) # Remove any ASC/DESC modifiers - s.gsub(/\s+(?:ASC|DESC)?\s*(?:NULLS\s+(?:FIRST|LAST)\s*)?/i, '') + s.gsub(/\s+(?:ASC|DESC)\b/i, '') + .gsub(/\s+NULLS\s+(?:FIRST|LAST)\b/i, '') }.reject(&:blank?).map.with_index { |column, i| "#{column} AS alias_#{i}" } [super, *order_columns].join(', ') diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/utils.rb b/activerecord/lib/active_record/connection_adapters/postgresql/utils.rb index 0290bcb48c..9a0b80d7d3 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/utils.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/utils.rb @@ -18,7 +18,11 @@ module ActiveRecord end def quoted - parts.map { |p| PGconn.quote_ident(p) }.join SEPARATOR + if schema + PGconn.quote_ident(schema) << SEPARATOR << PGconn.quote_ident(identifier) + else + PGconn.quote_ident(identifier) + end end def ==(o) @@ -32,8 +36,11 @@ module ActiveRecord protected def unquote(part) - return unless part - part.gsub(/(^"|"$)/,'') + if part && part.start_with?('"') + part[1..-2] + else + part + end end def parts @@ -57,7 +64,11 @@ module ActiveRecord # * <tt>"schema_name".table_name</tt> # * <tt>"schema.name"."table name"</tt> def extract_schema_qualified_name(string) - table, schema = string.scan(/[^".\s]+|"[^"]*"/)[0..1].reverse + schema, table = string.scan(/[^".\s]+|"[^"]*"/) + if table.nil? + table = schema + schema = nil + end PostgreSQL::Name.new(schema, table) end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index 71b05cdbae..02cafc8079 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -13,7 +13,7 @@ require 'active_record/connection_adapters/postgresql/database_statements' require 'arel/visitors/bind_visitor' # Make sure we're using pg high enough for PGResult#values -gem 'pg', '~> 0.11' +gem 'pg', '~> 0.15' require 'pg' require 'ipaddr' @@ -74,10 +74,11 @@ module ActiveRecord # In addition, default connection parameters of libpq can be set per environment variables. # See http://www.postgresql.org/docs/9.1/static/libpq-envars.html . class PostgreSQLAdapter < AbstractAdapter - ADAPTER_NAME = 'PostgreSQL' + ADAPTER_NAME = 'PostgreSQL'.freeze NATIVE_DATABASE_TYPES = { primary_key: "serial primary key", + bigserial: "bigserial", string: { name: "character varying" }, text: { name: "text" }, integer: { name: "integer" }, @@ -94,6 +95,7 @@ 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 +104,7 @@ module ActiveRecord macaddr: { name: "macaddr" }, uuid: { name: "uuid" }, json: { name: "json" }, + jsonb: { name: "jsonb" }, ltree: { name: "ltree" }, citext: { name: "citext" }, point: { name: "point" }, @@ -118,25 +121,20 @@ module ActiveRecord include PostgreSQL::DatabaseStatements include Savepoints - # Returns 'PostgreSQL' as adapter name for identification purposes. - def adapter_name - ADAPTER_NAME - end - def schema_creation # :nodoc: PostgreSQL::SchemaCreation.new self end - # Adds `:array` option to the default set provided by the + # Adds +:array+ option to the default set provided by the # AbstractAdapter - def prepare_column_options(column, types) # :nodoc: + def prepare_column_options(column) # :nodoc: spec = super spec[:array] = 'true' if column.respond_to?(:array) && column.array spec[:default] = "\"#{column.default_function}\"" if column.default_function spec end - # Adds `:array` as a valid migration key + # Adds +:array+ as a valid migration key def migration_keys super + [:array] end @@ -159,6 +157,14 @@ module ActiveRecord true end + def supports_foreign_keys? + true + end + + def supports_views? + true + end + def index_algorithms { concurrently: 'CONCURRENTLY' } end @@ -252,6 +258,10 @@ module ActiveRecord @statements.clear end + def truncate(table_name, name = nil) + exec_query "TRUNCATE TABLE #{quote_table_name(table_name)}", name, [] + end + # Is this connection alive and ready for queries? def active? @connection.query 'SELECT 1' @@ -306,10 +316,6 @@ module ActiveRecord self.client_min_messages = old end - def supports_insert_with_returning? - true - end - def supports_ddl_transactions? true end @@ -348,14 +354,13 @@ module ActiveRecord if supports_extensions? res = exec_query "SELECT EXISTS(SELECT * FROM pg_available_extensions WHERE name = '#{name}' AND installed_version IS NOT NULL) as enabled", 'SCHEMA' - res.column_types['enabled'].type_cast_from_database res.rows.first.first + res.cast_values.first end end def extensions if supports_extensions? - res = exec_query "SELECT extname from pg_extension", "SCHEMA" - res.rows.map { |r| res.column_types['extname'].type_cast_from_database r.first } + exec_query("SELECT extname from pg_extension", "SCHEMA").cast_values else super end @@ -384,6 +389,21 @@ module ActiveRecord PostgreSQL::Table.new(table_name, base) end + def lookup_cast_type(sql_type) # :nodoc: + oid = execute("SELECT #{quote(sql_type)}::regtype::oid", "SCHEMA").first['oid'].to_i + super(oid) + end + + def column_name_for_operation(operation, node) # :nodoc: + OPERATION_ALIASES.fetch(operation) { operation.downcase } + end + + OPERATION_ALIASES = { # :nodoc: + "maximum" => "max", + "minimum" => "min", + "average" => "avg", + } + protected # Returns the version of the connected PostgreSQL server. @@ -425,8 +445,8 @@ module ActiveRecord def initialize_type_map(m) # :nodoc: register_class_with_limit m, 'int2', OID::Integer - m.alias_type 'int4', 'int2' - m.alias_type 'int8', 'int2' + register_class_with_limit m, 'int4', OID::Integer + register_class_with_limit m, 'int8', OID::Integer m.alias_type 'oid', 'int2' m.register_type 'float4', OID::Float.new m.alias_type 'float8', 'float4' @@ -447,10 +467,11 @@ module ActiveRecord m.register_type 'point', OID::Point.new m.register_type 'hstore', OID::Hstore.new m.register_type 'json', OID::Json.new + m.register_type 'jsonb', OID::Jsonb.new m.register_type 'cidr', OID::Cidr.new m.register_type 'inet', OID::Inet.new m.register_type 'uuid', OID::Uuid.new - m.register_type 'xml', OID::SpecializedString.new(:xml) + m.register_type 'xml', OID::Xml.new m.register_type 'tsvector', OID::SpecializedString.new(:tsvector) m.register_type 'macaddr', OID::SpecializedString.new(:macaddr) m.register_type 'citext', OID::SpecializedString.new(:citext) @@ -495,9 +516,12 @@ module ActiveRecord def extract_limit(sql_type) # :nodoc: case sql_type - when /^bigint/i; 8 - when /^smallint/i; 2 - else super + when /^bigint/i, /^int8/i + 8 + when /^smallint/i + 2 + else + super end end @@ -511,7 +535,7 @@ module ActiveRecord when 'true', 'false' default # Numeric types - when /\A\(?(-?\d+(\.\d*)?\)?(::bigint)?)\z/ + when /\A\(?(-?\d+(\.\d*)?)\)?(::bigint)?\z/ $1 # Object identifier types when /\A-?\d+\z/ @@ -565,7 +589,7 @@ module ActiveRecord end def exec_no_cache(sql, name, binds) - log(sql, name, binds) { @connection.async_exec(sql) } + log(sql, name, binds) { @connection.async_exec(sql, []) } end def exec_cache(sql, name, binds) @@ -575,9 +599,7 @@ module ActiveRecord } log(sql, name, type_casted_binds, stmt_key) do - @connection.send_query_prepared(stmt_key, type_casted_binds.map { |_, val| val }) - @connection.block - @connection.get_last_result + @connection.exec_prepared(stmt_key, type_casted_binds.map { |_, val| val }) end rescue ActiveRecord::StatementInvalid => e pgerror = e.original_exception @@ -669,9 +691,9 @@ module ActiveRecord variables.map do |k, v| if v == ':default' || v == :default # Sets the value to the global or compile default - execute("SET SESSION #{k.to_s} TO DEFAULT", 'SCHEMA') + execute("SET SESSION #{k} TO DEFAULT", 'SCHEMA') elsif !v.nil? - execute("SET SESSION #{k.to_s} TO #{quote(v)}", 'SCHEMA') + execute("SET SESSION #{k} TO #{quote(v)}", 'SCHEMA') end end end @@ -689,12 +711,6 @@ module ActiveRecord exec_query("SELECT currval('#{sequence_name}')", 'SQL') end - # Executes a SELECT query and returns the results, performing any data type - # conversions that are required to be performed here instead of in PostgreSQLColumn. - def select(sql, name = nil, binds = []) - exec_query(sql, name, binds) - end - # Returns the list of a table's column names, data types, and default values. # # The underlying query is roughly: diff --git a/activerecord/lib/active_record/connection_adapters/schema_cache.rb b/activerecord/lib/active_record/connection_adapters/schema_cache.rb index 4d8afcf16a..37ff4e4613 100644 --- a/activerecord/lib/active_record/connection_adapters/schema_cache.rb +++ b/activerecord/lib/active_record/connection_adapters/schema_cache.rb @@ -1,4 +1,3 @@ - module ActiveRecord module ConnectionAdapters class SchemaCache @@ -20,6 +19,7 @@ module ActiveRecord # A cached lookup for table existence. def table_exists?(name) + prepare_tables if @tables.empty? return @tables[name] if @tables.key? name @tables[name] = connection.table_exists?(name) @@ -61,9 +61,7 @@ module ActiveRecord end def size - [@columns, @columns_hash, @primary_keys, @tables].map { |x| - x.size - }.inject :+ + [@columns, @columns_hash, @primary_keys, @tables].map(&:size).inject :+ end # Clear out internal caches for table with +table_name+. @@ -83,6 +81,12 @@ module ActiveRecord def marshal_load(array) @version, @columns, @columns_hash, @primary_keys, @tables = array end + + private + + def prepare_tables + connection.tables.each { |table| @tables[table] = true } + 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 e6163771e8..f3d2b25bfe 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb @@ -14,9 +14,9 @@ module ActiveRecord raise ArgumentError, "No database file specified. Missing argument: database" end - # Allow database path relative to Rails.root, but only if - # the database path is not the special path that tells - # Sqlite to build a database only in memory. + # Allow database path relative to Rails.root, but only if the database + # path is not the special path that tells sqlite to build a database only + # in memory. if ':memory:' != config[:database] config[:database] = File.expand_path(config[:database], Rails.root) if defined?(Rails.root) dirname = File.dirname(config[:database]) @@ -50,6 +50,16 @@ module ActiveRecord end end + class SQLite3String < Type::String # :nodoc: + def type_cast_for_database(value) + if value.is_a?(::String) && value.encoding == Encoding::ASCII_8BIT + value.encode(Encoding::UTF_8) + else + super + end + end + end + # The SQLite3 adapter works SQLite 3.6.16 or newer # with the sqlite3-ruby drivers (available as gem from https://rubygems.org/gems/sqlite3). # @@ -57,6 +67,7 @@ module ActiveRecord # # * <tt>:database</tt> - Path to the database file. class SQLite3Adapter < AbstractAdapter + ADAPTER_NAME = 'SQLite'.freeze include Savepoints NATIVE_DATABASE_TYPES = { @@ -77,11 +88,11 @@ module ActiveRecord include Comparable def initialize(version_string) - @version = version_string.split('.').map { |v| v.to_i } + @version = version_string.split('.').map(&:to_i) end def <=>(version_string) - @version <=> version_string.split('.').map { |v| v.to_i } + @version <=> version_string.split('.').map(&:to_i) end end @@ -104,7 +115,7 @@ module ActiveRecord end def clear - cache.values.each do |hash| + cache.each_value do |hash| dealloc hash[:stmt] end cache.clear @@ -137,10 +148,6 @@ module ActiveRecord end end - def adapter_name #:nodoc: - 'SQLite' - end - def supports_ddl_transactions? true end @@ -172,7 +179,7 @@ module ActiveRecord true end - def supports_add_column? + def supports_views? true end @@ -220,13 +227,23 @@ module ActiveRecord # QUOTING ================================================== def _quote(value) # :nodoc: - if value.is_a?(Type::Binary::Data) + case value + when Type::Binary::Data "x'#{value.hex}'" else super end end + def _type_cast(value) # :nodoc: + case value + when BigDecimal + value.to_f + else + super + end + end + def quote_string(s) #:nodoc: @connection.class.quote(s) end @@ -239,30 +256,9 @@ module ActiveRecord %Q("#{name.to_s.gsub('"', '""')}") end - # Quote date/time values for use in SQL input. Includes microseconds - # if the value is a Time responding to usec. - def quoted_date(value) #:nodoc: - if value.respond_to?(:usec) - "#{super}.#{sprintf("%06d", value.usec)}" - else - super - end - end - - def type_cast(value, column) # :nodoc: - return value.to_f if BigDecimal === value - return super unless String === value - return super unless column && value - - value = super - if column.type == :string && value.encoding == Encoding::ASCII_8BIT - logger.error "Binary data inserted for `string` type on column `#{column.name}`" if logger - value = value.encode Encoding::UTF_8 - end - value - end - + #-- # DATABASE STATEMENTS ====================================== + #++ def explain(arel, binds = []) sql = "EXPLAIN QUERY PLAN #{to_sql(arel, binds)}" @@ -365,7 +361,7 @@ module ActiveRecord sql = <<-SQL SELECT name FROM sqlite_master - WHERE type = 'table' AND NOT name = 'sqlite_sequence' + WHERE (type = 'table' OR type = 'view') AND NOT name = 'sqlite_sequence' SQL sql << " AND name = #{quote_table_name(table_name)}" if table_name @@ -378,7 +374,7 @@ module ActiveRecord table_name && tables(nil, table_name).any? end - # Returns an array of +SQLite3Column+ objects for the table specified by +table_name+. + # Returns an array of +Column+ objects for the table specified by +table_name+. def columns(table_name) #:nodoc: table_structure(table_name).map do |field| case field["dflt_value"] @@ -422,10 +418,9 @@ module ActiveRecord end def primary_key(table_name) #:nodoc: - column = table_structure(table_name).find { |field| - field['pk'] == 1 - } - column && column['name'] + pks = table_structure(table_name).select { |f| f['pk'] > 0 } + return nil unless pks.count == 1 + pks[0]['name'] end def remove_index!(table_name, index_name) #:nodoc: @@ -443,12 +438,12 @@ module ActiveRecord # See: http://www.sqlite.org/lang_altertable.html # SQLite has an additional restriction on the ALTER TABLE statement - def valid_alter_table_options( type, options) + def valid_alter_table_type?(type) type.to_sym != :primary_key end def add_column(table_name, column_name, type, options = {}) #:nodoc: - if supports_add_column? && valid_alter_table_options( type, options ) + if valid_alter_table_type?(type) super(table_name, column_name, type, options) else alter_table(table_name) do |definition| @@ -503,10 +498,7 @@ module ActiveRecord def initialize_type_map(m) super m.register_type(/binary/i, SQLite3Binary.new) - end - - def select(sql, name = nil, binds = []) #:nodoc: - exec_query(sql, name, binds) + register_class_with_limit m, %r(char)i, SQLite3String end def table_structure(table_name) @@ -553,7 +545,7 @@ module ActiveRecord end copy_table_indexes(from, to, options[:rename] || {}) copy_table_contents(from, to, - @definition.columns.map {|column| column.name}, + @definition.columns.map(&:name), options[:rename] || {}) end @@ -566,7 +558,7 @@ module ActiveRecord name = name[1..-1] end - to_column_names = columns(to).map { |c| c.name } + to_column_names = columns(to).map(&:name) columns = index.columns.map {|c| rename[c] || c }.select do |column| to_column_names.include?(column) end @@ -583,25 +575,14 @@ module ActiveRecord def copy_table_contents(from, to, columns, rename = {}) #:nodoc: column_mappings = Hash[columns.map {|name| [name, name]}] rename.each { |a| column_mappings[a.last] = a.first } - from_columns = columns(from).collect {|col| col.name} + from_columns = columns(from).collect(&:name) columns = columns.find_all{|col| from_columns.include?(column_mappings[col])} + from_columns_to_copy = columns.map { |col| column_mappings[col] } quoted_columns = columns.map { |col| quote_column_name(col) } * ',' + quoted_from_columns = from_columns_to_copy.map { |col| quote_column_name(col) } * ',' - quoted_to = quote_table_name(to) - - raw_column_mappings = Hash[columns(from).map { |c| [c.name, c] }] - - exec_query("SELECT * FROM #{quote_table_name(from)}").each do |row| - sql = "INSERT INTO #{quoted_to} (#{quoted_columns}) VALUES (" - - column_values = columns.map do |col| - quote(row[column_mappings[col]], raw_column_mappings[col]) - end - - sql << column_values * ', ' - sql << ')' - exec_query sql - end + exec_query("INSERT INTO #{quote_table_name(to)} (#{quoted_columns}) + SELECT #{quoted_from_columns} FROM #{quote_table_name(from)}") end def sqlite_version diff --git a/activerecord/lib/active_record/connection_handling.rb b/activerecord/lib/active_record/connection_handling.rb index 31e7390bf7..984af79642 100644 --- a/activerecord/lib/active_record/connection_handling.rb +++ b/activerecord/lib/active_record/connection_handling.rb @@ -1,6 +1,6 @@ module ActiveRecord module ConnectionHandling - RAILS_ENV = -> { Rails.env if defined?(Rails) } + RAILS_ENV = -> { (Rails.env if defined?(Rails.env)) || ENV["RAILS_ENV"] || ENV["RACK_ENV"] } DEFAULT_ENV = -> { RAILS_ENV.call || "default_env" } # Establishes the connection to the database. Accepts a hash as input where diff --git a/activerecord/lib/active_record/core.rb b/activerecord/lib/active_record/core.rb index d39e5fddfe..38b2d632d2 100644 --- a/activerecord/lib/active_record/core.rb +++ b/activerecord/lib/active_record/core.rb @@ -1,6 +1,7 @@ +require 'thread' require 'active_support/core_ext/hash/indifferent_access' require 'active_support/core_ext/object/duplicable' -require 'thread' +require 'active_support/core_ext/string/filters' module ActiveRecord module Core @@ -16,7 +17,6 @@ module ActiveRecord mattr_accessor :logger, instance_writer: false ## - # :singleton-method: # Contains the database configuration - as is typically stored in config/database.yml - # as a Hash. # @@ -85,12 +85,13 @@ module ActiveRecord mattr_accessor :dump_schema_after_migration, instance_writer: false self.dump_schema_after_migration = true - # :nodoc: mattr_accessor :maintain_test_schema, instance_accessor: false def self.disable_implicit_join_references=(value) - ActiveSupport::Deprecation.warn("Implicit join references were removed with Rails 4.1." \ - "Make sure to remove this configuration because it does nothing.") + ActiveSupport::Deprecation.warn(<<-MSG.squish) + Implicit join references were removed with Rails 4.1. + Make sure to remove this configuration because it does nothing. + MSG end class_attribute :default_connection_handler, instance_writer: false @@ -108,6 +109,11 @@ module ActiveRecord end module ClassMethods + def allocate + define_attribute_methods + super + end + def initialize_find_by_cache self.find_by_statement_cache = {}.extend(Mutex_m) end @@ -120,17 +126,22 @@ module ActiveRecord def find(*ids) # We don't have cache keys for this stuff yet return super unless ids.length == 1 + # Allow symbols to super to maintain compatibility for deprecated finders until Rails 5 + return super if ids.first.kind_of?(Symbol) return super if block_given? || primary_key.nil? || default_scopes.any? || + current_scope || columns_hash.include?(inheritance_column) || ids.first.kind_of?(Array) id = ids.first if ActiveRecord::Base === id id = id.id - ActiveSupport::Deprecation.warn "You are passing an instance of ActiveRecord::Base to `find`." \ - "Please pass the id of the object by calling `.id`" + ActiveSupport::Deprecation.warn(<<-MSG.squish) + You are passing an instance of ActiveRecord::Base to `find`. + Please pass the id of the object by calling `.id` + MSG end key = primary_key @@ -144,10 +155,13 @@ module ActiveRecord raise RecordNotFound, "Couldn't find #{name} with '#{primary_key}'=#{id}" end record + rescue RangeError + raise RecordNotFound, "Couldn't find #{name} with an out of range value for '#{primary_key}'" end def find_by(*args) - return super if current_scope || args.length > 1 || reflect_on_all_aggregations.any? + return super if current_scope || !(Hash === args.first) || reflect_on_all_aggregations.any? + return super if default_scopes.any? hash = args.first @@ -155,6 +169,9 @@ module ActiveRecord v.nil? || Array === v || Hash === v } + # We can't cache Post.find_by(author: david) ...yet + return super unless hash.keys.all? { |k| columns_hash.has_key?(k.to_s) } + key = hash.keys klass = self @@ -170,12 +187,16 @@ module ActiveRecord s.execute(hash.values, self, connection).first rescue TypeError => e raise ActiveRecord::StatementInvalid.new(e.message, e) + rescue RangeError + nil end end - def initialize_generated_modules - super + def find_by!(*args) + find_by(*args) or raise RecordNotFound.new("Couldn't find #{name}") + end + def initialize_generated_modules generated_association_methods end @@ -214,7 +235,7 @@ module ActiveRecord # scope :published_and_commented, -> { published.and(self.arel_table[:comments_count].gt(0)) } # end def arel_table # :nodoc: - @arel_table ||= Arel::Table.new(table_name, arel_engine) + @arel_table ||= Arel::Table.new(table_name, type_caster: type_caster) end # Returns the Arel engine. @@ -227,10 +248,18 @@ module ActiveRecord end end + def predicate_builder # :nodoc: + @predicate_builder ||= PredicateBuilder.new(table_metadata) + end + + def type_caster # :nodoc: + TypeCaster::Map.new(self) + end + private - def relation #:nodoc: - relation = Relation.create(self, arel_table) + def relation # :nodoc: + relation = Relation.create(self, arel_table, predicate_builder) if finder_needs_type_condition? relation.where(type_condition).create_with(inheritance_column.to_sym => sti_name) @@ -238,6 +267,10 @@ module ActiveRecord relation end end + + def table_metadata # :nodoc: + TableMetadata.new(self, arel_table) + end end # New objects can be instantiated as either empty (pass no construction parameter) or pre-set with @@ -249,14 +282,7 @@ module ActiveRecord # # Instantiates a single new object # User.new(first_name: 'Jamie') def initialize(attributes = nil, options = {}) - defaults = {} - self.class.raw_column_defaults.each do |k, v| - default = v.duplicable? ? v.dup : v - defaults[k] = Attribute.from_database(default, type_for_attribute(k)) - end - - @attributes = defaults - @column_types = self.class.column_types + @attributes = self.class._default_attributes.dup init_internals initialize_internals_callback @@ -267,7 +293,7 @@ module ActiveRecord init_attributes(attributes, options) if attributes yield self if block_given? - run_callbacks :initialize unless _initialize_callbacks.empty? + _run_initialize_callbacks end # Initialize an empty model object from +coder+. +coder+ must contain @@ -282,7 +308,6 @@ module ActiveRecord # post.title # => 'hello world' def init_with(coder) @attributes = coder['attributes'] - @column_types = self.class.column_types init_internals @@ -290,8 +315,8 @@ module ActiveRecord self.class.define_attribute_methods - run_callbacks :find - run_callbacks :initialize + _run_find_callbacks + _run_initialize_callbacks self end @@ -324,11 +349,10 @@ module ActiveRecord ## def initialize_dup(other) # :nodoc: - pk = self.class.primary_key - @attributes = other.clone_attributes - @attributes[pk] = Attribute.from_database(nil, type_for_attribute(pk)) + @attributes = @attributes.dup + @attributes.reset(self.class.primary_key) - run_callbacks(:initialize) unless _initialize_callbacks.empty? + _run_initialize_callbacks @aggregation_cache = {} @association_cache = {} @@ -524,9 +548,6 @@ module ActiveRecord end def init_internals - pk = self.class.primary_key - @attributes[pk] ||= Attribute.from_database(nil, type_for_attribute(pk)) - @aggregation_cache = {} @association_cache = {} @readonly = false diff --git a/activerecord/lib/active_record/counter_cache.rb b/activerecord/lib/active_record/counter_cache.rb index 05c4b13016..7d8e0a2063 100644 --- a/activerecord/lib/active_record/counter_cache.rb +++ b/activerecord/lib/active_record/counter_cache.rb @@ -20,7 +20,7 @@ module ActiveRecord def reset_counters(id, *counters) object = find(id) counters.each do |counter_association| - has_many_association = _reflect_on_association(counter_association.to_sym) + has_many_association = _reflect_on_association(counter_association) unless has_many_association has_many = reflect_on_all_associations(:has_many) has_many_association = has_many.find { |association| association.counter_cache_column && association.counter_cache_column.to_sym == counter_association.to_sym } @@ -34,13 +34,12 @@ module ActiveRecord foreign_key = has_many_association.foreign_key.to_s child_class = has_many_association.klass - reflection = child_class._reflections.values.find { |e| :belongs_to == e.macro && e.foreign_key.to_s == foreign_key && e.options[:counter_cache].present? } + reflection = child_class._reflections.values.find { |e| e.belongs_to? && e.foreign_key.to_s == foreign_key && e.options[:counter_cache].present? } counter_name = reflection.counter_cache_column - stmt = unscoped.where(arel_table[primary_key].eq(object.id)).arel.compile_update({ - arel_table[counter_name] => object.send(counter_association).count - }, primary_key) - connection.update stmt + unscoped.where(primary_key => object.id).update_all( + counter_name => object.send(counter_association).count(:all) + ) end return true end @@ -123,16 +122,6 @@ module ActiveRecord end end - protected - - def actually_destroyed? - @_actually_destroyed - end - - def clear_destroy_state - @_actually_destroyed = nil - end - private def _create_record(*) diff --git a/activerecord/lib/active_record/enum.rb b/activerecord/lib/active_record/enum.rb index 38c6dcf88d..f053372cfb 100644 --- a/activerecord/lib/active_record/enum.rb +++ b/activerecord/lib/active_record/enum.rb @@ -69,12 +69,12 @@ module ActiveRecord # # Where conditions on an enum attribute must use the ordinal value of an enum. module Enum - def self.extended(base) + def self.extended(base) # :nodoc: base.class_attribute(:defined_enums) base.defined_enums = {} end - def inherited(base) + def inherited(base) # :nodoc: base.defined_enums = defined_enums.deep_dup super end @@ -142,14 +142,14 @@ module ActiveRecord private def save_changed_attribute(attr_name, old) if (mapping = self.class.defined_enums[attr_name.to_s]) - value = read_attribute(attr_name) + value = _read_attribute(attr_name) if attribute_changed?(attr_name) if mapping[old] == value - changed_attributes.delete(attr_name) + clear_attribute_changes([attr_name]) end else if old != value - changed_attributes[attr_name] = mapping.key old + set_attribute_was(attr_name, mapping.key(old)) end end else diff --git a/activerecord/lib/active_record/errors.rb b/activerecord/lib/active_record/errors.rb index 52c70977ef..fc28ab585f 100644 --- a/activerecord/lib/active_record/errors.rb +++ b/activerecord/lib/active_record/errors.rb @@ -52,10 +52,29 @@ module ActiveRecord # Raised by ActiveRecord::Base.save! and ActiveRecord::Base.create! methods when record cannot be # saved because record is invalid. class RecordNotSaved < ActiveRecordError + attr_reader :record + + def initialize(message, record = nil) + @record = record + super(message) + end end # Raised by ActiveRecord::Base.destroy! when a call to destroy would return false. + # + # begin + # complex_operation_that_internally_calls_destroy! + # rescue ActiveRecord::RecordNotDestroyed => invalid + # puts invalid.record.errors + # end + # class RecordNotDestroyed < ActiveRecordError + attr_reader :record + + def initialize(record) + @record = record + super() + end end # Superclass for all database execution errors. @@ -167,7 +186,7 @@ module ActiveRecord def initialize(record, attribute) @record = record @attribute = attribute.to_s - super("unknown attribute: #{attribute}") + super("unknown attribute '#{attribute}' for #{@record.class}.") end end @@ -177,6 +196,7 @@ module ActiveRecord # offending attribute. class AttributeAssignmentError < ActiveRecordError attr_reader :exception, :attribute + def initialize(message, exception, attribute) super(message) @exception = exception @@ -189,6 +209,7 @@ module ActiveRecord # objects, each corresponding to the error while assigning to an attribute. class MultiparameterAssignmentErrors < ActiveRecordError attr_reader :errors + def initialize(errors) @errors = errors end diff --git a/activerecord/lib/active_record/explain.rb b/activerecord/lib/active_record/explain.rb index e65dab07ba..727a9befc1 100644 --- a/activerecord/lib/active_record/explain.rb +++ b/activerecord/lib/active_record/explain.rb @@ -27,7 +27,7 @@ module ActiveRecord end.join("\n") end.join("\n") - # Overriding inspect to be more human readable, specially in the console. + # Overriding inspect to be more human readable, especially in the console. def str.inspect self end diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb index 4cd5f92207..4732462b05 100644 --- a/activerecord/lib/active_record/fixtures.rb +++ b/activerecord/lib/active_record/fixtures.rb @@ -2,7 +2,7 @@ require 'erb' require 'yaml' require 'zlib' require 'active_support/dependencies' -require 'active_support/core_ext/securerandom' +require 'active_support/core_ext/digest/uuid' require 'active_record/fixture_set/file' require 'active_record/errors' @@ -126,7 +126,7 @@ module ActiveRecord # that is included in <tt>ActiveRecord::FixtureSet.context_class</tt>. # # - define a helper method in `test_helper.rb` - # class FixtureFileHelpers + # module FixtureFileHelpers # def file_sha(path) # Digest::SHA2.hexdigest(File.read(Rails.root.join('test/fixtures', path))) # end @@ -181,6 +181,9 @@ module ActiveRecord # * Stable, autogenerated IDs # * Label references for associations (belongs_to, has_one, has_many) # * HABTM associations as inline lists + # + # There are some more advanced features available even if the id is specified: + # # * Autofilled timestamp columns # * Fixture label interpolation # * Support for YAML defaults @@ -515,14 +518,14 @@ module ActiveRecord ::File.join(fixtures_directory, fs_name)) end - all_loaded_fixtures.update(fixtures_map) + update_all_loaded_fixtures fixtures_map connection.transaction(:requires_new => true) do fixture_sets.each do |fs| conn = fs.model_class.respond_to?(:connection) ? fs.model_class.connection : connection table_rows = fs.table_rows - table_rows.keys.each do |table| + table_rows.each_key do |table| conn.delete "DELETE FROM #{conn.quote_table_name(table)}", 'Fixture Delete' end @@ -551,7 +554,7 @@ module ActiveRecord # Integer identifiers are values less than 2^30. UUIDs are RFC 4122 version 5 SHA-1 hashes. def self.identify(label, column_type = :integer) if column_type == :uuid - SecureRandom.uuid_v5(SecureRandom::UUID_OID_NAMESPACE, label.to_s) + Digest::UUID.uuid_v5(Digest::UUID::OID_NAMESPACE, label.to_s) else Zlib.crc32(label.to_s) % MAX_ID end @@ -562,6 +565,10 @@ module ActiveRecord @context_class ||= Class.new end + def self.update_all_loaded_fixtures(fixtures_map) # :nodoc: + all_loaded_fixtures.update(fixtures_map) + end + attr_reader :table_name, :name, :fixtures, :model_class, :config def initialize(connection, name, class_name, path, config = ActiveRecord::Base) @@ -642,7 +649,7 @@ module ActiveRecord model_class end - reflection_class._reflections.values.each do |association| + reflection_class._reflections.each_value do |association| case association.macro when :belongs_to # Do not replace association name with association foreign key if they are named the same @@ -654,7 +661,7 @@ module ActiveRecord row[association.foreign_type] = $1 end - fk_type = association.active_record.columns_hash[association.foreign_key].type + fk_type = association.active_record.columns_hash[fk_name].type row[fk_name] = ActiveRecord::FixtureSet.identify(value, fk_type) end when :has_many @@ -738,7 +745,7 @@ module ActiveRecord end def column_names - @column_names ||= @connection.columns(@table_name).collect { |c| c.name } + @column_names ||= @connection.columns(@table_name).collect(&:name) end def read_fixture_files(path, model_class) @@ -799,7 +806,9 @@ module ActiveRecord def find if model_class - model_class.find(fixture[model_class.primary_key]) + model_class.unscoped do + model_class.find(fixture[model_class.primary_key]) + end else raise FixtureClassNotFound, "No class attached to find." end @@ -859,38 +868,13 @@ module ActiveRecord fixture_set_names = Dir["#{fixture_path}/{**,*}/*.{yml}"] fixture_set_names.map! { |f| f[(fixture_path.to_s.size + 1)..-5] } else - fixture_set_names = fixture_set_names.flatten.map { |n| n.to_s } + fixture_set_names = fixture_set_names.flatten.map(&:to_s) end self.fixture_table_names |= fixture_set_names - require_fixture_classes(fixture_set_names, self.config) setup_fixture_accessors(fixture_set_names) end - def try_to_load_dependency(file_name) - require_dependency file_name - rescue LoadError => e - # Let's hope the developer has included it - # Let's warn in case this is a subdependency, otherwise - # subdependency error messages are totally cryptic - if ActiveRecord::Base.logger - ActiveRecord::Base.logger.warn("Unable to load #{file_name}, underlying cause #{e.message} \n\n #{e.backtrace.join("\n")}") - end - end - - def require_fixture_classes(fixture_set_names = nil, config = ActiveRecord::Base) - if fixture_set_names - fixture_set_names = fixture_set_names.map { |n| n.to_s } - else - fixture_set_names = fixture_table_names - end - - fixture_set_names.each do |file_name| - file_name = file_name.singularize if config.pluralize_table_names - try_to_load_dependency(file_name) - end - end - def setup_fixture_accessors(fixture_set_names = nil) fixture_set_names = Array(fixture_set_names || fixture_table_names) methods = Module.new do @@ -924,7 +908,7 @@ module ActiveRecord def uses_transaction(*methods) @uses_transaction = [] unless defined?(@uses_transaction) - @uses_transaction.concat methods.map { |m| m.to_s } + @uses_transaction.concat methods.map(&:to_s) end def uses_transaction?(method) @@ -967,7 +951,7 @@ module ActiveRecord end # Instantiate fixtures for every test if requested. - instantiate_fixtures(config) if use_instantiated_fixtures + instantiate_fixtures if use_instantiated_fixtures end def teardown_fixtures @@ -994,16 +978,9 @@ module ActiveRecord Hash[fixtures.map { |f| [f.name, f] }] end - # for pre_loaded_fixtures, only require the classes once. huge speed improvement - @@required_fixture_classes = false - - def instantiate_fixtures(config) + def instantiate_fixtures if pre_loaded_fixtures raise RuntimeError, 'Load fixtures before instantiating them.' if ActiveRecord::FixtureSet.all_loaded_fixtures.empty? - unless @@required_fixture_classes - self.class.require_fixture_classes ActiveRecord::FixtureSet.all_loaded_fixtures.keys, config - @@required_fixture_classes = true - end ActiveRecord::FixtureSet.instantiate_all_loaded_fixtures(self, load_instances?) else raise RuntimeError, 'Load fixtures before instantiating them.' if @loaded_fixtures.nil? diff --git a/activerecord/lib/active_record/gem_version.rb b/activerecord/lib/active_record/gem_version.rb index 4a7aace460..a388b529c9 100644 --- a/activerecord/lib/active_record/gem_version.rb +++ b/activerecord/lib/active_record/gem_version.rb @@ -1,12 +1,12 @@ module ActiveRecord - # Returns the version of the currently loaded ActiveRecord as a <tt>Gem::Version</tt> + # Returns the version of the currently loaded Active Record as a <tt>Gem::Version</tt> def self.gem_version Gem::Version.new VERSION::STRING end module VERSION - MAJOR = 4 - MINOR = 2 + MAJOR = 5 + MINOR = 0 TINY = 0 PRE = "alpha" diff --git a/activerecord/lib/active_record/inheritance.rb b/activerecord/lib/active_record/inheritance.rb index 08fc91c9df..b91e9ac137 100644 --- a/activerecord/lib/active_record/inheritance.rb +++ b/activerecord/lib/active_record/inheritance.rb @@ -1,6 +1,37 @@ require 'active_support/core_ext/hash/indifferent_access' module ActiveRecord + # == Single table inheritance + # + # Active Record allows inheritance by storing the name of the class in a column that by + # default is named "type" (can be changed by overwriting <tt>Base.inheritance_column</tt>). + # This means that an inheritance looking like this: + # + # class Company < ActiveRecord::Base; end + # class Firm < Company; end + # class Client < Company; end + # class PriorityClient < Client; end + # + # When you do <tt>Firm.create(name: "37signals")</tt>, this record will be saved in + # the companies table with type = "Firm". You can then fetch this row again using + # <tt>Company.where(name: '37signals').first</tt> and it will return a Firm object. + # + # Be aware that because the type column is an attribute on the record every new + # subclass will instantly be marked as dirty and the type column will be included + # in the list of changed attributes on the record. This is different from non + # STI classes: + # + # Company.new.changed? # => false + # Firm.new.changed? # => true + # Firm.new.changes # => {"type"=>["","Firm"]} + # + # If you don't have a type column defined in your table, single-table inheritance won't + # be triggered. In that case, it'll work just like normal subclasses with no special magic + # for differentiating between them or reloading the right type with find. + # + # Note, all the attributes for all the cases are kept in the same table. Read more: + # http://www.martinfowler.com/eaaCatalog/singleTableInheritance.html + # module Inheritance extend ActiveSupport::Concern @@ -49,12 +80,12 @@ module ActiveRecord end def symbolized_base_class - ActiveSupport::Deprecation.warn("ActiveRecord::Base.symbolized_base_class is deprecated and will be removed without replacement.") + ActiveSupport::Deprecation.warn('`ActiveRecord::Base.symbolized_base_class` is deprecated and will be removed without replacement.') @symbolized_base_class ||= base_class.to_s.to_sym end def symbolized_sti_name - ActiveSupport::Deprecation.warn("ActiveRecord::Base.symbolized_sti_name is deprecated and will be removed without replacement.") + ActiveSupport::Deprecation.warn('`ActiveRecord::Base.symbolized_sti_name` is deprecated and will be removed without replacement.') @symbolized_sti_name ||= sti_name.present? ? sti_name.to_sym : symbolized_base_class end @@ -120,14 +151,8 @@ module ActiveRecord candidates << type_name candidates.each do |candidate| - begin - constant = ActiveSupport::Dependencies.constantize(candidate) - return constant if candidate == constant.to_s - # We don't want to swallow NoMethodError < NameError errors - rescue NoMethodError - raise - rescue NameError - end + constant = ActiveSupport::Dependencies.safe_constantize(candidate) + return constant if candidate == constant.to_s end raise NameError.new("uninitialized constant #{candidates.first}", candidates.first) @@ -167,7 +192,7 @@ module ActiveRecord def type_condition(table = arel_table) sti_column = table[inheritance_column] - sti_names = ([self] + descendants).map { |model| model.sti_name } + sti_names = ([self] + descendants).map(&:sti_name) sti_column.in(sti_names) end diff --git a/activerecord/lib/active_record/integration.rb b/activerecord/lib/active_record/integration.rb index 31e2518540..15b2f65dcb 100644 --- a/activerecord/lib/active_record/integration.rb +++ b/activerecord/lib/active_record/integration.rb @@ -55,16 +55,16 @@ module ActiveRecord def cache_key(*timestamp_names) case when new_record? - "#{self.class.model_name.cache_key}/new" + "#{model_name.cache_key}/new" when timestamp_names.any? timestamp = max_updated_column_timestamp(timestamp_names) timestamp = timestamp.utc.to_s(cache_timestamp_format) - "#{self.class.model_name.cache_key}/#{id}-#{timestamp}" + "#{model_name.cache_key}/#{id}-#{timestamp}" when timestamp = max_updated_column_timestamp timestamp = timestamp.utc.to_s(cache_timestamp_format) - "#{self.class.model_name.cache_key}/#{id}-#{timestamp}" + "#{model_name.cache_key}/#{id}-#{timestamp}" else - "#{self.class.model_name.cache_key}/#{id}" + "#{model_name.cache_key}/#{id}" end end diff --git a/activerecord/lib/active_record/locking/optimistic.rb b/activerecord/lib/active_record/locking/optimistic.rb index 4528d8783c..9f053453bd 100644 --- a/activerecord/lib/active_record/locking/optimistic.rb +++ b/activerecord/lib/active_record/locking/optimistic.rb @@ -11,7 +11,7 @@ module ActiveRecord # # == Usage # - # Active Records support optimistic locking if the field +lock_version+ is present. Each update to the + # Active Record supports optimistic locking if the +lock_version+ field is present. Each update to the # record increments the +lock_version+ column and the locking facilities ensure that records instantiated twice # will let the last one saved raise a +StaleObjectError+ if the first was also updated. Example: # @@ -80,17 +80,15 @@ module ActiveRecord begin relation = self.class.unscoped - stmt = relation.where( - relation.table[self.class.primary_key].eq(id).and( - relation.table[lock_col].eq(self.class.quote_value(previous_lock_value, column_for_attribute(lock_col))) - ) - ).arel.compile_update( - arel_attributes_with_values_for_update(attribute_names), - self.class.primary_key + affected_rows = relation.where( + self.class.primary_key => id, + lock_col => previous_lock_value, + ).update_all( + attribute_names.map do |name| + [name, _read_attribute(name)] + end.to_h ) - affected_rows = self.class.connection.update stmt - unless affected_rows == 1 raise ActiveRecord::StaleObjectError.new(self, "update") end @@ -120,7 +118,7 @@ module ActiveRecord if locking_enabled? column_name = self.class.locking_column column = self.class.columns_hash[column_name] - substitute = self.class.connection.substitute_at(column, relation.bind_values.length) + substitute = self.class.connection.substitute_at(column) relation = relation.where(self.class.arel_table[column_name].eq(substitute)) relation.bind_values << [column, self[column_name].to_i] @@ -141,7 +139,7 @@ module ActiveRecord # Set the column to use for optimistic locking. Defaults to +lock_version+. def locking_column=(value) - @column_defaults = nil + clear_caches_calculated_from_columns @locking_column = value.to_s end @@ -163,18 +161,42 @@ module ActiveRecord super end - def column_defaults - @column_defaults ||= begin - defaults = super - - if defaults.key?(locking_column) && lock_optimistically - defaults[locking_column] ||= 0 + private + + # We need to apply this decorator here, rather than on module inclusion. The closure + # created by the matcher would otherwise evaluate for `ActiveRecord::Base`, not the + # sub class being decorated. As such, changes to `lock_optimistically`, or + # `locking_column` would not be picked up. + def inherited(subclass) + subclass.class_eval do + is_lock_column = ->(name, _) { lock_optimistically && name == locking_column } + decorate_matching_attribute_types(is_lock_column, :_optimistic_locking) do |type| + LockingType.new(type) end - - defaults end + super end end end + + class LockingType < SimpleDelegator # :nodoc: + def type_cast_from_database(value) + # `nil` *should* be changed to 0 + super.to_i + end + + def changed?(old_value, *) + # Ensure we save if the default was `nil` + super || old_value == 0 + end + + def init_with(coder) + __setobj__(coder['subtype']) + end + + def encode_with(coder) + coder['subtype'] = __getobj__ + end + end end end diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb index 481e5c17e4..46f4794010 100644 --- a/activerecord/lib/active_record/migration.rb +++ b/activerecord/lib/active_record/migration.rb @@ -39,7 +39,7 @@ module ActiveRecord class PendingMigrationError < MigrationError#:nodoc: def initialize - if defined?(Rails) + if defined?(Rails.env) super("Migrations are pending. To resolve this issue, run:\n\n\tbin/rake db:migrate RAILS_ENV=#{::Rails.env}") else super("Migrations are pending. To resolve this issue, run:\n\n\tbin/rake db:migrate") @@ -161,21 +161,14 @@ module ActiveRecord # in the <tt>db/migrate/</tt> directory where <tt>timestamp</tt> is the # UTC formatted date and time that the migration was generated. # - # You may then edit the <tt>up</tt> and <tt>down</tt> methods of - # MyNewMigration. - # # There is a special syntactic shortcut to generate migrations that add fields to a table. # # rails generate migration add_fieldname_to_tablename fieldname:string # # This will generate the file <tt>timestamp_add_fieldname_to_tablename</tt>, which will look like this: # class AddFieldnameToTablename < ActiveRecord::Migration - # def up - # add_column :tablenames, :fieldname, :string - # end - # - # def down - # remove_column :tablenames, :fieldname + # def change + # add_column :tablenames, :field, :string # end # end # @@ -188,9 +181,12 @@ module ActiveRecord # # 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 - # you wish to downgrade. If any of the migrations throw an - # <tt>ActiveRecord::IrreversibleMigration</tt> exception, that step will fail and you'll - # have some manual work to do. + # 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 + # the latest two migrations. + # + # If any of the migrations throw an <tt>ActiveRecord::IrreversibleMigration</tt> exception, + # that step will fail and you'll have some manual work to do. # # == Database support # @@ -372,13 +368,21 @@ module ActiveRecord end def call(env) - mtime = ActiveRecord::Migrator.last_migration.mtime.to_i - if @last_check < mtime - ActiveRecord::Migration.check_pending! - @last_check = mtime + if connection.supports_migrations? + mtime = ActiveRecord::Migrator.last_migration.mtime.to_i + if @last_check < mtime + ActiveRecord::Migration.check_pending!(connection) + @last_check = mtime + end end @app.call(env) end + + private + + def connection + ActiveRecord::Base.connection + end end class << self @@ -390,8 +394,15 @@ module ActiveRecord end def load_schema_if_pending! - if ActiveRecord::Migrator.needs_migration? - ActiveRecord::Tasks::DatabaseTasks.load_schema + if ActiveRecord::Migrator.needs_migration? || !ActiveRecord::Migrator.any_migrations? + # Roundrip to Rake to allow plugins to hook into database initialization. + FileUtils.cd Rails.root do + current_config = Base.connection_config + Base.clear_all_connections! + system("bin/rake db:test:prepare") + # Establish a new connection, the old database may be gone (db:test:prepare uses purge) + Base.establish_connection(current_config) + end check_pending! end end @@ -636,13 +647,15 @@ module ActiveRecord end def method_missing(method, *arguments, &block) - arg_list = arguments.map{ |a| a.inspect } * ', ' + arg_list = arguments.map(&:inspect) * ', ' say_with_time "#{method}(#{arg_list})" do unless @connection.respond_to? :revert unless arguments.empty? || [:execute, :enable_extension, :disable_extension].include?(method) arguments[0] = proper_table_name(arguments.first, table_name_options) - arguments[1] = proper_table_name(arguments.second, table_name_options) if method == :rename_table + if [:rename_table, :add_foreign_key].include?(method) + arguments[1] = proper_table_name(arguments.second, table_name_options) + end end end return super unless connection.respond_to?(method) @@ -804,43 +817,46 @@ module ActiveRecord migrations = migrations(migrations_paths) migrations.select! { |m| yield m } if block_given? - self.new(:up, migrations, target_version).migrate + new(:up, migrations, target_version).migrate end def down(migrations_paths, target_version = nil, &block) migrations = migrations(migrations_paths) migrations.select! { |m| yield m } if block_given? - self.new(:down, migrations, target_version).migrate + new(:down, migrations, target_version).migrate end def run(direction, migrations_paths, target_version) - self.new(direction, migrations(migrations_paths), target_version).run + new(direction, migrations(migrations_paths), target_version).run end def open(migrations_paths) - self.new(:up, migrations(migrations_paths), nil) + new(:up, migrations(migrations_paths), nil) end def schema_migrations_table_name SchemaMigration.table_name end - def get_all_versions - SchemaMigration.all.map { |x| x.version.to_i }.sort + 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 + [] + end end def current_version(connection = Base.connection) - sm_table = schema_migrations_table_name - if connection.table_exists?(sm_table) - get_all_versions.max || 0 - else - 0 - end + get_all_versions(connection).max || 0 end def needs_migration?(connection = Base.connection) - current_version(connection) < last_version + (migrations(migrations_paths).collect(&:version) - get_all_versions(connection)).size > 0 + end + + def any_migrations? + migrations(migrations_paths).any? end def last_version @@ -882,7 +898,7 @@ module ActiveRecord private def move(direction, migrations_paths, steps) - migrator = self.new(direction, migrations(migrations_paths)) + migrator = new(direction, migrations(migrations_paths)) start_index = migrator.migrations.index(migrator.current_migration) if start_index diff --git a/activerecord/lib/active_record/migration/command_recorder.rb b/activerecord/lib/active_record/migration/command_recorder.rb index c44d8c1665..36256415df 100644 --- a/activerecord/lib/active_record/migration/command_recorder.rb +++ b/activerecord/lib/active_record/migration/command_recorder.rb @@ -74,7 +74,9 @@ module ActiveRecord :rename_index, :rename_column, :add_index, :remove_index, :add_timestamps, :remove_timestamps, :change_column_default, :add_reference, :remove_reference, :transaction, :drop_join_table, :drop_table, :execute_block, :enable_extension, - :change_column, :execute, :remove_columns, :change_column_null # irreversible methods need to be here too + :change_column, :execute, :remove_columns, :change_column_null, + :add_foreign_key, :remove_foreign_key + # irreversible methods need to be here too ].each do |method| class_eval <<-EOV, __FILE__, __LINE__ + 1 def #{method}(*args, &block) # def create_table(*args, &block) @@ -85,7 +87,7 @@ module ActiveRecord alias :add_belongs_to :add_reference alias :remove_belongs_to :remove_reference - def change_table(table_name, options = {}) + def change_table(table_name, options = {}) # :nodoc: yield delegate.update_table_definition(table_name, self) end @@ -167,6 +169,21 @@ module ActiveRecord [:change_column_null, args] end + def invert_add_foreign_key(args) + from_table, to_table, add_options = args + add_options ||= {} + + if add_options[:name] + options = { name: add_options[:name] } + elsif add_options[:column] + options = { column: add_options[:column] } + else + options = to_table + end + + [:remove_foreign_key, [from_table, options]] + end + # Forwards any missing method call to the \target. def method_missing(method, *args, &block) if @delegate.respond_to?(method) diff --git a/activerecord/lib/active_record/migration/join_table.rb b/activerecord/lib/active_record/migration/join_table.rb index ebf64cbcdc..05569fadbd 100644 --- a/activerecord/lib/active_record/migration/join_table.rb +++ b/activerecord/lib/active_record/migration/join_table.rb @@ -8,7 +8,7 @@ module ActiveRecord end def join_table_name(table_1, table_2) - [table_1.to_s, table_2.to_s].sort.join("_").to_sym + ModelSchema.derive_join_table_name(table_1, table_2).to_sym end end end diff --git a/activerecord/lib/active_record/model_schema.rb b/activerecord/lib/active_record/model_schema.rb index 9e1afd32e6..641512d323 100644 --- a/activerecord/lib/active_record/model_schema.rb +++ b/activerecord/lib/active_record/model_schema.rb @@ -55,6 +55,17 @@ module ActiveRecord delegate :type_for_attribute, to: :class end + # Derives the join table name for +first_table+ and +second_table+. The + # table names appear in alphabetical order. A common prefix is removed + # (useful for namespaced models like Music::Artist and Music::Record): + # + # artists, records => artists_records + # records, artists => artists_records + # music_artists, music_records => music_artists_records + def self.derive_join_table_name(first_table, second_table) # :nodoc: + [first_table.to_s, second_table.to_s].sort.join("\0").gsub(/^(.*_)(.+)\0\1(.+)/, '\1\2_\3').tr("\0", "_") + end + module ClassMethods # Guesses the table name (in forced lower-case) based on the name of the class in the # inheritance hierarchy descending directly from ActiveRecord::Base. So if the hierarchy @@ -136,7 +147,7 @@ module ActiveRecord @quoted_table_name = nil @arel_table = nil @sequence_name = nil unless defined?(@explicit_sequence_name) && @explicit_sequence_name - @relation = Relation.create(self, arel_table) + @predicate_builder = nil end # Returns a quoted version of the table name, used to construct SQL statements. @@ -219,45 +230,34 @@ module ActiveRecord connection.schema_cache.table_exists?(table_name) end - def column_types # :nodoc: - @column_types ||= decorate_types(build_types_hash) - end - - def type_for_attribute(attr_name) # :nodoc: - column_types.fetch(attr_name) { Type::Value.new } + def attributes_builder # :nodoc: + @attributes_builder ||= AttributeSet::Builder.new(column_types, primary_key) end - def decorate_types(types) # :nodoc: - return if types.empty? - - @time_zone_column_names ||= self.columns_hash.find_all do |name, col| - create_time_zone_conversion_attribute?(name, col) - end.map!(&:first) - - @time_zone_column_names.each do |name| - types[name] = AttributeMethods::TimeZoneConversion::Type.new(types[name]) + def column_types # :nodoc: + @column_types ||= columns_hash.transform_values(&:cast_type).tap do |h| + h.default = Type::Value.new end + end - types + def type_for_attribute(attr_name) # :nodoc: + column_types[attr_name] end # Returns a hash where the keys are column names and the values are # default values when instantiating the AR object for this table. def column_defaults - @column_defaults ||= Hash[columns.map { |c| [c.name, c.default] }] + _default_attributes.to_hash end - # Returns a hash where the keys are the column names and the values - # are the default values suitable for use in `@raw_attriubtes` - def raw_column_defaults # :nodoc: - @raw_column_defauts ||= Hash[column_defaults.map { |name, default| - [name, columns_hash[name].type_cast_for_database(default)] - }] + def _default_attributes # :nodoc: + @default_attributes ||= attributes_builder.build_from_database( + raw_default_values) end # Returns an array of column names as strings. def column_names - @column_names ||= columns.map { |column| column.name } + @column_names ||= columns.map(&:name) end # Returns an array of column objects where the primary id, all columns ending in "_id" or "_count", @@ -297,17 +297,13 @@ module ActiveRecord undefine_attribute_methods connection.schema_cache.clear_table_cache!(table_name) if table_exists? - @arel_engine = nil - @column_defaults = nil - @raw_column_defauts = nil - @column_names = nil - @column_types = nil - @content_columns = nil - @dynamic_methods_hash = nil - @inheritance_column = nil unless defined?(@explicit_inheritance_column) && @explicit_inheritance_column - @relation = nil - @time_zone_column_names = nil - @cached_time_zone = nil + @arel_engine = nil + @arel_table = nil + @column_names = nil + @column_types = nil + @content_columns = nil + @default_attributes = nil + @inheritance_column = nil unless defined?(@explicit_inheritance_column) && @explicit_inheritance_column end private @@ -336,8 +332,8 @@ module ActiveRecord end end - def build_types_hash - Hash[columns.map { |column| [column.name, column.cast_type] }] + def raw_default_values + columns_hash.transform_values(&:default) end end end diff --git a/activerecord/lib/active_record/nested_attributes.rb b/activerecord/lib/active_record/nested_attributes.rb index 8a2a06f2ca..0f9b52f69f 100644 --- a/activerecord/lib/active_record/nested_attributes.rb +++ b/activerecord/lib/active_record/nested_attributes.rb @@ -81,6 +81,9 @@ module ActiveRecord # # Note that the model will _not_ be destroyed until the parent is saved. # + # Also note that the model will not be destroyed unless you also specify + # its id in the updated hash. + # # === One-to-many # # Consider a member that has a number of posts: diff --git a/activerecord/lib/active_record/no_touching.rb b/activerecord/lib/active_record/no_touching.rb index dbf4564ae5..edb5066fa0 100644 --- a/activerecord/lib/active_record/no_touching.rb +++ b/activerecord/lib/active_record/no_touching.rb @@ -45,7 +45,7 @@ module ActiveRecord NoTouching.applied_to?(self.class) end - def touch(*) + def touch(*) # :nodoc: super unless no_touching? end end diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb index 5c744762d9..f53c5f17ef 100644 --- a/activerecord/lib/active_record/persistence.rb +++ b/activerecord/lib/active_record/persistence.rb @@ -36,6 +36,23 @@ module ActiveRecord end end + # Creates an object (or multiple objects) and saves it to the database, + # if validations pass. Raises a RecordInvalid error if validations fail, + # unlike Base#create. + # + # The +attributes+ parameter can be either a Hash or an Array of Hashes. + # These describe which attributes to be created on the object, or + # multiple objects when given an Array of Hashes. + def create!(attributes = nil, &block) + if attributes.is_a?(Array) + attributes.collect { |attr| create!(attr, &block) } + else + object = new(attributes, &block) + object.save! + object + end + end + # Given an attributes hash, +instantiate+ returns a new instance of # the appropriate class. Accepts only keys as strings. # @@ -48,11 +65,7 @@ module ActiveRecord # how this "single-table" inheritance mapping is implemented. def instantiate(attributes, column_types = {}) klass = discriminate_class_for_record(attributes) - - attributes = attributes.each_with_object({}) do |(name, value), h| - type = column_types.fetch(name) { klass.type_for_attribute(name) } - h[name] = Attribute.from_database(value, type) - end + attributes = klass.attributes_builder.build_from_database(attributes, column_types) klass.allocate.init_with('attributes' => attributes, 'new_record' => false) end @@ -96,6 +109,10 @@ module ActiveRecord # 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 + # timestamps will not be updated. + # # There's a series of callbacks associated with +save+. If any of the # <tt>before_*</tt> callbacks return +false+ the action is cancelled and # +save+ returns +false+. See ActiveRecord::Callbacks for further @@ -103,21 +120,25 @@ module ActiveRecord # # Attributes marked as readonly are silently ignored if the record is # being updated. - def save(*) - create_or_update + def save(*args) + create_or_update(*args) rescue ActiveRecord::RecordInvalid false end # 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. # # With <tt>save!</tt> validations always run. If any of them fail # ActiveRecord::RecordInvalid gets raised. 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 + # timestamps will not be updated. + # # There's a series of callbacks associated with <tt>save!</tt>. If any of # the <tt>before_*</tt> callbacks return +false+ the action is cancelled # and <tt>save!</tt> raises ActiveRecord::RecordNotSaved. See @@ -125,8 +146,8 @@ module ActiveRecord # # Attributes marked as readonly are silently ignored if the record is # being updated. - def save!(*) - create_or_update || raise(RecordNotSaved) + def save!(*args) + create_or_update(*args) || raise(RecordNotSaved.new(nil, self)) end # Deletes the record in the database and freezes this instance to @@ -136,6 +157,8 @@ module ActiveRecord # The row is simply removed with an SQL +DELETE+ statement on the # record's primary key, and no callbacks are executed. # + # Note that this will also delete records marked as <tt>readonly?</tt>. + # # To enforce the object's +before_destroy+ and +after_destroy+ # callbacks or any <tt>:dependent</tt> association # options, use <tt>#destroy</tt>. @@ -153,7 +176,7 @@ module ActiveRecord # and <tt>destroy</tt> returns +false+. See # ActiveRecord::Callbacks for further details. def destroy - raise ReadOnlyRecord if readonly? + raise ReadOnlyRecord, "#{self.class} is marked as readonly" if readonly? destroy_associations destroy_row if persisted? @destroyed = true @@ -168,7 +191,7 @@ module ActiveRecord # and <tt>destroy!</tt> raises ActiveRecord::RecordNotDestroyed. See # ActiveRecord::Callbacks for further details. def destroy! - destroy || raise(ActiveRecord::RecordNotDestroyed) + destroy || raise(ActiveRecord::RecordNotDestroyed, self) end # Returns an instance of the specified +klass+ with the attributes of the @@ -274,7 +297,8 @@ module ActiveRecord # This method raises an +ActiveRecord::ActiveRecordError+ when called on new # objects, or when at least one of the attributes is marked as readonly. def update_columns(attributes) - raise ActiveRecordError, "cannot update on a new record object" unless persisted? + raise ActiveRecordError, "cannot update a new record" if new_record? + raise ActiveRecordError, "cannot update a destroyed record" if destroyed? attributes.each_key do |key| verify_readonly_attribute(key.to_s) @@ -399,9 +423,8 @@ module ActiveRecord self.class.unscoped { self.class.find(id) } end - @attributes.update(fresh_object.instance_variable_get('@attributes')) - - @column_types = self.class.column_types + @attributes = fresh_object.instance_variable_get('@attributes') + @new_record = false self end @@ -453,7 +476,7 @@ module ActiveRecord changes[self.class.locking_column] = increment_lock if locking_enabled? - changed_attributes.except!(*changes.keys) + clear_attribute_changes(changes.keys) primary_key = self.class.primary_key self.class.unscoped.where(primary_key => self[primary_key]).update_all(changes) == 1 else @@ -474,7 +497,7 @@ module ActiveRecord def relation_for_destroy pk = self.class.primary_key column = self.class.columns_hash[pk] - substitute = self.class.connection.substitute_at(column, 0) + substitute = self.class.connection.substitute_at(column) relation = self.class.unscoped.where( self.class.arel_table[pk].eq(substitute)) @@ -483,9 +506,9 @@ module ActiveRecord relation end - def create_or_update - raise ReadOnlyRecord if readonly? - result = new_record? ? _create_record : _update_record + def create_or_update(*args) + raise ReadOnlyRecord, "#{self.class} is marked as readonly" if readonly? + result = new_record? ? _create_record : _update_record(*args) result != false end diff --git a/activerecord/lib/active_record/query_cache.rb b/activerecord/lib/active_record/query_cache.rb index df8654e5c1..dcb2bd3d84 100644 --- a/activerecord/lib/active_record/query_cache.rb +++ b/activerecord/lib/active_record/query_cache.rb @@ -1,4 +1,3 @@ - module ActiveRecord # = Active Record Query Cache class QueryCache @@ -29,9 +28,10 @@ module ActiveRecord end def call(env) - enabled = ActiveRecord::Base.connection.query_cache_enabled + connection = ActiveRecord::Base.connection + enabled = connection.query_cache_enabled connection_id = ActiveRecord::Base.connection_id - ActiveRecord::Base.connection.enable_query_cache! + connection.enable_query_cache! response = @app.call(env) response[2] = Rack::BodyProxy.new(response[2]) do diff --git a/activerecord/lib/active_record/querying.rb b/activerecord/lib/active_record/querying.rb index 39817703cd..e8de4db3a7 100644 --- a/activerecord/lib/active_record/querying.rb +++ b/activerecord/lib/active_record/querying.rb @@ -37,15 +37,18 @@ module ActiveRecord # 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) - column_types = {} + column_types = result_set.column_types.dup + columns_hash.each_key { |k| column_types.delete k } + message_bus = ActiveSupport::Notifications.instrumenter - if result_set.respond_to? :column_types - column_types = result_set.column_types.except(*columns_hash.keys) - else - ActiveSupport::Deprecation.warn "the object returned from `select_all` must respond to `column_types`" - end + payload = { + record_count: result_set.length, + class_name: name + } - result_set.map { |record| instantiate(record, column_types) } + message_bus.instrument('instantiation.active_record', payload) do + result_set.map { |record| instantiate(record, column_types) } + end end # Returns the result of an SQL statement that should only include a COUNT(*) in the SELECT part. diff --git a/activerecord/lib/active_record/railtie.rb b/activerecord/lib/active_record/railtie.rb index a4ceacbf44..f1bdbc845c 100644 --- a/activerecord/lib/active_record/railtie.rb +++ b/activerecord/lib/active_record/railtie.rb @@ -36,8 +36,6 @@ module ActiveRecord config.eager_load_namespaces << ActiveRecord rake_tasks do - require "active_record/base" - namespace :db do task :load_config do ActiveRecord::Tasks::DatabaseTasks.database_configuration = Rails.application.config.database_configuration diff --git a/activerecord/lib/active_record/railties/databases.rake b/activerecord/lib/active_record/railties/databases.rake index 6dca206f2a..04c2be045d 100644 --- a/activerecord/lib/active_record/railties/databases.rake +++ b/activerecord/lib/active_record/railties/databases.rake @@ -28,12 +28,20 @@ db_namespace = namespace :db do ActiveRecord::Tasks::DatabaseTasks.drop_current end + namespace :purge do + task :all => :load_config 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:drop:all to drop all databases in the config). Without RAILS_ENV it defaults to purging the development and test databases." + task :purge => [:load_config] do + ActiveRecord::Tasks::DatabaseTasks.purge_current + end + desc "Migrate the database (options: VERSION=x, VERBOSE=false, SCOPE=blog)." task :migrate => [:environment, :load_config] do - ActiveRecord::Migration.verbose = ENV["VERBOSE"] ? ENV["VERBOSE"] == "true" : true - ActiveRecord::Migrator.migrate(ActiveRecord::Migrator.migrations_paths, ENV["VERSION"] ? ENV["VERSION"].to_i : nil) do |migration| - ENV["SCOPE"].blank? || (ENV["SCOPE"] == migration.scope) - end + ActiveRecord::Tasks::DatabaseTasks.migrate db_namespace['_dump'].invoke if ActiveRecord::Base.dump_schema_after_migration end @@ -82,22 +90,21 @@ db_namespace = namespace :db do desc 'Display status of migrations' task :status => [:environment, :load_config] do - unless ActiveRecord::Base.connection.table_exists?(ActiveRecord::Migrator.schema_migrations_table_name) + unless ActiveRecord::SchemaMigration.table_exists? abort 'Schema migrations table does not exist yet.' end - db_list = ActiveRecord::Base.connection.select_values("SELECT version FROM #{ActiveRecord::Migrator.schema_migrations_table_name}") - db_list.map! { |version| ActiveRecord::SchemaMigration.normalize_migration_number(version) } - file_list = [] - ActiveRecord::Migrator.migrations_paths.each do |path| - Dir.foreach(path) do |file| - # match "20091231235959_some_name.rb" and "001_some_name.rb" pattern - if match_data = /^(\d{3,})_(.+)\.rb$/.match(file) - version = ActiveRecord::SchemaMigration.normalize_migration_number(match_data[1]) - status = db_list.delete(version) ? 'up' : 'down' - file_list << [status, version, match_data[2].humanize] + db_list = ActiveRecord::SchemaMigration.normalized_versions + + file_list = + ActiveRecord::Migrator.migrations_paths.flat_map do |path| + # match "20091231235959_some_name.rb" and "001_some_name.rb" pattern + Dir.foreach(path).grep(/^(\d{3,})_(.+)\.rb$/) do + version = ActiveRecord::SchemaMigration.normalize_migration_number($1) + status = db_list.delete(version) ? 'up' : 'down' + [status, version, $2.humanize] + end end - end - end + db_list.map! do |version| ['up', version, '********** NO FILE **********'] end @@ -105,8 +112,8 @@ db_namespace = namespace :db do puts "\ndatabase: #{ActiveRecord::Base.connection_config[:database]}\n\n" puts "#{'Status'.center(8)} #{'Migration ID'.ljust(14)} Migration Name" puts "-" * 50 - (db_list + file_list).sort_by {|migration| migration[1]}.each do |migration| - puts "#{migration[0].center(8)} #{migration[1].ljust(14)} #{migration[2]}" + (db_list + file_list).sort_by { |_, version, _| version }.each do |status, version, name| + puts "#{status.center(8)} #{version.ljust(14)} #{name}" end puts end @@ -178,17 +185,22 @@ db_namespace = namespace :db do task :load => [:environment, :load_config] do require 'active_record/fixtures' - base_dir = if ENV['FIXTURES_PATH'] - File.join [Rails.root, ENV['FIXTURES_PATH'] || %w{test fixtures}].flatten - else - ActiveRecord::Tasks::DatabaseTasks.fixtures_path - end + base_dir = ActiveRecord::Tasks::DatabaseTasks.fixtures_path - fixtures_dir = File.join [base_dir, ENV['FIXTURES_DIR']].compact + fixtures_dir = if ENV['FIXTURES_DIR'] + File.join base_dir, ENV['FIXTURES_DIR'] + else + base_dir + end - (ENV['FIXTURES'] ? ENV['FIXTURES'].split(',') : Dir["#{fixtures_dir}/**/*.yml"].map {|f| f[(fixtures_dir.size + 1)..-5] }).each do |fixture_file| - ActiveRecord::FixtureSet.create_fixtures(fixtures_dir, fixture_file) - end + fixture_files = if ENV['FIXTURES'] + ENV['FIXTURES'].split(',') + else + # The use of String#[] here is to support namespaced fixtures + Dir["#{fixtures_dir}/**/*.yml"].map {|f| f[(fixtures_dir.size + 1)..-5] } + end + + ActiveRecord::FixtureSet.create_fixtures(fixtures_dir, fixture_files) end # desc "Search for a fixture given a LABEL or ID. Specify an alternative path (eg. spec/fixtures) using FIXTURES_PATH=spec/fixtures." @@ -200,16 +212,11 @@ db_namespace = namespace :db do puts %Q(The fixture ID for "#{label}" is #{ActiveRecord::FixtureSet.identify(label)}.) if label - base_dir = if ENV['FIXTURES_PATH'] - File.join [Rails.root, ENV['FIXTURES_PATH'] || %w{test fixtures}].flatten - else - ActiveRecord::Tasks::DatabaseTasks.fixtures_path - end - + base_dir = ActiveRecord::Tasks::DatabaseTasks.fixtures_path Dir["#{base_dir}/**/*.yml"].each do |file| if data = YAML::load(ERB.new(IO.read(file)).result) - data.keys.each do |key| + data.each_key do |key| key_id = ActiveRecord::FixtureSet.identify(key) if key == label || key_id == id.to_i @@ -233,8 +240,8 @@ db_namespace = namespace :db do end desc 'Load a schema.rb file into the database' - task :load => [:environment, :load_config] do - ActiveRecord::Tasks::DatabaseTasks.load_schema(:ruby, ENV['SCHEMA']) + task :load => [:load_config] do + ActiveRecord::Tasks::DatabaseTasks.load_schema_current(:ruby, ENV['SCHEMA']) end task :load_if_ruby => ['db:create', :environment] do @@ -280,7 +287,7 @@ db_namespace = namespace :db do desc "Recreate the databases from the structure.sql file" task :load => [:environment, :load_config] do - ActiveRecord::Tasks::DatabaseTasks.load_schema(:sql, ENV['DB_STRUCTURE']) + ActiveRecord::Tasks::DatabaseTasks.load_schema_current(:sql, ENV['DB_STRUCTURE']) end task :load_if_sql => ['db:create', :environment] do @@ -298,7 +305,7 @@ db_namespace = namespace :db do end # desc "Recreate the test database from the current schema" - task :load => %w(db:test:deprecated db:test:purge) do + task :load => %w(db:test:purge) do case ActiveRecord::Base.schema_format when :ruby db_namespace["test:load_schema"].invoke @@ -308,12 +315,11 @@ db_namespace = namespace :db do end # desc "Recreate the test database from an existent schema.rb file" - task :load_schema => %w(db:test:deprecated db:test:purge) do + task :load_schema => %w(db:test:purge) do begin should_reconnect = ActiveRecord::Base.connection_pool.active_connection? - ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations['test']) ActiveRecord::Schema.verbose = false - db_namespace["schema:load"].invoke + ActiveRecord::Tasks::DatabaseTasks.load_schema ActiveRecord::Base.configurations['test'], :ruby, ENV['SCHEMA'] ensure if should_reconnect ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations[ActiveRecord::Tasks::DatabaseTasks.env]) @@ -322,13 +328,8 @@ db_namespace = namespace :db do end # desc "Recreate the test database from an existent structure.sql file" - task :load_structure => %w(db:test:deprecated db:test:purge) do - begin - ActiveRecord::Tasks::DatabaseTasks.current_config(:config => ActiveRecord::Base.configurations['test']) - db_namespace["structure:load"].invoke - ensure - ActiveRecord::Tasks::DatabaseTasks.current_config(:config => nil) - end + task :load_structure => %w(db:test:purge) do + ActiveRecord::Tasks::DatabaseTasks.load_schema ActiveRecord::Base.configurations['test'], :sql, ENV['SCHEMA'] end # desc "Recreate the test database from a fresh schema" @@ -348,12 +349,12 @@ 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(db:test:deprecated environment load_config) do + task :purge => %w(environment load_config) do ActiveRecord::Tasks::DatabaseTasks.purge ActiveRecord::Base.configurations['test'] end # desc 'Check for pending migrations and load the test schema' - task :prepare => %w(db:test:deprecated environment load_config) do + task :prepare => %w(environment load_config) do unless ActiveRecord::Base.configurations.blank? db_namespace['test:load'].invoke end @@ -365,7 +366,7 @@ namespace :railties do namespace :install do # desc "Copies missing migrations from Railties (e.g. engines). You can specify Railties to use with FROM=railtie1,railtie2" task :migrations => :'db:load_config' do - to_load = ENV['FROM'].blank? ? :all : ENV['FROM'].split(",").map {|n| n.strip } + to_load = ENV['FROM'].blank? ? :all : ENV['FROM'].split(",").map(&:strip) railties = {} Rails.application.migration_railties.each do |railtie| next unless to_load == :all || to_load.include?(railtie.railtie_name) diff --git a/activerecord/lib/active_record/readonly_attributes.rb b/activerecord/lib/active_record/readonly_attributes.rb index b3ddfd63d4..ce78f1756d 100644 --- a/activerecord/lib/active_record/readonly_attributes.rb +++ b/activerecord/lib/active_record/readonly_attributes.rb @@ -1,4 +1,3 @@ - module ActiveRecord module ReadonlyAttributes extend ActiveSupport::Concern @@ -12,7 +11,7 @@ module ActiveRecord # Attributes listed as readonly will be used to create a new record but update operations will # ignore these fields. def attr_readonly(*attributes) - self._attr_readonly = Set.new(attributes.map { |a| a.to_s }) + (self._attr_readonly || []) + self._attr_readonly = Set.new(attributes.map(&:to_s)) + (self._attr_readonly || []) end # Returns an array of all the attributes that have been specified as readonly. diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb index f23ed263fb..dd746a4e10 100644 --- a/activerecord/lib/active_record/reflection.rb +++ b/activerecord/lib/active_record/reflection.rb @@ -1,4 +1,5 @@ require 'thread' +require 'active_support/core_ext/string/filters' module ActiveRecord # = Active Record Reflection @@ -13,14 +14,21 @@ module ActiveRecord end def self.create(macro, name, scope, options, ar) - case macro - when :has_many, :belongs_to, :has_one - klass = options[:through] ? ThroughReflection : AssociationReflection - when :composed_of - klass = AggregateReflection - end - - klass.new(macro, name, scope, options, ar) + klass = case macro + when :composed_of + AggregateReflection + when :has_many + HasManyReflection + when :has_one + HasOneReflection + when :belongs_to + BelongsToReflection + else + raise "Unsupported Macro: #{macro}" + end + + reflection = klass.new(name, scope, options, ar) + options[:through] ? ThroughReflection.new(reflection) : reflection end def self.add_reflection(ar, name, reflection) @@ -31,9 +39,9 @@ module ActiveRecord ar.aggregate_reflections = ar.aggregate_reflections.merge(name.to_s => reflection) end - # \Reflection enables to interrogate Active Record classes and objects - # about their associations and aggregations. This information can, - # for example, be used in a form builder that takes an Active Record object + # \Reflection enables the ability to examine the associations and aggregations of + # Active Record classes and objects. This information, for example, + # can be used in a form builder that takes an Active Record object # and creates input fields for all of the attributes depending on their type # and displays the associations to other objects. # @@ -55,7 +63,7 @@ module ActiveRecord # Returns a Hash of name of the reflection as the key and a AssociationReflection as the value. # - # Account.reflections # => {balance: AggregateReflection} + # Account.reflections # => {"balance" => AggregateReflection} # # @api public def reflections @@ -110,26 +118,67 @@ module ActiveRecord end end + # Holds all the methods that are shared between MacroReflection, AssociationReflection + # and ThroughReflection + class AbstractReflection # :nodoc: + def table_name + klass.table_name + end + + # Returns a new, unsaved instance of the associated class. +attributes+ will + # be passed to the class's constructor. + def build_association(attributes, &block) + klass.new(attributes, &block) + end + + def quoted_table_name + klass.quoted_table_name + end + + def primary_key_type + klass.type_for_attribute(klass.primary_key) + end + + # Returns the class name for the macro. + # + # <tt>composed_of :balance, class_name: 'Money'</tt> returns <tt>'Money'</tt> + # <tt>has_many :clients</tt> returns <tt>'Client'</tt> + def class_name + @class_name ||= (options[:class_name] || derive_class_name).to_s + end + + JoinKeys = Struct.new(:key, :foreign_key) # :nodoc: + + def join_keys(assoc_klass) + JoinKeys.new(foreign_key, active_record_primary_key) + end + + def source_macro + ActiveSupport::Deprecation.warn(<<-MSG.squish) + ActiveRecord::Base.source_macro is deprecated and will be removed + without replacement. + MSG + + macro + end + end # Base class for AggregateReflection and AssociationReflection. Objects of # AggregateReflection and AssociationReflection are returned by the Reflection::ClassMethods. # # MacroReflection - # AggregateReflection # AssociationReflection - # ThroughReflection - class MacroReflection + # AggregateReflection + # HasManyReflection + # HasOneReflection + # BelongsToReflection + # ThroughReflection + class MacroReflection < AbstractReflection # Returns the name of the macro. # # <tt>composed_of :balance, class_name: 'Money'</tt> returns <tt>:balance</tt> # <tt>has_many :clients</tt> returns <tt>:clients</tt> attr_reader :name - # Returns the macro type. - # - # <tt>composed_of :balance, class_name: 'Money'</tt> returns <tt>:composed_of</tt> - # <tt>has_many :clients</tt> returns <tt>:has_many</tt> - attr_reader :macro - attr_reader :scope # Returns the hash of options used for the macro. @@ -142,8 +191,7 @@ module ActiveRecord attr_reader :plural_name # :nodoc: - def initialize(macro, name, scope, options, active_record) - @macro = macro + def initialize(name, scope, options, active_record) @name = name @scope = scope @options = options @@ -167,15 +215,11 @@ module ActiveRecord # <tt>composed_of :balance, class_name: 'Money'</tt> returns the Money class # <tt>has_many :clients</tt> returns the Client class def klass - @klass ||= class_name.constantize + @klass ||= compute_class(class_name) end - # Returns the class name for the macro. - # - # <tt>composed_of :balance, class_name: 'Money'</tt> returns <tt>'Money'</tt> - # <tt>has_many :clients</tt> returns <tt>'Client'</tt> - def class_name - @class_name ||= (options[:class_name] || derive_class_name).to_s + def compute_class(name) + name.constantize end # Returns +true+ if +self+ and +other_aggregation+ have the same +name+ attribute, +active_record+ attribute, @@ -188,23 +232,6 @@ module ActiveRecord active_record == other_aggregation.active_record end - JoinKeys = Struct.new(:key, :foreign_key) # :nodoc: - - def join_keys(assoc_klass) - if source_macro == :belongs_to - if polymorphic? - reflection_key = association_primary_key(assoc_klass) - else - reflection_key = association_primary_key - end - reflection_foreign_key = foreign_key - else - reflection_key = foreign_key - reflection_foreign_key = active_record_primary_key - end - JoinKeys.new(reflection_key, reflection_foreign_key) - end - private def derive_class_name name.to_s.camelize @@ -237,17 +264,20 @@ module ActiveRecord # a new association object. Use +build_association+ or +create_association+ # instead. This allows plugins to hook into association object creation. def klass - @klass ||= active_record.send(:compute_type, class_name) + @klass ||= compute_class(class_name) + end + + def compute_class(name) + active_record.send(:compute_type, name) end attr_reader :type, :foreign_type attr_accessor :parent_reflection # [:name, Reflection] - def initialize(macro, name, scope, options, active_record) + def initialize(name, scope, options, active_record) super - @collection = macro == :has_many @automatic_inverse_of = nil - @type = options[:as] && "#{options[:as]}_type" + @type = options[:as] && (options[:foreign_type] || "#{options[:as]}_type") @foreign_type = options[:foreign_type] || "#{name}_type" @constructable = calculate_constructable(macro, options) @association_scope_cache = {} @@ -257,31 +287,17 @@ module ActiveRecord def association_scope_cache(conn, owner) key = conn.prepared_statements if polymorphic? - key = [key, owner.read_attribute(@foreign_type)] + key = [key, owner._read_attribute(@foreign_type)] end @association_scope_cache[key] ||= @scope_lock.synchronize { @association_scope_cache[key] ||= yield } end - # Returns a new, unsaved instance of the associated class. +attributes+ will - # be passed to the class's constructor. - def build_association(attributes, &block) - klass.new(attributes, &block) - end - def constructable? # :nodoc: @constructable end - def table_name - klass.table_name - end - - def quoted_table_name - klass.quoted_table_name - end - def join_table @join_table ||= options[:join_table] || derive_join_table end @@ -290,10 +306,6 @@ module ActiveRecord @foreign_key ||= options[:foreign_key] || derive_foreign_key end - def primary_key_column - klass.columns_hash[klass.primary_key] - end - def association_foreign_key @association_foreign_key ||= options[:association_foreign_key] || class_name.foreign_key end @@ -331,19 +343,20 @@ module ActiveRecord return unless scope if scope.arity > 0 - ActiveSupport::Deprecation.warn <<-WARNING -The association scope '#{name}' is instance dependent (the scope block takes an argument). -Preloading happens before the individual instances are created. This means that there is no instance -being passed to the association scope. This will most likely result in broken or incorrect behavior. -Joining, Preloading and eager loading of these associations is deprecated and will be removed in the future. - WARNING + ActiveSupport::Deprecation.warn(<<-MSG.squish) + The association scope '#{name}' is instance dependent (the scope + block takes an argument). Preloading happens before the individual + instances are created. This means that there is no instance being + passed to the association scope. This will most likely result in + broken or incorrect behavior. Joining, Preloading and eager loading + of these associations is deprecated and will be removed in the future. + MSG end end alias :check_eager_loadable! :check_preloadable! - def join_id_for(owner) #:nodoc: - key = (source_macro == :belongs_to) ? foreign_key : active_record_primary_key - owner[key] + def join_id_for(owner) # :nodoc: + owner[active_record_primary_key] end def through_reflection @@ -370,8 +383,6 @@ Joining, Preloading and eager loading of these associations is deprecated and wi scope ? [[scope]] : [[]] end - alias :source_macro :macro - def has_inverse? inverse_name end @@ -392,11 +403,16 @@ Joining, Preloading and eager loading of these associations is deprecated and wi end end + # Returns the macro type. + # + # <tt>has_many :clients</tt> returns <tt>:has_many</tt> + def macro; raise NotImplementedError; end + # Returns whether or not this association reflection is for a collection # association. Returns +true+ if the +macro+ is either +has_many+ or # +has_and_belongs_to_many+, +false+ otherwise. def collection? - @collection + false end # Returns whether or not the association should be validated as part of @@ -413,14 +429,10 @@ Joining, Preloading and eager loading of these associations is deprecated and wi end # Returns +true+ if +self+ is a +belongs_to+ reflection. - def belongs_to? - macro == :belongs_to - end + def belongs_to?; false; end # Returns +true+ if +self+ is a +has_one+ reflection. - def has_one? - macro == :has_one - end + def has_one?; false; end def association_class case macro @@ -552,7 +564,7 @@ Joining, Preloading and eager loading of these associations is deprecated and wi end def derive_join_table - [active_record.table_name, klass.table_name].sort.join("\0").gsub(/^(.*_)(.+)\0\1(.+)/, '\1\2_\3').gsub("\0", "_") + ModelSchema.derive_join_table_name active_record.table_name, klass.table_name end def primary_key(klass) @@ -560,22 +572,72 @@ Joining, Preloading and eager loading of these associations is deprecated and wi end end - class HasAndBelongsToManyReflection < AssociationReflection #:nodoc: - def initialize(macro, name, scope, options, active_record) + 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) + end + + 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) + end + + def macro; :belongs_to; end + + def belongs_to?; true; end + + def join_keys(assoc_klass) + key = polymorphic? ? association_primary_key(assoc_klass) : association_primary_key + JoinKeys.new(key, foreign_key) + end + + def join_id_for(owner) # :nodoc: + owner[foreign_key] + end + end + + class HasAndBelongsToManyReflection < AssociationReflection # :nodoc: + def initialize(name, scope, options, active_record) super - @collection = true + end + + def macro; :has_and_belongs_to_many; end + + def collection? + true end end # Holds all the meta-data about a :through association as it was specified # in the Active Record class. - class ThroughReflection < AssociationReflection #:nodoc: + class ThroughReflection < AbstractReflection #:nodoc: + attr_reader :delegate_reflection delegate :foreign_key, :foreign_type, :association_foreign_key, :active_record_primary_key, :type, :to => :source_reflection - def initialize(macro, name, scope, options, active_record) - super - @source_reflection_name = options[:source] + def initialize(delegate_reflection) + @delegate_reflection = delegate_reflection + @klass = delegate_reflection.options[:class] + @source_reflection_name = delegate_reflection.options[:source] + end + + def klass + @klass ||= delegate_reflection.compute_class(class_name) end # Returns the source of the through reflection. It checks both a singularized @@ -593,7 +655,7 @@ Joining, Preloading and eager loading of these associations is deprecated and wi # # tags_reflection = Post.reflect_on_association(:tags) # tags_reflection.source_reflection - # # => <ActiveRecord::Reflection::AssociationReflection: @macro=:belongs_to, @name=:tag, @active_record=Tagging, @plural_name="tags"> + # # => <ActiveRecord::Reflection::BelongsToReflection: @name=:tag, @active_record=Tagging, @plural_name="tags"> # def source_reflection through_reflection.klass._reflect_on_association(source_reflection_name) @@ -609,7 +671,7 @@ Joining, Preloading and eager loading of these associations is deprecated and wi # # tags_reflection = Post.reflect_on_association(:tags) # tags_reflection.through_reflection - # # => <ActiveRecord::Reflection::AssociationReflection: @macro=:has_many, @name=:taggings, @active_record=Post, @plural_name="taggings"> + # # => <ActiveRecord::Reflection::HasManyReflection: @name=:taggings, @active_record=Post, @plural_name="taggings"> # def through_reflection active_record._reflect_on_association(options[:through]) @@ -629,8 +691,8 @@ Joining, Preloading and eager loading of these associations is deprecated and wi # # tags_reflection = Post.reflect_on_association(:tags) # tags_reflection.chain - # # => [<ActiveRecord::Reflection::ThroughReflection: @macro=:has_many, @name=:tags, @options={:through=>:taggings}, @active_record=Post>, - # <ActiveRecord::Reflection::AssociationReflection: @macro=:has_many, @name=:taggings, @options={}, @active_record=Post>] + # # => [<ActiveRecord::Reflection::ThroughReflection: @delegate_reflection=#<ActiveRecord::Reflection::HasManyReflection: @name=:tags...>, + # <ActiveRecord::Reflection::HasManyReflection: @name=:taggings, @options={}, @active_record=Post>] # def chain @chain ||= begin @@ -671,8 +733,11 @@ Joining, Preloading and eager loading of these associations is deprecated and wi through_scope_chain = through_reflection.scope_chain.map(&:dup) if options[:source_type] - through_scope_chain.first << - through_reflection.klass.where(foreign_type => options[:source_type]) + type = foreign_type + source_type = options[:source_type] + through_scope_chain.first << lambda { |object| + where(type => source_type) + } end # Recursively fill out the rest of the array from the through reflection @@ -680,8 +745,17 @@ Joining, Preloading and eager loading of these associations is deprecated and wi end end + def join_keys(assoc_klass) + source_reflection.join_keys(assoc_klass) + end + # The macro used by the source association def source_macro + ActiveSupport::Deprecation.warn(<<-MSG.squish) + ActiveRecord::Base.source_macro is deprecated and will be removed + without replacement. + MSG + source_reflection.source_macro end @@ -717,7 +791,7 @@ Joining, Preloading and eager loading of these associations is deprecated and wi def source_reflection_name # :nodoc: return @source_reflection_name if @source_reflection_name - names = [name.to_s.singularize, name].collect { |n| n.to_sym }.uniq + names = [name.to_s.singularize, name].collect(&:to_sym).uniq names = names.find_all { |n| through_reflection.klass._reflect_on_association(n) } @@ -725,15 +799,13 @@ Joining, Preloading and eager loading of these associations is deprecated and wi if names.length > 1 example_options = options.dup example_options[:source] = source_reflection_names.first - ActiveSupport::Deprecation.warn <<-eowarn -Ambiguous source reflection for through association. Please specify a :source -directive on your declaration like: - - class #{active_record.name} < ActiveRecord::Base - #{macro} :#{name}, #{example_options} - end - - eowarn + ActiveSupport::Deprecation.warn \ + "Ambiguous source reflection for through association. Please " \ + "specify a :source directive on your declaration like:\n" \ + "\n" \ + " class #{active_record.name} < ActiveRecord::Base\n" \ + " #{macro} :#{name}, #{example_options}\n" \ + " end" end @source_reflection_name = names.first @@ -747,13 +819,21 @@ directive on your declaration like: through_reflection.options end + def join_id_for(owner) # :nodoc: + source_reflection.join_id_for(owner) + end + def check_validity! if through_reflection.nil? raise HasManyThroughAssociationNotFoundError.new(active_record.name, self) end if through_reflection.polymorphic? - raise HasManyThroughAssociationPolymorphicThroughError.new(active_record.name, self) + if has_one? + raise HasOneAssociationPolymorphicThroughError.new(active_record.name, self) + else + raise HasManyThroughAssociationPolymorphicThroughError.new(active_record.name, self) + end end if source_reflection.nil? @@ -777,15 +857,25 @@ directive on your declaration like: protected - def actual_source_reflection # FIXME: this is a horrible name - source_reflection.actual_source_reflection - end + def actual_source_reflection # FIXME: this is a horrible name + source_reflection.send(:actual_source_reflection) + end + + def primary_key(klass) + klass.primary_key || raise(UnknownPrimaryKey.new(klass)) + end private def derive_class_name # get the class_name of the belongs_to association of the through reflection options[:source_type] || source_reflection.class_name end + + delegate_methods = AssociationReflection.public_instance_methods - + public_instance_methods + + delegate(*delegate_methods, to: :delegate_reflection) + end end end diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb index cef40be7ce..ab3debc03b 100644 --- a/activerecord/lib/active_record/relation.rb +++ b/activerecord/lib/active_record/relation.rb @@ -4,8 +4,6 @@ require 'arel/collectors/bind' module ActiveRecord # = Active Record Relation class Relation - JoinOperation = Struct.new(:relation, :join_class, :on) - MULTI_VALUE_METHODS = [:includes, :eager_load, :preload, :select, :group, :order, :joins, :where, :having, :bind, :references, :extending, :unscope] @@ -18,16 +16,17 @@ module ActiveRecord include FinderMethods, Calculations, SpawnMethods, QueryMethods, Batches, Explain, Delegation - attr_reader :table, :klass, :loaded + attr_reader :table, :klass, :loaded, :predicate_builder alias :model :klass alias :loaded? :loaded - def initialize(klass, table, values = {}) + def initialize(klass, table, predicate_builder, values = {}) @klass = klass @table = table @values = values @offsets = {} @loaded = false + @predicate_builder = predicate_builder end def initialize_copy(other) @@ -81,22 +80,26 @@ module ActiveRecord scope.unscope!(where: @klass.inheritance_column) end - um = scope.where(@klass.arel_table[@klass.primary_key].eq(id_was || id)).arel.compile_update(substitutes, @klass.primary_key) + relation = scope.where(@klass.primary_key => (id_was || id)) + bvs = binds + relation.bind_values + um = relation + .arel + .compile_update(substitutes, @klass.primary_key) @klass.connection.update( um, 'SQL', - binds) + bvs, + ) end def substitute_values(values) # :nodoc: - substitutes = values.sort_by { |arel_attr,_| arel_attr.name } - binds = substitutes.map do |arel_attr, value| + binds = values.map do |arel_attr, value| [@klass.columns_hash[arel_attr.name], value] end - substitutes.each_with_index do |tuple, i| - tuple[1] = @klass.connection.substitute_at(binds[i][0], i) + substitutes = values.each_with_index.map do |(arel_attr, _), i| + [arel_attr, @klass.connection.substitute_at(binds[i][0])] end [substitutes, binds] @@ -242,6 +245,11 @@ module ActiveRecord @records end + # Serializes the relation objects Array. + def encode_with(coder) + coder.represent_seq(nil, to_a) + end + def as_json(options = nil) #:nodoc: to_a.as_json(options) end @@ -297,10 +305,11 @@ module ActiveRecord klass.current_scope = previous end - # Updates all records with details given if they match a set of conditions supplied, limits and order can - # also be supplied. 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. + # 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 + # ActiveRecord's type-casting behavior. It should receive only values that can be passed as-is to the SQL + # database. # # ==== Parameters # @@ -319,7 +328,7 @@ module ActiveRecord def update_all(updates) raise ArgumentError, "Empty list of attributes to change" if updates.blank? - stmt = Arel::UpdateManager.new(arel.engine) + stmt = Arel::UpdateManager.new stmt.set Arel.sql(@klass.send(:sanitize_sql_for_assignment, updates)) stmt.table(table) @@ -333,7 +342,7 @@ module ActiveRecord stmt.wheres = arel.constraints end - bvs = bind_values + arel.bind_values + bvs = arel.bind_values + bind_values @klass.connection.update stmt, 'SQL', bvs end @@ -353,9 +362,21 @@ module ActiveRecord # # Updates multiple records # people = { 1 => { "first_name" => "David" }, 2 => { "first_name" => "Jeremy" } } # Person.update(people.keys, people.values) - def update(id, attributes) + # + # # Updates multiple records from the result of a relation + # people = Person.where(group: 'expert') + # people.update(group: 'masters') + # + # Note: Updating a large number of records will run a + # UPDATE query for each record, which may cause a performance + # issue. So if it is not needed to run callbacks for each update, it is + # preferred to use <tt>update_all</tt> for updating all records using + # a single query. + def update(id = :all, attributes) if id.is_a?(Array) id.map.with_index { |one_id, idx| update(one_id, attributes[idx]) } + elsif id == :all + to_a.each { |record| record.update(attributes) } else object = find(id) object.update(attributes) @@ -392,7 +413,7 @@ module ActiveRecord if conditions where(conditions).destroy_all else - to_a.each {|object| object.destroy }.tap { reset } + to_a.each(&:destroy).tap { reset } end end @@ -457,7 +478,7 @@ module ActiveRecord if conditions where(conditions).delete_all else - stmt = Arel::DeleteManager.new(arel.engine) + stmt = Arel::DeleteManager.new stmt.from(table) if joins_values.any? @@ -560,7 +581,7 @@ module ActiveRecord [name, binds.fetch(name.to_s) { case where.right when Array then where.right.map(&:val) - else + when Arel::Nodes::Casted, Arel::Nodes::Quoted where.right.val end }] @@ -631,17 +652,21 @@ module ActiveRecord preload = preload_values preload += includes_values unless eager_loading? - preloader = ActiveRecord::Associations::Preloader.new + preloader = build_preloader preload.each do |associations| preloader.preload @records, associations end - @records.each { |record| record.readonly! } if readonly_value + @records.each(&:readonly!) if readonly_value @loaded = true @records end + def build_preloader + ActiveRecord::Associations::Preloader.new + end + def references_eager_loaded_tables? joined_tables = arel.join_sources.map do |join| if join.is_a?(Arel::Nodes::StringJoin) @@ -654,7 +679,7 @@ module ActiveRecord joined_tables += [table.name, table.table_alias] # always convert table names to downcase as in Oracle quoted table names are in uppercase - joined_tables = joined_tables.flatten.compact.map { |t| t.downcase }.uniq + joined_tables = joined_tables.flatten.compact.map(&:downcase).uniq (references_values - joined_tables).any? end @@ -663,7 +688,7 @@ module ActiveRecord return [] if string.blank? # always convert table names to downcase as in Oracle quoted table names are in uppercase # ignore raw_sql_ that is used by Oracle adapter as alias for limit/offset subqueries - string.scan(/([a-zA-Z_][.\w]+).?\./).flatten.map{ |s| s.downcase }.uniq - ['raw_sql_'] + string.scan(/([a-zA-Z_][.\w]+).?\./).flatten.map(&:downcase).uniq - ['raw_sql_'] end end end diff --git a/activerecord/lib/active_record/relation/batches.rb b/activerecord/lib/active_record/relation/batches.rb index 29fc150b3d..4f0502ae75 100644 --- a/activerecord/lib/active_record/relation/batches.rb +++ b/activerecord/lib/active_record/relation/batches.rb @@ -1,4 +1,3 @@ - module ActiveRecord module Batches # Looping through a collection of records from the database @@ -28,7 +27,7 @@ module ActiveRecord # # ==== Options # * <tt>:batch_size</tt> - Specifies the size of the batch. Default to 1000. - # * <tt>:start</tt> - Specifies the starting point for the batch processing. + # * <tt>:start</tt> - Specifies the primary key value to start from. # 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 @@ -41,8 +40,8 @@ module ActiveRecord # # NOTE: It's not possible to set the order. That is automatically set to # ascending on the primary key ("id ASC") to make the batch ordering - # work. This also means that this method only works with integer-based - # primary keys. + # work. This also means that this method only works when the primary key is + # orderable (e.g. an integer or string). # # NOTE: You can't set the limit either, that's used to control # the batch sizes. @@ -78,7 +77,7 @@ module ActiveRecord # # ==== Options # * <tt>:batch_size</tt> - Specifies the size of the batch. Default to 1000. - # * <tt>:start</tt> - Specifies the starting point for the batch processing. + # * <tt>:start</tt> - Specifies the primary key value to start from. # 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 @@ -91,8 +90,8 @@ module ActiveRecord # # NOTE: It's not possible to set the order. That is automatically set to # ascending on the primary key ("id ASC") to make the batch ordering - # work. This also means that this method only works with integer-based - # primary keys. + # work. This also means that this method only works when the primary key is + # orderable (e.g. an integer or string). # # NOTE: You can't set the limit either, that's used to control # the batch sizes. diff --git a/activerecord/lib/active_record/relation/calculations.rb b/activerecord/lib/active_record/relation/calculations.rb index 028e4d80ab..71673324eb 100644 --- a/activerecord/lib/active_record/relation/calculations.rb +++ b/activerecord/lib/active_record/relation/calculations.rb @@ -20,13 +20,21 @@ module ActiveRecord # Person.group(:city).count # # => { 'Rome' => 5, 'Paris' => 3 } # + # If +count+ is used with +group+ for multiple columns, it returns a Hash whose + # keys are an array containing the individual values of each column and the value + # of each key would be the +count+. + # + # Article.group(:status, :category).count + # # => {["draft", "business"]=>10, ["draft", "technology"]=>4, + # ["published", "business"]=>0, ["published", "technology"]=>2} + # # If +count+ is used with +select+, it will count the selected columns: # # Person.select(:age).count # # => counts the number of different age values # # Note: not all valid +select+ expressions are valid +count+ expressions. The specifics differ - # between databases. In invalid cases, an error from the databsae is thrown. + # between databases. In invalid cases, an error from the database is thrown. def count(column_name = nil, options = {}) # TODO: Remove options argument as soon we remove support to # activerecord-deprecated_finders. @@ -169,17 +177,8 @@ module ActiveRecord relation.select_values = column_names.map { |cn| columns_hash.key?(cn) ? arel_table[cn] : cn } - result = klass.connection.select_all(relation.arel, nil, bind_values) - columns = result.columns.map do |key| - klass.column_types.fetch(key) { - result.column_types.fetch(key) { result.identity_type } - } - end - - result = result.rows.map do |values| - columns.zip(values).map { |column, value| column.type_cast_from_database value } - end - columns.one? ? result.map!(&:first) : result + result = klass.connection.select_all(relation.arel, nil, relation.arel.bind_values + bind_values) + result.cast_values(klass.column_types) end end @@ -255,6 +254,7 @@ module ActiveRecord select_value = operation_over_aggregate_column(column, operation, distinct) column_alias = select_value.alias + column_alias ||= @klass.connection.column_name_for_operation(operation, select_value) relation.select_values = [select_value] query_builder = relation.arel @@ -265,7 +265,7 @@ module ActiveRecord row = result.first value = row && row.values.first column = result.column_types.fetch(column_alias) do - column_for(column_name) + type_for(column_name) end type_cast_calculated_value(value, column, operation) @@ -275,7 +275,7 @@ module ActiveRecord group_attrs = group_values if group_attrs.first.respond_to?(:to_sym) - association = @klass._reflect_on_association(group_attrs.first.to_sym) + association = @klass._reflect_on_association(group_attrs.first) associated = group_attrs.size == 1 && association && association.belongs_to? # only count belongs_to associations group_fields = Array(associated ? association.foreign_key : group_attrs) else @@ -317,7 +317,7 @@ module ActiveRecord relation.group_values = group relation.select_values = select_values - calculated_data = @klass.connection.select_all(relation, nil, bind_values) + calculated_data = @klass.connection.select_all(relation, nil, relation.arel.bind_values + bind_values) if association key_ids = calculated_data.collect { |row| row[group_aliases.first] } @@ -328,14 +328,14 @@ module ActiveRecord Hash[calculated_data.map do |row| key = group_columns.map { |aliaz, col_name| column = calculated_data.column_types.fetch(aliaz) do - column_for(col_name) + type_for(col_name) end type_cast_calculated_value(row[aliaz], column) } key = key.first if key.size == 1 key = key_records[key] if associated - column_type = calculated_data.column_types.fetch(aggregate_alias) { column_for(column_name) } + column_type = calculated_data.column_types.fetch(aggregate_alias) { type_for(column_name) } [key, type_cast_calculated_value(row[aggregate_alias], column_type, operation)] end] end @@ -362,24 +362,20 @@ module ActiveRecord @klass.connection.table_alias_for(table_name) end - def column_for(field) + def type_for(field) field_name = field.respond_to?(:name) ? field.name.to_s : field.to_s.split('.').last - @klass.columns_hash[field_name] + @klass.type_for_attribute(field_name) end - def type_cast_calculated_value(value, column, operation = nil) + def type_cast_calculated_value(value, type, operation = nil) case operation when 'count' then value.to_i - when 'sum' then type_cast_using_column(value || 0, column) + when 'sum' then type.type_cast_from_database(value || 0) when 'average' then value.respond_to?(:to_d) ? value.to_d : value - else type_cast_using_column(value, column) + else type.type_cast_from_database(value) end end - def type_cast_using_column(value, column) - column ? column.type_cast_from_database(value) : value - end - # TODO: refactor to allow non-string `select_values` (eg. Arel nodes). def select_for_count if select_values.present? diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb index 0c9c761f97..088bc203b7 100644 --- a/activerecord/lib/active_record/relation/finder_methods.rb +++ b/activerecord/lib/active_record/relation/finder_methods.rb @@ -1,4 +1,5 @@ require 'active_support/deprecation' +require 'active_support/core_ext/string/filters' module ActiveRecord module FinderMethods @@ -81,12 +82,16 @@ module ActiveRecord # Post.find_by "published_at < ?", 2.weeks.ago def find_by(*args) where(*args).take + rescue RangeError + nil end # Like <tt>find_by</tt>, except that if no record is found, raises # an <tt>ActiveRecord::RecordNotFound</tt> error. def find_by!(*args) where(*args).take! + rescue RangeError + raise RecordNotFound, "Couldn't find #{@klass.name} with an out of range value" end # Gives a record (or N records if a parameter is supplied) without any implied @@ -103,7 +108,7 @@ module ActiveRecord # Same as +take+ but raises <tt>ActiveRecord::RecordNotFound</tt> if no record # is found. Note that <tt>take!</tt> accepts no arguments. def take! - take or raise RecordNotFound + take or raise RecordNotFound.new("Couldn't find #{@klass.name} with [#{arel.where_sql(@klass.arel_engine)}]") end # Find the first record (or first N records if a parameter is supplied). @@ -138,7 +143,7 @@ module ActiveRecord # Same as +first+ but raises <tt>ActiveRecord::RecordNotFound</tt> if no record # is found. Note that <tt>first!</tt> accepts no arguments. def first! - first or raise RecordNotFound + find_nth! 0 end # Find the last record (or last N records if a parameter is supplied). @@ -171,7 +176,7 @@ module ActiveRecord # Same as +last+ but raises <tt>ActiveRecord::RecordNotFound</tt> if no record # is found. Note that <tt>last!</tt> accepts no arguments. def last! - last or raise RecordNotFound + last or raise RecordNotFound.new("Couldn't find #{@klass.name} with [#{arel.where_sql(@klass.arel_engine)}]") end # Find the second record. @@ -187,7 +192,7 @@ module ActiveRecord # Same as +second+ but raises <tt>ActiveRecord::RecordNotFound</tt> if no record # is found. def second! - second or raise RecordNotFound + find_nth! 1 end # Find the third record. @@ -203,7 +208,7 @@ module ActiveRecord # Same as +third+ but raises <tt>ActiveRecord::RecordNotFound</tt> if no record # is found. def third! - third or raise RecordNotFound + find_nth! 2 end # Find the fourth record. @@ -219,7 +224,7 @@ module ActiveRecord # Same as +fourth+ but raises <tt>ActiveRecord::RecordNotFound</tt> if no record # is found. def fourth! - fourth or raise RecordNotFound + find_nth! 3 end # Find the fifth record. @@ -235,7 +240,7 @@ module ActiveRecord # Same as +fifth+ but raises <tt>ActiveRecord::RecordNotFound</tt> if no record # is found. def fifth! - fifth or raise RecordNotFound + find_nth! 4 end # Find the forty-second record. Also known as accessing "the reddit". @@ -251,7 +256,7 @@ module ActiveRecord # Same as +forty_two+ but raises <tt>ActiveRecord::RecordNotFound</tt> if no record # is found. def forty_two! - forty_two or raise RecordNotFound + find_nth! 41 end # Returns +true+ if a record exists in the table that matches the +id+ or @@ -284,8 +289,10 @@ module ActiveRecord def exists?(conditions = :none) if Base === conditions conditions = conditions.id - ActiveSupport::Deprecation.warn "You are passing an instance of ActiveRecord::Base to `exists?`." \ - "Please pass the id of the object by calling `.id`" + ActiveSupport::Deprecation.warn(<<-MSG.squish) + You are passing an instance of ActiveRecord::Base to `exists?`. + Please pass the id of the object by calling `.id` + MSG end return false if !conditions @@ -304,7 +311,7 @@ module ActiveRecord end end - connection.select_value(relation, "#{name} Exists", relation.bind_values) ? true : false + connection.select_value(relation, "#{name} Exists", relation.arel.bind_values + relation.bind_values) ? true : false end # This method is called whenever no records are found with either a single @@ -316,7 +323,7 @@ module ActiveRecord # the expected number of results should be provided in the +expected_size+ # argument. def raise_record_not_found_exception!(ids, result_size, expected_size) #:nodoc: - conditions = arel.where_sql + conditions = arel.where_sql(@klass.arel_engine) conditions = " [#{conditions}]" if conditions if Array(ids).size == 1 @@ -390,7 +397,7 @@ module ActiveRecord else if relation.limit_value limited_ids = limited_ids_for(relation) - limited_ids.empty? ? relation.none! : relation.where!(table[primary_key].in(limited_ids)) + limited_ids.empty? ? relation.none! : relation.where!(primary_key => limited_ids) end relation.except(:limit, :offset) end @@ -401,13 +408,14 @@ module ActiveRecord "#{quoted_table_name}.#{quoted_primary_key}", relation.order_values) relation = relation.except(:select).select(values).distinct! + arel = relation.arel - id_rows = @klass.connection.select_all(relation.arel, 'SQL', relation.bind_values) + id_rows = @klass.connection.select_all(arel, 'SQL', arel.bind_values + relation.bind_values) id_rows.map {|row| row[primary_key]} end def using_limitable_reflections?(reflections) - reflections.none? { |r| r.collection? } + reflections.none?(&:collection?) end protected @@ -429,19 +437,20 @@ module ActiveRecord else find_some(ids) end + rescue RangeError + raise RecordNotFound, "Couldn't find #{@klass.name} with an out of range ID" end def find_one(id) if ActiveRecord::Base === id id = id.id - ActiveSupport::Deprecation.warn "You are passing an instance of ActiveRecord::Base to `find`." \ - "Please pass the id of the object by calling `.id`" + ActiveSupport::Deprecation.warn(<<-MSG.squish) + You are passing an instance of ActiveRecord::Base to `find`. + Please pass the id of the object by calling `.id` + MSG end - column = columns_hash[primary_key] - substitute = connection.substitute_at(column, bind_values.length) - relation = where(table[primary_key].eq(substitute)) - relation.bind_values += [[column, id]] + relation = where(primary_key => id) record = relation.take raise_record_not_found_exception!(id, 0, 1) unless record @@ -450,7 +459,7 @@ module ActiveRecord end def find_some(ids) - result = where(table[primary_key].in(ids)).to_a + result = where(primary_key => ids).to_a expected_size = if limit_value && ids.size > limit_value @@ -488,6 +497,10 @@ module ActiveRecord 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)}]") + end + def find_nth_with_limit(offset, limit) relation = if order_values.empty? && primary_key order(arel_table[primary_key].asc) diff --git a/activerecord/lib/active_record/relation/merger.rb b/activerecord/lib/active_record/relation/merger.rb index ac41d0aa80..afb0b208c3 100644 --- a/activerecord/lib/active_record/relation/merger.rb +++ b/activerecord/lib/active_record/relation/merger.rb @@ -13,7 +13,7 @@ module ActiveRecord @hash = hash end - def merge + def merge #:nodoc: Merger.new(relation, other).merge end @@ -22,7 +22,7 @@ module ActiveRecord # build a relation to merge in rather than directly merging # the values. def other - other = Relation.create(relation.klass, relation.table) + other = Relation.create(relation.klass, relation.table, relation.predicate_builder) hash.each { |k, v| if k == :joins if Hash === v @@ -83,12 +83,12 @@ module ActiveRecord private def merge_joins - return if values[:joins].blank? + return if other.joins_values.blank? if other.klass == relation.klass - relation.joins!(*values[:joins]) + relation.joins!(*other.joins_values) else - joins_dependency, rest = values[:joins].partition do |join| + joins_dependency, rest = other.joins_values.partition do |join| case join when Hash, Symbol, Array true @@ -108,48 +108,36 @@ module ActiveRecord def merge_multi_values lhs_wheres = relation.where_values - rhs_wheres = values[:where] || [] + rhs_wheres = other.where_values lhs_binds = relation.bind_values - rhs_binds = values[:bind] || [] + rhs_binds = other.bind_values removed, kept = partition_overwrites(lhs_wheres, rhs_wheres) where_values = kept + rhs_wheres bind_values = filter_binds(lhs_binds, removed) + rhs_binds - conn = relation.klass.connection - bv_index = 0 - where_values.map! do |node| - if Arel::Nodes::Equality === node && Arel::Nodes::BindParam === node.right - substitute = conn.substitute_at(bind_values[bv_index].first, bv_index) - bv_index += 1 - Arel::Nodes::Equality.new(node.left, substitute) - else - node - end - end - relation.where_values = where_values relation.bind_values = bind_values - if values[:reordering] + if other.reordering_value # override any order specified in the original relation - relation.reorder! values[:order] - elsif values[:order] + relation.reorder! other.order_values + elsif other.order_values # merge in order_values from relation - relation.order! values[:order] + relation.order! other.order_values end - relation.extend(*values[:extending]) unless values[:extending].blank? + relation.extend(*other.extending_values) unless other.extending_values.blank? end def merge_single_values - relation.from_value = values[:from] unless relation.from_value - relation.lock_value = values[:lock] unless relation.lock_value + relation.from_value = other.from_value unless relation.from_value + relation.lock_value = other.lock_value unless relation.lock_value - unless values[:create_with].blank? - relation.create_with_value = (relation.create_with_value || {}).merge(values[:create_with]) + unless other.create_with_value.blank? + relation.create_with_value = (relation.create_with_value || {}).merge(other.create_with_value) end end diff --git a/activerecord/lib/active_record/relation/predicate_builder.rb b/activerecord/lib/active_record/relation/predicate_builder.rb index eff5c8f09c..567efce8ae 100644 --- a/activerecord/lib/active_record/relation/predicate_builder.rb +++ b/activerecord/lib/active_record/relation/predicate_builder.rb @@ -1,82 +1,44 @@ module ActiveRecord class PredicateBuilder # :nodoc: - @handlers = [] - - autoload :RelationHandler, 'active_record/relation/predicate_builder/relation_handler' - autoload :ArrayHandler, 'active_record/relation/predicate_builder/array_handler' - - def self.resolve_column_aliases(klass, hash) - hash = hash.dup - hash.keys.grep(Symbol) do |key| - if klass.attribute_alias? key - hash[klass.attribute_alias(key)] = hash.delete key - end - end - hash + require 'active_record/relation/predicate_builder/array_handler' + require 'active_record/relation/predicate_builder/association_query_handler' + require 'active_record/relation/predicate_builder/base_handler' + require 'active_record/relation/predicate_builder/basic_object_handler' + require 'active_record/relation/predicate_builder/class_handler' + require 'active_record/relation/predicate_builder/range_handler' + require 'active_record/relation/predicate_builder/relation_handler' + + delegate :resolve_column_aliases, to: :table + + def initialize(table) + @table = table + @handlers = [] + + register_handler(BasicObject, BasicObjectHandler.new(self)) + register_handler(Class, ClassHandler.new(self)) + register_handler(Base, BaseHandler.new(self)) + register_handler(Range, RangeHandler.new(self)) + register_handler(Relation, RelationHandler.new) + register_handler(Array, ArrayHandler.new(self)) + register_handler(AssociationQueryValue, AssociationQueryHandler.new(self)) end - def self.build_from_hash(klass, attributes, default_table) - queries = [] - - attributes.each do |column, value| - table = default_table - - if value.is_a?(Hash) - if value.empty? - queries << '1=0' - else - table = Arel::Table.new(column, default_table.engine) - association = klass._reflect_on_association(column.to_sym) - - value.each do |k, v| - queries.concat expand(association && association.klass, table, k, v) - end - end - else - column = column.to_s - - if column.include?('.') - table_name, column = column.split('.', 2) - table = Arel::Table.new(table_name, default_table.engine) - end - - queries.concat expand(klass, table, column, value) - end - end - - queries + def build_from_hash(attributes) + attributes = convert_dot_notation_to_hash(attributes.stringify_keys) + expand_from_hash(attributes) end - def self.expand(klass, table, column, value) - queries = [] - + def expand(column, value) # Find the foreign key when using queries such as: # Post.where(author: author) # # For polymorphic relationships, find the foreign key and type: # PriceEstimate.where(estimate_of: treasure) - if klass && reflection = klass._reflect_on_association(column.to_sym) - if reflection.polymorphic? && base_class = polymorphic_base_class_from_value(value) - queries << build(table[reflection.foreign_type], base_class) - end - - column = reflection.foreign_key + if table.associated_with?(column) + value = AssociationQueryValue.new(table.associated_table(column), value) end - queries << build(table[column], value) - queries - end - - def self.polymorphic_base_class_from_value(value) - case value - when Relation - value.klass.base_class - when Array - val = value.compact.first - val.class.base_class if val.is_a?(Base) - when Base - value.class.base_class - end + build(table.arel_attribute(column), value) end def self.references(attributes) @@ -101,26 +63,49 @@ module ActiveRecord # ) # end # ActiveRecord::PredicateBuilder.register_handler(MyCustomDateRange, handler) - def self.register_handler(klass, handler) + def register_handler(klass, handler) @handlers.unshift([klass, handler]) end - register_handler(BasicObject, ->(attribute, value) { attribute.eq(value) }) - # FIXME: I think we need to deprecate this behavior - register_handler(Class, ->(attribute, value) { attribute.eq(value.name) }) - register_handler(Base, ->(attribute, value) { attribute.eq(value.id) }) - register_handler(Range, ->(attribute, value) { attribute.in(value) }) - register_handler(Relation, RelationHandler.new) - register_handler(Array, ArrayHandler.new) - - def self.build(attribute, value) + def build(attribute, value) handler_for(value).call(attribute, value) end - private_class_method :build - def self.handler_for(object) + protected + + attr_reader :table + + def expand_from_hash(attributes) + return ["1=0"] if attributes.empty? + + attributes.flat_map do |key, value| + if value.is_a?(Hash) + builder = self.class.new(table.associated_table(key)) + builder.expand_from_hash(value) + else + expand(key, value) + end + end + end + + private + + def convert_dot_notation_to_hash(attributes) + dot_notation = attributes.keys.select { |s| s.include?(".") } + + dot_notation.each do |key| + table_name, column_name = key.split(".") + value = attributes.delete(key) + attributes[table_name] ||= {} + + attributes[table_name] = attributes[table_name].merge(column_name => value) + end + + attributes + end + + def handler_for(object) @handlers.detect { |klass, _| klass === object }.last end - private_class_method :handler_for end end diff --git a/activerecord/lib/active_record/relation/predicate_builder/array_handler.rb b/activerecord/lib/active_record/relation/predicate_builder/array_handler.rb index 78dba8be06..4b5f5773a0 100644 --- a/activerecord/lib/active_record/relation/predicate_builder/array_handler.rb +++ b/activerecord/lib/active_record/relation/predicate_builder/array_handler.rb @@ -1,30 +1,41 @@ +require 'active_support/core_ext/string/filters' + module ActiveRecord class PredicateBuilder class ArrayHandler # :nodoc: - def call(attribute, value) - return attribute.in([]) if value.empty? + def initialize(predicate_builder) + @predicate_builder = predicate_builder + end + def call(attribute, value) values = value.map { |x| x.is_a?(Base) ? x.id : x } - ranges, values = values.partition { |v| v.is_a?(Range) } nils, values = values.partition(&:nil?) + return attribute.in([]) if values.empty? && nils.empty? + + ranges, values = values.partition { |v| v.is_a?(Range) } + values_predicate = case values.length when 0 then NullPredicate - when 1 then attribute.eq(values.first) + when 1 then predicate_builder.build(attribute, values.first) else attribute.in(values) end unless nils.empty? - values_predicate = values_predicate.or(attribute.eq(nil)) + values_predicate = values_predicate.or(predicate_builder.build(attribute, nil)) end - array_predicates = ranges.map { |range| attribute.in(range) } - array_predicates << values_predicate + array_predicates = ranges.map { |range| predicate_builder.build(attribute, range) } + array_predicates.unshift(values_predicate) array_predicates.inject { |composite, predicate| composite.or(predicate) } end - module NullPredicate + protected + + attr_reader :predicate_builder + + module NullPredicate # :nodoc: def self.or(other) other 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 new file mode 100644 index 0000000000..aabcf20c1d --- /dev/null +++ b/activerecord/lib/active_record/relation/predicate_builder/association_query_handler.rb @@ -0,0 +1,58 @@ +module ActiveRecord + class PredicateBuilder + class AssociationQueryHandler # :nodoc: + def initialize(predicate_builder) + @predicate_builder = predicate_builder + end + + def call(attribute, value) + queries = {} + + table = value.associated_table + if value.base_class + queries[table.association_foreign_type] = value.base_class.name + end + + queries[table.association_foreign_key] = value.ids + predicate_builder.build_from_hash(queries) + end + + protected + + attr_reader :predicate_builder + end + + class AssociationQueryValue # :nodoc: + attr_reader :associated_table, :value + + def initialize(associated_table, value) + @associated_table = associated_table + @value = value + end + + def ids + value + end + + def base_class + if associated_table.polymorphic_association? + @base_class ||= polymorphic_base_class_from_value + end + end + + private + + def polymorphic_base_class_from_value + case value + when Relation + value.klass.base_class + when Array + val = value.compact.first + val.class.base_class if val.is_a?(Base) + when Base + value.class.base_class + end + end + end + end +end diff --git a/activerecord/lib/active_record/relation/predicate_builder/base_handler.rb b/activerecord/lib/active_record/relation/predicate_builder/base_handler.rb new file mode 100644 index 0000000000..6fa5b16f73 --- /dev/null +++ b/activerecord/lib/active_record/relation/predicate_builder/base_handler.rb @@ -0,0 +1,17 @@ +module ActiveRecord + class PredicateBuilder + class BaseHandler # :nodoc: + def initialize(predicate_builder) + @predicate_builder = predicate_builder + end + + def call(attribute, value) + predicate_builder.build(attribute, value.id) + end + + protected + + attr_reader :predicate_builder + end + end +end diff --git a/activerecord/lib/active_record/relation/predicate_builder/basic_object_handler.rb b/activerecord/lib/active_record/relation/predicate_builder/basic_object_handler.rb new file mode 100644 index 0000000000..6cec75dc0a --- /dev/null +++ b/activerecord/lib/active_record/relation/predicate_builder/basic_object_handler.rb @@ -0,0 +1,17 @@ +module ActiveRecord + class PredicateBuilder + class BasicObjectHandler # :nodoc: + def initialize(predicate_builder) + @predicate_builder = predicate_builder + end + + def call(attribute, value) + attribute.eq(value) + end + + protected + + attr_reader :predicate_builder + end + end +end diff --git a/activerecord/lib/active_record/relation/predicate_builder/class_handler.rb b/activerecord/lib/active_record/relation/predicate_builder/class_handler.rb new file mode 100644 index 0000000000..ed313fc9d4 --- /dev/null +++ b/activerecord/lib/active_record/relation/predicate_builder/class_handler.rb @@ -0,0 +1,27 @@ +module ActiveRecord + class PredicateBuilder + class ClassHandler # :nodoc: + def initialize(predicate_builder) + @predicate_builder = predicate_builder + end + + def call(attribute, value) + print_deprecation_warning + predicate_builder.build(attribute, value.name) + end + + protected + + attr_reader :predicate_builder + + private + + def print_deprecation_warning + ActiveSupport::Deprecation.warn(<<-MSG.squish) + Passing a class as a value in an Active Record query is deprecated and + will be removed. Pass a string instead. + MSG + end + end + end +end diff --git a/activerecord/lib/active_record/relation/predicate_builder/range_handler.rb b/activerecord/lib/active_record/relation/predicate_builder/range_handler.rb new file mode 100644 index 0000000000..1b3849e3ad --- /dev/null +++ b/activerecord/lib/active_record/relation/predicate_builder/range_handler.rb @@ -0,0 +1,17 @@ +module ActiveRecord + class PredicateBuilder + class RangeHandler # :nodoc: + def initialize(predicate_builder) + @predicate_builder = predicate_builder + end + + def call(attribute, value) + attribute.between(value) + end + + protected + + attr_reader :predicate_builder + end + end +end diff --git a/activerecord/lib/active_record/relation/predicate_builder/relation_handler.rb b/activerecord/lib/active_record/relation/predicate_builder/relation_handler.rb index 618fa3cdd9..063150958a 100644 --- a/activerecord/lib/active_record/relation/predicate_builder/relation_handler.rb +++ b/activerecord/lib/active_record/relation/predicate_builder/relation_handler.rb @@ -6,7 +6,7 @@ module ActiveRecord value = value.select(value.klass.arel_table[value.klass.primary_key]) end - attribute.in(value.arel.ast) + attribute.in(value.arel) end end end diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb index 1262b2c291..ef380abfe8 100644 --- a/activerecord/lib/active_record/relation/query_methods.rb +++ b/activerecord/lib/active_record/relation/query_methods.rb @@ -1,9 +1,13 @@ require 'active_support/core_ext/array/wrap' +require 'active_support/core_ext/string/filters' +require 'active_model/forbidden_attributes_protection' module ActiveRecord module QueryMethods extend ActiveSupport::Concern + include ActiveModel::ForbiddenAttributesProtection + # 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 @@ -91,10 +95,10 @@ module ActiveRecord def check_cached_relation # :nodoc: if defined?(@arel) && @arel @arel = nil - ActiveSupport::Deprecation.warn <<-WARNING -Modifying already cached Relation. The cache will be reset. -Use a cloned Relation to prevent this warning. -WARNING + ActiveSupport::Deprecation.warn(<<-MSG.squish) + Modifying already cached Relation. The cache will be reset. Use a + cloned Relation to prevent this warning. + MSG end end @@ -423,19 +427,17 @@ WARNING # => SELECT "users".* FROM "users" LEFT JOIN bookmarks ON bookmarks.bookmarkable_type = 'Post' AND bookmarks.user_id = users.id def joins(*args) check_if_method_has_arguments!(:joins, args) - - args.compact! - args.flatten! - spawn.joins!(*args) end def joins!(*args) # :nodoc: + args.compact! + args.flatten! self.joins_values += args self end - def bind(value) + def bind(value) # :nodoc: spawn.bind!(value) end @@ -574,7 +576,10 @@ WARNING end def where!(opts, *rest) # :nodoc: - references!(PredicateBuilder.references(opts)) if Hash === opts + if Hash === opts + opts = sanitize_forbidden_attributes(opts) + references!(PredicateBuilder.references(opts)) + end self.where_values += build_where(opts, rest) self @@ -683,11 +688,11 @@ WARNING # end # def none - extending(NullRelation) + where("1=0").extending!(NullRelation) end def none! # :nodoc: - extending!(NullRelation) + where!("1=0").extending!(NullRelation) end # Sets readonly attributes for the returned relation. If value is @@ -723,7 +728,13 @@ WARNING end def create_with!(value) # :nodoc: - self.create_with_value = value ? create_with_value.merge(value) : {} + if value + value = sanitize_forbidden_attributes(value) + self.create_with_value = create_with_value.merge(value) + else + self.create_with_value = {} + end + self end @@ -847,7 +858,7 @@ WARNING private def build_arel - arel = Arel::SelectManager.new(table.engine, table) + arel = Arel::SelectManager.new(table) build_joins(arel, joins_values.flatten) unless joins_values.empty? @@ -868,15 +879,6 @@ WARNING arel.from(build_from) if from_value arel.lock(lock_value) if lock_value - # Reorder bind indexes if joins produced bind values - if arel.bind_values.any? - bvs = arel.bind_values + bind_values - arel.ast.grep(Arel::Nodes::BindParam).each_with_index do |bp, i| - column = bvs[i].first - bp.replace connection.substitute_at(column, i) - end - end - arel end @@ -905,9 +907,9 @@ WARNING where_values.reject! do |rel| case rel - when Arel::Nodes::In, Arel::Nodes::NotIn, Arel::Nodes::Equality, Arel::Nodes::NotEqual + when Arel::Nodes::Between, Arel::Nodes::In, Arel::Nodes::NotIn, Arel::Nodes::Equality, Arel::Nodes::NotEqual, Arel::Nodes::LessThanOrEqual, Arel::Nodes::GreaterThanOrEqual subrelation = (rel.left.kind_of?(Arel::Attributes::Attribute) ? rel.left : rel.right) - subrelation.name == target_value + subrelation.name.to_s == target_value end end @@ -945,24 +947,21 @@ WARNING when String, Array [@klass.send(:sanitize_sql, other.empty? ? opts : ([opts] + other))] when Hash - opts = PredicateBuilder.resolve_column_aliases(klass, opts) + opts = predicate_builder.resolve_column_aliases(opts) - bv_len = bind_values.length - tmp_opts, bind_values = create_binds(opts, bv_len) + tmp_opts, bind_values = create_binds(opts) self.bind_values += bind_values attributes = @klass.send(:expand_hash_conditions_for_aggregates, tmp_opts) - attributes.values.grep(ActiveRecord::Relation) do |rel| - self.bind_values += rel.bind_values - end + add_relations_to_bind_values(attributes) - PredicateBuilder.build_from_hash(klass, attributes, table) + predicate_builder.build_from_hash(attributes) else [opts] end end - def create_binds(opts, idx) + def create_binds(opts) bindable, non_binds = opts.partition do |column, value| case value when String, Integer, ActiveRecord::StatementCache::Substitute @@ -972,12 +971,23 @@ WARNING end end + association_binds, non_binds = non_binds.partition do |column, value| + value.is_a?(Hash) && association_for_table(column) + end + new_opts = {} binds = [] - bindable.each_with_index do |(column,value), index| + bindable.each do |(column,value)| binds.push [@klass.columns_hash[column.to_s], value] - new_opts[column] = connection.substitute_at(column, index + idx) + new_opts[column] = connection.substitute_at(column) + end + + association_binds.each do |(column, value)| + association_relation = association_for_table(column).klass.send(:relation) + association_new_opts, association_bind = association_relation.send(:create_binds, value) + new_opts[column] = association_new_opts + binds += association_bind end non_binds.each { |column,value| new_opts[column] = value } @@ -985,6 +995,12 @@ WARNING [new_opts, binds] end + def association_for_table(table_name) + table_name = table_name.to_s + @klass._reflect_on_association(table_name) || + @klass._reflect_on_association(table_name.singularize) + end + def build_from opts, name = from_value case opts @@ -1041,8 +1057,13 @@ WARNING def build_select(arel, selects) if !selects.empty? expanded_select = selects.map do |field| - columns_hash.key?(field.to_s) ? arel_table[field] : field + if (Symbol === field || String === field) && columns_hash.key?(field.to_s) + arel_table[field] + else + field + end end + arel.project(*expanded_select) else arel.project(@klass.arel_table[Arel.star]) @@ -1137,5 +1158,19 @@ WARNING raise ArgumentError, "The method .#{method_name}() must contain arguments." end end + + # This function is recursive just for better readablity. + # #where argument doesn't support more than one level nested hash in real world. + def add_relations_to_bind_values(attributes) + if attributes.is_a?(Hash) + attributes.each_value do |value| + if value.is_a?(ActiveRecord::Relation) + self.bind_values += value.bind_values + else + add_relations_to_bind_values(value) + end + end + end + end end end diff --git a/activerecord/lib/active_record/relation/spawn_methods.rb b/activerecord/lib/active_record/relation/spawn_methods.rb index 57d66bce4b..01bddea6c9 100644 --- a/activerecord/lib/active_record/relation/spawn_methods.rb +++ b/activerecord/lib/active_record/relation/spawn_methods.rb @@ -67,7 +67,7 @@ module ActiveRecord private def relation_with(values) # :nodoc: - result = Relation.create(klass, table, values) + result = Relation.create(klass, table, predicate_builder, values) result.extend(*extending_values) if extending_values.any? result end diff --git a/activerecord/lib/active_record/result.rb b/activerecord/lib/active_record/result.rb index 8c29c69a9b..3a3e65ef32 100644 --- a/activerecord/lib/active_record/result.rb +++ b/activerecord/lib/active_record/result.rb @@ -42,12 +42,8 @@ module ActiveRecord @column_types = column_types end - def identity_type # :nodoc: - IDENTITY_TYPE - end - - def column_type(name) - @column_types[name] || identity_type + def length + @rows.length end def each @@ -82,6 +78,15 @@ module ActiveRecord hash_rows.last end + def cast_values(type_overrides = {}) # :nodoc: + types = columns.map { |name| column_type(name, type_overrides) } + result = rows.map do |values| + types.zip(values).map { |type, value| type.type_cast_from_database(value) } + end + + columns.one? ? result.map!(&:first) : result + end + def initialize_copy(other) @columns = columns.dup @rows = rows.dup @@ -91,6 +96,12 @@ module ActiveRecord private + def column_type(name, type_overrides = {}) + type_overrides.fetch(name) do + column_types.fetch(name, IDENTITY_TYPE) + end + end + def hash_rows @hash_rows ||= begin diff --git a/activerecord/lib/active_record/sanitization.rb b/activerecord/lib/active_record/sanitization.rb index 1aa93ffbb3..f5aa60a69a 100644 --- a/activerecord/lib/active_record/sanitization.rb +++ b/activerecord/lib/active_record/sanitization.rb @@ -87,11 +87,15 @@ module ActiveRecord # { address: Address.new("123 abc st.", "chicago") } # # => "address_street='123 abc st.' and address_city='chicago'" def sanitize_sql_hash_for_conditions(attrs, default_table_name = self.table_name) - attrs = PredicateBuilder.resolve_column_aliases self, attrs + table = Arel::Table.new(table_name).alias(default_table_name) + predicate_builder = PredicateBuilder.new(TableMetadata.new(self, table)) + ActiveSupport::Deprecation.warn(<<-EOWARN) +sanitize_sql_hash_for_conditions is deprecated, and will be removed in Rails 5.0 + EOWARN + attrs = predicate_builder.resolve_column_aliases(attrs) attrs = expand_hash_conditions_for_aggregates(attrs) - table = Arel::Table.new(table_name, arel_engine).alias(default_table_name) - PredicateBuilder.build_from_hash(self, attrs, table).map { |b| + predicate_builder.build_from_hash(attrs).map { |b| connection.visitor.compile b }.join(' AND ') end @@ -103,11 +107,12 @@ module ActiveRecord def sanitize_sql_hash_for_assignment(attrs, table) c = connection attrs.map do |attr, value| - "#{c.quote_table_name_for_assignment(table, attr)} = #{quote_bound_value(value, c, columns_hash[attr.to_s])}" + value = type_for_attribute(attr.to_s).type_cast_for_database(value) + "#{c.quote_table_name_for_assignment(table, attr)} = #{c.quote(value)}" end.join(', ') end - # Sanitizes a +string+ so that it is safe to use within a sql + # Sanitizes a +string+ so that it is safe to use within an SQL # LIKE statement. This method uses +escape_character+ to escape all occurrences of "\", "_" and "%" def sanitize_sql_like(string, escape_character = "\\") pattern = Regexp.union(escape_character, "%", "_") @@ -134,7 +139,7 @@ module ActiveRecord raise_if_bind_arity_mismatch(statement, statement.count('?'), values.size) bound = values.dup c = connection - statement.gsub('?') do + statement.gsub(/\?/) do replace_bind_variable(bound.shift, c) end end @@ -159,10 +164,8 @@ module ActiveRecord end end - def quote_bound_value(value, c = connection, column = nil) #:nodoc: - if column - c.quote(value, column) - elsif value.respond_to?(:map) && !value.acts_like?(:string) + def quote_bound_value(value, c = connection) #:nodoc: + if value.respond_to?(:map) && !value.acts_like?(:string) if value.respond_to?(:empty?) && value.empty? c.quote(nil) else diff --git a/activerecord/lib/active_record/schema.rb b/activerecord/lib/active_record/schema.rb index 4bfd0167a4..0a5546a760 100644 --- a/activerecord/lib/active_record/schema.rb +++ b/activerecord/lib/active_record/schema.rb @@ -1,4 +1,3 @@ - module ActiveRecord # = Active Record Schema # diff --git a/activerecord/lib/active_record/schema_dumper.rb b/activerecord/lib/active_record/schema_dumper.rb index e055d571ab..2a570e1323 100644 --- a/activerecord/lib/active_record/schema_dumper.rb +++ b/activerecord/lib/active_record/schema_dumper.rb @@ -44,7 +44,6 @@ module ActiveRecord def initialize(connection, options = {}) @connection = connection - @types = @connection.native_database_types @version = Migrator::current_version rescue nil @options = options end @@ -91,16 +90,17 @@ HEADER end def tables(stream) - @connection.tables.sort.each do |tbl| - next if ['schema_migrations', ignore_tables].flatten.any? do |ignored| - case ignored - when String; remove_prefix_and_suffix(tbl) == ignored - when Regexp; remove_prefix_and_suffix(tbl) =~ ignored - else - raise StandardError, 'ActiveRecord::SchemaDumper.ignore_tables accepts an array of String and / or Regexp values.' - end + sorted_tables = @connection.tables.sort + + sorted_tables.each do |table_name| + table(table_name, stream) unless ignored?(table_name) + end + + # dump foreign keys at the end to make sure all dependent tables exist. + if @connection.supports_foreign_keys? + sorted_tables.each do |tbl| + foreign_keys(tbl, stream) unless ignored?(tbl) end - table(tbl, stream) end end @@ -110,32 +110,30 @@ HEADER tbl = StringIO.new # first dump primary key column - if @connection.respond_to?(:pk_and_sequence_for) - pk, _ = @connection.pk_and_sequence_for(table) - elsif @connection.respond_to?(:primary_key) - pk = @connection.primary_key(table) - end + pk = @connection.primary_key(table) tbl.print " create_table #{remove_prefix_and_suffix(table).inspect}" pkcol = columns.detect { |c| c.name == pk } if pkcol if pk != 'id' tbl.print %Q(, primary_key: "#{pk}") + elsif pkcol.sql_type == 'bigint' + tbl.print ", id: :bigserial" elsif pkcol.sql_type == 'uuid' tbl.print ", id: :uuid" - tbl.print %Q(, default: "#{pkcol.default_function}") if pkcol.default_function + tbl.print %Q(, default: #{pkcol.default_function.inspect}) end else tbl.print ", id: false" end - tbl.print ", force: true" + tbl.print ", force: :cascade" tbl.puts " do |t|" # then dump all non-primary key columns column_specs = columns.map do |column| raise StandardError, "Unknown type '#{column.sql_type}' for column '#{column.name}'" unless @connection.valid_type?(column.type) next if column.name == pk - @connection.column_spec(column, @types) + @connection.column_spec(column) end.compact # find all migration keys used in this table @@ -186,34 +184,67 @@ HEADER if (indexes = @connection.indexes(table)).any? add_index_statements = indexes.map do |index| statement_parts = [ - ('add_index ' + remove_prefix_and_suffix(index.table).inspect), + "add_index #{remove_prefix_and_suffix(index.table).inspect}", index.columns.inspect, - ('name: ' + index.name.inspect), + "name: #{index.name.inspect}", ] statement_parts << 'unique: true' if index.unique index_lengths = (index.lengths || []).compact - statement_parts << ('length: ' + Hash[index.columns.zip(index.lengths)].inspect) unless index_lengths.empty? + statement_parts << "length: #{Hash[index.columns.zip(index.lengths)].inspect}" if index_lengths.any? - index_orders = (index.orders || {}) - statement_parts << ('order: ' + index.orders.inspect) unless index_orders.empty? + index_orders = index.orders || {} + statement_parts << "order: #{index.orders.inspect}" if index_orders.any? + statement_parts << "where: #{index.where.inspect}" if index.where + statement_parts << "using: #{index.using.inspect}" if index.using + statement_parts << "type: #{index.type.inspect}" if index.type - statement_parts << ('where: ' + index.where.inspect) if index.where + " #{statement_parts.join(', ')}" + end - statement_parts << ('using: ' + index.using.inspect) if index.using + stream.puts add_index_statements.sort.join("\n") + stream.puts + end + end - statement_parts << ('type: ' + index.type.inspect) if index.type + def foreign_keys(table, stream) + if (foreign_keys = @connection.foreign_keys(table)).any? + add_foreign_key_statements = foreign_keys.map do |foreign_key| + parts = [ + "add_foreign_key #{remove_prefix_and_suffix(foreign_key.from_table).inspect}", + remove_prefix_and_suffix(foreign_key.to_table).inspect, + ] - ' ' + statement_parts.join(', ') + if foreign_key.column != @connection.foreign_key_column_for(foreign_key.to_table) + parts << "column: #{foreign_key.column.inspect}" + end + + if foreign_key.custom_primary_key? + parts << "primary_key: #{foreign_key.primary_key.inspect}" + end + + if foreign_key.name !~ /^fk_rails_[0-9a-f]{10}$/ + parts << "name: #{foreign_key.name.inspect}" + end + + parts << "on_update: #{foreign_key.on_update.inspect}" if foreign_key.on_update + parts << "on_delete: #{foreign_key.on_delete.inspect}" if foreign_key.on_delete + + " #{parts.join(', ')}" end - stream.puts add_index_statements.sort.join("\n") - stream.puts + stream.puts add_foreign_key_statements.sort.join("\n") end end def remove_prefix_and_suffix(table) table.gsub(/^(#{@options[:table_name_prefix]})(.+)(#{@options[:table_name_suffix]})$/, "\\2") end + + def ignored?(table_name) + ['schema_migrations', ignore_tables].flatten.any? do |ignored| + ignored === remove_prefix_and_suffix(table_name) + end + end end end diff --git a/activerecord/lib/active_record/schema_migration.rb b/activerecord/lib/active_record/schema_migration.rb index 3a004d58c9..b5038104ac 100644 --- a/activerecord/lib/active_record/schema_migration.rb +++ b/activerecord/lib/active_record/schema_migration.rb @@ -43,6 +43,10 @@ module ActiveRecord def normalize_migration_number(number) "%.3d" % number.to_i end + + def normalized_versions + pluck(:version).map { |v| normalize_migration_number v } + end end def version diff --git a/activerecord/lib/active_record/scoping/named.rb b/activerecord/lib/active_record/scoping/named.rb index 49cadb66d0..35420e6551 100644 --- a/activerecord/lib/active_record/scoping/named.rb +++ b/activerecord/lib/active_record/scoping/named.rb @@ -139,6 +139,10 @@ module ActiveRecord # Article.published.featured.latest_article # Article.featured.titles def scope(name, body, &block) + unless body.respond_to?(:call) + raise ArgumentError, 'The scope body needs to be callable.' + end + if dangerous_class_method?(name) raise ArgumentError, "You tried to define a scope named \"#{name}\" " \ "on the model \"#{self.name}\", but Active Record already defined " \ diff --git a/activerecord/lib/active_record/serialization.rb b/activerecord/lib/active_record/serialization.rb index bd9079b596..48c12dcf9f 100644 --- a/activerecord/lib/active_record/serialization.rb +++ b/activerecord/lib/active_record/serialization.rb @@ -11,7 +11,7 @@ module ActiveRecord #:nodoc: def serializable_hash(options = nil) options = options.try(:clone) || {} - options[:except] = Array(options[:except]).map { |n| n.to_s } + options[:except] = Array(options[:except]).map(&:to_s) options[:except] |= Array(self.class.inheritance_column) super(options) diff --git a/activerecord/lib/active_record/statement_cache.rb b/activerecord/lib/active_record/statement_cache.rb index aece446384..192a19f05d 100644 --- a/activerecord/lib/active_record/statement_cache.rb +++ b/activerecord/lib/active_record/statement_cache.rb @@ -1,22 +1,33 @@ module ActiveRecord # Statement cache is used to cache a single statement in order to avoid creating the AST again. - # Initializing the cache is done by passing the statement in the initialization block: + # Initializing the cache is done by passing the statement in the create block: # - # cache = ActiveRecord::StatementCache.new do - # Book.where(name: "my book").limit(100) + # cache = StatementCache.create(Book.connection) do |params| + # Book.where(name: "my book").where("author_id > 3") # end # # The cached statement is executed by using the +execute+ method: # - # cache.execute + # cache.execute([], Book, Book.connection) # # The relation returned by the block is cached, and for each +execute+ call the cached relation gets duped. # Database is queried when +to_a+ is called on the relation. - class StatementCache - class Substitute; end + # + # If you want to cache the statement without the values you can use the +bind+ method of the + # block parameter. + # + # cache = StatementCache.create(Book.connection) do |params| + # Book.where(name: params.bind) + # end + # + # And pass the bind values as the first argument of +execute+ call. + # + # cache.execute(["my book"], Book, Book.connection) + class StatementCache # :nodoc: + class Substitute; end # :nodoc: - class Query + class Query # :nodoc: def initialize(sql) @sql = sql end @@ -26,7 +37,7 @@ module ActiveRecord end end - class PartialQuery < Query + class PartialQuery < Query # :nodoc: def initialize values @values = values @indexes = values.each_with_index.find_all { |thing,i| @@ -51,11 +62,11 @@ module ActiveRecord PartialQuery.new collected end - class Params + class Params # :nodoc: def bind; Substitute.new; end end - class BindMap + class BindMap # :nodoc: def initialize(bind_values) @indexes = [] @bind_values = bind_values @@ -68,7 +79,7 @@ module ActiveRecord end def bind(values) - bvs = @bind_values.map { |pair| pair.dup } + bvs = @bind_values.map(&:dup) @indexes.each_with_index { |offset,i| bvs[offset][1] = values[i] } bvs end diff --git a/activerecord/lib/active_record/store.rb b/activerecord/lib/active_record/store.rb index 7014bc6d45..919bc58ba5 100644 --- a/activerecord/lib/active_record/store.rb +++ b/activerecord/lib/active_record/store.rb @@ -15,11 +15,15 @@ module ActiveRecord # You can set custom coder to encode/decode your serialized attributes to/from different formats. # JSON, YAML, Marshal are supported out of the box. Generally it can be any wrapper that provides +load+ and +dump+. # - # NOTE - If you are using PostgreSQL specific columns like +hstore+ or +json+ there is no need for + # NOTE: If you are using PostgreSQL specific columns like +hstore+ or +json+ there is no need for # the serialization provided by +store+. Simply use +store_accessor+ instead to generate # the accessor methods. Be aware that these columns use a string keyed hash and do not allow access # using a symbol. # + # NOTE: The default validations with the exception of +uniqueness+ will work. + # For example, if you want to check for +uniqueness+ with +hstore+ you will + # need to use a custom validation to handle it. + # # Examples: # # class User < ActiveRecord::Base @@ -99,7 +103,7 @@ module ActiveRecord self.local_stored_attributes[store_attribute] |= keys end - def _store_accessors_module + def _store_accessors_module # :nodoc: @_store_accessors_module ||= begin mod = Module.new include mod @@ -129,10 +133,10 @@ module ActiveRecord private def store_accessor_for(store_attribute) - @column_types[store_attribute.to_s].accessor + type_for_attribute(store_attribute.to_s).accessor end - class HashAccessor + class HashAccessor # :nodoc: def self.read(object, attribute, key) prepare(object, attribute) object.public_send(attribute)[key] @@ -151,7 +155,7 @@ module ActiveRecord end end - class StringKeyedHashAccessor < HashAccessor + class StringKeyedHashAccessor < HashAccessor # :nodoc: def self.read(object, attribute, key) super object, attribute, key.to_s end @@ -161,7 +165,7 @@ module ActiveRecord end end - class IndifferentHashAccessor < ActiveRecord::Store::HashAccessor + class IndifferentHashAccessor < ActiveRecord::Store::HashAccessor # :nodoc: def self.prepare(object, store_attribute) attribute = object.send(store_attribute) unless attribute.is_a?(ActiveSupport::HashWithIndifferentAccess) diff --git a/activerecord/lib/active_record/table_metadata.rb b/activerecord/lib/active_record/table_metadata.rb new file mode 100644 index 0000000000..11e33e8dfe --- /dev/null +++ b/activerecord/lib/active_record/table_metadata.rb @@ -0,0 +1,53 @@ +module ActiveRecord + class TableMetadata # :nodoc: + delegate :foreign_type, :foreign_key, to: :association, prefix: true + + def initialize(klass, arel_table, association = nil) + @klass = klass + @arel_table = arel_table + @association = association + end + + def resolve_column_aliases(hash) + hash = hash.dup + hash.keys.grep(Symbol) do |key| + if klass.attribute_alias? key + hash[klass.attribute_alias(key)] = hash.delete key + end + end + hash + end + + def arel_attribute(column_name) + arel_table[column_name] + end + + def associated_with?(association_name) + klass && klass._reflect_on_association(association_name) + end + + def associated_table(table_name) + return self if table_name == arel_table.name + + association = klass._reflect_on_association(table_name) + if association && !association.polymorphic? + association_klass = association.klass + arel_table = association_klass.arel_table + else + type_caster = TypeCaster::Connection.new(klass.connection, table_name) + association_klass = nil + arel_table = Arel::Table.new(table_name, type_caster: type_caster) + end + + TableMetadata.new(association_klass, arel_table, association) + end + + def polymorphic_association? + association && association.polymorphic? + end + + protected + + attr_reader :klass, :arel_table, :association + end +end diff --git a/activerecord/lib/active_record/tasks/database_tasks.rb b/activerecord/lib/active_record/tasks/database_tasks.rb index 649f316ed8..69aceb66b1 100644 --- a/activerecord/lib/active_record/tasks/database_tasks.rb +++ b/activerecord/lib/active_record/tasks/database_tasks.rb @@ -1,3 +1,5 @@ +require 'active_support/core_ext/string/filters' + module ActiveRecord module Tasks # :nodoc: class DatabaseAlreadyExists < StandardError; end # :nodoc: @@ -58,7 +60,11 @@ module ActiveRecord end def fixtures_path - @fixtures_path ||= File.join(root, 'test', 'fixtures') + @fixtures_path ||= if ENV['FIXTURES_PATH'] + File.join(root, ENV['FIXTURES_PATH']) + else + File.join(root, 'test', 'fixtures') + end end def root @@ -106,6 +112,8 @@ module ActiveRecord def drop(*arguments) configuration = arguments.first class_for_adapter(configuration['adapter']).new(*arguments).drop + rescue ActiveRecord::NoDatabaseError + $stderr.puts "Database '#{configuration['database']}' does not exist" rescue Exception => error $stderr.puts error, *(error.backtrace) $stderr.puts "Couldn't drop #{configuration['database']}" @@ -121,6 +129,18 @@ module ActiveRecord } end + def migrate + verbose = ENV["VERBOSE"] ? ENV["VERBOSE"] == "true" : true + version = ENV["VERSION"] ? ENV["VERSION"].to_i : nil + scope = ENV['SCOPE'] + verbose_was, Migration.verbose = Migration.verbose, verbose + Migrator.migrate(Migrator.migrations_paths, version) do |migration| + scope.blank? || scope == migration.scope + end + ensure + Migration.verbose = verbose_was + end + def charset_current(environment = env) charset ActiveRecord::Base.configurations[environment] end @@ -143,6 +163,19 @@ module ActiveRecord class_for_adapter(configuration['adapter']).new(configuration).purge end + def purge_all + each_local_configuration { |configuration| + purge configuration + } + end + + def purge_current(environment = env) + each_current_configuration(environment) { |configuration| + purge configuration + } + ActiveRecord::Base.establish_connection(environment.to_sym) + end + def structure_dump(*arguments) configuration = arguments.first filename = arguments.delete_at 1 @@ -155,23 +188,52 @@ module ActiveRecord class_for_adapter(configuration['adapter']).new(*arguments).structure_load(filename) end - def load_schema(format = ActiveRecord::Base.schema_format, file = nil) + def load_schema(configuration, format = ActiveRecord::Base.schema_format, file = nil) # :nodoc: + file ||= schema_file(format) + case format when :ruby - file ||= File.join(db_dir, "schema.rb") check_schema_file(file) - purge(current_config) + ActiveRecord::Base.establish_connection(configuration) load(file) when :sql - file ||= File.join(db_dir, "structure.sql") check_schema_file(file) - purge(current_config) - structure_load(current_config, file) + structure_load(configuration, file) else raise ArgumentError, "unknown format #{format.inspect}" end end + def load_schema_for(*args) + ActiveSupport::Deprecation.warn(<<-MSG.squish) + This method was renamed to `#load_schema` and will be removed in the future. + Use `#load_schema` instead. + MSG + load_schema(*args) + end + + def schema_file(format = ActiveSupport::Base.schema_format) + case format + when :ruby + File.join(db_dir, "schema.rb") + when :sql + File.join(db_dir, "structure.sql") + end + end + + def load_schema_current_if_exists(format = ActiveRecord::Base.schema_format, file = nil, environment = env) + if File.exist?(file || schema_file(format)) + load_schema_current(format, file, environment) + end + end + + def load_schema_current(format = ActiveRecord::Base.schema_format, file = nil, environment = env) + each_current_configuration(environment) { |configuration| + load_schema configuration, format, file + } + ActiveRecord::Base.establish_connection(environment.to_sym) + end + 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.} diff --git a/activerecord/lib/active_record/tasks/mysql_database_tasks.rb b/activerecord/lib/active_record/tasks/mysql_database_tasks.rb index c755831e6d..eafbb2c249 100644 --- a/activerecord/lib/active_record/tasks/mysql_database_tasks.rb +++ b/activerecord/lib/active_record/tasks/mysql_database_tasks.rb @@ -31,6 +31,7 @@ module ActiveRecord end establish_connection configuration else + $stderr.puts error.inspect $stderr.puts "Couldn't create database for #{configuration.inspect}, #{creation_options.inspect}" $stderr.puts "(If you set the charset manually, make sure you have a matching collation)" if configuration['encoding'] end @@ -42,7 +43,7 @@ module ActiveRecord end def purge - establish_connection :test + establish_connection configuration connection.recreate_database configuration['database'], creation_options end @@ -124,7 +125,7 @@ IDENTIFIED BY '#{configuration['password']}' WITH GRANT OPTION; end def root_password - $stdout.print "Please provide the root password for your mysql installation\n>" + $stdout.print "Please provide the root password for your MySQL installation\n>" $stdin.gets.strip end diff --git a/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb b/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb index 3d02ee07d0..ce1de4b76e 100644 --- a/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb +++ b/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb @@ -54,7 +54,7 @@ module ActiveRecord command = "pg_dump -i -s -x -O -f #{Shellwords.escape(filename)} #{search_path} #{Shellwords.escape(configuration['database'])}" raise 'Error dumping database' unless Kernel.system(command) - File.open(filename, "a") { |f| f << "SET search_path TO #{ActiveRecord::Base.connection.schema_search_path};\n\n" } + File.open(filename, "a") { |f| f << "SET search_path TO #{connection.schema_search_path};\n\n" } end def structure_load(filename) diff --git a/activerecord/lib/active_record/timestamp.rb b/activerecord/lib/active_record/timestamp.rb index e2e37e7c00..20e4235788 100644 --- a/activerecord/lib/active_record/timestamp.rb +++ b/activerecord/lib/active_record/timestamp.rb @@ -1,4 +1,3 @@ - module ActiveRecord # = Active Record Timestamp # @@ -48,8 +47,9 @@ module ActiveRecord current_time = current_time_from_proper_timezone all_timestamp_attributes.each do |column| - if respond_to?(column) && respond_to?("#{column}=") && self.send(column).nil? - write_attribute(column.to_s, current_time) + column = column.to_s + if has_attribute?(column) && !attribute_present?(column) + write_attribute(column, current_time) end end end @@ -57,8 +57,8 @@ module ActiveRecord super end - def _update_record(*args) - if should_record_timestamps? + def _update_record(*args, touch: true, **options) + if touch && should_record_timestamps? current_time = current_time_from_proper_timezone timestamp_attributes_for_update_in_model.each do |column| @@ -67,7 +67,7 @@ module ActiveRecord write_attribute(column, current_time) end end - super + super(*args) end def should_record_timestamps? @@ -114,7 +114,7 @@ module ActiveRecord def clear_timestamp_attributes all_timestamp_attributes_in_model.each do |attribute_name| self[attribute_name] = nil - changed_attributes.delete(attribute_name) + clear_attribute_changes([attribute_name]) end end end diff --git a/activerecord/lib/active_record/transactions.rb b/activerecord/lib/active_record/transactions.rb index 7e4dc4c895..de701edca0 100644 --- a/activerecord/lib/active_record/transactions.rb +++ b/activerecord/lib/active_record/transactions.rb @@ -2,12 +2,26 @@ module ActiveRecord # See ActiveRecord::Transactions::ClassMethods for documentation. module Transactions extend ActiveSupport::Concern + #:nodoc: ACTIONS = [:create, :destroy, :update] + #:nodoc: + CALLBACK_WARN_MESSAGE = "Currently, Active Record suppresses errors raised " \ + "within `after_rollback`/`after_commit` callbacks and only print them to " \ + "the logs. In the next version, these errors will no longer be suppressed. " \ + "Instead, the errors will propagate normally just like in other Active " \ + "Record callbacks.\n" \ + "\n" \ + "You can opt into the new behavior and remove this warning by setting:\n" \ + "\n" \ + " config.active_record.raise_in_transactional_callbacks = true\n\n" included do define_callbacks :commit, :rollback, terminator: ->(_, result) { result == false }, scope: [:kind, :name] + + mattr_accessor :raise_in_transactional_callbacks, instance_writer: false + self.raise_in_transactional_callbacks = false end # = Active Record Transactions @@ -223,6 +237,9 @@ module ActiveRecord def after_commit(*args, &block) set_options_for_callbacks!(args) set_callback(:commit, :after, *args, &block) + unless ActiveRecord::Base.raise_in_transactional_callbacks + ActiveSupport::Deprecation.warn(CALLBACK_WARN_MESSAGE) + end end # This callback is called after a create, update, or destroy are rolled back. @@ -231,6 +248,9 @@ module ActiveRecord def after_rollback(*args, &block) set_options_for_callbacks!(args) set_callback(:rollback, :after, *args, &block) + unless ActiveRecord::Base.raise_in_transactional_callbacks + ActiveSupport::Deprecation.warn(CALLBACK_WARN_MESSAGE) + end end private @@ -247,7 +267,7 @@ module ActiveRecord def assert_valid_transaction_action(actions) if (actions - ACTIONS).any? - raise ArgumentError, ":on conditions for after_commit and after_rollback callbacks have to be one of #{ACTIONS.join(",")}" + raise ArgumentError, ":on conditions for after_commit and after_rollback callbacks have to be one of #{ACTIONS}" end end end @@ -290,16 +310,16 @@ module ActiveRecord # # Ensure that it is not called if the object was never persisted (failed create), # but call it after the commit of a destroyed object. - def committed! #:nodoc: - run_callbacks :commit if destroyed? || persisted? + def committed!(should_run_callbacks = true) #:nodoc: + _run_commit_callbacks if should_run_callbacks && destroyed? || persisted? ensure force_clear_transaction_record_state end # Call the +after_rollback+ callbacks. The +force_restore_state+ argument indicates if the record # state should be rolled back to the beginning or just to the last savepoint. - def rolledback!(force_restore_state = false) #:nodoc: - run_callbacks :rollback + def rolledback!(force_restore_state = false, should_run_callbacks = true) #:nodoc: + _run_rollback_callbacks if should_run_callbacks ensure restore_transaction_record_state(force_restore_state) clear_transaction_record_state @@ -340,14 +360,12 @@ module ActiveRecord # Save the new record state and id of a record so it can be restored later if a transaction fails. def remember_transaction_record_state #:nodoc: @_start_transaction_state[:id] = id - unless @_start_transaction_state.include?(:new_record) - @_start_transaction_state[:new_record] = @new_record - end - unless @_start_transaction_state.include?(:destroyed) - @_start_transaction_state[:destroyed] = @destroyed - end + @_start_transaction_state.reverse_merge!( + new_record: @new_record, + destroyed: @destroyed, + frozen?: frozen?, + ) @_start_transaction_state[:level] = (@_start_transaction_state[:level] || 0) + 1 - @_start_transaction_state[:frozen?] = frozen? end # Clear the new record state and id of a record. diff --git a/activerecord/lib/active_record/type.rb b/activerecord/lib/active_record/type.rb index f1384e0bb2..250e8d5b23 100644 --- a/activerecord/lib/active_record/type.rb +++ b/activerecord/lib/active_record/type.rb @@ -1,8 +1,10 @@ +require 'active_record/type/decorator' require 'active_record/type/mutable' require 'active_record/type/numeric' require 'active_record/type/time_value' require 'active_record/type/value' +require 'active_record/type/big_integer' require 'active_record/type/binary' require 'active_record/type/boolean' require 'active_record/type/date' @@ -15,6 +17,7 @@ require 'active_record/type/serialized' require 'active_record/type/string' require 'active_record/type/text' require 'active_record/type/time' +require 'active_record/type/unsigned_integer' require 'active_record/type/type_map' require 'active_record/type/hash_lookup_type_map' diff --git a/activerecord/lib/active_record/type/big_integer.rb b/activerecord/lib/active_record/type/big_integer.rb new file mode 100644 index 0000000000..0c72d8914f --- /dev/null +++ b/activerecord/lib/active_record/type/big_integer.rb @@ -0,0 +1,13 @@ +require 'active_record/type/integer' + +module ActiveRecord + module Type + class BigInteger < Integer # :nodoc: + private + + def max_value + ::Float::INFINITY + end + end + end +end diff --git a/activerecord/lib/active_record/type/binary.rb b/activerecord/lib/active_record/type/binary.rb index 3bf29b5026..005a48ef0d 100644 --- a/activerecord/lib/active_record/type/binary.rb +++ b/activerecord/lib/active_record/type/binary.rb @@ -22,18 +22,28 @@ module ActiveRecord Data.new(super) end - class Data + def changed_in_place?(raw_old_value, value) + old_value = type_cast_from_database(raw_old_value) + old_value != value + end + + class Data # :nodoc: def initialize(value) - @value = value + @value = value.to_s end def to_s @value end + alias_method :to_str, :to_s def hex @value.unpack('H*')[0] end + + def ==(other) + other == to_s || super + end end end end diff --git a/activerecord/lib/active_record/type/boolean.rb b/activerecord/lib/active_record/type/boolean.rb index 06dd17ed28..978d16d524 100644 --- a/activerecord/lib/active_record/type/boolean.rb +++ b/activerecord/lib/active_record/type/boolean.rb @@ -10,8 +10,19 @@ module ActiveRecord def cast_value(value) if value == '' nil + elsif ConnectionAdapters::Column::TRUE_VALUES.include?(value) + true else - ConnectionAdapters::Column::TRUE_VALUES.include?(value) + if !ConnectionAdapters::Column::FALSE_VALUES.include?(value) + ActiveSupport::Deprecation.warn(<<-MSG.squish) + You attempted to assign a value which is not explicitly `true` or `false` + to a boolean column. Currently this value casts to `false`. This will + change to match Ruby's semantics, and will cast to `true` in Rails 5. + If you would like to maintain the current behavior, you should + explicitly handle the values you would like cast to `false`. + MSG + end + false end end end diff --git a/activerecord/lib/active_record/type/date_time.rb b/activerecord/lib/active_record/type/date_time.rb index 560d63c101..5f19608a33 100644 --- a/activerecord/lib/active_record/type/date_time.rb +++ b/activerecord/lib/active_record/type/date_time.rb @@ -7,6 +7,16 @@ module ActiveRecord :datetime end + def type_cast_for_database(value) + zone_conversion_method = ActiveRecord::Base.default_timezone == :utc ? :getutc : :getlocal + + if value.acts_like?(:time) + value.send(zone_conversion_method) + else + super + end + end + private def cast_value(string) diff --git a/activerecord/lib/active_record/type/decimal.rb b/activerecord/lib/active_record/type/decimal.rb index a9db51c6ba..7b2bee2c42 100644 --- a/activerecord/lib/active_record/type/decimal.rb +++ b/activerecord/lib/active_record/type/decimal.rb @@ -14,10 +14,33 @@ module ActiveRecord private def cast_value(value) - if value.respond_to?(:to_d) + case value + when ::Float + convert_float_to_big_decimal(value) + when ::Numeric, ::String + BigDecimal(value, precision.to_i) + else + if value.respond_to?(:to_d) + value.to_d + else + cast_value(value.to_s) + end + end + end + + def convert_float_to_big_decimal(value) + if precision + BigDecimal(value, float_precision) + else value.to_d + end + end + + def float_precision + if precision.to_i > ::Float::DIG + 1 + ::Float::DIG + 1 else - value.to_s.to_d + precision.to_i end end end diff --git a/activerecord/lib/active_record/type/decimal_without_scale.rb b/activerecord/lib/active_record/type/decimal_without_scale.rb index cabdcecdd7..ff5559e300 100644 --- a/activerecord/lib/active_record/type/decimal_without_scale.rb +++ b/activerecord/lib/active_record/type/decimal_without_scale.rb @@ -1,8 +1,8 @@ -require 'active_record/type/integer' +require 'active_record/type/big_integer' module ActiveRecord module Type - class DecimalWithoutScale < Integer # :nodoc: + class DecimalWithoutScale < BigInteger # :nodoc: def type :decimal end diff --git a/activerecord/lib/active_record/type/decorator.rb b/activerecord/lib/active_record/type/decorator.rb new file mode 100644 index 0000000000..9fce38ea44 --- /dev/null +++ b/activerecord/lib/active_record/type/decorator.rb @@ -0,0 +1,14 @@ +module ActiveRecord + module Type + module Decorator # :nodoc: + def init_with(coder) + @subtype = coder['subtype'] + __setobj__(@subtype) + end + + def encode_with(coder) + coder['subtype'] = __getobj__ + end + end + end +end diff --git a/activerecord/lib/active_record/type/hash_lookup_type_map.rb b/activerecord/lib/active_record/type/hash_lookup_type_map.rb index bf92680268..82d9327fc0 100644 --- a/activerecord/lib/active_record/type/hash_lookup_type_map.rb +++ b/activerecord/lib/active_record/type/hash_lookup_type_map.rb @@ -3,16 +3,14 @@ module ActiveRecord class HashLookupTypeMap < TypeMap # :nodoc: delegate :key?, to: :@mapping - def lookup(type, *args) - @mapping.fetch(type, proc { default_value }).call(type, *args) + def alias_type(type, alias_type) + register_type(type) { |_, *args| lookup(alias_type, *args) } end - def fetch(type, *args, &block) - @mapping.fetch(type, block).call(type, *args) - end + private - def alias_type(type, alias_type) - register_type(type) { |_, *args| lookup(alias_type, *args) } + def perform_fetch(type, *args, &block) + @mapping.fetch(type, block).call(type, *args) end end end diff --git a/activerecord/lib/active_record/type/integer.rb b/activerecord/lib/active_record/type/integer.rb index 08477d1303..fc260a081a 100644 --- a/activerecord/lib/active_record/type/integer.rb +++ b/activerecord/lib/active_record/type/integer.rb @@ -3,21 +3,53 @@ module ActiveRecord class Integer < Value # :nodoc: include Numeric + def initialize(*) + super + @range = min_value...max_value + end + def type :integer end alias type_cast_for_database type_cast + def type_cast_from_database(value) + return if value.nil? + value.to_i + end + + protected + + attr_reader :range + private def cast_value(value) case value when true then 1 when false then 0 - else value.to_i rescue nil + else + result = value.to_i rescue nil + ensure_in_range(result) if result + result end end + + def ensure_in_range(value) + unless range.cover?(value) + raise RangeError, "#{value} is out of range for #{self.class} with limit #{limit || 4}" + end + end + + def max_value + limit = self.limit || 4 + 1 << (limit * 8 - 1) # 8 bits per byte with one bit for sign + end + + def min_value + -max_value + end end end end diff --git a/activerecord/lib/active_record/type/mutable.rb b/activerecord/lib/active_record/type/mutable.rb index 64cf4b9b93..066617ea59 100644 --- a/activerecord/lib/active_record/type/mutable.rb +++ b/activerecord/lib/active_record/type/mutable.rb @@ -1,6 +1,6 @@ module ActiveRecord module Type - module Mutable + module Mutable # :nodoc: def type_cast_from_user(value) type_cast_from_database(type_cast_for_database(value)) end @@ -8,7 +8,7 @@ module ActiveRecord # +raw_old_value+ will be the `_before_type_cast` version of the # value (likely a string). +new_value+ will be the current, type # cast value. - def changed_in_place?(raw_old_value, new_value) # :nodoc: + def changed_in_place?(raw_old_value, new_value) raw_old_value != type_cast_for_database(new_value) end end diff --git a/activerecord/lib/active_record/type/numeric.rb b/activerecord/lib/active_record/type/numeric.rb index 137c9e4c99..674f996f38 100644 --- a/activerecord/lib/active_record/type/numeric.rb +++ b/activerecord/lib/active_record/type/numeric.rb @@ -16,26 +16,20 @@ module ActiveRecord end def changed?(old_value, _new_value, new_value_before_type_cast) # :nodoc: - # 0 => 'wibble' should mark as changed so numericality validations run - if nil_or_zero?(old_value) && non_numeric_string?(new_value_before_type_cast) - # nil => '' should not mark as changed - old_value != new_value_before_type_cast.presence - else - super - end + super || number_to_non_number?(old_value, new_value_before_type_cast) end private + def number_to_non_number?(old_value, new_value_before_type_cast) + old_value != nil && non_numeric_string?(new_value_before_type_cast) + end + def non_numeric_string?(value) # 'wibble'.to_i will give zero, we want to make sure # that we aren't marking int zero to string zero as # changed. - value !~ /\A\d+\.?\d*\z/ - end - - def nil_or_zero?(value) - value.nil? || value == 0 + value.to_s !~ /\A-?\d+\.?\d*\z/ end end end diff --git a/activerecord/lib/active_record/type/serialized.rb b/activerecord/lib/active_record/type/serialized.rb index 42bbed7103..3cac03464e 100644 --- a/activerecord/lib/active_record/type/serialized.rb +++ b/activerecord/lib/active_record/type/serialized.rb @@ -1,7 +1,8 @@ module ActiveRecord module Type - class Serialized < SimpleDelegator # :nodoc: + class Serialized < DelegateClass(Type::Value) # :nodoc: include Mutable + include Decorator attr_reader :subtype, :coder @@ -12,7 +13,7 @@ module ActiveRecord end def type_cast_from_database(value) - if is_default_value?(value) + if default_value?(value) value else coder.load(super) @@ -21,29 +22,33 @@ module ActiveRecord def type_cast_for_database(value) return if value.nil? - unless is_default_value?(value) + unless default_value?(value) super coder.dump(value) end end + def changed_in_place?(raw_old_value, value) + return false if value.nil? + subtype.changed_in_place?(raw_old_value, type_cast_for_database(value)) + end + def accessor ActiveRecord::Store::IndifferentHashAccessor end def init_with(coder) - @subtype = coder['subtype'] @coder = coder['coder'] - __setobj__(@subtype) + super end def encode_with(coder) - coder['subtype'] = @subtype coder['coder'] = @coder + super end private - def is_default_value?(value) + def default_value?(value) value == coder.load(nil) end end diff --git a/activerecord/lib/active_record/type/string.rb b/activerecord/lib/active_record/type/string.rb index 3b1554bd5a..cf95e25be0 100644 --- a/activerecord/lib/active_record/type/string.rb +++ b/activerecord/lib/active_record/type/string.rb @@ -5,6 +5,22 @@ module ActiveRecord :string end + def changed_in_place?(raw_old_value, new_value) + if new_value.is_a?(::String) + raw_old_value != new_value + end + end + + def type_cast_for_database(value) + case value + when ::Numeric, ActiveSupport::Duration then value.to_s + when ::String then ::String.new(value) + when true then "t" + when false then "f" + else super + end + end + def text? true end @@ -13,9 +29,10 @@ module ActiveRecord def cast_value(value) case value - when true then "1" - when false then "0" - else value.to_s + when true then "t" + when false then "f" + # String.new is slightly faster than dup + else ::String.new(value.to_s) end end end diff --git a/activerecord/lib/active_record/type/type_map.rb b/activerecord/lib/active_record/type/type_map.rb index 88c5f9c497..09f5ba6b74 100644 --- a/activerecord/lib/active_record/type/type_map.rb +++ b/activerecord/lib/active_record/type/type_map.rb @@ -1,24 +1,28 @@ +require 'thread_safe' + module ActiveRecord module Type class TypeMap # :nodoc: def initialize @mapping = {} + @cache = ThreadSafe::Cache.new do |h, key| + h.fetch_or_store(key, ThreadSafe::Cache.new) + end end def lookup(lookup_key, *args) - matching_pair = @mapping.reverse_each.detect do |key, _| - key === lookup_key - end + fetch(lookup_key, *args) { default_value } + end - if matching_pair - matching_pair.last.call(lookup_key, *args) - else - default_value + def fetch(lookup_key, *args, &block) + @cache[lookup_key].fetch_or_store(args) do + perform_fetch(lookup_key, *args, &block) end end def register_type(key, value = nil, &block) raise ::ArgumentError unless value || block + @cache.clear if block @mapping[key] = block @@ -40,6 +44,18 @@ module ActiveRecord private + def perform_fetch(lookup_key, *args) + matching_pair = @mapping.reverse_each.detect do |key, _| + key === lookup_key + end + + if matching_pair + matching_pair.last.call(lookup_key, *args) + else + yield lookup_key, *args + end + end + def default_value @default_value ||= Value.new end diff --git a/activerecord/lib/active_record/type/unsigned_integer.rb b/activerecord/lib/active_record/type/unsigned_integer.rb new file mode 100644 index 0000000000..ed3e527483 --- /dev/null +++ b/activerecord/lib/active_record/type/unsigned_integer.rb @@ -0,0 +1,15 @@ +module ActiveRecord + module Type + class UnsignedInteger < Integer # :nodoc: + private + + def max_value + super * 2 + end + + def min_value + 0 + end + end + end +end diff --git a/activerecord/lib/active_record/type/value.rb b/activerecord/lib/active_record/type/value.rb index 875fb98c4b..60ae47db3d 100644 --- a/activerecord/lib/active_record/type/value.rb +++ b/activerecord/lib/active_record/type/value.rb @@ -3,8 +3,8 @@ module ActiveRecord class Value # :nodoc: attr_reader :precision, :scale, :limit - # Valid options are +precision+, +scale+, and +limit+. - # They are only used when dumping schema. + # Valid options are +precision+, +scale+, and +limit+. They are only + # used when dumping schema. def initialize(options = {}) options.assert_valid_keys(:precision, :scale, :limit) @precision = options[:precision] @@ -12,65 +12,92 @@ module ActiveRecord @limit = options[:limit] end - # The simplified type that this object represents. Subclasses - # must override this method. + # The simplified type that this object represents. Returns a symbol such + # as +:string+ or +:integer+ def type; end + # Type casts a string from the database into the appropriate ruby type. + # Classes which do not need separate type casting behavior for database + # and user provided values should override +cast_value+ instead. def type_cast_from_database(value) type_cast(value) end + # Type casts a value from user input (e.g. from a setter). This value may + # be a string from the form builder, or an already type cast value + # provided manually to a setter. + # + # Classes which do not need separate type casting behavior for database + # and user provided values should override +type_cast+ or +cast_value+ + # instead. def type_cast_from_user(value) type_cast(value) end + # Cast a value from the ruby type to a type that the database knows how + # to understand. The returned value from this method should be a + # +String+, +Numeric+, +Date+, +Time+, +Symbol+, +true+, +false+, or + # +nil+ def type_cast_for_database(value) value end - def type_cast_for_schema(value) + # Type cast a value for schema dumping. This method is private, as we are + # hoping to remove it entirely. + def type_cast_for_schema(value) # :nodoc: value.inspect end - def text? + # These predicates are not documented, as I need to look further into + # their use, and see if they can be removed entirely. + def text? # :nodoc: false end - def number? + def number? # :nodoc: false end - def binary? + def binary? # :nodoc: false end def klass # :nodoc: end - # +old_value+ will always be type-cast. - # +new_value+ will come straight from the database - # or from assignment, so it could be anything. Types - # which cannot typecast arbitrary values should override - # this method. - def changed?(old_value, new_value, _new_value_before_type_cast) # :nodoc: + # Determines whether a value has changed for dirty checking. +old_value+ + # and +new_value+ will always be type-cast. Types should not need to + # override this method. + def changed?(old_value, new_value, _new_value_before_type_cast) old_value != new_value end - def changed_in_place?(*) # :nodoc: + # Determines whether the mutable value has been modified since it was + # read. Returns +false+ by default. This method should not be overridden + # directly. Types which return a mutable value should include + # +Type::Mutable+, which will define this method. + def changed_in_place?(*) false end + def ==(other) + self.class == other.class && + precision == other.precision && + scale == other.scale && + limit == other.limit + end + private - # Takes an input from the database, or from attribute setters, - # and casts it to a type appropriate for this object. This method - # should not be overriden by subclasses. Instead, override `cast_value`. - def type_cast(value) # :api: public + + def type_cast(value) cast_value(value) unless value.nil? end - # Responsible for casting values from external sources to the appropriate - # type. Called by `type_cast` for all values except `nil`. - def cast_value(value) # :api: public + # Convenience method for types which do not need separate type casting + # behavior for user and database inputs. Called by + # +type_cast_from_database+ and +type_cast_from_user+ for all values + # except +nil+. + def cast_value(value) # :doc: value end end diff --git a/activerecord/lib/active_record/type_caster.rb b/activerecord/lib/active_record/type_caster.rb new file mode 100644 index 0000000000..63ba10c289 --- /dev/null +++ b/activerecord/lib/active_record/type_caster.rb @@ -0,0 +1,7 @@ +require 'active_record/type_caster/map' +require 'active_record/type_caster/connection' + +module ActiveRecord + module TypeCaster + end +end diff --git a/activerecord/lib/active_record/type_caster/connection.rb b/activerecord/lib/active_record/type_caster/connection.rb new file mode 100644 index 0000000000..9e4a130b40 --- /dev/null +++ b/activerecord/lib/active_record/type_caster/connection.rb @@ -0,0 +1,34 @@ +module ActiveRecord + module TypeCaster + class Connection + def initialize(connection, table_name) + @connection = connection + @table_name = table_name + end + + def type_cast_for_database(attribute_name, value) + return value if value.is_a?(Arel::Nodes::BindParam) + type = type_for(attribute_name) + type.type_cast_for_database(value) + end + + protected + + attr_reader :connection, :table_name + + private + + def type_for(attribute_name) + if connection.schema_cache.table_exists?(table_name) + column_for(attribute_name).cast_type + else + Type::Value.new + end + end + + def column_for(attribute_name) + connection.schema_cache.columns_hash(table_name)[attribute_name.to_s] + end + end + end +end diff --git a/activerecord/lib/active_record/type_caster/map.rb b/activerecord/lib/active_record/type_caster/map.rb new file mode 100644 index 0000000000..03c9e8ff83 --- /dev/null +++ b/activerecord/lib/active_record/type_caster/map.rb @@ -0,0 +1,19 @@ +module ActiveRecord + module TypeCaster + class Map + def initialize(types) + @types = types + end + + def type_cast_for_database(attr_name, value) + return value if value.is_a?(Arel::Nodes::BindParam) + type = types.type_for_attribute(attr_name.to_s) + type.type_cast_for_database(value) + end + + protected + + attr_reader :types + end + end +end diff --git a/activerecord/lib/active_record/validations.rb b/activerecord/lib/active_record/validations.rb index 9999624fcf..f27adc9c40 100644 --- a/activerecord/lib/active_record/validations.rb +++ b/activerecord/lib/active_record/validations.rb @@ -5,13 +5,14 @@ module ActiveRecord # +record+ method to retrieve the record which did not validate. # # begin - # complex_operation_that_calls_save!_internally + # complex_operation_that_internally_calls_save! # rescue ActiveRecord::RecordInvalid => invalid # puts invalid.record.errors # end class RecordInvalid < ActiveRecordError - attr_reader :record # :nodoc: - def initialize(record) # :nodoc: + attr_reader :record + + def initialize(record) @record = record errors = @record.errors.full_messages.join(", ") super(I18n.t(:"#{@record.class.i18n_scope}.errors.messages.record_invalid", :errors => errors, :default => :"errors.messages.record_invalid")) @@ -29,21 +30,6 @@ module ActiveRecord extend ActiveSupport::Concern include ActiveModel::Validations - module ClassMethods - # Creates an object just like Base.create but calls <tt>save!</tt> instead of +save+ - # so an exception is raised if the record is invalid. - def create!(attributes = nil, &block) - if attributes.is_a?(Array) - attributes.collect { |attr| create!(attr, &block) } - else - object = new(attributes) - yield(object) if block_given? - object.save! - object - end - end - end - # The validation process on save can be skipped by passing <tt>validate: false</tt>. # The regular Base#save method is replaced with this when the validations # module is mixed in, which it is by default. @@ -54,7 +40,7 @@ module ActiveRecord # Attempts to save the record just like Base#save but will raise a +RecordInvalid+ # exception instead of returning +false+ if the record is not valid. def save!(options={}) - perform_validations(options) ? super : raise(RecordInvalid.new(self)) + perform_validations(options) ? super : raise_record_invalid end # Runs all the validations within the specified context. Returns +true+ if @@ -75,8 +61,24 @@ module ActiveRecord alias_method :validate, :valid? + # Runs all the validations within the specified context. Returns +true+ if + # no errors are found, raises +RecordInvalid+ otherwise. + # + # If the argument is +false+ (default is +nil+), the context is set to <tt>:create</tt> if + # <tt>new_record?</tt> is +true+, and to <tt>:update</tt> if it is not. + # + # Validations with no <tt>:on</tt> option will run no matter the context. Validations with + # some <tt>:on</tt> option will only run in the specified context. + def validate!(context = nil) + valid?(context) || raise_record_invalid + end + protected + def raise_record_invalid + raise(RecordInvalid.new(self)) + end + def perform_validations(options={}) # :nodoc: options[:validate] == false || valid?(options[:context]) end @@ -86,3 +88,4 @@ end require "active_record/validations/associated" require "active_record/validations/uniqueness" require "active_record/validations/presence" +require "active_record/validations/length" diff --git a/activerecord/lib/active_record/validations/associated.rb b/activerecord/lib/active_record/validations/associated.rb index b4785d3ba4..47ccef31a5 100644 --- a/activerecord/lib/active_record/validations/associated.rb +++ b/activerecord/lib/active_record/validations/associated.rb @@ -29,9 +29,11 @@ module ActiveRecord # Configuration options: # # * <tt>:message</tt> - A custom error message (default is: "is invalid"). - # * <tt>:on</tt> - Specifies when this validation is active. Runs in all - # validation contexts by default (+nil+), other options are <tt>:create</tt> - # and <tt>:update</tt>. + # * <tt>:on</tt> - Specifies the contexts where this validation is active. + # Runs in all validation contexts by default (nil). You can pass a symbol + # or an array of symbols. (e.g. <tt>on: :create</tt> or + # <tt>on: :custom_validation_context</tt> or + # <tt>on: [:create, :custom_validation_context]</tt>) # * <tt>:if</tt> - Specifies a method, proc or string to call to determine # if the validation should occur (e.g. <tt>if: :allow_validation</tt>, # or <tt>if: Proc.new { |user| user.signup_step > 2 }</tt>). The method, diff --git a/activerecord/lib/active_record/validations/length.rb b/activerecord/lib/active_record/validations/length.rb new file mode 100644 index 0000000000..ef5a6cbbe7 --- /dev/null +++ b/activerecord/lib/active_record/validations/length.rb @@ -0,0 +1,21 @@ +module ActiveRecord + module Validations + class LengthValidator < ActiveModel::Validations::LengthValidator # :nodoc: + def validate_each(record, attribute, association_or_value) + 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 + end + + module ClassMethods + # See <tt>ActiveModel::Validation::LengthValidator</tt> for more information. + def validates_length_of(*attr_names) + validates_with LengthValidator, _merge_attributes(attr_names) + end + + alias_method :validates_size_of, :validates_length_of + end + end +end diff --git a/activerecord/lib/active_record/validations/presence.rb b/activerecord/lib/active_record/validations/presence.rb index e586744818..61b30749d9 100644 --- a/activerecord/lib/active_record/validations/presence.rb +++ b/activerecord/lib/active_record/validations/presence.rb @@ -8,7 +8,7 @@ module ActiveRecord associated_records = Array.wrap(record.send(attribute)) # Superclass validates presence. Ensure present records aren't about to be destroyed. - if associated_records.present? && associated_records.all? { |r| r.marked_for_destruction? } + if associated_records.present? && associated_records.all?(&:marked_for_destruction?) record.errors.add(attribute, :blank, options) end end @@ -44,9 +44,11 @@ module ActiveRecord # # Configuration options: # * <tt>:message</tt> - A custom error message (default is: "can't be blank"). - # * <tt>:on</tt> - Specifies when this validation is active. Runs in all - # validation contexts by default (+nil+), other options are <tt>:create</tt> - # and <tt>:update</tt>. + # * <tt>:on</tt> - Specifies the contexts where this validation is active. + # Runs in all validation contexts by default (nil). You can pass a symbol + # or an array of symbols. (e.g. <tt>on: :create</tt> or + # <tt>on: :custom_validation_context</tt> or + # <tt>on: [:create, :custom_validation_context]</tt>) # * <tt>:if</tt> - Specifies a method, proc or string to call to determine if # the validation should occur (e.g. <tt>if: :allow_validation</tt>, or # <tt>if: Proc.new { |user| user.signup_step > 2 }</tt>). The method, proc diff --git a/activerecord/lib/active_record/validations/uniqueness.rb b/activerecord/lib/active_record/validations/uniqueness.rb index 2e7b1d7206..f52f91e89c 100644 --- a/activerecord/lib/active_record/validations/uniqueness.rb +++ b/activerecord/lib/active_record/validations/uniqueness.rb @@ -16,9 +16,8 @@ module ActiveRecord value = map_enum_attribute(finder_class, attribute, value) relation = build_relation(finder_class, table, attribute, value) - relation = relation.and(table[finder_class.primary_key.to_sym].not_eq(record.id)) if record.persisted? + relation = relation.where.not(finder_class.primary_key => record.id) if record.persisted? relation = scope_relation(record, table, relation) - relation = finder_class.unscoped.where(relation) relation = relation.merge(options[:conditions]) if options[:conditions] if relation.exists? @@ -48,7 +47,7 @@ module ActiveRecord def build_relation(klass, table, attribute, value) #:nodoc: if reflection = klass._reflect_on_association(attribute) attribute = reflection.foreign_key - value = value.attributes[reflection.primary_key_column.name] unless value.nil? + value = value.attributes[reflection.klass.primary_key] unless value.nil? end attribute_name = attribute.to_s @@ -60,15 +59,21 @@ module ActiveRecord end column = klass.columns_hash[attribute_name] - value = klass.connection.type_cast(value, column) - value = value.to_s[0, column.limit] if value && column.limit && column.text? + value = klass.type_for_attribute(attribute_name).type_cast_for_database(value) + value = klass.connection.type_cast(value) + if value.is_a?(String) && column.limit + value = value.to_s[0, column.limit] + end + + value = Arel::Nodes::Quoted.new(value) - if !options[:case_sensitive] && value && column.text? + comparison = if !options[:case_sensitive] && value && column.text? # will use SQL LOWER function before comparison, unless it detects a case insensitive collation klass.connection.case_insensitive_comparison(table, attribute, column, value) else klass.connection.case_sensitive_comparison(table, attribute, column, value) end + klass.unscoped.where(comparison) end def scope_relation(record, table, relation) @@ -77,9 +82,9 @@ module ActiveRecord scope_value = record.send(reflection.foreign_key) scope_item = reflection.foreign_key else - scope_value = record.read_attribute(scope_item) + scope_value = record._read_attribute(scope_item) end - relation = relation.and(table[scope_item].eq(scope_value)) + relation = relation.where(scope_item => scope_value) end relation @@ -150,7 +155,7 @@ module ActiveRecord # or <tt>if: Proc.new { |user| user.signup_step > 2 }</tt>). The method, # proc or string should return or evaluate to a +true+ or +false+ value. # * <tt>:unless</tt> - Specifies a method, proc or string to call to - # determine if the validation should ot occur (e.g. <tt>unless: :skip_validation</tt>, + # determine if the validation should not occur (e.g. <tt>unless: :skip_validation</tt>, # or <tt>unless: Proc.new { |user| user.signup_step <= 2 }</tt>). The # method, proc or string should return or evaluate to a +true+ or +false+ # value. diff --git a/activerecord/lib/rails/generators/active_record/migration/migration_generator.rb b/activerecord/lib/rails/generators/active_record/migration/migration_generator.rb index d3c853cfea..7a3c6f5e95 100644 --- a/activerecord/lib/rails/generators/active_record/migration/migration_generator.rb +++ b/activerecord/lib/rails/generators/active_record/migration/migration_generator.rb @@ -55,7 +55,7 @@ module ActiveRecord def attributes_with_index attributes.select { |a| !a.reference? && a.has_index? } end - + def validate_file_name! unless file_name =~ /^[_a-z0-9]+$/ raise IllegalMigrationNameError.new(file_name) 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 fd94a2d038..f7bf6987c4 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 @@ -9,7 +9,7 @@ class <%= migration_class_name %> < ActiveRecord::Migration <% end -%> <% end -%> <% if options[:timestamps] %> - t.timestamps + t.timestamps null: false <% end -%> end <% attributes_with_index.each do |attribute| -%> diff --git a/activerecord/lib/rails/generators/active_record/model/templates/model.rb b/activerecord/lib/rails/generators/active_record/model/templates/model.rb index 808598699b..539d969fce 100644 --- a/activerecord/lib/rails/generators/active_record/model/templates/model.rb +++ b/activerecord/lib/rails/generators/active_record/model/templates/model.rb @@ -1,7 +1,7 @@ <% module_namespacing do -%> class <%= class_name %> < <%= parent_class_name.classify %> <% attributes.select(&:reference?).each do |attribute| -%> - belongs_to :<%= attribute.name %><%= ', polymorphic: true' if attribute.polymorphic? %> + belongs_to :<%= attribute.name %><%= ', polymorphic: true' if attribute.polymorphic? %><%= ', required: true' if attribute.required? %> <% end -%> <% if attributes.any?(&:password_digest?) -%> has_secure_password diff --git a/activerecord/test/cases/adapter_test.rb b/activerecord/test/cases/adapter_test.rb index 778c4ed7e5..6f84bae432 100644 --- a/activerecord/test/cases/adapter_test.rb +++ b/activerecord/test/cases/adapter_test.rb @@ -192,7 +192,7 @@ module ActiveRecord def test_select_methods_passing_a_association_relation author = Author.create!(name: 'john') Post.create!(author: author, title: 'foo', body: 'bar') - query = author.posts.select(:title) + query = author.posts.where(title: 'foo').select(:title) assert_equal({"title" => "foo"}, @connection.select_one(query.arel, nil, query.bind_values)) assert_equal({"title" => "foo"}, @connection.select_one(query)) assert @connection.select_all(query).is_a?(ActiveRecord::Result) diff --git a/activerecord/test/cases/adapters/mysql/active_schema_test.rb b/activerecord/test/cases/adapters/mysql/active_schema_test.rb index 7c0f11b033..6577d56240 100644 --- a/activerecord/test/cases/adapters/mysql/active_schema_test.rb +++ b/activerecord/test/cases/adapters/mysql/active_schema_test.rb @@ -92,7 +92,7 @@ class ActiveSchemaTest < ActiveRecord::TestCase with_real_execute do begin ActiveRecord::Base.connection.create_table :delete_me - ActiveRecord::Base.connection.add_timestamps :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 @@ -105,9 +105,9 @@ class ActiveSchemaTest < ActiveRecord::TestCase with_real_execute do begin ActiveRecord::Base.connection.create_table :delete_me do |t| - t.timestamps + t.timestamps null: true end - ActiveRecord::Base.connection.remove_timestamps :delete_me + 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 diff --git a/activerecord/test/cases/adapters/mysql/connection_test.rb b/activerecord/test/cases/adapters/mysql/connection_test.rb index 4c90d06732..ce01b16362 100644 --- a/activerecord/test/cases/adapters/mysql/connection_test.rb +++ b/activerecord/test/cases/adapters/mysql/connection_test.rb @@ -47,9 +47,7 @@ class MysqlConnectionTest < ActiveRecord::TestCase assert !@connection.active? # Repair all fixture connections so other tests won't break. - @fixture_connections.each do |c| - c.verify! - end + @fixture_connections.each(&:verify!) end def test_successful_reconnection_after_timeout_with_manual_reconnect @@ -69,8 +67,8 @@ class MysqlConnectionTest < ActiveRecord::TestCase end def test_bind_value_substitute - bind_param = @connection.substitute_at('foo', 0) - assert_equal Arel.sql('?'), bind_param + bind_param = @connection.substitute_at('foo') + assert_equal Arel.sql('?'), bind_param.to_sql end def test_exec_no_binds @@ -129,17 +127,21 @@ class MysqlConnectionTest < ActiveRecord::TestCase 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_dont_override_global_sql_mode + def test_mysql_strict_mode_disabled run_without_connection do |orig_connection| ActiveRecord::Base.establish_connection(orig_connection.merge({:strict => false})) - 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 + result = ActiveRecord::Base.connection.exec_query "SELECT @@SESSION.sql_mode" + assert_equal [['']], result.rows end end @@ -151,7 +153,7 @@ class MysqlConnectionTest < ActiveRecord::TestCase end end - def test_mysql_sql_mode_variable_overides_strict_mode + 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' diff --git a/activerecord/test/cases/adapters/mysql/consistency_test.rb b/activerecord/test/cases/adapters/mysql/consistency_test.rb index 083d533bb2..e972d6b330 100644 --- a/activerecord/test/cases/adapters/mysql/consistency_test.rb +++ b/activerecord/test/cases/adapters/mysql/consistency_test.rb @@ -12,6 +12,7 @@ class MysqlConsistencyTest < ActiveRecord::TestCase 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" diff --git a/activerecord/test/cases/adapters/mysql/mysql_adapter_test.rb b/activerecord/test/cases/adapters/mysql/mysql_adapter_test.rb index 28106d3772..85db8f4614 100644 --- a/activerecord/test/cases/adapters/mysql/mysql_adapter_test.rb +++ b/activerecord/test/cases/adapters/mysql/mysql_adapter_test.rb @@ -99,6 +99,12 @@ module ActiveRecord end end + def test_composite_primary_key + with_example_table '`id` INT(11), `number` INT(11), foo INT(11), 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') diff --git a/activerecord/test/cases/adapters/mysql/quoting_test.rb b/activerecord/test/cases/adapters/mysql/quoting_test.rb index d8a954efa8..a2206153e9 100644 --- a/activerecord/test/cases/adapters/mysql/quoting_test.rb +++ b/activerecord/test/cases/adapters/mysql/quoting_test.rb @@ -9,15 +9,11 @@ module ActiveRecord end def test_type_cast_true - c = Column.new(nil, 1, Type::Boolean.new) - assert_equal 1, @conn.type_cast(true, nil) - assert_equal 1, @conn.type_cast(true, c) + assert_equal 1, @conn.type_cast(true) end def test_type_cast_false - c = Column.new(nil, 1, Type::Boolean.new) - assert_equal 0, @conn.type_cast(false, nil) - assert_equal 0, @conn.type_cast(false, c) + assert_equal 0, @conn.type_cast(false) end end end diff --git a/activerecord/test/cases/adapters/mysql/reserved_word_test.rb b/activerecord/test/cases/adapters/mysql/reserved_word_test.rb index 61ae0abfd1..403f7cbc74 100644 --- a/activerecord/test/cases/adapters/mysql/reserved_word_test.rb +++ b/activerecord/test/cases/adapters/mysql/reserved_word_test.rb @@ -101,7 +101,7 @@ class MysqlReservedWordTest < ActiveRecord::TestCase gs = nil assert_nothing_raised { gs = Select.find(2).groups } assert_equal gs.length, 2 - assert(gs.collect{|x| x.id}.sort == [2, 3]) + assert(gs.collect(&:id).sort == [2, 3]) end # has_and_belongs_to_many with reserved-word table name @@ -110,7 +110,7 @@ class MysqlReservedWordTest < ActiveRecord::TestCase s = nil assert_nothing_raised { s = Distinct.find(1).selects } assert_equal s.length, 2 - assert(s.collect{|x|x.id}.sort == [1, 2]) + assert(s.collect(&:id).sort == [1, 2]) end # activerecord model introspection with reserved-word table and column names diff --git a/activerecord/test/cases/adapters/mysql/schema_test.rb b/activerecord/test/cases/adapters/mysql/schema_test.rb index 87c5277e64..ab547747df 100644 --- a/activerecord/test/cases/adapters/mysql/schema_test.rb +++ b/activerecord/test/cases/adapters/mysql/schema_test.rb @@ -81,7 +81,7 @@ module ActiveRecord table = 'key_tests' - indexes = @connection.indexes(table).sort_by {|i| i.name} + indexes = @connection.indexes(table).sort_by(&:name) assert_equal 3,indexes.size index_a = indexes.select{|i| i.name == index_a_name}[0] diff --git a/activerecord/test/cases/adapters/mysql/unsigned_type_test.rb b/activerecord/test/cases/adapters/mysql/unsigned_type_test.rb new file mode 100644 index 0000000000..8f521e9181 --- /dev/null +++ b/activerecord/test/cases/adapters/mysql/unsigned_type_test.rb @@ -0,0 +1,30 @@ +require "cases/helper" + +class UnsignedTypeTest < ActiveRecord::TestCase + self.use_transactional_fixtures = false + + class UnsignedType < ActiveRecord::Base + end + + setup do + @connection = ActiveRecord::Base.connection + @connection.create_table("unsigned_types", force: true) do |t| + t.column :unsigned_integer, "int unsigned" + end + end + + teardown do + @connection.drop_table "unsigned_types" + 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 + 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 cefc3e3c7e..e87cd3886a 100644 --- a/activerecord/test/cases/adapters/mysql2/active_schema_test.rb +++ b/activerecord/test/cases/adapters/mysql2/active_schema_test.rb @@ -92,7 +92,7 @@ class ActiveSchemaTest < ActiveRecord::TestCase with_real_execute do begin ActiveRecord::Base.connection.create_table :delete_me - ActiveRecord::Base.connection.add_timestamps :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 @@ -105,9 +105,9 @@ class ActiveSchemaTest < ActiveRecord::TestCase with_real_execute do begin ActiveRecord::Base.connection.create_table :delete_me do |t| - t.timestamps + t.timestamps null: true end - ActiveRecord::Base.connection.remove_timestamps :delete_me + 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 diff --git a/activerecord/test/cases/adapters/mysql2/boolean_test.rb b/activerecord/test/cases/adapters/mysql2/boolean_test.rb index 267aa232d9..0e641ba3bf 100644 --- a/activerecord/test/cases/adapters/mysql2/boolean_test.rb +++ b/activerecord/test/cases/adapters/mysql2/boolean_test.rb @@ -9,6 +9,7 @@ class Mysql2BooleanTest < ActiveRecord::TestCase setup do @connection = ActiveRecord::Base.connection + @connection.clear_cache! @connection.create_table("mysql_booleans") do |t| t.boolean "archived" t.string "published", limit: 1 @@ -46,8 +47,7 @@ class Mysql2BooleanTest < ActiveRecord::TestCase assert_equal 1, attributes["archived"] assert_equal "1", attributes["published"] - assert_equal 1, @connection.type_cast(true, boolean_column) - assert_equal 1, @connection.type_cast(true, string_column) + assert_equal 1, @connection.type_cast(true) end test "test type casting without emulated booleans" do @@ -59,8 +59,7 @@ class Mysql2BooleanTest < ActiveRecord::TestCase assert_equal 1, attributes["archived"] assert_equal "1", attributes["published"] - assert_equal 1, @connection.type_cast(true, boolean_column) - assert_equal 1, @connection.type_cast(true, string_column) + assert_equal 1, @connection.type_cast(true) end test "with booleans stored as 1 and 0" do diff --git a/activerecord/test/cases/adapters/mysql2/connection_test.rb b/activerecord/test/cases/adapters/mysql2/connection_test.rb index 65f50e77bb..d261e2db55 100644 --- a/activerecord/test/cases/adapters/mysql2/connection_test.rb +++ b/activerecord/test/cases/adapters/mysql2/connection_test.rb @@ -4,6 +4,8 @@ require 'support/connection_helper' class MysqlConnectionTest < ActiveRecord::TestCase include ConnectionHelper + fixtures :comments + def setup super @subscriber = SQLSubscriber.new @@ -24,6 +26,17 @@ class MysqlConnectionTest < ActiveRecord::TestCase end end + def test_truncate + rows = ActiveRecord::Base.connection.exec_query("select count(*) from comments") + count = rows.first.values.first + assert_operator count, :>, 0 + + ActiveRecord::Base.connection.truncate("comments") + rows = ActiveRecord::Base.connection.exec_query("select count(*) from comments") + count = rows.first.values.first + assert_equal 0, count + end + def test_no_automatic_reconnection_after_timeout assert @connection.active? @connection.update('set @@wait_timeout=1') @@ -31,9 +44,7 @@ class MysqlConnectionTest < ActiveRecord::TestCase assert !@connection.active? # Repair all fixture connections so other tests won't break. - @fixture_connections.each do |c| - c.verify! - end + @fixture_connections.each(&:verify!) end def test_successful_reconnection_after_timeout_with_manual_reconnect @@ -52,6 +63,11 @@ class MysqlConnectionTest < ActiveRecord::TestCase assert @connection.active? end + def test_mysql_connection_collation_is_configured + assert_equal 'utf8_unicode_ci', @connection.show_variable('collation_connection') + assert_equal 'utf8_general_ci', ARUnit2Model.connection.show_variable('collation_connection') + 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. @@ -60,12 +76,11 @@ class MysqlConnectionTest < ActiveRecord::TestCase assert_equal [["STRICT_ALL_TABLES"]], result.rows end - def test_mysql_strict_mode_disabled_dont_override_global_sql_mode + def test_mysql_strict_mode_disabled run_without_connection do |orig_connection| ActiveRecord::Base.establish_connection(orig_connection.merge({:strict => false})) - 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 + result = ActiveRecord::Base.connection.exec_query "SELECT @@SESSION.sql_mode" + assert_equal [['']], result.rows end end @@ -77,7 +92,7 @@ class MysqlConnectionTest < ActiveRecord::TestCase end end - def test_mysql_sql_mode_variable_overides_strict_mode + 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' diff --git a/activerecord/test/cases/adapters/mysql2/explain_test.rb b/activerecord/test/cases/adapters/mysql2/explain_test.rb index 675703caa1..2b01d941b8 100644 --- a/activerecord/test/cases/adapters/mysql2/explain_test.rb +++ b/activerecord/test/cases/adapters/mysql2/explain_test.rb @@ -1,5 +1,6 @@ require "cases/helper" require 'models/developer' +require 'models/computer' module ActiveRecord module ConnectionAdapters @@ -17,7 +18,7 @@ module ActiveRecord 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` IN (1)), 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/mysql2/reserved_word_test.rb b/activerecord/test/cases/adapters/mysql2/reserved_word_test.rb index 799d927ee4..7f97b454bb 100644 --- a/activerecord/test/cases/adapters/mysql2/reserved_word_test.rb +++ b/activerecord/test/cases/adapters/mysql2/reserved_word_test.rb @@ -100,7 +100,7 @@ class MysqlReservedWordTest < ActiveRecord::TestCase gs = nil assert_nothing_raised { gs = Select.find(2).groups } assert_equal gs.length, 2 - assert(gs.collect{|x| x.id}.sort == [2, 3]) + assert(gs.collect(&:id).sort == [2, 3]) end # has_and_belongs_to_many with reserved-word table name @@ -109,7 +109,7 @@ class MysqlReservedWordTest < ActiveRecord::TestCase s = nil assert_nothing_raised { s = Distinct.find(1).selects } assert_equal s.length, 2 - assert(s.collect{|x|x.id}.sort == [1, 2]) + assert(s.collect(&:id).sort == [1, 2]) end # activerecord model introspection with reserved-word table and column names diff --git a/activerecord/test/cases/adapters/mysql2/schema_migrations_test.rb b/activerecord/test/cases/adapters/mysql2/schema_migrations_test.rb index ec73ec35aa..9c49599d34 100644 --- a/activerecord/test/cases/adapters/mysql2/schema_migrations_test.rb +++ b/activerecord/test/cases/adapters/mysql2/schema_migrations_test.rb @@ -6,12 +6,12 @@ module ActiveRecord class SchemaMigrationsTest < ActiveRecord::TestCase def test_renaming_index_on_foreign_key connection.add_index "engines", "car_id" - connection.execute "ALTER TABLE engines ADD CONSTRAINT fk_engines_cars FOREIGN KEY (car_id) REFERENCES cars(id)" + connection.add_foreign_key :engines, :cars, name: "fk_engines_cars" connection.rename_index("engines", "index_engines_on_car_id", "idx_renamed") assert_equal ["idx_renamed"], connection.indexes("engines").map(&:name) ensure - connection.execute "ALTER TABLE engines DROP FOREIGN KEY fk_engines_cars" + connection.remove_foreign_key :engines, name: "fk_engines_cars" end def test_initializes_schema_migrations_for_encoding_utf8mb4 diff --git a/activerecord/test/cases/adapters/mysql2/schema_test.rb b/activerecord/test/cases/adapters/mysql2/schema_test.rb index 43c9116b5a..47707b7d4f 100644 --- a/activerecord/test/cases/adapters/mysql2/schema_test.rb +++ b/activerecord/test/cases/adapters/mysql2/schema_test.rb @@ -51,7 +51,7 @@ module ActiveRecord table = 'key_tests' - indexes = @connection.indexes(table).sort_by {|i| i.name} + indexes = @connection.indexes(table).sort_by(&:name) assert_equal 3,indexes.size index_a = indexes.select{|i| i.name == index_a_name}[0] @@ -66,12 +66,14 @@ module ActiveRecord assert_equal :fulltext, index_c.type end - def test_drop_temporary_table - @connection.transaction do - @connection.create_table(:temp_table, temporary: true) - # if it doesn't properly say DROP TEMPORARY TABLE, the transaction commit - # will complain that no transaction is active - @connection.drop_table(:temp_table, temporary: true) + unless mysql_enforcing_gtid_consistency? + def test_drop_temporary_table + @connection.transaction do + @connection.create_table(:temp_table, temporary: true) + # if it doesn't properly say DROP TEMPORARY TABLE, the transaction commit + # will complain that no transaction is active + @connection.drop_table(:temp_table, temporary: true) + end end end end diff --git a/activerecord/test/cases/adapters/mysql2/unsigned_type_test.rb b/activerecord/test/cases/adapters/mysql2/unsigned_type_test.rb new file mode 100644 index 0000000000..8f521e9181 --- /dev/null +++ b/activerecord/test/cases/adapters/mysql2/unsigned_type_test.rb @@ -0,0 +1,30 @@ +require "cases/helper" + +class UnsignedTypeTest < ActiveRecord::TestCase + self.use_transactional_fixtures = false + + class UnsignedType < ActiveRecord::Base + end + + setup do + @connection = ActiveRecord::Base.connection + @connection.create_table("unsigned_types", force: true) do |t| + t.column :unsigned_integer, "int unsigned" + end + end + + teardown do + @connection.drop_table "unsigned_types" + 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 + end +end diff --git a/activerecord/test/cases/adapters/postgresql/array_test.rb b/activerecord/test/cases/adapters/postgresql/array_test.rb index 0b1e3295cc..042beab23f 100644 --- a/activerecord/test/cases/adapters/postgresql/array_test.rb +++ b/activerecord/test/cases/adapters/postgresql/array_test.rb @@ -1,8 +1,11 @@ # encoding: utf-8 require "cases/helper" +require 'support/schema_dumping_helper' class PostgresqlArrayTest < ActiveRecord::TestCase + include SchemaDumpingHelper include InTimeZone + OID = ActiveRecord::ConnectionAdapters::PostgreSQL::OID class PgArray < ActiveRecord::Base self.table_name = 'pg_arrays' @@ -10,11 +13,15 @@ class PostgresqlArrayTest < ActiveRecord::TestCase def setup @connection = ActiveRecord::Base.connection + + enable_extension!('hstore', @connection) + @connection.transaction do @connection.create_table('pg_arrays') do |t| t.string 'tags', array: true t.integer 'ratings', array: true t.datetime :datetimes, array: true + t.hstore :hstores, array: true end end @column = PgArray.columns_hash['tags'] @@ -22,13 +29,13 @@ class PostgresqlArrayTest < ActiveRecord::TestCase teardown do @connection.execute 'drop table if exists pg_arrays' + disable_extension!('hstore', @connection) end def test_column assert_equal :string, @column.type assert_equal "character varying", @column.sql_type assert @column.array - assert_not @column.text? assert_not @column.number? assert_not @column.binary? @@ -41,9 +48,8 @@ class PostgresqlArrayTest < ActiveRecord::TestCase def test_default @connection.add_column 'pg_arrays', 'score', :integer, array: true, default: [4, 4, 2] PgArray.reset_column_information - column = PgArray.columns_hash["score"] - assert_equal([4, 4, 2], column.default) + assert_equal([4, 4, 2], PgArray.column_defaults['score']) assert_equal([4, 4, 2], PgArray.new.score) ensure PgArray.reset_column_information @@ -52,9 +58,8 @@ class PostgresqlArrayTest < ActiveRecord::TestCase def test_default_strings @connection.add_column 'pg_arrays', 'names', :string, array: true, default: ["foo", "bar"] PgArray.reset_column_information - column = PgArray.columns_hash["names"] - assert_equal(["foo", "bar"], column.default) + assert_equal(["foo", "bar"], PgArray.column_defaults['names']) assert_equal(["foo", "bar"], PgArray.new.names) ensure PgArray.reset_column_information @@ -68,7 +73,7 @@ class PostgresqlArrayTest < ActiveRecord::TestCase column = PgArray.columns_hash['snippets'] assert_equal :text, column.type - assert_equal [], column.default + assert_equal [], PgArray.column_defaults['snippets'] assert column.array end @@ -85,8 +90,7 @@ class PostgresqlArrayTest < ActiveRecord::TestCase @connection.change_column_default :pg_arrays, :tags, [] PgArray.reset_column_information - column = PgArray.columns_hash['tags'] - assert_equal [], column.default + assert_equal [], PgArray.column_defaults['tags'] end def test_type_cast_array @@ -106,6 +110,12 @@ class PostgresqlArrayTest < ActiveRecord::TestCase assert_equal([1, 2], x.ratings) end + def test_schema_dump_with_shorthand + output = dump_table_schema "pg_arrays" + assert_match %r[t.string\s+"tags",\s+array: true], output + assert_match %r[t.integer\s+"ratings",\s+array: true], output + end + def test_select_with_strings @connection.execute "insert into pg_arrays (tags) VALUES ('{1,2,3}')" x = PgArray.first @@ -185,16 +195,6 @@ class PostgresqlArrayTest < ActiveRecord::TestCase assert_equal("[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, ...]", record.attribute_for_inspect(:ratings)) end - def test_update_all - pg_array = PgArray.create! tags: ["one", "two", "three"] - - PgArray.update_all tags: ["four", "five"] - assert_equal ["four", "five"], pg_array.reload.tags - - PgArray.update_all tags: [] - assert_equal [], pg_array.reload.tags - end - def test_escaping unknown = 'foo\\",bar,baz,\\' tags = ["hello_#{unknown}"] @@ -203,6 +203,45 @@ class PostgresqlArrayTest < ActiveRecord::TestCase assert_equal tags, ar.tags end + def test_string_quoting_rules_match_pg_behavior + tags = ["", "one{", "two}", %(three"), "four\\", "five ", "six\t", "seven\n", "eight,", "nine", "ten\r", "NULL"] + x = PgArray.create!(tags: tags) + x.reload + + assert_equal x.tags_before_type_cast, PgArray.columns_hash['tags'].type_cast_for_database(tags) + end + + def test_quoting_non_standard_delimiters + strings = ["hello,", "world;"] + comma_delim = OID::Array.new(ActiveRecord::Type::String.new, ',') + semicolon_delim = OID::Array.new(ActiveRecord::Type::String.new, ';') + + assert_equal %({"hello,",world;}), comma_delim.type_cast_for_database(strings) + assert_equal %({hello,;"world;"}), semicolon_delim.type_cast_for_database(strings) + end + + def test_mutate_array + x = PgArray.create!(tags: %w(one two)) + + x.tags << "three" + x.save! + x.reload + + assert_equal %w(one two three), x.tags + assert_not x.changed? + end + + def test_mutate_value_in_array + x = PgArray.create!(hstores: [{ a: 'a' }, { b: 'b' }]) + + x.hstores.first['a'] = 'c' + x.save! + x.reload + + assert_equal [{ 'a' => 'c' }, { 'b' => 'b' }], x.hstores + assert_not x.changed? + end + def test_datetime_with_timezone_awareness tz = "Pacific Time (US & Canada)" @@ -223,6 +262,45 @@ class PostgresqlArrayTest < ActiveRecord::TestCase end end + def test_assigning_non_array_value + record = PgArray.new(tags: "not-an-array") + assert_equal [], record.tags + assert_equal "not-an-array", record.tags_before_type_cast + assert record.save + assert_equal record.tags, record.reload.tags + end + + def test_assigning_empty_string + record = PgArray.new(tags: "") + assert_equal [], record.tags + assert_equal "", record.tags_before_type_cast + assert record.save + assert_equal record.tags, record.reload.tags + end + + def test_assigning_valid_pg_array_literal + record = PgArray.new(tags: "{1,2,3}") + assert_equal ["1", "2", "3"], record.tags + assert_equal "{1,2,3}", record.tags_before_type_cast + assert record.save + assert_equal record.tags, record.reload.tags + end + + def test_uniqueness_validation + klass = Class.new(PgArray) do + validates_uniqueness_of :tags + + def self.model_name; ActiveModel::Name.new(PgArray) end + end + e1 = klass.create("tags" => ["black", "blue"]) + assert e1.persisted?, "Saving e1" + + e2 = klass.create("tags" => ["black", "blue"]) + assert !e2.persisted?, "e2 shouldn't be valid" + assert e2.errors[:tags].any?, "Should have errors for tags" + assert_equal ["has already been taken"], e2.errors[:tags], "Should have uniqueness message for tags" + end + private def assert_cycle field, array # test creation diff --git a/activerecord/test/cases/adapters/postgresql/bit_string_test.rb b/activerecord/test/cases/adapters/postgresql/bit_string_test.rb index 3a9397bc26..72222c01fd 100644 --- a/activerecord/test/cases/adapters/postgresql/bit_string_test.rb +++ b/activerecord/test/cases/adapters/postgresql/bit_string_test.rb @@ -26,7 +26,6 @@ class PostgresqlBitStringTest < ActiveRecord::TestCase column = PostgresqlBitString.columns_hash["a_bit"] assert_equal :bit, column.type assert_equal "bit(8)", column.sql_type - assert_not column.text? assert_not column.number? assert_not column.binary? assert_not column.array @@ -36,19 +35,16 @@ class PostgresqlBitStringTest < ActiveRecord::TestCase column = PostgresqlBitString.columns_hash["a_bit_varying"] assert_equal :bit_varying, column.type assert_equal "bit varying(4)", column.sql_type - assert_not column.text? assert_not column.number? assert_not column.binary? assert_not column.array end def test_default - column = PostgresqlBitString.columns_hash["a_bit"] - assert_equal "00000011", column.default + assert_equal "00000011", PostgresqlBitString.column_defaults['a_bit'] assert_equal "00000011", PostgresqlBitString.new.a_bit - column = PostgresqlBitString.columns_hash["a_bit_varying"] - assert_equal "0011", column.default + assert_equal "0011", PostgresqlBitString.column_defaults['a_bit_varying'] assert_equal "0011", PostgresqlBitString.new.a_bit_varying end diff --git a/activerecord/test/cases/adapters/postgresql/bytea_test.rb b/activerecord/test/cases/adapters/postgresql/bytea_test.rb index 7872f91943..aeebec034d 100644 --- a/activerecord/test/cases/adapters/postgresql/bytea_test.rb +++ b/activerecord/test/cases/adapters/postgresql/bytea_test.rb @@ -17,7 +17,6 @@ class PostgresqlByteaTest < ActiveRecord::TestCase end end @column = ByteaDataType.columns_hash['payload'] - assert(@column.is_a?(ActiveRecord::ConnectionAdapters::PostgreSQLColumn)) end teardown do @@ -25,9 +24,17 @@ class PostgresqlByteaTest < ActiveRecord::TestCase end def test_column + assert @column.is_a?(ActiveRecord::ConnectionAdapters::PostgreSQLColumn) assert_equal :binary, @column.type end + def test_binary_columns_are_limitless_the_upper_limit_is_one_GB + assert_equal 'bytea', @connection.type_to_sql(:binary, 100_000) + assert_raise ActiveRecord::ActiveRecordError do + @connection.type_to_sql :binary, 4294967295 + end + end + def test_type_cast_binary_converts_the_encoding assert @column diff --git a/activerecord/test/cases/adapters/postgresql/change_schema_test.rb b/activerecord/test/cases/adapters/postgresql/change_schema_test.rb new file mode 100644 index 0000000000..6c1b29f7fe --- /dev/null +++ b/activerecord/test/cases/adapters/postgresql/change_schema_test.rb @@ -0,0 +1,31 @@ +require 'cases/helper' + +module ActiveRecord + class Migration + class PGChangeSchemaTest < ActiveRecord::TestCase + attr_reader :connection + + def setup + super + @connection = ActiveRecord::Base.connection + connection.create_table(:strings) do |t| + t.string :somedate + end + end + + def teardown + connection.drop_table :strings + end + + def test_change_string_to_date + connection.change_column :strings, :somedate, :timestamp, using: 'CAST("somedate" AS timestamp)' + assert_equal :datetime, connection.columns(:strings).find { |c| c.name == 'somedate' }.type + end + + def test_change_type_with_symbol + connection.change_column :strings, :somedate, :timestamp, cast_as: :timestamp + assert_equal :datetime, connection.columns(:strings).find { |c| c.name == 'somedate' }.type + end + end + end +end diff --git a/activerecord/test/cases/adapters/postgresql/cidr_test.rb b/activerecord/test/cases/adapters/postgresql/cidr_test.rb new file mode 100644 index 0000000000..54b679d3ab --- /dev/null +++ b/activerecord/test/cases/adapters/postgresql/cidr_test.rb @@ -0,0 +1,25 @@ +require "cases/helper" +require "ipaddr" + +module ActiveRecord + module ConnectionAdapters + class PostgreSQLAdapter + class CidrTest < ActiveRecord::TestCase + test "type casting IPAddr for database" do + type = OID::Cidr.new + ip = IPAddr.new("255.0.0.0/8") + ip2 = IPAddr.new("127.0.0.1") + + assert_equal "255.0.0.0/8", type.type_cast_for_database(ip) + assert_equal "127.0.0.1/32", type.type_cast_for_database(ip2) + end + + test "casting does nothing with non-IPAddr objects" do + type = OID::Cidr.new + + assert_equal "foo", type.type_cast_for_database("foo") + end + end + end + end +end diff --git a/activerecord/test/cases/adapters/postgresql/citext_test.rb b/activerecord/test/cases/adapters/postgresql/citext_test.rb index 90e837d426..85bff979c9 100644 --- a/activerecord/test/cases/adapters/postgresql/citext_test.rb +++ b/activerecord/test/cases/adapters/postgresql/citext_test.rb @@ -1,8 +1,10 @@ # encoding: utf-8 require 'cases/helper' +require 'support/schema_dumping_helper' if ActiveRecord::Base.connection.supports_extensions? class PostgresqlCitextTest < ActiveRecord::TestCase + include SchemaDumpingHelper class Citext < ActiveRecord::Base self.table_name = 'citexts' end @@ -10,12 +12,7 @@ if ActiveRecord::Base.connection.supports_extensions? def setup @connection = ActiveRecord::Base.connection - unless @connection.extension_enabled?('citext') - @connection.enable_extension 'citext' - @connection.commit_db_transaction - end - - @connection.reconnect! + enable_extension!('citext', @connection) @connection.create_table('citexts') do |t| t.citext 'cival' @@ -24,7 +21,7 @@ if ActiveRecord::Base.connection.supports_extensions? teardown do @connection.execute 'DROP TABLE IF EXISTS citexts;' - @connection.execute 'DROP EXTENSION IF EXISTS citext CASCADE;' + disable_extension!('citext', @connection) end def test_citext_enabled @@ -35,7 +32,6 @@ if ActiveRecord::Base.connection.supports_extensions? column = Citext.columns_hash['cival'] assert_equal :citext, column.type assert_equal 'citext', column.sql_type - assert_not column.text? assert_not column.number? assert_not column.binary? assert_not column.array @@ -73,5 +69,10 @@ if ActiveRecord::Base.connection.supports_extensions? x = Citext.where(cival: 'cased text').first assert_equal 'Cased Text', x.cival end + + def test_schema_dump_with_shorthand + output = dump_table_schema("citexts") + assert_match %r[t.citext "cival"], output + end end end diff --git a/activerecord/test/cases/adapters/postgresql/composite_test.rb b/activerecord/test/cases/adapters/postgresql/composite_test.rb index 42c68cdae7..cfab5ca902 100644 --- a/activerecord/test/cases/adapters/postgresql/composite_test.rb +++ b/activerecord/test/cases/adapters/postgresql/composite_test.rb @@ -51,7 +51,6 @@ class PostgresqlCompositeTest < ActiveRecord::TestCase assert_nil column.type assert_equal "full_address", column.sql_type assert_not column.number? - assert_not column.text? assert_not column.binary? assert_not column.array end @@ -113,7 +112,6 @@ class PostgresqlCompositeWithCustomOIDTest < ActiveRecord::TestCase assert_equal :full_address, column.type assert_equal "full_address", column.sql_type assert_not column.number? - assert_not column.text? assert_not column.binary? assert_not column.array end diff --git a/activerecord/test/cases/adapters/postgresql/connection_test.rb b/activerecord/test/cases/adapters/postgresql/connection_test.rb index d26cda46fa..ab7fd3c6d5 100644 --- a/activerecord/test/cases/adapters/postgresql/connection_test.rb +++ b/activerecord/test/cases/adapters/postgresql/connection_test.rb @@ -8,6 +8,8 @@ module ActiveRecord class NonExistentTable < ActiveRecord::Base end + fixtures :comments + def setup super @subscriber = SQLSubscriber.new @@ -20,6 +22,14 @@ module ActiveRecord super end + def test_truncate + count = ActiveRecord::Base.connection.execute("select count(*) from comments").first['count'].to_i + assert_operator count, :>, 0 + ActiveRecord::Base.connection.truncate("comments") + count = ActiveRecord::Base.connection.execute("select count(*) from comments").first['count'].to_i + assert_equal 0, count + end + def test_encoding assert_not_nil @connection.encoding end @@ -167,9 +177,7 @@ module ActiveRecord "successfully querying with the same connection pid." # Repair all fixture connections so other tests won't break. - @fixture_connections.each do |c| - c.verify! - end + @fixture_connections.each(&:verify!) end def test_set_session_variable_true diff --git a/activerecord/test/cases/adapters/postgresql/datatype_test.rb b/activerecord/test/cases/adapters/postgresql/datatype_test.rb index a0a34e4b87..4f48a7bce3 100644 --- a/activerecord/test/cases/adapters/postgresql/datatype_test.rb +++ b/activerecord/test/cases/adapters/postgresql/datatype_test.rb @@ -2,9 +2,6 @@ require "cases/helper" require 'support/ddl_helper' -class PostgresqlNumber < ActiveRecord::Base -end - class PostgresqlTime < ActiveRecord::Base end @@ -20,13 +17,6 @@ class PostgresqlDataTypeTest < ActiveRecord::TestCase def setup @connection = ActiveRecord::Base.connection - @connection.execute("INSERT INTO postgresql_numbers (id, single, double) VALUES (1, 123.456, 123456.789)") - @connection.execute("INSERT INTO postgresql_numbers (id, single, double) VALUES (2, '-Infinity', 'Infinity')") - @connection.execute("INSERT INTO postgresql_numbers (id, single, double) VALUES (3, 123.456, 'NaN')") - @first_number = PostgresqlNumber.find(1) - @second_number = PostgresqlNumber.find(2) - @third_number = PostgresqlNumber.find(3) - @connection.execute("INSERT INTO postgresql_times (id, time_interval, scaled_time_interval) VALUES (1, '1 year 2 days ago', '3 weeks ago')") @first_time = PostgresqlTime.find(1) @@ -35,12 +25,7 @@ class PostgresqlDataTypeTest < ActiveRecord::TestCase end teardown do - [PostgresqlNumber, PostgresqlTime, PostgresqlOid].each(&:delete_all) - end - - def test_data_type_of_number_types - assert_equal :float, @first_number.column_for_attribute(:single).type - assert_equal :float, @first_number.column_for_attribute(:double).type + [PostgresqlTime, PostgresqlOid].each(&:delete_all) end def test_data_type_of_time_types @@ -52,14 +37,6 @@ class PostgresqlDataTypeTest < ActiveRecord::TestCase assert_equal :integer, @first_oid.column_for_attribute(:obj_id).type end - def test_number_values - assert_equal 123.456, @first_number.single - assert_equal 123456.789, @first_number.double - assert_equal(-::Float::INFINITY, @second_number.single) - assert_equal ::Float::INFINITY, @second_number.double - assert_same ::Float::NAN, @third_number.double - end - def test_time_values assert_equal '-1 years -2 days', @first_time.time_interval assert_equal '-21 days', @first_time.scaled_time_interval @@ -69,17 +46,6 @@ class PostgresqlDataTypeTest < ActiveRecord::TestCase assert_equal 1234, @first_oid.obj_id end - def test_update_number - new_single = 789.012 - new_double = 789012.345 - @first_number.single = new_single - @first_number.double = new_double - assert @first_number.save - assert @first_number.reload - assert_equal new_single, @first_number.single - assert_equal new_double, @first_number.double - end - def test_update_time @first_time.time_interval = '2 years 3 minutes' assert @first_time.save @@ -94,6 +60,13 @@ class PostgresqlDataTypeTest < ActiveRecord::TestCase assert @first_oid.reload assert_equal new_value, @first_oid.obj_id end + + def test_text_columns_are_limitless_the_upper_limit_is_one_GB + assert_equal 'text', @connection.type_to_sql(:text, 100_000) + assert_raise ActiveRecord::ActiveRecordError do + @connection.type_to_sql :text, 4294967295 + end + end end class PostgresqlInternalDataTypeTest < ActiveRecord::TestCase diff --git a/activerecord/test/cases/adapters/postgresql/domain_test.rb b/activerecord/test/cases/adapters/postgresql/domain_test.rb index fd7fdecff1..1500adb42d 100644 --- a/activerecord/test/cases/adapters/postgresql/domain_test.rb +++ b/activerecord/test/cases/adapters/postgresql/domain_test.rb @@ -30,7 +30,6 @@ class PostgresqlDomainTest < ActiveRecord::TestCase assert_equal :decimal, column.type assert_equal "custom_money", column.sql_type assert column.number? - assert_not column.text? assert_not column.binary? assert_not column.array end diff --git a/activerecord/test/cases/adapters/postgresql/enum_test.rb b/activerecord/test/cases/adapters/postgresql/enum_test.rb index b809f1a79c..83cedc5a7b 100644 --- a/activerecord/test/cases/adapters/postgresql/enum_test.rb +++ b/activerecord/test/cases/adapters/postgresql/enum_test.rb @@ -1,8 +1,6 @@ # -*- coding: utf-8 -*- require "cases/helper" require 'support/connection_helper' -require 'active_record/base' -require 'active_record/connection_adapters/postgresql_adapter' class PostgresqlEnumTest < ActiveRecord::TestCase include ConnectionHelper @@ -34,7 +32,6 @@ class PostgresqlEnumTest < ActiveRecord::TestCase assert_equal :enum, column.type assert_equal "mood", column.sql_type assert_not column.number? - assert_not column.text? assert_not column.binary? assert_not column.array end @@ -42,9 +39,8 @@ class PostgresqlEnumTest < ActiveRecord::TestCase def test_enum_defaults @connection.add_column 'postgresql_enums', 'good_mood', :mood, default: 'happy' PostgresqlEnum.reset_column_information - column = PostgresqlEnum.columns_hash["good_mood"] - assert_equal "happy", column.default + assert_equal "happy", PostgresqlEnum.column_defaults['good_mood'] assert_equal "happy", PostgresqlEnum.new.good_mood ensure PostgresqlEnum.reset_column_information diff --git a/activerecord/test/cases/adapters/postgresql/explain_test.rb b/activerecord/test/cases/adapters/postgresql/explain_test.rb index 416f84cb38..6ffb4c9f33 100644 --- a/activerecord/test/cases/adapters/postgresql/explain_test.rb +++ b/activerecord/test/cases/adapters/postgresql/explain_test.rb @@ -1,5 +1,6 @@ require "cases/helper" require 'models/developer' +require 'models/computer' module ActiveRecord module ConnectionAdapters @@ -11,16 +12,13 @@ module ActiveRecord explain = Developer.where(:id => 1).explain assert_match %(EXPLAIN for: SELECT "developers".* FROM "developers" WHERE "developers"."id" = $1), explain assert_match %(QUERY PLAN), explain - assert_match %(Index Scan using developers_pkey on developers), explain end def test_explain_with_eager_loading explain = Developer.where(:id => 1).includes(:audit_logs).explain assert_match %(QUERY PLAN), explain assert_match %(EXPLAIN for: SELECT "developers".* FROM "developers" WHERE "developers"."id" = $1), explain - assert_match %(Index Scan using developers_pkey on developers), explain - assert_match %(EXPLAIN for: SELECT "audit_logs".* FROM "audit_logs" WHERE "audit_logs"."developer_id" IN (1)), explain - assert_match %(Seq Scan on audit_logs), explain + assert_match %(EXPLAIN for: SELECT "audit_logs".* FROM "audit_logs" WHERE "audit_logs"."developer_id" = 1), explain end end end diff --git a/activerecord/test/cases/adapters/postgresql/full_text_test.rb b/activerecord/test/cases/adapters/postgresql/full_text_test.rb index ec646de5e9..dca35422b9 100644 --- a/activerecord/test/cases/adapters/postgresql/full_text_test.rb +++ b/activerecord/test/cases/adapters/postgresql/full_text_test.rb @@ -1,22 +1,34 @@ # encoding: utf-8 require "cases/helper" +require 'support/schema_dumping_helper' class PostgresqlFullTextTest < ActiveRecord::TestCase - class PostgresqlTsvector < ActiveRecord::Base; end + include SchemaDumpingHelper + class Tsvector < ActiveRecord::Base; end + + setup do + @connection = ActiveRecord::Base.connection + @connection.create_table('tsvectors') do |t| + t.tsvector 'text_vector' + end + end + + teardown do + @connection.execute 'DROP TABLE IF EXISTS tsvectors;' + end def test_tsvector_column - column = PostgresqlTsvector.columns_hash["text_vector"] + column = Tsvector.columns_hash["text_vector"] assert_equal :tsvector, column.type assert_equal "tsvector", column.sql_type assert_not column.number? - assert_not column.text? assert_not column.binary? assert_not column.array end def test_update_tsvector - PostgresqlTsvector.create text_vector: "'text' 'vector'" - tsvector = PostgresqlTsvector.first + Tsvector.create text_vector: "'text' 'vector'" + tsvector = Tsvector.first assert_equal "'text' 'vector'", tsvector.text_vector tsvector.text_vector = "'new' 'text' 'vector'" @@ -24,4 +36,9 @@ class PostgresqlFullTextTest < ActiveRecord::TestCase assert tsvector.reload assert_equal "'new' 'text' 'vector'", tsvector.text_vector end + + def test_schema_dump_with_shorthand + output = dump_table_schema("tsvectors") + assert_match %r{t.tsvector "text_vector"}, output + end end diff --git a/activerecord/test/cases/adapters/postgresql/geometric_test.rb b/activerecord/test/cases/adapters/postgresql/geometric_test.rb index 2f106ee664..228221e034 100644 --- a/activerecord/test/cases/adapters/postgresql/geometric_test.rb +++ b/activerecord/test/cases/adapters/postgresql/geometric_test.rb @@ -11,12 +11,10 @@ class PostgresqlPointTest < ActiveRecord::TestCase def setup @connection = ActiveRecord::Base.connection - @connection.transaction do - @connection.create_table('postgresql_points') do |t| - t.point :x - t.point :y, default: [12.2, 13.3] - t.point :z, default: "(14.4,15.5)" - end + @connection.create_table('postgresql_points') do |t| + t.point :x + t.point :y, default: [12.2, 13.3] + t.point :z, default: "(14.4,15.5)" end end @@ -28,19 +26,16 @@ class PostgresqlPointTest < ActiveRecord::TestCase column = PostgresqlPoint.columns_hash["x"] assert_equal :point, column.type assert_equal "point", column.sql_type - assert_not column.text? assert_not column.number? assert_not column.binary? assert_not column.array end def test_default - column = PostgresqlPoint.columns_hash["y"] - assert_equal [12.2, 13.3], column.default + assert_equal [12.2, 13.3], PostgresqlPoint.column_defaults['y'] assert_equal [12.2, 13.3], PostgresqlPoint.new.y - column = PostgresqlPoint.columns_hash["z"] - assert_equal [14.4, 15.5], column.default + assert_equal [14.4, 15.5], PostgresqlPoint.column_defaults['z'] assert_equal [14.4, 15.5], PostgresqlPoint.new.z end @@ -61,4 +56,84 @@ class PostgresqlPointTest < ActiveRecord::TestCase assert record.reload assert_equal [1.1, 2.2], record.x end + + def test_mutation + p = PostgresqlPoint.create! x: [10, 20] + + p.x[1] = 25 + p.save! + p.reload + + assert_equal [10.0, 25.0], p.x + assert_not p.changed? + end +end + +class PostgresqlGeometricTest < ActiveRecord::TestCase + 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 + end + end + + teardown do + @connection.execute 'DROP TABLE IF EXISTS postgresql_geometrics' + end + + def test_geometric_types + g = PostgresqlGeometric.new( + :a_line_segment => '(2.0, 3), (5.5, 7.0)', + :a_box => '2.0, 3, 5.5, 7.0', + :a_path => '[(2.0, 3), (5.5, 7.0), (8.5, 11.0)]', + :a_polygon => '((2.0, 3), (5.5, 7.0), (8.5, 11.0))', + :a_circle => '<(5.3, 10.4), 2>' + ) + + g.save! + + h = PostgresqlGeometric.find(g.id) + + assert_equal '[(2,3),(5.5,7)]', h.a_line_segment + assert_equal '(5.5,7),(2,3)', h.a_box # reordered to store upper right corner then bottom left corner + assert_equal '[(2,3),(5.5,7),(8.5,11)]', h.a_path + assert_equal '((2,3),(5.5,7),(8.5,11))', h.a_polygon + assert_equal '<(5.3,10.4),2>', h.a_circle + end + + def test_alternative_format + g = PostgresqlGeometric.new( + :a_line_segment => '((2.0, 3), (5.5, 7.0))', + :a_box => '(2.0, 3), (5.5, 7.0)', + :a_path => '((2.0, 3), (5.5, 7.0), (8.5, 11.0))', + :a_polygon => '2.0, 3, 5.5, 7.0, 8.5, 11.0', + :a_circle => '((5.3, 10.4), 2)' + ) + + g.save! + + h = PostgresqlGeometric.find(g.id) + assert_equal '[(2,3),(5.5,7)]', h.a_line_segment + assert_equal '(5.5,7),(2,3)', h.a_box # reordered to store upper right corner then bottom left corner + assert_equal '((2,3),(5.5,7),(8.5,11))', h.a_path + assert_equal '((2,3),(5.5,7),(8.5,11))', h.a_polygon + assert_equal '<(5.3,10.4),2>', h.a_circle + end + + def test_geometric_function + PostgresqlGeometric.create! a_path: '[(2.0, 3), (5.5, 7.0), (8.5, 11.0)]' # [ ] is an open path + PostgresqlGeometric.create! a_path: '((2.0, 3), (5.5, 7.0), (8.5, 11.0))' # ( ) is a closed path + + objs = PostgresqlGeometric.find_by_sql "SELECT isopen(a_path) FROM postgresql_geometrics ORDER BY id ASC" + assert_equal [true, false], objs.map(&:isopen) + + objs = PostgresqlGeometric.find_by_sql "SELECT isclosed(a_path) FROM postgresql_geometrics ORDER BY id ASC" + assert_equal [false, true], objs.map(&:isclosed) + end end diff --git a/activerecord/test/cases/adapters/postgresql/hstore_test.rb b/activerecord/test/cases/adapters/postgresql/hstore_test.rb index 83b495d600..00ff456e16 100644 --- a/activerecord/test/cases/adapters/postgresql/hstore_test.rb +++ b/activerecord/test/cases/adapters/postgresql/hstore_test.rb @@ -1,41 +1,40 @@ # encoding: utf-8 - require "cases/helper" -require 'active_record/base' -require 'active_record/connection_adapters/postgresql_adapter' +require 'support/schema_dumping_helper' -class PostgresqlHstoreTest < ActiveRecord::TestCase - class Hstore < ActiveRecord::Base - self.table_name = 'hstores' +if ActiveRecord::Base.connection.supports_extensions? + class PostgresqlHstoreTest < ActiveRecord::TestCase + include SchemaDumpingHelper + class Hstore < ActiveRecord::Base + self.table_name = 'hstores' - store_accessor :settings, :language, :timezone - end + store_accessor :settings, :language, :timezone + end - def setup - @connection = ActiveRecord::Base.connection + def setup + @connection = ActiveRecord::Base.connection - unless @connection.extension_enabled?('hstore') - @connection.enable_extension 'hstore' - @connection.commit_db_transaction - end + unless @connection.extension_enabled?('hstore') + @connection.enable_extension 'hstore' + @connection.commit_db_transaction + end - @connection.reconnect! + @connection.reconnect! - @connection.transaction do - @connection.create_table('hstores') do |t| - t.hstore 'tags', :default => '' - t.hstore 'payload', array: true - t.hstore 'settings' + @connection.transaction do + @connection.create_table('hstores') do |t| + t.hstore 'tags', :default => '' + t.hstore 'payload', array: true + t.hstore 'settings' + end end + @column = Hstore.columns_hash['tags'] end - @column = Hstore.columns_hash['tags'] - end - teardown do - @connection.execute 'drop table if exists hstores' - end + teardown do + @connection.execute 'drop table if exists hstores' + end - if ActiveRecord::Base.connection.supports_extensions? def test_hstore_included_in_extensions assert @connection.respond_to?(:extensions), "connection should have a list of extensions" assert @connection.extensions.include?('hstore'), "extension list should include hstore" @@ -56,7 +55,6 @@ class PostgresqlHstoreTest < ActiveRecord::TestCase assert_equal :hstore, @column.type assert_equal "hstore", @column.sql_type assert_not @column.number? - assert_not @column.text? assert_not @column.binary? assert_not @column.array end @@ -64,9 +62,8 @@ class PostgresqlHstoreTest < ActiveRecord::TestCase def test_default @connection.add_column 'hstores', 'permissions', :hstore, default: '"users"=>"read", "articles"=>"write"' Hstore.reset_column_information - column = Hstore.columns_hash["permissions"] - assert_equal({"users"=>"read", "articles"=>"write"}, column.default) + assert_equal({"users"=>"read", "articles"=>"write"}, Hstore.column_defaults['permissions']) assert_equal({"users"=>"read", "articles"=>"write"}, Hstore.new.permissions) ensure Hstore.reset_column_information @@ -113,13 +110,7 @@ class PostgresqlHstoreTest < ActiveRecord::TestCase end def test_type_cast_hstore - assert @column - - data = "\"1\"=>\"2\"" - hash = @column.class.string_to_hstore data - assert_equal({'1' => '2'}, hash) - assert_equal({'1' => '2'}, @column.type_cast_from_database(data)) - + assert_equal({'1' => '2'}, @column.type_cast_from_database("\"1\"=>\"2\"")) assert_equal({}, @column.type_cast_from_database("")) assert_equal({'key'=>nil}, @column.type_cast_from_database('key => NULL')) assert_equal({'c'=>'}','"a"'=>'b "a b'}, @column.type_cast_from_database(%q(c=>"}", "\"a\""=>"b \"a b"))) @@ -170,22 +161,23 @@ class PostgresqlHstoreTest < ActiveRecord::TestCase hstore.reload assert_equal 'four', hstore.settings['three'] + assert_not hstore.changed? end def test_gen1 - assert_equal(%q(" "=>""), @column.class.hstore_to_string({' '=>''})) + assert_equal(%q(" "=>""), @column.cast_type.type_cast_for_database({' '=>''})) end def test_gen2 - assert_equal(%q(","=>""), @column.class.hstore_to_string({','=>''})) + assert_equal(%q(","=>""), @column.cast_type.type_cast_for_database({','=>''})) end def test_gen3 - assert_equal(%q("="=>""), @column.class.hstore_to_string({'='=>''})) + assert_equal(%q("="=>""), @column.cast_type.type_cast_for_database({'='=>''})) end def test_gen4 - assert_equal(%q(">"=>""), @column.class.hstore_to_string({'>'=>''})) + assert_equal(%q(">"=>""), @column.cast_type.type_cast_for_database({'>'=>''})) end def test_parse1 @@ -295,16 +287,6 @@ class PostgresqlHstoreTest < ActiveRecord::TestCase assert_cycle("a\nb" => "c\nd") end - def test_update_all - hstore = Hstore.create! tags: { "one" => "two" } - - Hstore.update_all tags: { "three" => "four" } - assert_equal({ "three" => "four" }, hstore.reload.tags) - - Hstore.update_all tags: { } - assert_equal({ }, hstore.reload.tags) - end - class TagCollection def initialize(hash); @hash = hash end def to_hash; @hash end @@ -332,10 +314,13 @@ class PostgresqlHstoreTest < ActiveRecord::TestCase dupe = record.dup assert_equal({"one" => "two"}, dupe.tags.to_hash) end - end - private + def test_schema_dump_with_shorthand + output = dump_table_schema("hstores") + assert_match %r[t.hstore "tags",\s+default: {}], output + end + private def assert_array_cycle(array) # test creation x = Hstore.create!(payload: array) @@ -363,4 +348,5 @@ class PostgresqlHstoreTest < ActiveRecord::TestCase x.reload assert_equal(hash, x.tags) end + end end diff --git a/activerecord/test/cases/adapters/postgresql/infinity_test.rb b/activerecord/test/cases/adapters/postgresql/infinity_test.rb new file mode 100644 index 0000000000..74163ac712 --- /dev/null +++ b/activerecord/test/cases/adapters/postgresql/infinity_test.rb @@ -0,0 +1,60 @@ +require "cases/helper" + +class PostgresqlInfinityTest < ActiveRecord::TestCase + include InTimeZone + + class PostgresqlInfinity < ActiveRecord::Base + end + + setup do + @connection = ActiveRecord::Base.connection + @connection.create_table(:postgresql_infinities) do |t| + t.float :float + t.datetime :datetime + end + end + + teardown do + @connection.execute("DROP TABLE IF EXISTS postgresql_infinities") + end + + test "type casting infinity on a float column" do + record = PostgresqlInfinity.create!(float: Float::INFINITY) + record.reload + assert_equal Float::INFINITY, record.float + end + + test "update_all with infinity on a float column" do + record = PostgresqlInfinity.create! + PostgresqlInfinity.update_all(float: Float::INFINITY) + record.reload + assert_equal Float::INFINITY, record.float + end + + test "type casting infinity on a datetime column" do + record = PostgresqlInfinity.create!(datetime: Float::INFINITY) + record.reload + assert_equal Float::INFINITY, record.datetime + end + + test "update_all with infinity on a datetime column" do + record = PostgresqlInfinity.create! + PostgresqlInfinity.update_all(datetime: Float::INFINITY) + record.reload + assert_equal Float::INFINITY, record.datetime + end + + test "assigning 'infinity' on a datetime column with TZ aware attributes" do + begin + in_time_zone "Pacific Time (US & Canada)" do + record = PostgresqlInfinity.create!(datetime: "infinity") + assert_equal Float::INFINITY, record.datetime + assert_equal record.datetime, record.reload.datetime + end + ensure + # setting time_zone_aware_attributes causes the types to change. + # There is no way to do this automatically since it can be set on a superclass + PostgresqlInfinity.reset_column_information + end + end +end diff --git a/activerecord/test/cases/adapters/postgresql/integer_test.rb b/activerecord/test/cases/adapters/postgresql/integer_test.rb new file mode 100644 index 0000000000..7f8751281e --- /dev/null +++ b/activerecord/test/cases/adapters/postgresql/integer_test.rb @@ -0,0 +1,25 @@ +require "cases/helper" +require "active_support/core_ext/numeric/bytes" + +class PostgresqlIntegerTest < ActiveRecord::TestCase + class PgInteger < ActiveRecord::Base + end + + def setup + @connection = ActiveRecord::Base.connection + + @connection.transaction do + @connection.create_table "pg_integers", force: true do |t| + t.integer :quota, limit: 8, default: 2.gigabytes + end + end + end + + teardown do + @connection.execute "drop table if exists pg_integers" + end + + test "schema properly respects bigint ranges" do + assert_equal 2.gigabytes, PgInteger.new.quota + end +end diff --git a/activerecord/test/cases/adapters/postgresql/json_test.rb b/activerecord/test/cases/adapters/postgresql/json_test.rb index a3400a5a19..340ca29c0e 100644 --- a/activerecord/test/cases/adapters/postgresql/json_test.rb +++ b/activerecord/test/cases/adapters/postgresql/json_test.rb @@ -1,10 +1,10 @@ # encoding: utf-8 - require "cases/helper" -require 'active_record/base' -require 'active_record/connection_adapters/postgresql_adapter' +require 'support/schema_dumping_helper' + +module PostgresqlJSONSharedTestCases + include SchemaDumpingHelper -class PostgresqlJSONTest < ActiveRecord::TestCase class JsonDataType < ActiveRecord::Base self.table_name = 'json_data_type' @@ -16,8 +16,8 @@ class PostgresqlJSONTest < ActiveRecord::TestCase begin @connection.transaction do @connection.create_table('json_data_type') do |t| - t.json 'payload', :default => {} - t.json 'settings' + t.public_send column_type, 'payload', default: {} # t.json 'payload', default: {} + t.public_send column_type, 'settings' # t.json 'settings' end end rescue ActiveRecord::StatementInvalid @@ -26,26 +26,24 @@ class PostgresqlJSONTest < ActiveRecord::TestCase @column = JsonDataType.columns_hash['payload'] end - teardown do + def teardown @connection.execute 'drop table if exists json_data_type' end def test_column column = JsonDataType.columns_hash["payload"] - assert_equal :json, column.type - assert_equal "json", column.sql_type + assert_equal column_type, column.type + assert_equal column_type.to_s, column.sql_type assert_not column.number? - assert_not column.text? assert_not column.binary? assert_not column.array end def test_default - @connection.add_column 'json_data_type', 'permissions', :json, default: '{"users": "read", "posts": ["read", "write"]}' + @connection.add_column 'json_data_type', 'permissions', column_type, default: '{"users": "read", "posts": ["read", "write"]}' JsonDataType.reset_column_information - column = JsonDataType.columns_hash["permissions"] - assert_equal({"users"=>"read", "posts"=>["read", "write"]}, column.default) + assert_equal({"users"=>"read", "posts"=>["read", "write"]}, JsonDataType.column_defaults['permissions']) assert_equal({"users"=>"read", "posts"=>["read", "write"]}, JsonDataType.new.permissions) ensure JsonDataType.reset_column_information @@ -54,11 +52,11 @@ class PostgresqlJSONTest < ActiveRecord::TestCase def test_change_table_supports_json @connection.transaction do @connection.change_table('json_data_type') do |t| - t.json 'users', default: '{}' + t.public_send column_type, 'users', default: '{}' # t.json 'users', default: '{}' end JsonDataType.reset_column_information column = JsonDataType.columns_hash['users'] - assert_equal :json, column.type + assert_equal column_type, column.type raise ActiveRecord::Rollback # reset the schema change end @@ -66,6 +64,11 @@ class PostgresqlJSONTest < ActiveRecord::TestCase JsonDataType.reset_column_information end + def test_schema_dumping + output = dump_table_schema("json_data_type") + assert_match(/t.#{column_type.to_s}\s+"payload",\s+default: {}/, output) + end + def test_cast_value_on_write x = JsonDataType.new payload: {"string" => "foo", :symbol => :bar} assert_equal({"string" => "foo", :symbol => :bar}, x.payload_before_type_cast) @@ -78,7 +81,7 @@ class PostgresqlJSONTest < ActiveRecord::TestCase column = JsonDataType.columns_hash["payload"] data = "{\"a_key\":\"a_value\"}" - hash = column.class.string_to_json data + hash = column.type_cast_from_database(data) assert_equal({'a_key' => 'a_value'}, hash) assert_equal({'a_key' => 'a_value'}, column.type_cast_from_database(data)) @@ -156,16 +159,6 @@ class PostgresqlJSONTest < ActiveRecord::TestCase assert_equal "320×480", y.resolution end - def test_update_all - json = JsonDataType.create! payload: { "one" => "two" } - - JsonDataType.update_all payload: { "three" => "four" } - assert_equal({ "three" => "four" }, json.reload.payload) - - JsonDataType.update_all payload: { } - assert_equal({ }, json.reload.payload) - end - def test_changes_in_place json = JsonDataType.new assert_not json.changed? @@ -183,6 +176,23 @@ class PostgresqlJSONTest < ActiveRecord::TestCase json.save! json.reload - assert json.payload['three'] = 'four' + assert_equal({ 'one' => 'two', 'three' => 'four' }, json.payload) + assert_not json.changed? + end +end + +class PostgresqlJSONTest < ActiveRecord::TestCase + include PostgresqlJSONSharedTestCases + + def column_type + :json + end +end + +class PostgresqlJSONBTest < ActiveRecord::TestCase + include PostgresqlJSONSharedTestCases + + def column_type + :jsonb end end diff --git a/activerecord/test/cases/adapters/postgresql/ltree_test.rb b/activerecord/test/cases/adapters/postgresql/ltree_test.rb index ddb7cd658c..5a0f505072 100644 --- a/activerecord/test/cases/adapters/postgresql/ltree_test.rb +++ b/activerecord/test/cases/adapters/postgresql/ltree_test.rb @@ -1,7 +1,9 @@ # encoding: utf-8 require "cases/helper" +require 'support/schema_dumping_helper' class PostgresqlLtreeTest < ActiveRecord::TestCase + include SchemaDumpingHelper class Ltree < ActiveRecord::Base self.table_name = 'ltrees' end @@ -9,9 +11,7 @@ class PostgresqlLtreeTest < ActiveRecord::TestCase def setup @connection = ActiveRecord::Base.connection - unless @connection.extension_enabled?('ltree') - @connection.enable_extension 'ltree' - end + enable_extension!('ltree', @connection) @connection.transaction do @connection.create_table('ltrees') do |t| @@ -31,7 +31,6 @@ class PostgresqlLtreeTest < ActiveRecord::TestCase assert_equal :ltree, column.type assert_equal "ltree", column.sql_type assert_not column.number? - assert_not column.text? assert_not column.binary? assert_not column.array end @@ -46,4 +45,9 @@ class PostgresqlLtreeTest < ActiveRecord::TestCase ltree = Ltree.first assert_equal '1.2.3', ltree.path end + + def test_schema_dump_with_shorthand + output = dump_table_schema("ltrees") + assert_match %r[t.ltree "path"], output + end end diff --git a/activerecord/test/cases/adapters/postgresql/money_test.rb b/activerecord/test/cases/adapters/postgresql/money_test.rb index bdfeedafab..54cff192c1 100644 --- a/activerecord/test/cases/adapters/postgresql/money_test.rb +++ b/activerecord/test/cases/adapters/postgresql/money_test.rb @@ -10,7 +10,7 @@ class PostgresqlMoneyTest < ActiveRecord::TestCase setup do @connection = ActiveRecord::Base.connection @connection.execute("set lc_monetary = 'C'") - @connection.create_table('postgresql_moneys') do |t| + @connection.create_table('postgresql_moneys', force: true) do |t| t.column "wealth", "money" t.column "depth", "money", default: "150.55" end @@ -26,14 +26,12 @@ class PostgresqlMoneyTest < ActiveRecord::TestCase assert_equal "money", column.sql_type assert_equal 2, column.scale assert column.number? - assert_not column.text? assert_not column.binary? assert_not column.array end def test_default - column = PostgresqlMoney.columns_hash["depth"] - assert_equal BigDecimal.new("150.55"), column.default + assert_equal BigDecimal.new("150.55"), PostgresqlMoney.column_defaults['depth'] assert_equal BigDecimal.new("150.55"), PostgresqlMoney.new.depth end @@ -71,4 +69,28 @@ class PostgresqlMoneyTest < ActiveRecord::TestCase money.reload assert_equal new_value, money.wealth end + + def test_update_all_with_money_string + money = PostgresqlMoney.create! + PostgresqlMoney.update_all(wealth: "987.65") + money.reload + + assert_equal 987.65, money.wealth + end + + def test_update_all_with_money_big_decimal + money = PostgresqlMoney.create! + PostgresqlMoney.update_all(wealth: '123.45'.to_d) + money.reload + + assert_equal 123.45, money.wealth + end + + def test_update_all_with_money_numeric + money = PostgresqlMoney.create! + PostgresqlMoney.update_all(wealth: 123.45) + money.reload + + assert_equal 123.45, money.wealth + end end diff --git a/activerecord/test/cases/adapters/postgresql/network_test.rb b/activerecord/test/cases/adapters/postgresql/network_test.rb index 32085cbb17..4e49ea1e02 100644 --- a/activerecord/test/cases/adapters/postgresql/network_test.rb +++ b/activerecord/test/cases/adapters/postgresql/network_test.rb @@ -1,8 +1,22 @@ # encoding: utf-8 require "cases/helper" +require 'support/schema_dumping_helper' class PostgresqlNetworkTest < ActiveRecord::TestCase - class PostgresqlNetworkAddress < ActiveRecord::Base + include SchemaDumpingHelper + class PostgresqlNetworkAddress < ActiveRecord::Base; end + + setup do + @connection = ActiveRecord::Base.connection + @connection.create_table('postgresql_network_addresses', force: true) do |t| + t.inet 'inet_address', default: "192.168.1.1" + t.cidr 'cidr_address', default: "192.168.1.0/24" + t.macaddr 'mac_address', default: "ff:ff:ff:ff:ff:ff" + end + end + + teardown do + @connection.execute 'DROP TABLE IF EXISTS postgresql_network_addresses' end def test_cidr_column @@ -10,7 +24,6 @@ class PostgresqlNetworkTest < ActiveRecord::TestCase assert_equal :cidr, column.type assert_equal "cidr", column.sql_type assert_not column.number? - assert_not column.text? assert_not column.binary? assert_not column.array end @@ -20,7 +33,6 @@ class PostgresqlNetworkTest < ActiveRecord::TestCase assert_equal :inet, column.type assert_equal "inet", column.sql_type assert_not column.number? - assert_not column.text? assert_not column.binary? assert_not column.array end @@ -30,7 +42,6 @@ class PostgresqlNetworkTest < ActiveRecord::TestCase assert_equal :macaddr, column.type assert_equal "macaddr", column.sql_type assert_not column.number? - assert_not column.text? assert_not column.binary? assert_not column.array end @@ -71,4 +82,11 @@ class PostgresqlNetworkTest < ActiveRecord::TestCase assert_nil invalid_address.cidr_address_before_type_cast assert_nil invalid_address.inet_address_before_type_cast end + + def test_schema_dump_with_shorthand + output = dump_table_schema("postgresql_network_addresses") + assert_match %r{t.inet\s+"inet_address",\s+default: "192.168.1.1"}, output + assert_match %r{t.cidr\s+"cidr_address",\s+default: "192.168.1.0/24"}, output + assert_match %r{t.macaddr\s+"mac_address",\s+default: "ff:ff:ff:ff:ff:ff"}, output + end end diff --git a/activerecord/test/cases/adapters/postgresql/numbers_test.rb b/activerecord/test/cases/adapters/postgresql/numbers_test.rb new file mode 100644 index 0000000000..70aa898439 --- /dev/null +++ b/activerecord/test/cases/adapters/postgresql/numbers_test.rb @@ -0,0 +1,49 @@ +require "cases/helper" + +class PostgresqlNumberTest < ActiveRecord::TestCase + class PostgresqlNumber < ActiveRecord::Base; end + + setup do + @connection = ActiveRecord::Base.connection + @connection.create_table('postgresql_numbers', force: true) do |t| + t.column 'single', 'REAL' + t.column 'double', 'DOUBLE PRECISION' + end + end + + teardown do + @connection.execute 'DROP TABLE IF EXISTS postgresql_numbers' + end + + def test_data_type + assert_equal :float, PostgresqlNumber.columns_hash["single"].type + assert_equal :float, PostgresqlNumber.columns_hash["double"].type + end + + def test_values + @connection.execute("INSERT INTO postgresql_numbers (id, single, double) VALUES (1, 123.456, 123456.789)") + @connection.execute("INSERT INTO postgresql_numbers (id, single, double) VALUES (2, '-Infinity', 'Infinity')") + @connection.execute("INSERT INTO postgresql_numbers (id, single, double) VALUES (3, 123.456, 'NaN')") + + first, second, third = PostgresqlNumber.find(1, 2, 3) + + assert_equal 123.456, first.single + assert_equal 123456.789, first.double + assert_equal(-::Float::INFINITY, second.single) + assert_equal ::Float::INFINITY, second.double + assert_same ::Float::NAN, third.double + end + + def test_update + record = PostgresqlNumber.create! single: "123.456", double: "123456.789" + new_single = 789.012 + new_double = 789012.345 + record.single = new_single + record.double = new_double + record.save! + + record.reload + assert_equal new_single, record.single + assert_equal new_double, record.double + end +end diff --git a/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb b/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb index cfff1f980b..6bb2b26cd5 100644 --- a/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb +++ b/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb @@ -54,6 +54,12 @@ 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') @@ -134,18 +140,18 @@ module ActiveRecord end def test_default_sequence_name - assert_equal PostgreSQL::Name.new('public', 'accounts_id_seq'), + assert_equal 'public.accounts_id_seq', @connection.default_sequence_name('accounts', 'id') - assert_equal PostgreSQL::Name.new('public', 'accounts_id_seq'), + assert_equal 'public.accounts_id_seq', @connection.default_sequence_name('accounts') end def test_default_sequence_name_bad_table - assert_equal PostgreSQL::Name.new(nil, 'zomg_id_seq'), + assert_equal 'zomg_id_seq', @connection.default_sequence_name('zomg', 'id') - assert_equal PostgreSQL::Name.new(nil, 'zomg_id_seq'), + assert_equal 'zomg_id_seq', @connection.default_sequence_name('zomg') end @@ -153,7 +159,7 @@ module ActiveRecord with_example_table do pk, seq = @connection.pk_and_sequence_for('ex') assert_equal 'id', pk - assert_equal @connection.default_sequence_name('ex', 'id'), seq + assert_equal @connection.default_sequence_name('ex', 'id'), seq.to_s end end @@ -161,7 +167,7 @@ module ActiveRecord with_example_table 'code serial primary key' do pk, seq = @connection.pk_and_sequence_for('ex') assert_equal 'code', pk - assert_equal @connection.default_sequence_name('ex', 'code'), seq + assert_equal @connection.default_sequence_name('ex', 'code'), seq.to_s end end @@ -304,11 +310,8 @@ module ActiveRecord end def test_substitute_at - bind = @connection.substitute_at(nil, 0) - assert_equal Arel.sql('$1'), bind - - bind = @connection.substitute_at(nil, 1) - assert_equal Arel.sql('$2'), bind + bind = @connection.substitute_at(nil) + assert_equal Arel.sql('$1'), bind.to_sql end def test_partial_index @@ -334,6 +337,14 @@ module ActiveRecord @connection.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', + @connection.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", @connection.columns_for_distinct("posts.id", ["posts.created_at desc", "", " "]) diff --git a/activerecord/test/cases/adapters/postgresql/quoting_test.rb b/activerecord/test/cases/adapters/postgresql/quoting_test.rb index 218c59247e..9ac0036d66 100644 --- a/activerecord/test/cases/adapters/postgresql/quoting_test.rb +++ b/activerecord/test/cases/adapters/postgresql/quoting_test.rb @@ -10,53 +10,38 @@ module ActiveRecord end def test_type_cast_true - c = PostgreSQLColumn.new(nil, 1, Type::Boolean.new, 'boolean') - assert_equal 't', @conn.type_cast(true, nil) - assert_equal 't', @conn.type_cast(true, c) + assert_equal 't', @conn.type_cast(true) end def test_type_cast_false - c = PostgreSQLColumn.new(nil, 1, Type::Boolean.new, 'boolean') - assert_equal 'f', @conn.type_cast(false, nil) - assert_equal 'f', @conn.type_cast(false, c) - end - - def test_type_cast_cidr - ip = IPAddr.new('255.0.0.0/8') - c = PostgreSQLColumn.new(nil, ip, OID::Cidr.new, 'cidr') - assert_equal ip, @conn.type_cast(ip, c) - end - - def test_type_cast_inet - ip = IPAddr.new('255.1.0.0/8') - c = PostgreSQLColumn.new(nil, ip, OID::Cidr.new, 'inet') - assert_equal ip, @conn.type_cast(ip, c) + assert_equal 'f', @conn.type_cast(false) end def test_quote_float_nan nan = 0.0/0 - c = PostgreSQLColumn.new(nil, 1, OID::Float.new, 'float') - assert_equal "'NaN'", @conn.quote(nan, c) + assert_equal "'NaN'", @conn.quote(nan) end def test_quote_float_infinity infinity = 1.0/0 - c = PostgreSQLColumn.new(nil, 1, OID::Float.new, 'float') - assert_equal "'Infinity'", @conn.quote(infinity, c) - end - - def test_quote_cast_numeric - fixnum = 666 - c = PostgreSQLColumn.new(nil, nil, Type::String.new, 'varchar') - assert_equal "'666'", @conn.quote(fixnum, c) - c = PostgreSQLColumn.new(nil, nil, Type::Text.new, 'text') - assert_equal "'666'", @conn.quote(fixnum, c) + assert_equal "'Infinity'", @conn.quote(infinity) end def test_quote_time_usec assert_equal "'1970-01-01 00:00:00.000000'", @conn.quote(Time.at(0)) assert_equal "'1970-01-01 00:00:00.000000'", @conn.quote(Time.at(0).to_datetime) end + + def test_quote_range + range = "1,2]'; SELECT * FROM users; --".."a" + c = PostgreSQLColumn.new(nil, nil, OID::Range.new(Type::Integer.new, :int8range)) + assert_equal "'[1,0]'", @conn.quote(range, c) + end + + def test_quote_bit_string + c = PostgreSQLColumn.new(nil, 1, OID::Bit.new) + assert_equal nil, @conn.quote("'); SELECT * FROM users; /*\n01\n*/--", c) + end end end end diff --git a/activerecord/test/cases/adapters/postgresql/range_test.rb b/activerecord/test/cases/adapters/postgresql/range_test.rb index 0f6e39322c..d812cd01c4 100644 --- a/activerecord/test/cases/adapters/postgresql/range_test.rb +++ b/activerecord/test/cases/adapters/postgresql/range_test.rb @@ -262,6 +262,23 @@ _SQL assert_raises(ArgumentError) { PostgresqlRange.create!(float_range: "(0.5, 0.7]") } end + def test_update_all_with_ranges + PostgresqlRange.create! + + PostgresqlRange.update_all(int8_range: 1..100) + + assert_equal 1...101, PostgresqlRange.first.int8_range + end + + def test_ranges_correctly_escape_input + range = "-1,2]'; DROP TABLE postgresql_ranges; --".."a" + PostgresqlRange.update_all(int8_range: range) + + assert_nothing_raised do + PostgresqlRange.first + end + end + private def assert_equal_round_trip(range, attribute, value) round_trip(range, attribute, value) diff --git a/activerecord/test/cases/adapters/postgresql/rename_table_test.rb b/activerecord/test/cases/adapters/postgresql/rename_table_test.rb new file mode 100644 index 0000000000..056a035622 --- /dev/null +++ b/activerecord/test/cases/adapters/postgresql/rename_table_test.rb @@ -0,0 +1,34 @@ +require "cases/helper" + +class PostgresqlRenameTableTest < ActiveRecord::TestCase + def setup + @connection = ActiveRecord::Base.connection + @connection.create_table :before_rename, force: true + end + + def teardown + @connection.execute 'DROP TABLE IF EXISTS "before_rename"' + @connection.execute 'DROP TABLE IF EXISTS "after_rename"' + end + + test "renaming a table also renames the primary key index" do + # sanity check + assert_equal 1, num_indices_named("before_rename_pkey") + assert_equal 0, num_indices_named("after_rename_pkey") + + @connection.rename_table :before_rename, :after_rename + + assert_equal 0, num_indices_named("before_rename_pkey") + assert_equal 1, num_indices_named("after_rename_pkey") + end + + private + + def num_indices_named(name) + @connection.execute(<<-SQL).values.length + SELECT 1 FROM "pg_index" + JOIN "pg_class" ON "pg_index"."indexrelid" = "pg_class"."oid" + WHERE "pg_class"."relname" = '#{name}' + SQL + end +end diff --git a/activerecord/test/cases/adapters/postgresql/schema_test.rb b/activerecord/test/cases/adapters/postgresql/schema_test.rb index 9e5fd17dc4..e99f1e2867 100644 --- a/activerecord/test/cases/adapters/postgresql/schema_test.rb +++ b/activerecord/test/cases/adapters/postgresql/schema_test.rb @@ -1,4 +1,6 @@ require "cases/helper" +require 'models/default' +require 'support/schema_dumping_helper' class SchemaTest < ActiveRecord::TestCase self.use_transactional_fixtures = false @@ -87,7 +89,7 @@ class SchemaTest < ActiveRecord::TestCase end def test_schema_names - assert_equal ["public", "schema_1", "test_schema", "test_schema2"], @connection.schema_names + assert_equal ["public", "test_schema", "test_schema2"], @connection.schema_names end def test_create_schema @@ -387,6 +389,14 @@ class SchemaTest < ActiveRecord::TestCase assert_equal "1", @connection.select_value("SELECT nextval('#{sequence_name}')") end + def test_set_pk_sequence + table_name = "#{SCHEMA_NAME}.#{PK_TABLE_NAME}" + _, sequence_name = @connection.pk_and_sequence_for table_name + @connection.set_pk_sequence! table_name, 123 + assert_equal "124", @connection.select_value("SELECT nextval('#{sequence_name}')") + @connection.reset_pk_sequence! table_name + end + private def columns(table_name) @connection.send(:column_definitions, table_name).map do |name, type, default| @@ -403,7 +413,7 @@ class SchemaTest < ActiveRecord::TestCase def do_dump_index_tests_for_schema(this_schema_name, first_index_column_name, second_index_column_name, third_index_column_name, fourth_index_column_name) with_schema_search_path(this_schema_name) do - indexes = @connection.indexes(TABLE_NAME).sort_by {|i| i.name} + indexes = @connection.indexes(TABLE_NAME).sort_by(&:name) assert_equal 4,indexes.size do_dump_index_assertions_for_one_index(indexes[0], INDEX_A_NAME, first_index_column_name) @@ -426,3 +436,77 @@ class SchemaTest < ActiveRecord::TestCase assert_equal this_index_name, this_index.name end end + +class SchemaForeignKeyTest < ActiveRecord::TestCase + include SchemaDumpingHelper + + setup do + @connection = ActiveRecord::Base.connection + end + + def test_dump_foreign_key_targeting_different_schema + @connection.create_schema "my_schema" + @connection.create_table "my_schema.trains" do |t| + t.string :name + end + @connection.create_table "wagons" do |t| + t.integer :train_id + end + @connection.add_foreign_key "wagons", "my_schema.trains", column: "train_id" + output = dump_table_schema "wagons" + assert_match %r{\s+add_foreign_key "wagons", "my_schema.trains", column: "train_id"$}, output + ensure + @connection.execute "DROP TABLE IF EXISTS wagons" + @connection.execute "DROP TABLE IF EXISTS my_schema.trains" + @connection.execute "DROP SCHEMA IF EXISTS my_schema" + end +end + +class DefaultsUsingMultipleSchemasAndDomainTest < ActiveSupport::TestCase + setup do + @connection = ActiveRecord::Base.connection + @connection.execute "DROP SCHEMA IF EXISTS schema_1 CASCADE" + @connection.execute "CREATE SCHEMA schema_1" + @connection.execute "CREATE DOMAIN schema_1.text AS text" + @connection.execute "CREATE DOMAIN schema_1.varchar AS varchar" + @connection.execute "CREATE DOMAIN schema_1.bpchar AS bpchar" + + @old_search_path = @connection.schema_search_path + @connection.schema_search_path = "schema_1, pg_catalog" + @connection.create_table "defaults" do |t| + t.text "text_col", default: "some value" + t.string "string_col", default: "some value" + end + Default.reset_column_information + end + + teardown do + @connection.schema_search_path = @old_search_path + @connection.execute "DROP SCHEMA IF EXISTS schema_1 CASCADE" + Default.reset_column_information + end + + def test_text_defaults_in_new_schema_when_overriding_domain + assert_equal "some value", Default.new.text_col, "Default of text column was not correctly parsed" + end + + def test_string_defaults_in_new_schema_when_overriding_domain + assert_equal "some value", Default.new.string_col, "Default of string column was not correctly parsed" + end + + def test_bpchar_defaults_in_new_schema_when_overriding_domain + @connection.execute "ALTER TABLE defaults ADD bpchar_col bpchar DEFAULT 'some value'" + Default.reset_column_information + assert_equal "some value", Default.new.bpchar_col, "Default of bpchar column was not correctly parsed" + end + + def test_text_defaults_after_updating_column_default + @connection.execute "ALTER TABLE defaults ALTER COLUMN text_col SET DEFAULT 'some text'::schema_1.text" + assert_equal "some text", Default.new.text_col, "Default of text column was not correctly parsed after updating default using '::text' since postgreSQL will add parens to the default in db" + end + + def test_default_containing_quote_and_colons + @connection.execute "ALTER TABLE defaults ALTER COLUMN string_col SET DEFAULT 'foo''::bar'" + assert_equal "foo'::bar", Default.new.string_col + end +end diff --git a/activerecord/test/cases/adapters/postgresql/sql_types_test.rb b/activerecord/test/cases/adapters/postgresql/sql_types_test.rb deleted file mode 100644 index d7d40f6385..0000000000 --- a/activerecord/test/cases/adapters/postgresql/sql_types_test.rb +++ /dev/null @@ -1,18 +0,0 @@ -require "cases/helper" - -class SqlTypesTest < ActiveRecord::TestCase - def test_binary_types - assert_equal 'bytea', type_to_sql(:binary, 100_000) - assert_raise ActiveRecord::ActiveRecordError do - type_to_sql :binary, 4294967295 - end - assert_equal 'text', type_to_sql(:text, 100_000) - assert_raise ActiveRecord::ActiveRecordError do - type_to_sql :text, 4294967295 - end - end - - def type_to_sql(*args) - ActiveRecord::Base.connection.type_to_sql(*args) - end -end diff --git a/activerecord/test/cases/adapters/postgresql/timestamp_test.rb b/activerecord/test/cases/adapters/postgresql/timestamp_test.rb index 3614b29190..eb32c4d2c2 100644 --- a/activerecord/test/cases/adapters/postgresql/timestamp_test.rb +++ b/activerecord/test/cases/adapters/postgresql/timestamp_test.rb @@ -87,7 +87,7 @@ class TimestampTest < ActiveRecord::TestCase def test_timestamps_helper_with_custom_precision ActiveRecord::Base.connection.create_table(:foos) do |t| - t.timestamps :precision => 4 + t.timestamps :null => true, :precision => 4 end assert_equal 4, activerecord_column_option('foos', 'created_at', 'precision') assert_equal 4, activerecord_column_option('foos', 'updated_at', 'precision') @@ -95,7 +95,7 @@ class TimestampTest < ActiveRecord::TestCase def test_passing_precision_to_timestamp_does_not_set_limit ActiveRecord::Base.connection.create_table(:foos) do |t| - t.timestamps :precision => 4 + t.timestamps :null => true, :precision => 4 end assert_nil activerecord_column_option("foos", "created_at", "limit") assert_nil activerecord_column_option("foos", "updated_at", "limit") @@ -104,14 +104,14 @@ class TimestampTest < ActiveRecord::TestCase def test_invalid_timestamp_precision_raises_error assert_raises ActiveRecord::ActiveRecordError do ActiveRecord::Base.connection.create_table(:foos) do |t| - t.timestamps :precision => 7 + t.timestamps :null => true, :precision => 7 end end end def test_postgres_agrees_with_activerecord_about_precision ActiveRecord::Base.connection.create_table(:foos) do |t| - t.timestamps :precision => 4 + t.timestamps :null => true, :precision => 4 end assert_equal '4', pg_datetime_precision('foos', 'created_at') assert_equal '4', pg_datetime_precision('foos', 'updated_at') diff --git a/activerecord/test/cases/adapters/postgresql/type_lookup_test.rb b/activerecord/test/cases/adapters/postgresql/type_lookup_test.rb new file mode 100644 index 0000000000..c88259d274 --- /dev/null +++ b/activerecord/test/cases/adapters/postgresql/type_lookup_test.rb @@ -0,0 +1,33 @@ +require 'cases/helper' + +class PostgresqlTypeLookupTest < ActiveRecord::TestCase + setup do + @connection = ActiveRecord::Base.connection + end + + test "array delimiters are looked up correctly" do + box_array = @connection.type_map.lookup(1020) + int_array = @connection.type_map.lookup(1007) + + assert_equal ';', box_array.delimiter + assert_equal ',', int_array.delimiter + end + + test "array types correctly respect registration of subtypes" do + int_array = @connection.type_map.lookup(1007, -1, "integer[]") + bigint_array = @connection.type_map.lookup(1016, -1, "bigint[]") + big_array = [123456789123456789] + + assert_raises(RangeError) { int_array.type_cast_from_user(big_array) } + assert_equal big_array, bigint_array.type_cast_from_user(big_array) + end + + test "range types correctly respect registration of subtypes" do + int_range = @connection.type_map.lookup(3904, -1, "int4range") + bigint_range = @connection.type_map.lookup(3926, -1, "int8range") + big_range = 0..123456789123456789 + + assert_raises(RangeError) { int_range.type_cast_for_database(big_range) } + assert_equal "[0,123456789123456789]", bigint_range.type_cast_for_database(big_range) + end +end diff --git a/activerecord/test/cases/adapters/postgresql/uuid_test.rb b/activerecord/test/cases/adapters/postgresql/uuid_test.rb index 40ed0f64a4..d5d2dd16e2 100644 --- a/activerecord/test/cases/adapters/postgresql/uuid_test.rb +++ b/activerecord/test/cases/adapters/postgresql/uuid_test.rb @@ -1,23 +1,12 @@ # encoding: utf-8 - require "cases/helper" -require 'active_record/base' -require 'active_record/connection_adapters/postgresql_adapter' +require 'support/schema_dumping_helper' module PostgresqlUUIDHelper def connection @connection ||= ActiveRecord::Base.connection end - def enable_uuid_ossp - unless connection.extension_enabled?('uuid-ossp') - connection.enable_extension 'uuid-ossp' - connection.commit_db_transaction - end - - connection.reconnect! - end - def drop_table(name) connection.execute "drop table if exists #{name}" end @@ -25,6 +14,7 @@ end class PostgresqlUUIDTest < ActiveRecord::TestCase include PostgresqlUUIDHelper + include SchemaDumpingHelper class UUIDType < ActiveRecord::Base self.table_name = "uuid_data_type" @@ -60,7 +50,6 @@ class PostgresqlUUIDTest < ActiveRecord::TestCase assert_equal :uuid, column.type assert_equal "uuid", column.sql_type assert_not column.number? - assert_not column.text? assert_not column.binary? assert_not column.array end @@ -70,6 +59,47 @@ class PostgresqlUUIDTest < ActiveRecord::TestCase assert_equal(nil, UUIDType.last.guid) end + def test_treat_invalid_uuid_as_nil + uuid = UUIDType.create! guid: 'foobar' + assert_equal(nil, uuid.guid) + end + + def test_invalid_uuid_dont_modify_before_type_cast + uuid = UUIDType.new guid: 'foobar' + assert_equal 'foobar', uuid.guid_before_type_cast + end + + def test_acceptable_uuid_regex + # Valid uuids + ['A0EEBC99-9C0B-4EF8-BB6D-6BB9BD380A11', + '{a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11}', + 'a0eebc999c0b4ef8bb6d6bb9bd380a11', + 'a0ee-bc99-9c0b-4ef8-bb6d-6bb9-bd38-0a11', + '{a0eebc99-9c0b4ef8-bb6d6bb9-bd380a11}', + # The following is not a valid RFC 4122 UUID, but PG doesn't seem to care, + # so we shouldn't block it either. (Pay attention to "fb6d" – the "f" here + # is invalid – it must be one of 8, 9, A, B, a, b according to the spec.) + '{a0eebc99-9c0b-4ef8-fb6d-6bb9bd380a11}', + ].each do |valid_uuid| + uuid = UUIDType.new guid: valid_uuid + assert_not_nil uuid.guid + end + + # Invalid uuids + [['A0EEBC99-9C0B-4EF8-BB6D-6BB9BD380A11'], + Hash.new, + 0, + 0.0, + true, + 'Z0000C99-9C0B-4EF8-BB6D-6BB9BD380A11', + 'a0eebc999r0b4ef8ab6d6bb9bd380a11', + 'a0ee-bc99------4ef8-bb6d-6bb9-bd38-0a11', + '{a0eebc99-bb6d6bb9-bd380a11}'].each do |invalid_uuid| + uuid = UUIDType.new guid: invalid_uuid + assert_nil uuid.guid + end + end + def test_uuid_formats ["A0EEBC99-9C0B-4EF8-BB6D-6BB9BD380A11", "{a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11}", @@ -81,26 +111,86 @@ class PostgresqlUUIDTest < ActiveRecord::TestCase assert_equal "a0eebc99-9c0b-4ef8-bb6d-6bb9bd380a11", uuid.guid end end + + def test_schema_dump_with_shorthand + output = dump_table_schema "uuid_data_type" + assert_match %r{t.uuid "guid"}, output + end + + def test_uniqueness_validation_ignores_uuid + klass = Class.new(ActiveRecord::Base) do + self.table_name = "uuid_data_type" + validates :guid, uniqueness: { case_sensitive: false } + + def self.name + "UUIDType" + end + end + + record = klass.create!(guid: "a0ee-bc99-9c0b-4ef8-bb6d-6bb9-bd38-0a11") + duplicate = klass.new(guid: record.guid) + + assert record.guid.present? # Ensure we actually are testing a UUID + assert_not duplicate.valid? + end +end + +class PostgresqlLargeKeysTest < ActiveRecord::TestCase + include PostgresqlUUIDHelper + include SchemaDumpingHelper + + def setup + connection.create_table('big_serials', id: :bigserial) do |t| + t.string 'name' + end + end + + def test_omg + schema = dump_table_schema "big_serials" + assert_match "create_table \"big_serials\", id: :bigserial", schema + end + + def teardown + drop_table "big_serials" + end end class PostgresqlUUIDGenerationTest < ActiveRecord::TestCase include PostgresqlUUIDHelper + include SchemaDumpingHelper class UUID < ActiveRecord::Base self.table_name = 'pg_uuids' end setup do - enable_uuid_ossp + 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()' end + + # Create custom PostgreSQL function to generate UUIDs + # to test dumping tables which columns have defaults with custom functions + connection.execute <<-SQL + CREATE OR REPLACE FUNCTION my_uuid_generator() RETURNS uuid + AS $$ SELECT * FROM uuid_generate_v4() $$ + LANGUAGE SQL VOLATILE; + SQL + + # Create such a table with custom function as default value generator + connection.create_table('pg_uuids_2', id: :uuid, default: 'my_uuid_generator()') do |t| + t.string 'name' + t.uuid 'other_uuid_2', default: 'my_uuid_generator()' + end end teardown do 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? @@ -127,19 +217,25 @@ class PostgresqlUUIDGenerationTest < ActiveRecord::TestCase end def test_schema_dumper_for_uuid_primary_key - schema = StringIO.new - ActiveRecord::SchemaDumper.dump(connection, schema) - assert_match(/\bcreate_table "pg_uuids", id: :uuid, default: "uuid_generate_v1\(\)"/, schema.string) - assert_match(/t\.uuid "other_uuid", default: "uuid_generate_v4\(\)"/, schema.string) + schema = dump_table_schema "pg_uuids" + assert_match(/\bcreate_table "pg_uuids", id: :uuid, default: "uuid_generate_v1\(\)"/, schema) + assert_match(/t\.uuid "other_uuid", default: "uuid_generate_v4\(\)"/, schema) + end + + def test_schema_dumper_for_uuid_primary_key_with_custom_default + schema = dump_table_schema "pg_uuids_2" + assert_match(/\bcreate_table "pg_uuids_2", id: :uuid, default: "my_uuid_generator\(\)"/, schema) + assert_match(/t\.uuid "other_uuid_2", default: "my_uuid_generator\(\)"/, schema) end end end class PostgresqlUUIDTestNilDefault < ActiveRecord::TestCase include PostgresqlUUIDHelper + include SchemaDumpingHelper setup do - enable_uuid_ossp + enable_extension!('uuid-ossp', connection) connection.create_table('pg_uuids', id: false) do |t| t.primary_key :id, :uuid, default: nil @@ -149,6 +245,7 @@ class PostgresqlUUIDTestNilDefault < ActiveRecord::TestCase teardown do drop_table "pg_uuids" + disable_extension!('uuid-ossp', connection) end if ActiveRecord::Base.connection.supports_extensions? @@ -159,6 +256,11 @@ class PostgresqlUUIDTestNilDefault < ActiveRecord::TestCase WHERE a.attname='id' AND a.attrelid = 'pg_uuids'::regclass").first assert_nil col_desc["default"] end + + def test_schema_dumper_for_uuid_primary_key_with_default_override_via_nil + schema = dump_table_schema "pg_uuids" + assert_match(/\bcreate_table "pg_uuids", id: :uuid, default: nil/, schema) + end end end @@ -176,24 +278,23 @@ class PostgresqlUUIDTestInverseOf < ActiveRecord::TestCase end setup do - enable_uuid_ossp + enable_extension!('uuid-ossp', connection) connection.transaction do connection.create_table('pg_uuid_posts', id: :uuid) do |t| t.string 'title' end connection.create_table('pg_uuid_comments', id: :uuid) do |t| - t.uuid :uuid_post_id, default: 'uuid_generate_v4()' + t.references :uuid_post, type: :uuid t.string 'content' end end end teardown do - connection.transaction do drop_table "pg_uuid_comments" drop_table "pg_uuid_posts" - end + disable_extension!('uuid-ossp', connection) end if ActiveRecord::Base.connection.supports_extensions? @@ -202,5 +303,19 @@ class PostgresqlUUIDTestInverseOf < ActiveRecord::TestCase comment = post.uuid_comments.create! assert post.uuid_comments.find(comment.id) end + + def test_find_with_uuid + UuidPost.create! + assert_raise ActiveRecord::RecordNotFound do + UuidPost.find(123456) + end + + end + + def test_find_by_with_uuid + UuidPost.create! + assert_nil UuidPost.find_by(id: 789) + end end + end diff --git a/activerecord/test/cases/adapters/postgresql/view_test.rb b/activerecord/test/cases/adapters/postgresql/view_test.rb index 47b7d38eda..8a8e1d3b17 100644 --- a/activerecord/test/cases/adapters/postgresql/view_test.rb +++ b/activerecord/test/cases/adapters/postgresql/view_test.rb @@ -1,67 +1,63 @@ require "cases/helper" +require "cases/view_test" -module ViewTestConcern - extend ActiveSupport::Concern +class UpdateableViewTest < ActiveRecord::TestCase + fixtures :books - included do - self.use_transactional_fixtures = false - mattr_accessor :view_type + class PrintedBook < ActiveRecord::Base + self.primary_key = "id" end - SCHEMA_NAME = 'test_schema' - TABLE_NAME = 'things' - COLUMNS = [ - 'id integer', - 'name character varying(50)', - 'email character varying(50)', - 'moment timestamp without time zone' - ] - - class ThingView < ActiveRecord::Base + setup do + @connection = ActiveRecord::Base.connection + @connection.execute <<-SQL + CREATE VIEW printed_books + AS SELECT id, name, status, format FROM books WHERE format = 'paperback' + SQL end - def setup - super - ThingView.table_name = "#{SCHEMA_NAME}.#{view_type}_things" - - @connection = ActiveRecord::Base.connection - @connection.execute "CREATE SCHEMA #{SCHEMA_NAME} CREATE TABLE #{TABLE_NAME} (#{COLUMNS.join(',')})" - @connection.execute "CREATE #{view_type.humanize} #{ThingView.table_name} AS SELECT * FROM #{SCHEMA_NAME}.#{TABLE_NAME}" + teardown do + @connection.execute "DROP VIEW printed_books" if @connection.table_exists? "printed_books" end - def teardown - super - @connection.execute "DROP SCHEMA #{SCHEMA_NAME} CASCADE" + def test_update_record + book = PrintedBook.first + book.name = "AWDwR" + book.save! + book.reload + assert_equal "AWDwR", book.name end - def test_table_exists - name = ThingView.table_name - assert @connection.table_exists?(name), "'#{name}' table should exist" + def test_insert_record + PrintedBook.create! name: "Rails in Action", status: 0, format: "paperback" + + new_book = PrintedBook.last + assert_equal "Rails in Action", new_book.name end - def test_column_definitions - assert_nothing_raised do - assert_equal COLUMNS, columns(ThingView.table_name) + def test_update_record_to_fail_view_conditions + book = PrintedBook.first + book.format = "ebook" + book.save! + + assert_raises ActiveRecord::RecordNotFound do + book.reload end end +end - private - def columns(table_name) - @connection.send(:column_definitions, table_name).map do |name, type, default| - "#{name} #{type}" + (default ? " default #{default}" : '') - end - end +if ActiveRecord::Base.connection.supports_materialized_views? +class MaterializedViewTest < ActiveRecord::TestCase + include ViewBehavior -end + private + def create_view(name, query) + @connection.execute "CREATE MATERIALIZED VIEW #{name} AS #{query}" + end -class ViewTest < ActiveRecord::TestCase - include ViewTestConcern - self.view_type = 'view' -end + def drop_view(name) + @connection.execute "DROP MATERIALIZED VIEW #{name}" if @connection.table_exists? name -if ActiveRecord::Base.connection.supports_materialized_views? - class MaterializedViewTest < ActiveRecord::TestCase - include ViewTestConcern - self.view_type = 'materialized_view' end end +end diff --git a/activerecord/test/cases/adapters/postgresql/xml_test.rb b/activerecord/test/cases/adapters/postgresql/xml_test.rb index 48c6eeb62c..5aba118518 100644 --- a/activerecord/test/cases/adapters/postgresql/xml_test.rb +++ b/activerecord/test/cases/adapters/postgresql/xml_test.rb @@ -1,7 +1,9 @@ # encoding: utf-8 require 'cases/helper' +require 'support/schema_dumping_helper' class PostgresqlXMLTest < ActiveRecord::TestCase + include SchemaDumpingHelper class XmlDataType < ActiveRecord::Base self.table_name = 'xml_data_type' end @@ -11,7 +13,7 @@ class PostgresqlXMLTest < ActiveRecord::TestCase begin @connection.transaction do @connection.create_table('xml_data_type') do |t| - t.xml 'payload', default: {} + t.xml 'payload' end end rescue ActiveRecord::StatementInvalid @@ -32,4 +34,22 @@ class PostgresqlXMLTest < ActiveRecord::TestCase @connection.execute %q|insert into xml_data_type (payload) VALUES(null)| assert_nil XmlDataType.first.payload end + + def test_round_trip + data = XmlDataType.new(payload: "<foo>bar</foo>") + assert_equal "<foo>bar</foo>", data.payload + data.save! + assert_equal "<foo>bar</foo>", data.reload.payload + end + + def test_update_all + data = XmlDataType.create! + XmlDataType.update_all(payload: "<bar>baz</bar>") + assert_equal "<bar>baz</bar>", data.reload.payload + end + + def test_schema_dump_with_shorthand + output = dump_table_schema("xml_data_type") + assert_match %r{t.xml "payload"}, output + end end diff --git a/activerecord/test/cases/adapters/sqlite3/explain_test.rb b/activerecord/test/cases/adapters/sqlite3/explain_test.rb index f1d6119d2e..7d66c44798 100644 --- a/activerecord/test/cases/adapters/sqlite3/explain_test.rb +++ b/activerecord/test/cases/adapters/sqlite3/explain_test.rb @@ -1,5 +1,6 @@ require "cases/helper" require 'models/developer' +require 'models/computer' module ActiveRecord module ConnectionAdapters @@ -17,7 +18,7 @@ module ActiveRecord explain = Developer.where(:id => 1).includes(:audit_logs).explain assert_match %(EXPLAIN for: SELECT "developers".* FROM "developers" WHERE "developers"."id" = ?), explain assert_match(/(SEARCH )?TABLE developers USING (INTEGER )?PRIMARY KEY/, explain) - assert_match %(EXPLAIN for: SELECT "audit_logs".* FROM "audit_logs" WHERE "audit_logs"."developer_id" IN (1)), explain + assert_match %(EXPLAIN for: SELECT "audit_logs".* FROM "audit_logs" WHERE "audit_logs"."developer_id" = 1), explain assert_match(/(SCAN )?TABLE audit_logs/, explain) end end diff --git a/activerecord/test/cases/adapters/sqlite3/quoting_test.rb b/activerecord/test/cases/adapters/sqlite3/quoting_test.rb index 8c9a051eea..df497e761c 100644 --- a/activerecord/test/cases/adapters/sqlite3/quoting_test.rb +++ b/activerecord/test/cases/adapters/sqlite3/quoting_test.rb @@ -15,73 +15,52 @@ module ActiveRecord def test_type_cast_binary_encoding_without_logger @conn.extend(Module.new { def logger; end }) - cast_type = Type::String.new binary = SecureRandom.hex expected = binary.dup.encode!(Encoding::UTF_8) - assert_equal expected, @conn.type_cast(binary, cast_type) + assert_equal expected, @conn.type_cast(binary) end def test_type_cast_symbol - assert_equal 'foo', @conn.type_cast(:foo, nil) + assert_equal 'foo', @conn.type_cast(:foo) end def test_type_cast_date date = Date.today expected = @conn.quoted_date(date) - assert_equal expected, @conn.type_cast(date, nil) + assert_equal expected, @conn.type_cast(date) end def test_type_cast_time time = Time.now expected = @conn.quoted_date(time) - assert_equal expected, @conn.type_cast(time, nil) + assert_equal expected, @conn.type_cast(time) end def test_type_cast_numeric - assert_equal 10, @conn.type_cast(10, nil) - assert_equal 2.2, @conn.type_cast(2.2, nil) + assert_equal 10, @conn.type_cast(10) + assert_equal 2.2, @conn.type_cast(2.2) end def test_type_cast_nil - assert_equal nil, @conn.type_cast(nil, nil) + assert_equal nil, @conn.type_cast(nil) end def test_type_cast_true - c = Column.new(nil, 1, Type::Integer.new) - assert_equal 't', @conn.type_cast(true, nil) - assert_equal 1, @conn.type_cast(true, c) + assert_equal 't', @conn.type_cast(true) end def test_type_cast_false - c = Column.new(nil, 1, Type::Integer.new) - assert_equal 'f', @conn.type_cast(false, nil) - assert_equal 0, @conn.type_cast(false, c) - end - - def test_type_cast_string - assert_equal '10', @conn.type_cast('10', nil) - - c = Column.new(nil, 1, Type::Integer.new) - assert_equal 10, @conn.type_cast('10', c) - - c = Column.new(nil, 1, Type::Float.new) - assert_equal 10.1, @conn.type_cast('10.1', c) - - c = Column.new(nil, 1, Type::Binary.new) - assert_equal '10.1', @conn.type_cast('10.1', c) - - c = Column.new(nil, 1, Type::Date.new) - assert_equal '10.1', @conn.type_cast('10.1', c) + assert_equal 'f', @conn.type_cast(false) end def test_type_cast_bigdecimal bd = BigDecimal.new '10.0' - assert_equal bd.to_f, @conn.type_cast(bd, nil) + assert_equal bd.to_f, @conn.type_cast(bd) end def test_type_cast_unknown_should_raise_error obj = Class.new.new - assert_raise(TypeError) { @conn.type_cast(obj, nil) } + assert_raise(TypeError) { @conn.type_cast(obj) } end def test_type_cast_object_which_responds_to_quoted_id @@ -94,14 +73,21 @@ module ActiveRecord 10 end }.new - assert_equal 10, @conn.type_cast(quoted_id_obj, nil) + assert_equal 10, @conn.type_cast(quoted_id_obj) quoted_id_obj = Class.new { def quoted_id "'zomg'" end }.new - assert_raise(TypeError) { @conn.type_cast(quoted_id_obj, nil) } + assert_raise(TypeError) { @conn.type_cast(quoted_id_obj) } + end + + def test_quoting_binary_strings + value = "hello".encode('ascii-8bit') + column = Column.new(nil, 1, SQLite3String.new) + + assert_equal "'hello'", @conn.quote(value, column) end end end diff --git a/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb b/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb index e55525177f..029663e7f4 100644 --- a/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb +++ b/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb @@ -57,10 +57,11 @@ module ActiveRecord end end - # sqlite databases should be able to support any type and not - # just the ones mentioned in the native_database_types. - # Therefore test_invalid column should always return true - # even if the type is not valid. + # sqlite3 databases should be able to support any type and not just the + # ones mentioned in the native_database_types. + # + # Therefore test_invalid column should always return true even if the + # type is not valid. def test_invalid_column assert @conn.valid_type?(:foobar) end @@ -132,8 +133,8 @@ module ActiveRecord end def test_bind_value_substitute - bind_param = @conn.substitute_at('foo', 0) - assert_equal Arel.sql('?'), bind_param + bind_param = @conn.substitute_at('foo') + assert_equal Arel.sql('?'), bind_param.to_sql end def test_exec_no_binds @@ -296,7 +297,7 @@ module ActiveRecord def test_tables_logs_name sql = <<-SQL SELECT name FROM sqlite_master - WHERE type = 'table' AND NOT name = 'sqlite_sequence' + WHERE (type = 'table' OR type = 'view') AND NOT name = 'sqlite_sequence' SQL assert_logged [[sql.squish, 'SCHEMA', []]] do @conn.tables('hello') @@ -315,7 +316,7 @@ module ActiveRecord with_example_table do sql = <<-SQL SELECT name FROM sqlite_master - WHERE type = 'table' + WHERE (type = 'table' OR type = 'view') AND NOT name = 'sqlite_sequence' AND name = \"ex\" SQL assert_logged [[sql.squish, 'SCHEMA', []]] do @@ -326,11 +327,11 @@ module ActiveRecord def test_columns with_example_table do - columns = @conn.columns('ex').sort_by { |x| x.name } + columns = @conn.columns('ex').sort_by(&:name) assert_equal 2, columns.length - assert_equal %w{ id number }.sort, columns.map { |x| x.name } - assert_equal [nil, nil], columns.map { |x| x.default } - assert_equal [true, true], columns.map { |x| x.null } + assert_equal %w{ id number }.sort, columns.map(&:name) + assert_equal [nil, nil], columns.map(&:default) + assert_equal [true, true], columns.map(&:null) end end @@ -339,7 +340,7 @@ module ActiveRecord column = @conn.columns('ex').find { |x| x.name == 'number' } - assert_equal 10, column.default + assert_equal '10', column.default end end @@ -404,6 +405,12 @@ 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/ar_schema_test.rb b/activerecord/test/cases/ar_schema_test.rb index 8700b20dee..025c9fe6f9 100644 --- a/activerecord/test/cases/ar_schema_test.rb +++ b/activerecord/test/cases/ar_schema_test.rb @@ -5,7 +5,9 @@ if ActiveRecord::Base.connection.supports_migrations? class ActiveRecordSchemaTest < ActiveRecord::TestCase self.use_transactional_fixtures = false - def setup + setup do + @original_verbose = ActiveRecord::Migration.verbose + ActiveRecord::Migration.verbose = false @connection = ActiveRecord::Base.connection ActiveRecord::SchemaMigration.drop_table end @@ -14,7 +16,9 @@ if ActiveRecord::Base.connection.supports_migrations? @connection.drop_table :fruits rescue nil @connection.drop_table :nep_fruits rescue nil @connection.drop_table :nep_schema_migrations rescue nil + @connection.drop_table :has_timestamps rescue nil ActiveRecord::SchemaMigration.delete_all rescue nil + ActiveRecord::Migration.verbose = @original_verbose end def test_has_no_primary_key @@ -88,5 +92,70 @@ if ActiveRecord::Base.connection.supports_migrations? assert_equal "017", ActiveRecord::SchemaMigration.normalize_migration_number("0017") assert_equal "20131219224947", ActiveRecord::SchemaMigration.normalize_migration_number("20131219224947") end + + def test_timestamps_without_null_is_deprecated_on_create_table + assert_deprecated do + ActiveRecord::Schema.define do + create_table :has_timestamps do |t| + t.timestamps + end + end + end + end + + def test_timestamps_without_null_is_deprecated_on_change_table + assert_deprecated do + ActiveRecord::Schema.define do + create_table :has_timestamps + + change_table :has_timestamps do |t| + t.timestamps + end + end + end + end + + def test_timestamps_without_null_is_deprecated_on_add_timestamps + assert_deprecated do + ActiveRecord::Schema.define do + create_table :has_timestamps + add_timestamps :has_timestamps + end + end + end + + def test_no_deprecation_warning_from_timestamps_on_create_table + assert_not_deprecated do + ActiveRecord::Schema.define do + create_table :has_timestamps do |t| + t.timestamps null: true + end + + drop_table :has_timestamps + + create_table :has_timestamps do |t| + t.timestamps null: false + end + end + end + end + + def test_no_deprecation_warning_from_timestamps_on_change_table + assert_not_deprecated do + ActiveRecord::Schema.define do + create_table :has_timestamps + change_table :has_timestamps do |t| + t.timestamps null: true + end + + drop_table :has_timestamps + + create_table :has_timestamps + change_table :has_timestamps do |t| + t.timestamps null: false, default: Time.now + end + end + end + end end end diff --git a/activerecord/test/cases/associations/belongs_to_associations_test.rb b/activerecord/test/cases/associations/belongs_to_associations_test.rb index 9c92dc1141..17394cb6f7 100644 --- a/activerecord/test/cases/associations/belongs_to_associations_test.rb +++ b/activerecord/test/cases/associations/belongs_to_associations_test.rb @@ -1,5 +1,6 @@ require 'cases/helper' require 'models/developer' +require 'models/computer' require 'models/project' require 'models/company' require 'models/topic' @@ -57,6 +58,35 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase end end + def test_default_scope_on_relations_is_not_cached + counter = 0 + + comments = Class.new(ActiveRecord::Base) { + self.table_name = 'comments' + self.inheritance_column = 'not_there' + + posts = Class.new(ActiveRecord::Base) { + self.table_name = 'posts' + self.inheritance_column = 'not_there' + + default_scope -> { + counter += 1 + where("id = :inc", :inc => counter) + } + + has_many :comments, :class => comments + } + belongs_to :post, :class => posts, :inverse_of => false + } + + assert_equal 0, counter + comment = comments.first + assert_equal 0, counter + sql = capture_sql { comment.post } + comment.reload + assert_not_equal sql, capture_sql { comment.post } + end + def test_proxy_assignment account = Account.find(1) assert_nothing_raised { account.firm = account.firm } @@ -787,8 +817,8 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase post = posts(:welcome) comment = comments(:greetings) - assert_difference lambda { post.reload.taggings_count }, -1 do - assert_difference 'comment.reload.taggings_count', +1 do + assert_difference lambda { post.reload.tags_count }, -1 do + assert_difference 'comment.reload.tags_count', +1 do tagging.taggable = comment end end @@ -935,3 +965,14 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase assert_equal 1, Column.count end end + +class BelongsToWithForeignKeyTest < ActiveRecord::TestCase + fixtures :authors, :author_addresses + + def test_destroy_linked_models + address = AuthorAddress.create! + author = Author.create! name: "Author", author_address_id: address.id + + author.destroy! + end +end diff --git a/activerecord/test/cases/associations/callbacks_test.rb b/activerecord/test/cases/associations/callbacks_test.rb index 5b7e462f64..a531e0e02c 100644 --- a/activerecord/test/cases/associations/callbacks_test.rb +++ b/activerecord/test/cases/associations/callbacks_test.rb @@ -3,6 +3,7 @@ require 'models/post' require 'models/author' require 'models/project' require 'models/developer' +require 'models/computer' require 'models/company' class AssociationCallbacksTest < ActiveRecord::TestCase diff --git a/activerecord/test/cases/associations/cascaded_eager_loading_test.rb b/activerecord/test/cases/associations/cascaded_eager_loading_test.rb index 71c0609df5..51d8e0523e 100644 --- a/activerecord/test/cases/associations/cascaded_eager_loading_test.rb +++ b/activerecord/test/cases/associations/cascaded_eager_loading_test.rb @@ -35,9 +35,9 @@ class CascadedEagerLoadingTest < ActiveRecord::TestCase def test_eager_association_loading_with_hmt_does_not_table_name_collide_when_joining_associations assert_nothing_raised do - Author.joins(:posts).eager_load(:comments).where(:posts => {:taggings_count => 1}).to_a + Author.joins(:posts).eager_load(:comments).where(:posts => {:tags_count => 1}).to_a end - authors = Author.joins(:posts).eager_load(:comments).where(:posts => {:taggings_count => 1}).to_a + authors = Author.joins(:posts).eager_load(:comments).where(:posts => {:tags_count => 1}).to_a assert_equal 1, assert_no_queries { authors.size } assert_equal 10, assert_no_queries { authors[0].comments.size } end diff --git a/activerecord/test/cases/associations/deprecated_counter_cache_on_has_many_through_test.rb b/activerecord/test/cases/associations/deprecated_counter_cache_on_has_many_through_test.rb new file mode 100644 index 0000000000..48f7ddbe83 --- /dev/null +++ b/activerecord/test/cases/associations/deprecated_counter_cache_on_has_many_through_test.rb @@ -0,0 +1,26 @@ +require "cases/helper" + +class DeprecatedCounterCacheOnHasManyThroughTest < ActiveRecord::TestCase + class Post < ActiveRecord::Base + has_many :taggings, as: :taggable + has_many :tags, through: :taggings + end + + class Tagging < ActiveRecord::Base + belongs_to :taggable, polymorphic: true + belongs_to :tag + end + + class Tag < ActiveRecord::Base + end + + test "counter caches are updated in the database if the belongs_to association doesn't specify a counter cache" do + post = Post.create!(title: 'Hello', body: 'World!') + assert_deprecated { post.tags << Tag.create!(name: 'whatever') } + + assert_equal 1, post.tags.size + assert_equal 1, post.tags_count + assert_equal 1, post.reload.tags.size + assert_equal 1, post.reload.tags_count + end +end diff --git a/activerecord/test/cases/associations/eager_load_nested_include_test.rb b/activerecord/test/cases/associations/eager_load_nested_include_test.rb index 0ff87d53ea..f571198079 100644 --- a/activerecord/test/cases/associations/eager_load_nested_include_test.rb +++ b/activerecord/test/cases/associations/eager_load_nested_include_test.rb @@ -70,9 +70,7 @@ class EagerLoadPolyAssocsTest < ActiveRecord::TestCase teardown do [Circle, Square, Triangle, PaintColor, PaintTexture, - ShapeExpression, NonPolyOne, NonPolyTwo].each do |c| - c.delete_all - end + ShapeExpression, NonPolyOne, NonPolyTwo].each(&:delete_all) end def generate_test_object_graphs diff --git a/activerecord/test/cases/associations/eager_test.rb b/activerecord/test/cases/associations/eager_test.rb index 910067666a..fdb437d11d 100644 --- a/activerecord/test/cases/associations/eager_test.rb +++ b/activerecord/test/cases/associations/eager_test.rb @@ -17,6 +17,7 @@ require 'models/subscriber' require 'models/subscription' require 'models/book' require 'models/developer' +require 'models/computer' require 'models/project' require 'models/member' require 'models/membership' @@ -269,6 +270,14 @@ class EagerAssociationTest < ActiveRecord::TestCase end end + def test_three_level_nested_preloading_does_not_raise_exception_when_association_does_not_exist + post_id = Comment.where(author_id: nil).where.not(post_id: nil).first.post_id + + assert_nothing_raised do + Post.preload(:comments => [{:author => :essays}]).find(post_id) + end + end + def test_nested_loading_through_has_one_association aa = AuthorAddress.all.merge!(:includes => {:author => :posts}).find(author_addresses(:david_address).id) assert_equal aa.author.posts.count, aa.author.posts.length @@ -329,31 +338,31 @@ class EagerAssociationTest < ActiveRecord::TestCase def test_eager_association_loading_with_belongs_to_and_limit comments = Comment.all.merge!(:includes => :post, :limit => 5, :order => 'comments.id').to_a assert_equal 5, comments.length - assert_equal [1,2,3,5,6], comments.collect { |c| c.id } + assert_equal [1,2,3,5,6], comments.collect(&:id) end def test_eager_association_loading_with_belongs_to_and_limit_and_conditions comments = Comment.all.merge!(:includes => :post, :where => 'post_id = 4', :limit => 3, :order => 'comments.id').to_a assert_equal 3, comments.length - assert_equal [5,6,7], comments.collect { |c| c.id } + assert_equal [5,6,7], comments.collect(&:id) end def test_eager_association_loading_with_belongs_to_and_limit_and_offset comments = Comment.all.merge!(:includes => :post, :limit => 3, :offset => 2, :order => 'comments.id').to_a assert_equal 3, comments.length - assert_equal [3,5,6], comments.collect { |c| c.id } + assert_equal [3,5,6], comments.collect(&:id) end def test_eager_association_loading_with_belongs_to_and_limit_and_offset_and_conditions comments = Comment.all.merge!(:includes => :post, :where => 'post_id = 4', :limit => 3, :offset => 1, :order => 'comments.id').to_a assert_equal 3, comments.length - assert_equal [6,7,8], comments.collect { |c| c.id } + assert_equal [6,7,8], comments.collect(&:id) end def test_eager_association_loading_with_belongs_to_and_limit_and_offset_and_conditions_array comments = Comment.all.merge!(:includes => :post, :where => ['post_id = ?',4], :limit => 3, :offset => 1, :order => 'comments.id').to_a assert_equal 3, comments.length - assert_equal [6,7,8], comments.collect { |c| c.id } + assert_equal [6,7,8], comments.collect(&:id) end def test_eager_association_loading_with_belongs_to_and_conditions_string_with_unquoted_table_name @@ -368,7 +377,7 @@ class EagerAssociationTest < ActiveRecord::TestCase comments = Comment.all.merge!(:includes => :post, :where => {:posts => {:id => 4}}, :limit => 3, :order => 'comments.id').to_a end assert_equal 3, comments.length - assert_equal [5,6,7], comments.collect { |c| c.id } + assert_equal [5,6,7], comments.collect(&:id) assert_no_queries do comments.first.post end @@ -397,13 +406,13 @@ class EagerAssociationTest < ActiveRecord::TestCase def test_eager_association_loading_with_belongs_to_and_limit_and_multiple_associations posts = Post.all.merge!(:includes => [:author, :very_special_comment], :limit => 1, :order => 'posts.id').to_a assert_equal 1, posts.length - assert_equal [1], posts.collect { |p| p.id } + assert_equal [1], posts.collect(&:id) end def test_eager_association_loading_with_belongs_to_and_limit_and_offset_and_multiple_associations posts = Post.all.merge!(:includes => [:author, :very_special_comment], :limit => 1, :offset => 1, :order => 'posts.id').to_a assert_equal 1, posts.length - assert_equal [2], posts.collect { |p| p.id } + assert_equal [2], posts.collect(&:id) end def test_eager_association_loading_with_belongs_to_inferred_foreign_key_from_association_name @@ -494,8 +503,8 @@ class EagerAssociationTest < ActiveRecord::TestCase end def test_eager_with_has_many_through_an_sti_join_model_with_conditions_on_both - author = Author.all.merge!(:includes => :special_nonexistant_post_comments, :order => 'authors.id').first - assert_equal [], author.special_nonexistant_post_comments + author = Author.all.merge!(:includes => :special_nonexistent_post_comments, :order => 'authors.id').first + assert_equal [], author.special_nonexistent_post_comments end def test_eager_with_has_many_through_join_model_with_conditions @@ -536,13 +545,13 @@ class EagerAssociationTest < ActiveRecord::TestCase def test_eager_with_has_many_and_limit_and_conditions posts = Post.all.merge!(:includes => [ :author, :comments ], :limit => 2, :where => "posts.body = 'hello'", :order => "posts.id").to_a assert_equal 2, posts.size - assert_equal [4,5], posts.collect { |p| p.id } + assert_equal [4,5], posts.collect(&:id) end def test_eager_with_has_many_and_limit_and_conditions_array posts = Post.all.merge!(:includes => [ :author, :comments ], :limit => 2, :where => [ "posts.body = ?", 'hello' ], :order => "posts.id").to_a assert_equal 2, posts.size - assert_equal [4,5], posts.collect { |p| p.id } + assert_equal [4,5], posts.collect(&:id) end def test_eager_with_has_many_and_limit_and_conditions_array_on_the_eagers @@ -903,6 +912,12 @@ class EagerAssociationTest < ActiveRecord::TestCase assert_no_queries {assert_equal posts(:sti_comments), comment.post} end + def test_eager_association_with_scope_with_joins + assert_nothing_raised do + Post.includes(:very_special_comment_with_post_with_joins).to_a + end + end + def test_preconfigured_includes_with_has_many posts = authors(:david).posts_with_comments one = posts.detect { |p| p.id == 1 } @@ -935,6 +950,42 @@ class EagerAssociationTest < ActiveRecord::TestCase assert_equal 3, authors(:david).posts_with_comments.where("length(comments.body) > 15").references(:comments).count end + def test_association_loading_notification + notifications = messages_for('instantiation.active_record') do + Developer.all.merge!(:includes => 'projects', :where => { 'developers_projects.access_level' => 1 }, :limit => 5).to_a.size + end + + message = notifications.first + payload = message.last + count = Developer.all.merge!(:includes => 'projects', :where => { 'developers_projects.access_level' => 1 }, :limit => 5).to_a.size + + # eagerloaded row count should be greater than just developer count + assert_operator payload[:record_count], :>, count + assert_equal Developer.name, payload[:class_name] + end + + def test_base_messages + notifications = messages_for('instantiation.active_record') do + Developer.all.to_a + end + message = notifications.first + payload = message.last + + assert_equal Developer.all.to_a.count, payload[:record_count] + assert_equal Developer.name, payload[:class_name] + end + + def messages_for(name) + notifications = [] + ActiveSupport::Notifications.subscribe(name) do |*args| + notifications << args + end + yield + notifications + ensure + ActiveSupport::Notifications.unsubscribe(name) + end + def test_load_with_sti_sharing_association assert_queries(2) do #should not do 1 query per subclass Comment.includes(:post).to_a @@ -1261,4 +1312,46 @@ class EagerAssociationTest < ActiveRecord::TestCase Author.eager_load(:posts_with_signature).to_a end end + + test "preloading readonly association" do + # has-one + firm = Firm.where(id: "1").preload(:readonly_account).first! + assert firm.readonly_account.readonly? + + # has_and_belongs_to_many + project = Project.where(id: "2").preload(:readonly_developers).first! + assert project.readonly_developers.first.readonly? + + # has-many :through + david = Author.where(id: "1").preload(:readonly_comments).first! + assert david.readonly_comments.first.readonly? + end + + test "eager-loading readonly association" do + # has-one + firm = Firm.where(id: "1").eager_load(:readonly_account).first! + assert firm.readonly_account.readonly? + + # has_and_belongs_to_many + project = Project.where(id: "2").eager_load(:readonly_developers).first! + assert project.readonly_developers.first.readonly? + + # has-many :through + david = Author.where(id: "1").eager_load(:readonly_comments).first! + assert david.readonly_comments.first.readonly? + + # belongs_to + post = Post.where(id: "1").eager_load(:author).first! + assert post.author.readonly? + end + + test "preloading a polymorphic association with references to the associated table" do + post = Post.includes(:tags).references(:tags).where('tags.name = ?', 'General').first + assert_equal posts(:welcome), post + end + + test "eager-loading a polymorphic association with references to the associated table" do + post = Post.eager_load(:tags).where('tags.name = ?', 'General').first + assert_equal posts(:welcome), post + end end diff --git a/activerecord/test/cases/associations/extension_test.rb b/activerecord/test/cases/associations/extension_test.rb index 4c1fdfdd9a..9d373cd73b 100644 --- a/activerecord/test/cases/associations/extension_test.rb +++ b/activerecord/test/cases/associations/extension_test.rb @@ -3,6 +3,7 @@ require 'models/post' require 'models/comment' require 'models/project' require 'models/developer' +require 'models/computer' require 'models/company_in_module' class AssociationsExtensionsTest < ActiveRecord::TestCase 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 080c499444..aea9207bfe 100644 --- a/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb +++ b/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb @@ -1,5 +1,6 @@ require "cases/helper" require 'models/developer' +require 'models/computer' require 'models/project' require 'models/company' require 'models/customer' @@ -78,9 +79,13 @@ class SubDeveloper < Developer :association_foreign_key => "developer_id" end +class DeveloperWithSymbolClassName < Developer + has_and_belongs_to_many :projects, class_name: :ProjectWithSymbolsForKeys +end + class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase fixtures :accounts, :companies, :categories, :posts, :categories_posts, :developers, :projects, :developers_projects, - :parrots, :pirates, :parrots_pirates, :treasures, :price_estimates, :tags, :taggings + :parrots, :pirates, :parrots_pirates, :treasures, :price_estimates, :tags, :taggings, :computers def setup_data_for_habtm_case ActiveRecord::Base.connection.execute('delete from countries_treaties') @@ -254,7 +259,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase def test_build devel = Developer.find(1) - proj = assert_no_queries { devel.projects.build("name" => "Projekt") } + proj = assert_no_queries(ignore_none: false) { devel.projects.build("name" => "Projekt") } assert !devel.projects.loaded? assert_equal devel.projects.last, proj @@ -269,7 +274,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase def test_new_aliased_to_build devel = Developer.find(1) - proj = assert_no_queries { devel.projects.new("name" => "Projekt") } + proj = assert_no_queries(ignore_none: false) { devel.projects.new("name" => "Projekt") } assert !devel.projects.loaded? assert_equal devel.projects.last, proj @@ -503,7 +508,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase developer = project.developers.first - assert_no_queries do + assert_no_queries(ignore_none: false) do assert project.developers.loaded? assert project.developers.include?(developer) end @@ -550,7 +555,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase def test_dynamic_find_all_should_respect_readonly_access projects(:active_record).readonly_developers.each { |d| assert_raise(ActiveRecord::ReadOnlyRecord) { d.save! } if d.valid?} - projects(:active_record).readonly_developers.each { |d| d.readonly? } + projects(:active_record).readonly_developers.each(&:readonly?) end def test_new_with_values_in_collection @@ -824,7 +829,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase def test_has_and_belongs_to_many_associations_on_new_records_use_null_relations projects = Developer.new.projects - assert_no_queries do + assert_no_queries(ignore_none: false) do assert_equal [], projects assert_equal [], projects.where(title: 'omg') assert_equal [], projects.pluck(:title) @@ -860,7 +865,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase assert_equal 'edges', Vertex.reflect_on_association(:sources).join_table end - def test_namespaced_habtm + def test_has_and_belongs_to_many_in_a_namespaced_model_pointing_to_a_namespaced_model magazine = Publisher::Magazine.create article = Publisher::Article.create magazine.articles << article @@ -869,9 +874,32 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase assert_includes magazine.articles, article end + def test_has_and_belongs_to_many_in_a_namespaced_model_pointing_to_a_non_namespaced_model + article = Publisher::Article.create + tag = Tag.create + article.tags << tag + article.save + + assert_includes article.tags, tag + end + def test_redefine_habtm child = SubDeveloper.new("name" => "Aredridel") child.special_projects << SpecialProject.new("name" => "Special Project") assert child.save, 'child object should be saved' end + + def test_habtm_with_reflection_using_class_name_and_fixtures + assert_not_nil Developer._reflections['shared_computers'] + # Checking the fixture for named association is important here, because it's the only way + # we've been able to reproduce this bug + assert_not_nil File.read(File.expand_path("../../../fixtures/developers.yml", __FILE__)).index("shared_computers") + assert_equal developers(:david).shared_computers.first, computers(:laptop) + end + + def test_with_symbol_class_name + assert_nothing_raised NoMethodError do + DeveloperWithSymbolClassName.new + end + end end diff --git a/activerecord/test/cases/associations/has_many_associations_test.rb b/activerecord/test/cases/associations/has_many_associations_test.rb index 5f01352ab4..21a45042fa 100644 --- a/activerecord/test/cases/associations/has_many_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_associations_test.rb @@ -1,11 +1,13 @@ require "cases/helper" require 'models/developer' +require 'models/computer' require 'models/project' require 'models/company' require 'models/contract' require 'models/topic' require 'models/reply' require 'models/category' +require 'models/image' require 'models/post' require 'models/author' require 'models/essay' @@ -28,6 +30,9 @@ require 'models/college' require 'models/student' require 'models/pirate' require 'models/ship' +require 'models/tyre' +require 'models/subscriber' +require 'models/subscription' class HasManyAssociationsTestForReorderWithJoinDependency < ActiveRecord::TestCase fixtures :authors, :posts, :comments @@ -36,16 +41,63 @@ class HasManyAssociationsTestForReorderWithJoinDependency < ActiveRecord::TestCa author = authors(:david) # this can fail on adapters which require ORDER BY expressions to be included in the SELECT expression # if the reorder clauses are not correctly handled - assert author.posts_with_comments_sorted_by_comment_id.where('comments.id > 0').reorder('posts.comments_count DESC', 'posts.taggings_count DESC').last + assert author.posts_with_comments_sorted_by_comment_id.where('comments.id > 0').reorder('posts.comments_count DESC', 'posts.tags_count DESC').last end end +class HasManyAssociationsTestPrimaryKeys < ActiveRecord::TestCase + fixtures :authors, :essays, :subscribers, :subscriptions, :people + + def test_custom_primary_key_on_new_record_should_fetch_with_query + subscriber = Subscriber.new(nick: 'webster132') + assert !subscriber.subscriptions.loaded? + + assert_queries 1 do + assert_equal 2, subscriber.subscriptions.size + end + + assert_equal subscriber.subscriptions, Subscription.where(subscriber_id: 'webster132') + end + + def test_association_primary_key_on_new_record_should_fetch_with_query + author = Author.new(:name => "David") + assert !author.essays.loaded? + + assert_queries 1 do + assert_equal 1, author.essays.size + end + + assert_equal author.essays, Essay.where(writer_id: "David") + end + + def test_has_many_custom_primary_key + david = authors(:david) + assert_equal david.essays, Essay.where(writer_id: "David") + end + + def test_has_many_assignment_with_custom_primary_key + david = people(:david) + + assert_equal ["A Modest Proposal"], david.essays.map(&:name) + david.essays = [Essay.create!(name: "Remote Work" )] + assert_equal ["Remote Work"], david.essays.map(&:name) + end + + def test_blank_custom_primary_key_on_new_record_should_not_run_queries + author = Author.new + assert !author.essays.loaded? + + assert_queries 0 do + assert_equal 0, author.essays.size + end + end +end class HasManyAssociationsTest < ActiveRecord::TestCase fixtures :accounts, :categories, :companies, :developers, :projects, :developers_projects, :topics, :authors, :comments, - :people, :posts, :readers, :taggings, :cars, :essays, - :categorizations, :jobs, :tags + :posts, :readers, :taggings, :cars, :jobs, :tags, + :categorizations def setup Client.destroyed_client_ids.clear @@ -75,6 +127,32 @@ class HasManyAssociationsTest < ActiveRecord::TestCase dev.developer_projects.map(&:project_id).sort end + def test_default_scope_on_relations_is_not_cached + counter = 0 + posts = Class.new(ActiveRecord::Base) { + self.table_name = 'posts' + self.inheritance_column = 'not_there' + post = self + + comments = Class.new(ActiveRecord::Base) { + self.table_name = 'comments' + self.inheritance_column = 'not_there' + belongs_to :post, :class => post + default_scope -> { + counter += 1 + where("id = :inc", :inc => counter) + } + } + has_many :comments, :class => comments, :foreign_key => 'post_id' + } + assert_equal 0, counter + post = posts.first + assert_equal 0, counter + sql = capture_sql { post.comments.to_a } + post.comments.reset + assert_not_equal sql, capture_sql { post.comments.to_a } + end + def test_has_many_build_with_options college = College.create(name: 'UFMT') Student.create(active: true, college_id: college.id, name: 'Sarah') @@ -386,6 +464,13 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_equal "Summit", Firm.all.merge!(:order => "id").first.clients_using_primary_key.first.name end + def test_update_all_on_association_accessed_before_save + firm = Firm.new(name: 'Firm') + firm.clients << Client.first + firm.save! + assert_equal firm.clients.count, firm.clients.update_all(description: 'Great!') + end + def test_belongs_to_sanity c = Client.new assert_nil c.firm, "belongs_to failed sanity check on new object" @@ -554,17 +639,21 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end def test_create_with_bang_on_has_many_when_parent_is_new_raises - assert_raise(ActiveRecord::RecordNotSaved) do + error = assert_raise(ActiveRecord::RecordNotSaved) do firm = Firm.new firm.plain_clients.create! :name=>"Whoever" end + + assert_equal "You cannot call create unless the parent is saved", error.message end def test_regular_create_on_has_many_when_parent_is_new_raises - assert_raise(ActiveRecord::RecordNotSaved) do + error = assert_raise(ActiveRecord::RecordNotSaved) do firm = Firm.new firm.plain_clients.create :name=>"Whoever" end + + assert_equal "You cannot call create unless the parent is saved", error.message end def test_create_with_bang_on_has_many_raises_when_record_not_saved @@ -575,9 +664,11 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end def test_create_with_bang_on_habtm_when_parent_is_new_raises - assert_raise(ActiveRecord::RecordNotSaved) do + error = assert_raise(ActiveRecord::RecordNotSaved) do Developer.new("name" => "Aredridel").projects.create! end + + assert_equal "You cannot call create unless the parent is saved", error.message end def test_adding_a_mismatch_class @@ -606,7 +697,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end def test_transactions_when_adding_to_new_record - assert_no_queries do + assert_no_queries(ignore_none: false) do firm = Firm.new firm.clients_of_firm.concat(Client.new("name" => "Natural Company")) end @@ -621,7 +712,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_new_aliased_to_build company = companies(:first_firm) - new_client = assert_no_queries { company.clients_of_firm.new("name" => "Another Client") } + new_client = assert_no_queries(ignore_none: false) { company.clients_of_firm.new("name" => "Another Client") } assert !company.clients_of_firm.loaded? assert_equal "Another Client", new_client.name @@ -631,7 +722,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_build company = companies(:first_firm) - new_client = assert_no_queries { company.clients_of_firm.build("name" => "Another Client") } + new_client = assert_no_queries(ignore_none: false) { company.clients_of_firm.build("name" => "Another Client") } assert !company.clients_of_firm.loaded? assert_equal "Another Client", new_client.name @@ -667,7 +758,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_build_many company = companies(:first_firm) - new_clients = assert_no_queries { company.clients_of_firm.build([{"name" => "Another Client"}, {"name" => "Another Client II"}]) } + new_clients = assert_no_queries(ignore_none: false) { company.clients_of_firm.build([{"name" => "Another Client"}, {"name" => "Another Client II"}]) } assert_equal 2, new_clients.size end @@ -693,7 +784,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_build_via_block company = companies(:first_firm) - new_client = assert_no_queries { company.clients_of_firm.build {|client| client.name = "Another Client" } } + new_client = assert_no_queries(ignore_none: false) { company.clients_of_firm.build {|client| client.name = "Another Client" } } assert !company.clients_of_firm.loaded? assert_equal "Another Client", new_client.name @@ -703,7 +794,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_build_many_via_block company = companies(:first_firm) - new_clients = assert_no_queries do + new_clients = assert_no_queries(ignore_none: false) do company.clients_of_firm.build([{"name" => "Another Client"}, {"name" => "Another Client II"}]) do |client| client.name = "changed" end @@ -772,6 +863,36 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_equal topic.replies.to_a.size, topic.replies_count end + def test_counter_cache_updates_in_memory_after_concat + topic = Topic.create title: "Zoom-zoom-zoom" + + topic.replies << Reply.create(title: "re: zoom", content: "speedy quick!") + assert_equal 1, topic.replies_count + assert_equal 1, topic.replies.size + assert_equal 1, topic.reload.replies.size + end + + def test_counter_cache_updates_in_memory_after_create + topic = Topic.create title: "Zoom-zoom-zoom" + + topic.replies.create!(title: "re: zoom", content: "speedy quick!") + assert_equal 1, topic.replies_count + assert_equal 1, topic.replies.size + assert_equal 1, topic.reload.replies.size + end + + def test_counter_cache_updates_in_memory_after_create_with_array + topic = Topic.create title: "Zoom-zoom-zoom" + + topic.replies.create!([ + { title: "re: zoom", content: "speedy quick!" }, + { title: "re: zoom 2", content: "OMG lol!" }, + ]) + assert_equal 2, topic.replies_count + assert_equal 2, topic.replies.size + assert_equal 2, topic.reload.replies.size + end + def test_pushing_association_updates_counter_cache topic = Topic.order("id ASC").first reply = Reply.create! @@ -784,14 +905,14 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_deleting_updates_counter_cache_without_dependent_option post = posts(:welcome) - assert_difference "post.reload.taggings_count", -1 do + assert_difference "post.reload.tags_count", -1 do post.taggings.delete(post.taggings.first) end end def test_deleting_updates_counter_cache_with_dependent_delete_all post = posts(:welcome) - post.update_columns(taggings_with_delete_all_count: post.taggings_count) + post.update_columns(taggings_with_delete_all_count: post.tags_count) assert_difference "post.reload.taggings_with_delete_all_count", -1 do post.taggings_with_delete_all.delete(post.taggings_with_delete_all.first) @@ -800,13 +921,20 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_deleting_updates_counter_cache_with_dependent_destroy post = posts(:welcome) - post.update_columns(taggings_with_destroy_count: post.taggings_count) + post.update_columns(taggings_with_destroy_count: post.tags_count) assert_difference "post.reload.taggings_with_destroy_count", -1 do post.taggings_with_destroy.delete(post.taggings_with_destroy.first) end end + def test_calling_empty_with_counter_cache + post = posts(:welcome) + assert_queries(0) do + assert_not post.comments.empty? + end + end + def test_custom_named_counter_cache topic = topics(:first) @@ -891,7 +1019,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end def test_transaction_when_deleting_new_record - assert_no_queries do + assert_no_queries(ignore_none: false) do firm = Firm.new client = Client.new("name" => "New Client") firm.clients_of_firm << client @@ -1141,7 +1269,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert !clients.empty?, "37signals has clients after load" destroyed = companies(:first_firm).clients_of_firm.destroy_all assert_equal clients.sort_by(&:id), destroyed.sort_by(&:id) - assert destroyed.all? { |client| client.frozen? }, "destroyed clients should be frozen" + assert destroyed.all?(&:frozen?), "destroyed clients should be frozen" assert companies(:first_firm).clients_of_firm.empty?, "37signals has no clients after destroy all" assert companies(:first_firm).clients_of_firm(true).empty?, "37signals has no clients after destroy all and refresh" end @@ -1281,10 +1409,13 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert !account.valid? assert !orig_accounts.empty? - assert_raise ActiveRecord::RecordNotSaved do + error = assert_raise ActiveRecord::RecordNotSaved do firm.accounts = [account] end + assert_equal orig_accounts, firm.accounts + assert_equal "Failed to replace accounts because one or more of the " \ + "new records could not be saved.", error.message end def test_replace_with_same_content @@ -1312,7 +1443,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end def test_transactions_when_replacing_on_new_record - assert_no_queries do + assert_no_queries(ignore_none: false) do firm = Firm.new firm.clients_of_firm = [Client.new("name" => "New Client")] end @@ -1450,7 +1581,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase firm.clients.load_target assert firm.clients.loaded? - assert_no_queries do + assert_no_queries(ignore_none: false) do firm.clients.first assert_equal 2, firm.clients.first(2).size firm.clients.last @@ -1496,39 +1627,6 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end end - def test_custom_primary_key_on_new_record_should_fetch_with_query - author = Author.new(:name => "David") - assert !author.essays.loaded? - - assert_queries 1 do - assert_equal 1, author.essays.size - end - - assert_equal author.essays, Essay.where(writer_id: "David") - end - - def test_has_many_custom_primary_key - david = authors(:david) - assert_equal david.essays, Essay.where(writer_id: "David") - end - - def test_has_many_assignment_with_custom_primary_key - david = people(:david) - - assert_equal ["A Modest Proposal"], david.essays.map(&:name) - david.essays = [Essay.create!(name: "Remote Work" )] - assert_equal ["Remote Work"], david.essays.map(&:name) - end - - def test_blank_custom_primary_key_on_new_record_should_not_run_queries - author = Author.new - assert !author.essays.loaded? - - assert_queries 0 do - assert_equal 0, author.essays.size - end - end - def test_calling_first_or_last_with_integer_on_association_should_not_load_association firm = companies(:first_firm) firm.clients.create(:name => 'Foo') @@ -1702,6 +1800,15 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_equal [tagging], post.taggings end + def test_with_polymorphic_has_many_with_custom_columns_name + post = Post.create! :title => 'foo', :body => 'bar' + image = Image.create! + + post.images << image + + assert_equal [image], post.images + end + def test_build_with_polymorphic_has_many_does_not_allow_to_override_type_and_id welcome = posts(:welcome) tagging = welcome.taggings.build(:taggable_id => 99, :taggable_type => 'ShouldNotChange') @@ -1798,7 +1905,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase test "has many associations on new records use null relations" do post = Post.new - assert_no_queries do + assert_no_queries(ignore_none: false) do assert_equal [], post.comments assert_equal [], post.comments.where(body: 'omg') assert_equal [], post.comments.pluck(:body) @@ -1857,6 +1964,14 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_equal [bulb1, bulb2], car.all_bulbs.sort_by(&:id) end + test 'unscopes the default scope of associated model when used with include' do + car = Car.create! + bulb = Bulb.create! name: "other", car: car + + assert_equal bulb, Car.find(car.id).all_bulbs.first + assert_equal bulb, Car.includes(:all_bulbs).find(car.id).all_bulbs.first + end + test "raises RecordNotDestroyed when replaced child can't be destroyed" do car = Car.create! original_child = FailedBulb.create!(car: car) @@ -1904,4 +2019,81 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_equal [], authors(:david).posts_with_signature.map(&:title) end + + test 'associations autosaves when object is already persited' do + bulb = Bulb.create! + tyre = Tyre.create! + + car = Car.create! do |c| + c.bulbs << bulb + c.tyres << tyre + end + + assert_equal 1, car.bulbs.count + assert_equal 1, car.tyres.count + end + + test 'associations replace in memory when records have the same id' do + bulb = Bulb.create! + car = Car.create!(bulbs: [bulb]) + + new_bulb = Bulb.find(bulb.id) + new_bulb.name = "foo" + car.bulbs = [new_bulb] + + assert_equal "foo", car.bulbs.first.name + end + + test 'in memory replacement executes no queries' do + bulb = Bulb.create! + car = Car.create!(bulbs: [bulb]) + + new_bulb = Bulb.find(bulb.id) + + assert_no_queries do + car.bulbs = [new_bulb] + end + end + + test 'in memory replacements do not execute callbacks' do + raise_after_add = false + klass = Class.new(ActiveRecord::Base) do + self.table_name = :cars + has_many :bulbs, after_add: proc { raise if raise_after_add } + + def self.name + "Car" + end + end + bulb = Bulb.create! + car = klass.create!(bulbs: [bulb]) + + new_bulb = Bulb.find(bulb.id) + raise_after_add = true + + assert_nothing_raised do + car.bulbs = [new_bulb] + end + end + + test 'in memory replacements sets inverse instance' do + bulb = Bulb.create! + car = Car.create!(bulbs: [bulb]) + + new_bulb = Bulb.find(bulb.id) + car.bulbs = [new_bulb] + + assert_same car, new_bulb.car + end + + test 'in memory replacement maintains order' do + first_bulb = Bulb.create! + second_bulb = Bulb.create! + car = Car.create!(bulbs: [first_bulb, second_bulb]) + + same_bulb = Bulb.find(first_bulb.id) + car.bulbs = [second_bulb, same_bulb] + + assert_equal [first_bulb, second_bulb], car.bulbs + end end diff --git a/activerecord/test/cases/associations/has_many_through_associations_test.rb b/activerecord/test/cases/associations/has_many_through_associations_test.rb index 8641584c0c..589a232bdb 100644 --- a/activerecord/test/cases/associations/has_many_through_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_through_associations_test.rb @@ -15,6 +15,7 @@ require 'models/toy' require 'models/contract' require 'models/company' require 'models/developer' +require 'models/computer' require 'models/subscriber' require 'models/book' require 'models/subscription' @@ -40,7 +41,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase def test_preload_sti_rhs_class developers = Developer.includes(:firms).all.to_a assert_no_queries do - developers.each { |d| d.firms } + developers.each(&:firms) end end @@ -489,7 +490,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase post = posts(:welcome) tag = post.tags.create!(:name => 'doomed') - assert_difference ['post.reload.taggings_count', 'post.reload.tags_count'], -1 do + assert_difference ['post.reload.tags_count'], -1 do posts(:welcome).tags.delete(tag) end end @@ -499,7 +500,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase tag = post.tags.create!(:name => 'doomed') post.update_columns(tags_with_destroy_count: post.tags.count) - assert_difference ['post.reload.taggings_count', 'post.reload.tags_with_destroy_count'], -1 do + assert_difference ['post.reload.tags_with_destroy_count'], -1 do posts(:welcome).tags_with_destroy.delete(tag) end end @@ -509,7 +510,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase tag = post.tags.create!(:name => 'doomed') post.update_columns(tags_with_nullify_count: post.tags.count) - assert_no_difference 'post.reload.taggings_count' do + assert_no_difference 'post.reload.tags_count' do assert_difference 'post.reload.tags_with_nullify_count', -1 do posts(:welcome).tags_with_nullify.delete(tag) end @@ -524,14 +525,14 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase tag.tagged_posts = [] post.reload - assert_equal(post.taggings.count, post.taggings_count) + assert_equal(post.taggings.count, post.tags_count) end def test_update_counter_caches_on_destroy post = posts(:welcome) tag = post.tags.create!(name: 'doomed') - assert_difference 'post.reload.taggings_count', -1 do + assert_difference 'post.reload.tags_count', -1 do tag.tagged_posts.destroy(post) end end @@ -614,8 +615,11 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase def test_create_on_new_record p = Post.new - assert_raises(ActiveRecord::RecordNotSaved) { p.people.create(:first_name => "mew") } - assert_raises(ActiveRecord::RecordNotSaved) { p.people.create!(:first_name => "snow") } + error = assert_raises(ActiveRecord::RecordNotSaved) { p.people.create(:first_name => "mew") } + assert_equal "You cannot call create unless the parent is saved", error.message + + error = assert_raises(ActiveRecord::RecordNotSaved) { p.people.create!(:first_name => "snow") } + assert_equal "You cannot call create unless the parent is saved", error.message end def test_associate_with_create_and_invalid_options @@ -1095,7 +1099,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase def test_has_many_through_associations_on_new_records_use_null_relations person = Person.new - assert_no_queries do + assert_no_queries(ignore_none: false) do assert_equal [], person.posts assert_equal [], person.posts.where(body: 'omg') assert_equal [], person.posts.pluck(:body) @@ -1139,4 +1143,12 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase assert_equal 2, post.lazy_readers_unscope_skimmers.to_a.size assert_equal 2, post.lazy_people_unscope_skimmers.to_a.size end + + def test_has_many_through_add_with_sti_middle_relation + club = SuperClub.create!(name: 'Fight Club') + member = Member.create!(name: 'Tyler Durden') + + club.members << member + assert_equal 1, SuperMembership.where(member_id: member.id, club_id: club.id).count + end end diff --git a/activerecord/test/cases/associations/has_one_associations_test.rb b/activerecord/test/cases/associations/has_one_associations_test.rb index a4650ccdf2..9b6757e256 100644 --- a/activerecord/test/cases/associations/has_one_associations_test.rb +++ b/activerecord/test/cases/associations/has_one_associations_test.rb @@ -1,5 +1,6 @@ require "cases/helper" require 'models/developer' +require 'models/computer' require 'models/project' require 'models/company' require 'models/ship' @@ -7,6 +8,7 @@ require 'models/pirate' require 'models/car' require 'models/bulb' require 'models/author' +require 'models/image' require 'models/post' class HasOneAssociationsTest < ActiveRecord::TestCase @@ -200,7 +202,7 @@ class HasOneAssociationsTest < ActiveRecord::TestCase end def test_build_association_dont_create_transaction - assert_no_queries { + assert_no_queries(ignore_none: false) { Firm.new.build_account } end @@ -271,6 +273,14 @@ class HasOneAssociationsTest < ActiveRecord::TestCase assert_equal account, firm.reload.account end + def test_create_with_inexistent_foreign_key_failing + firm = Firm.create(name: 'GlobalMegaCorp') + + assert_raises(ActiveRecord::UnknownAttributeError) do + firm.create_account_with_inexistent_foreign_key + end + end + def test_build firm = Firm.new("name" => "GlobalMegaCorp") firm.save @@ -409,9 +419,11 @@ class HasOneAssociationsTest < ActiveRecord::TestCase pirate = pirates(:redbeard) new_ship = Ship.new - assert_raise(ActiveRecord::RecordNotSaved) do + error = assert_raise(ActiveRecord::RecordNotSaved) do pirate.ship = new_ship end + + assert_equal "Failed to save the new associated ship.", error.message assert_nil pirate.ship assert_nil new_ship.pirate_id end @@ -421,20 +433,25 @@ class HasOneAssociationsTest < ActiveRecord::TestCase pirate.ship.name = nil assert !pirate.ship.valid? - assert_raise(ActiveRecord::RecordNotSaved) do + error = assert_raise(ActiveRecord::RecordNotSaved) do pirate.ship = ships(:interceptor) end + assert_equal ships(:black_pearl), pirate.ship assert_equal pirate.id, pirate.ship.pirate_id + assert_equal "Failed to remove the existing associated ship. " + + "The record failed to save after its foreign key was set to nil.", error.message end def test_replacement_failure_due_to_new_record_should_raise_error pirate = pirates(:blackbeard) new_ship = Ship.new - assert_raise(ActiveRecord::RecordNotSaved) do + error = assert_raise(ActiveRecord::RecordNotSaved) do pirate.ship = new_ship end + + assert_equal "Failed to save the new associated ship.", error.message assert_equal ships(:black_pearl), pirate.ship assert_equal pirate.id, pirate.ship.pirate_id assert_equal pirate.id, ships(:black_pearl).reload.pirate_id @@ -557,6 +574,12 @@ class HasOneAssociationsTest < ActiveRecord::TestCase assert_equal author.post, post end + def test_has_one_loading_for_new_record + post = Post.create!(author_id: 42, title: 'foo', body: 'bar') + author = Author.new(id: 42) + assert_equal post, author.post + end + def test_has_one_relationship_cannot_have_a_counter_cache assert_raise(ArgumentError) do Class.new(ActiveRecord::Base) do @@ -565,6 +588,16 @@ class HasOneAssociationsTest < ActiveRecord::TestCase end end + def test_with_polymorphic_has_one_with_custom_columns_name + post = Post.create! :title => 'foo', :body => 'bar' + image = Image.create! + + post.main_image = image + post.reload + + assert_equal image, post.main_image + end + test 'dangerous association name raises ArgumentError' do [:errors, 'errors', :save, 'save'].each do |name| assert_raises(ArgumentError, "Association #{name} should not be allowed") do diff --git a/activerecord/test/cases/associations/has_one_through_associations_test.rb b/activerecord/test/cases/associations/has_one_through_associations_test.rb index 089cb0a3a2..f8772547a2 100644 --- a/activerecord/test/cases/associations/has_one_through_associations_test.rb +++ b/activerecord/test/cases/associations/has_one_through_associations_test.rb @@ -15,6 +15,7 @@ require 'models/essay' require 'models/owner' require 'models/post' require 'models/comment' +require 'models/categorization' class HasOneThroughAssociationsTest < ActiveRecord::TestCase fixtures :member_types, :members, :clubs, :memberships, :sponsors, :organizations, :minivans, @@ -289,6 +290,12 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase end end + def test_has_one_through_polymorphic_association + assert_raise(ActiveRecord::HasOneAssociationPolymorphicThroughError) do + @member.premium_club + end + end + def test_has_one_through_belongs_to_should_update_when_the_through_foreign_key_changes minivan = minivans(:cool_first) diff --git a/activerecord/test/cases/associations/inner_join_association_test.rb b/activerecord/test/cases/associations/inner_join_association_test.rb index 07cf65a760..b3fe759ad9 100644 --- a/activerecord/test/cases/associations/inner_join_association_test.rb +++ b/activerecord/test/cases/associations/inner_join_association_test.rb @@ -54,7 +54,7 @@ class InnerJoinAssociationTest < ActiveRecord::TestCase def test_find_with_implicit_inner_joins_without_select_does_not_imply_readonly authors = Author.joins(:posts) assert_not authors.empty?, "expected authors to be non-empty" - assert authors.none? {|a| a.readonly? }, "expected no authors to be readonly" + assert authors.none?(&:readonly?), "expected no authors to be readonly" end def test_find_with_implicit_inner_joins_honors_readonly_with_select @@ -102,7 +102,7 @@ class InnerJoinAssociationTest < ActiveRecord::TestCase def test_find_with_conditions_on_reflection assert !posts(:welcome).comments.empty? - assert Post.joins(:nonexistant_comments).where(:id => posts(:welcome).id).empty? # [sic!] + assert Post.joins(:nonexistent_comments).where(:id => posts(:welcome).id).empty? # [sic!] end def test_find_with_conditions_on_through_reflection diff --git a/activerecord/test/cases/associations/join_model_test.rb b/activerecord/test/cases/associations/join_model_test.rb index aabeea025f..9918601623 100644 --- a/activerecord/test/cases/associations/join_model_test.rb +++ b/activerecord/test/cases/associations/join_model_test.rb @@ -326,11 +326,11 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase end def test_belongs_to_polymorphic_with_counter_cache - assert_equal 1, posts(:welcome)[:taggings_count] + assert_equal 1, posts(:welcome)[:tags_count] tagging = posts(:welcome).taggings.create(:tag => tags(:general)) - assert_equal 2, posts(:welcome, :reload)[:taggings_count] + assert_equal 2, posts(:welcome, :reload)[:tags_count] tagging.destroy - assert_equal 1, posts(:welcome, :reload)[:taggings_count] + assert_equal 1, posts(:welcome, :reload)[:tags_count] end def test_unavailable_through_reflection @@ -393,18 +393,18 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase end def test_has_many_through_polymorphic_has_one - assert_equal Tagging.find(1,2).sort_by { |t| t.id }, authors(:david).taggings_2 + assert_equal Tagging.find(1,2).sort_by(&:id), authors(:david).taggings_2 end def test_has_many_through_polymorphic_has_many - assert_equal taggings(:welcome_general, :thinking_general), authors(:david).taggings.distinct.sort_by { |t| t.id } + assert_equal taggings(:welcome_general, :thinking_general), authors(:david).taggings.distinct.sort_by(&:id) end def test_include_has_many_through_polymorphic_has_many author = Author.includes(:taggings).find authors(:david).id expected_taggings = taggings(:welcome_general, :thinking_general) assert_no_queries do - assert_equal expected_taggings, author.taggings.distinct.sort_by { |t| t.id } + assert_equal expected_taggings, author.taggings.distinct.sort_by(&:id) end end @@ -444,7 +444,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase def test_has_many_through_uses_conditions_specified_on_the_has_many_association author = Author.first assert author.comments.present? - assert author.nonexistant_comments.blank? + assert author.nonexistent_comments.blank? end def test_has_many_through_uses_correct_attributes @@ -489,7 +489,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase message = "Expected a Tag in tags collection, got #{wrong.class}.") assert_nil( wrong = post_thinking.taggings.detect { |t| t.class != Tagging }, message = "Expected a Tagging in taggings collection, got #{wrong.class}.") - assert_equal(count + 1, post_thinking.tags.size) + assert_equal(count + 1, post_thinking.reload.tags.size) assert_equal(count + 1, post_thinking.tags(true).size) assert_kind_of Tag, post_thinking.tags.create!(:name => 'foo') @@ -497,7 +497,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase message = "Expected a Tag in tags collection, got #{wrong.class}.") assert_nil( wrong = post_thinking.taggings.detect { |t| t.class != Tagging }, message = "Expected a Tagging in taggings collection, got #{wrong.class}.") - assert_equal(count + 2, post_thinking.tags.size) + assert_equal(count + 2, post_thinking.reload.tags.size) assert_equal(count + 2, post_thinking.tags(true).size) assert_nothing_raised { post_thinking.tags.concat(Tag.create!(:name => 'abc'), Tag.create!(:name => 'def')) } @@ -505,7 +505,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase message = "Expected a Tag in tags collection, got #{wrong.class}.") assert_nil( wrong = post_thinking.taggings.detect { |t| t.class != Tagging }, message = "Expected a Tagging in taggings collection, got #{wrong.class}.") - assert_equal(count + 4, post_thinking.tags.size) + assert_equal(count + 4, post_thinking.reload.tags.size) assert_equal(count + 4, post_thinking.tags(true).size) # Raises if the wrong reflection name is used to set the Edge belongs_to @@ -554,34 +554,35 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase def test_delete_associate_when_deleting_from_has_many_through count = posts(:thinking).tags.count - tags_before = posts(:thinking).tags + tags_before = posts(:thinking).tags.sort tag = Tag.create!(:name => 'doomed') post_thinking = posts(:thinking) post_thinking.tags << tag assert_equal(count + 1, post_thinking.taggings(true).size) - assert_equal(count + 1, post_thinking.tags(true).size) + assert_equal(count + 1, post_thinking.reload.tags(true).size) + assert_not_equal(tags_before, post_thinking.tags.sort) assert_nothing_raised { post_thinking.tags.delete(tag) } assert_equal(count, post_thinking.tags.size) assert_equal(count, post_thinking.tags(true).size) assert_equal(count, post_thinking.taggings(true).size) - assert_equal(tags_before.sort, post_thinking.tags.sort) + assert_equal(tags_before, post_thinking.tags.sort) end def test_delete_associate_when_deleting_from_has_many_through_with_multiple_tags count = posts(:thinking).tags.count - tags_before = posts(:thinking).tags + tags_before = posts(:thinking).tags.sort doomed = Tag.create!(:name => 'doomed') doomed2 = Tag.create!(:name => 'doomed2') quaked = Tag.create!(:name => 'quaked') post_thinking = posts(:thinking) post_thinking.tags << doomed << doomed2 - assert_equal(count + 2, post_thinking.tags(true).size) + assert_equal(count + 2, post_thinking.reload.tags(true).size) assert_nothing_raised { post_thinking.tags.delete(doomed, doomed2, quaked) } assert_equal(count, post_thinking.tags.size) assert_equal(count, post_thinking.tags(true).size) - assert_equal(tags_before.sort, post_thinking.tags.sort) + assert_equal(tags_before, post_thinking.tags.sort) end def test_deleting_junk_from_has_many_through_should_raise_type_mismatch diff --git a/activerecord/test/cases/associations/nested_through_associations_test.rb b/activerecord/test/cases/associations/nested_through_associations_test.rb index 8ef351cda8..31b68c940e 100644 --- a/activerecord/test/cases/associations/nested_through_associations_test.rb +++ b/activerecord/test/cases/associations/nested_through_associations_test.rb @@ -130,7 +130,7 @@ class NestedThroughAssociationsTest < ActiveRecord::TestCase def test_has_many_through_has_one_through_with_has_one_source_reflection_preload members = assert_queries(4) { Member.includes(:nested_sponsors).to_a } mustache = sponsors(:moustache_club_sponsor_for_groucho) - assert_no_queries do + assert_no_queries(ignore_none: false) do assert_equal [mustache], members.first.nested_sponsors end end @@ -153,6 +153,7 @@ class NestedThroughAssociationsTest < ActiveRecord::TestCase end def test_has_many_through_has_one_with_has_many_through_source_reflection_preload + ActiveRecord::Base.connection.table_alias_length # preheat cache members = assert_queries(4) { Member.includes(:organization_member_details).to_a.sort_by(&:id) } groucho_details, other_details = member_details(:groucho), member_details(:some_other_guy) diff --git a/activerecord/test/cases/associations/required_test.rb b/activerecord/test/cases/associations/required_test.rb new file mode 100644 index 0000000000..321fb6c8dd --- /dev/null +++ b/activerecord/test/cases/associations/required_test.rb @@ -0,0 +1,82 @@ +require "cases/helper" + +class RequiredAssociationsTest < ActiveRecord::TestCase + self.use_transactional_fixtures = false + + class Parent < ActiveRecord::Base + end + + class Child < ActiveRecord::Base + end + + setup do + @connection = ActiveRecord::Base.connection + @connection.create_table :parents, force: true + @connection.create_table :children, force: true do |t| + t.belongs_to :parent + end + end + + teardown do + @connection.drop_table 'parents' if @connection.table_exists? 'parents' + @connection.drop_table 'children' if @connection.table_exists? 'children' + end + + test "belongs_to associations are not required by default" do + model = subclass_of(Child) do + belongs_to :parent, inverse_of: false, + class_name: "RequiredAssociationsTest::Parent" + end + + assert model.new.save + assert model.new(parent: Parent.new).save + end + + test "required belongs_to associations have presence validated" do + model = subclass_of(Child) do + belongs_to :parent, required: true, inverse_of: false, + class_name: "RequiredAssociationsTest::Parent" + end + + record = model.new + assert_not record.save + assert_equal ["Parent can't be blank"], record.errors.full_messages + + record.parent = Parent.new + assert record.save + end + + test "has_one associations are not required by default" do + model = subclass_of(Parent) do + has_one :child, inverse_of: false, + class_name: "RequiredAssociationsTest::Child" + end + + assert model.new.save + assert model.new(child: Child.new).save + end + + test "required has_one associations have presence validated" do + model = subclass_of(Parent) do + has_one :child, required: true, inverse_of: false, + class_name: "RequiredAssociationsTest::Child" + end + + record = model.new + assert_not record.save + assert_equal ["Child can't be blank"], record.errors.full_messages + + record.child = Child.new + assert record.save + end + + private + + def subclass_of(klass, &block) + subclass = Class.new(klass, &block) + def subclass.name + superclass.name + end + subclass + end +end diff --git a/activerecord/test/cases/associations_test.rb b/activerecord/test/cases/associations_test.rb index f663b5490c..72963fd56c 100644 --- a/activerecord/test/cases/associations_test.rb +++ b/activerecord/test/cases/associations_test.rb @@ -1,6 +1,7 @@ require "cases/helper" require 'models/computer' require 'models/developer' +require 'models/computer' require 'models/project' require 'models/company' require 'models/categorization' @@ -23,7 +24,7 @@ require 'models/interest' class AssociationsTest < ActiveRecord::TestCase fixtures :accounts, :companies, :developers, :projects, :developers_projects, - :computers, :people, :readers + :computers, :people, :readers, :authors, :author_favorites def test_eager_loading_should_not_change_count_of_children liquid = Liquid.create(:name => 'salty') @@ -35,12 +36,19 @@ class AssociationsTest < ActiveRecord::TestCase assert_equal 1, liquids[0].molecules.length end + def test_subselect + author = authors :david + favs = author.author_favorites + fav2 = author.author_favorites.where(:author => Author.where(id: author.id)).to_a + assert_equal favs, fav2 + end + def test_clear_association_cache_stored firm = Firm.find(1) assert_kind_of Firm, firm firm.clear_association_cache - assert_equal Firm.find(1).clients.collect{ |x| x.name }.sort, firm.clients.collect{ |x| x.name }.sort + assert_equal Firm.find(1).clients.collect(&:name).sort, firm.clients.collect(&:name).sort end def test_clear_association_cache_new_record @@ -256,6 +264,11 @@ class AssociationProxyTest < ActiveRecord::TestCase end end + test "first! works on loaded associations" do + david = authors(:david) + assert_equal david.posts.first, david.posts.reload.first! + end + def test_reset_unloads_target david = authors(:david) david.posts.reload @@ -350,4 +363,18 @@ class GeneratedMethodsTest < ActiveRecord::TestCase def test_model_method_overrides_association_method assert_equal(comments(:greetings).body, posts(:welcome).first_comment) end + + module MyModule + def comments; :none end + end + + class MyArticle < ActiveRecord::Base + self.table_name = "articles" + include MyModule + has_many :comments, inverse_of: false + end + + def test_included_module_overwrites_association_methods + assert_equal :none, MyArticle.new.comments + end end diff --git a/activerecord/test/cases/attribute_decorators_test.rb b/activerecord/test/cases/attribute_decorators_test.rb index 35393753a2..53bd58e22e 100644 --- a/activerecord/test/cases/attribute_decorators_test.rb +++ b/activerecord/test/cases/attribute_decorators_test.rb @@ -28,7 +28,7 @@ module ActiveRecord teardown do return unless @connection - @connection.execute 'DROP TABLE IF EXISTS attribute_decorators_model' + @connection.drop_table 'attribute_decorators_model' if @connection.table_exists? 'attribute_decorators_model' Model.attribute_type_decorations.clear Model.reset_column_information end @@ -44,8 +44,8 @@ module ActiveRecord end test "decoration does not eagerly load existing columns" do + Model.reset_column_information assert_no_queries do - Model.reset_column_information Model.decorate_attribute_type(:a_string, :test) { |t| StringDecorator.new(t) } end end @@ -98,17 +98,28 @@ module ActiveRecord assert_equal 'Hello! decorated!', model.a_string assert_equal 'whatever', model.another_string assert_equal 'Hello! decorated! decorated!', child.a_string - # We are round tripping the default, and we don't undo our decoration - assert_equal 'whatever decorated! decorated!', child.another_string + assert_equal 'whatever decorated!', child.another_string end - test "defaults are decorated on the column" do - Model.attribute :a_string, Type::String.new, default: 'whatever' - Model.decorate_attribute_type(:a_string, :test) { |t| StringDecorator.new(t) } + class Multiplier < SimpleDelegator + def type_cast_from_user(value) + return if value.nil? + value * 2 + end + alias type_cast_from_database type_cast_from_user + end + + test "decorating with a proc" do + Model.attribute :an_int, Type::Integer.new + type_is_integer = proc { |_, type| type.type == :integer } + Model.decorate_matching_attribute_types type_is_integer, :multiplier do |type| + Multiplier.new(type) + end - column = Model.columns_hash['a_string'] + model = Model.new(a_string: 'whatever', an_int: 1) - assert_equal 'whatever decorated!', column.default + assert_equal 'whatever', model.a_string + assert_equal 2, model.an_int end end end diff --git a/activerecord/test/cases/attribute_methods/read_test.rb b/activerecord/test/cases/attribute_methods/read_test.rb index c0659fddef..e38b32d7fc 100644 --- a/activerecord/test/cases/attribute_methods/read_test.rb +++ b/activerecord/test/cases/attribute_methods/read_test.rb @@ -12,6 +12,8 @@ module ActiveRecord @klass = Class.new do def self.superclass; Base; end def self.base_class; self; end + def self.decorate_matching_attribute_types(*); end + def self.initialize_generated_modules; end include ActiveRecord::AttributeMethods diff --git a/activerecord/test/cases/attribute_methods_test.rb b/activerecord/test/cases/attribute_methods_test.rb index f832ca3451..731d433706 100644 --- a/activerecord/test/cases/attribute_methods_test.rb +++ b/activerecord/test/cases/attribute_methods_test.rb @@ -1,6 +1,7 @@ require "cases/helper" require 'models/minimalistic' require 'models/developer' +require 'models/computer' require 'models/auto_id' require 'models/boolean' require 'models/computer' @@ -143,7 +144,11 @@ class AttributeMethodsTest < ActiveRecord::TestCase # Syck calls respond_to? before actually calling initialize def test_respond_to_with_allocated_object - topic = Topic.allocate + klass = Class.new(ActiveRecord::Base) do + self.table_name = 'topics' + end + + topic = klass.allocate assert !topic.respond_to?("nothingness") assert !topic.respond_to?(:nothingness) assert_respond_to topic, "title" @@ -253,6 +258,15 @@ class AttributeMethodsTest < ActiveRecord::TestCase assert_equal @loaded_fixtures['computers']['workstation'].to_hash, Computer.first.attributes end + def test_attributes_without_primary_key + klass = Class.new(ActiveRecord::Base) do + self.table_name = 'developers_projects' + end + + assert_equal klass.column_names, klass.new.attributes.keys + assert_not klass.new.has_attribute?('id') + end + def test_hashes_not_mangled new_topic = { :title => "New Topic" } new_topic_values = { :title => "AnotherTopic" } @@ -668,6 +682,14 @@ class AttributeMethodsTest < ActiveRecord::TestCase end end + def test_yaml_dumping_record_with_time_zone_aware_attribute + in_time_zone "Pacific Time (US & Canada)" do + record = Topic.new(id: 1) + record.written_on = "Jan 01 00:00:00 2014" + assert_equal record, YAML.load(YAML.dump(record)) + 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] @@ -713,13 +735,13 @@ class AttributeMethodsTest < ActiveRecord::TestCase assert_raise(ActiveRecord::UnknownAttributeError) { @target.new.attributes = { :title => "Ants in pants" } } end - def test_bulk_update_raise_unknown_attribute_errro + def test_bulk_update_raise_unknown_attribute_error error = assert_raises(ActiveRecord::UnknownAttributeError) { - @target.new(:hello => "world") + Topic.new(hello: "world") } - assert @target, error.record - assert "hello", error.attribute - assert "unknown attribute: hello", error.message + assert_instance_of Topic, error.record + assert_equal "hello", error.attribute + assert_equal "unknown attribute 'hello' for Topic.", error.message end def test_methods_override_in_multi_level_subclass @@ -797,6 +819,24 @@ class AttributeMethodsTest < ActiveRecord::TestCase assert_equal "lol", topic.author_name end + def test_inherited_custom_accessors_with_reserved_names + klass = Class.new(ActiveRecord::Base) do + self.table_name = 'computers' + self.abstract_class = true + def system; "omg"; end + def system=(val); self.developer = val; end + end + + subklass = Class.new(klass) + [klass, subklass].each(&:define_attribute_methods) + + computer = subklass.find(1) + assert_equal "omg", computer.system + + computer.developer = 99 + assert_equal 99, computer.developer + end + def test_on_the_fly_super_invokable_generated_attribute_methods_via_method_missing klass = new_topic_like_ar_class do def title @@ -831,6 +871,37 @@ class AttributeMethodsTest < ActiveRecord::TestCase end end + def test_attribute_method? + assert @target.attribute_method?(:title) + assert @target.attribute_method?(:title=) + assert_not @target.attribute_method?(:wibble) + end + + def test_attribute_method_returns_false_if_table_does_not_exist + @target.table_name = 'wibble' + assert_not @target.attribute_method?(:title) + end + + def test_attribute_names_on_new_record + model = @target.new + + assert_equal @target.column_names, model.attribute_names + end + + def test_attribute_names_on_queried_record + model = @target.last! + + assert_equal @target.column_names, model.attribute_names + end + + def test_attribute_names_with_custom_select + model = @target.select('id').last! + + assert_equal ['id'], model.attribute_names + # Sanity check, make sure other columns exist + assert_not_equal ['id'], @target.column_names + end + private def new_topic_like_ar_class(&block) diff --git a/activerecord/test/cases/attribute_set_test.rb b/activerecord/test/cases/attribute_set_test.rb new file mode 100644 index 0000000000..ba53612d30 --- /dev/null +++ b/activerecord/test/cases/attribute_set_test.rb @@ -0,0 +1,190 @@ +require 'cases/helper' + +module ActiveRecord + class AttributeSetTest < ActiveRecord::TestCase + test "building a new set from raw attributes" do + builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::Float.new) + attributes = builder.build_from_database(foo: '1.1', bar: '2.2') + + assert_equal 1, attributes[:foo].value + assert_equal 2.2, attributes[:bar].value + assert_equal :foo, attributes[:foo].name + assert_equal :bar, attributes[:bar].name + end + + test "building with custom types" do + builder = AttributeSet::Builder.new(foo: Type::Float.new) + attributes = builder.build_from_database({ foo: '3.3', bar: '4.4' }, { bar: Type::Integer.new }) + + assert_equal 3.3, attributes[:foo].value + assert_equal 4, attributes[:bar].value + end + + test "[] returns a null object" do + builder = AttributeSet::Builder.new(foo: Type::Float.new) + attributes = builder.build_from_database(foo: '3.3') + + assert_equal '3.3', attributes[:foo].value_before_type_cast + assert_equal nil, attributes[:bar].value_before_type_cast + assert_equal :bar, attributes[:bar].name + end + + test "duping creates a new hash and dups each attribute" do + builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::String.new) + attributes = builder.build_from_database(foo: 1, bar: 'foo') + + # Ensure the type cast value is cached + attributes[:foo].value + attributes[:bar].value + + duped = attributes.dup + duped.write_from_database(:foo, 2) + duped[:bar].value << 'bar' + + assert_equal 1, attributes[:foo].value + assert_equal 2, duped[:foo].value + assert_equal 'foo', attributes[:bar].value + assert_equal 'foobar', duped[:bar].value + end + + test "freezing cloned set does not freeze original" do + attributes = AttributeSet.new({}) + clone = attributes.clone + + clone.freeze + + assert clone.frozen? + assert_not attributes.frozen? + end + + test "to_hash returns a hash of the type cast values" do + builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::Float.new) + attributes = builder.build_from_database(foo: '1.1', bar: '2.2') + + assert_equal({ foo: 1, bar: 2.2 }, attributes.to_hash) + assert_equal({ foo: 1, bar: 2.2 }, attributes.to_h) + end + + test "values_before_type_cast" do + builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::Integer.new) + attributes = builder.build_from_database(foo: '1.1', bar: '2.2') + + assert_equal({ foo: '1.1', bar: '2.2' }, attributes.values_before_type_cast) + end + + test "known columns are built with uninitialized attributes" do + attributes = attributes_with_uninitialized_key + assert attributes[:foo].initialized? + assert_not attributes[:bar].initialized? + end + + test "uninitialized attributes are not included in the attributes hash" do + attributes = attributes_with_uninitialized_key + assert_equal({ foo: 1 }, attributes.to_hash) + end + + test "uninitialized attributes are not included in keys" do + attributes = attributes_with_uninitialized_key + assert_equal [:foo], attributes.keys + end + + test "uninitialized attributes return false for key?" do + attributes = attributes_with_uninitialized_key + assert attributes.key?(:foo) + assert_not attributes.key?(:bar) + end + + test "unknown attributes return false for key?" do + attributes = attributes_with_uninitialized_key + assert_not attributes.key?(:wibble) + end + + test "fetch_value returns the value for the given initialized attribute" do + builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::Float.new) + attributes = builder.build_from_database(foo: '1.1', bar: '2.2') + + assert_equal 1, attributes.fetch_value(:foo) + assert_equal 2.2, attributes.fetch_value(:bar) + end + + test "fetch_value returns nil for unknown attributes" do + attributes = attributes_with_uninitialized_key + assert_nil attributes.fetch_value(:wibble) { "hello" } + end + + test "fetch_value returns nil for unknown attributes when types has a default" do + types = Hash.new(Type::Value.new) + builder = AttributeSet::Builder.new(types) + attributes = builder.build_from_database + + assert_nil attributes.fetch_value(:wibble) { "hello" } + end + + test "fetch_value uses the given block for uninitialized attributes" do + attributes = attributes_with_uninitialized_key + value = attributes.fetch_value(:bar) { |n| n.to_s + '!' } + assert_equal 'bar!', value + end + + test "fetch_value returns nil for uninitialized attributes if no block is given" do + attributes = attributes_with_uninitialized_key + assert_nil attributes.fetch_value(:bar) + end + + test "the primary_key is always initialized" do + builder = AttributeSet::Builder.new({ foo: Type::Integer.new }, :foo) + attributes = builder.build_from_database + + assert attributes.key?(:foo) + assert_equal [:foo], attributes.keys + assert attributes[:foo].initialized? + end + + class MyType + def type_cast_from_user(value) + return if value.nil? + value + " from user" + end + + def type_cast_from_database(value) + return if value.nil? + value + " from database" + end + end + + test "write_from_database sets the attribute with database typecasting" do + builder = AttributeSet::Builder.new(foo: MyType.new) + attributes = builder.build_from_database + + assert_nil attributes.fetch_value(:foo) + + attributes.write_from_database(:foo, "value") + + assert_equal "value from database", attributes.fetch_value(:foo) + end + + test "write_from_user sets the attribute with user typecasting" do + builder = AttributeSet::Builder.new(foo: MyType.new) + attributes = builder.build_from_database + + assert_nil attributes.fetch_value(:foo) + + attributes.write_from_user(:foo, "value") + + assert_equal "value from user", attributes.fetch_value(:foo) + end + + def attributes_with_uninitialized_key + builder = AttributeSet::Builder.new(foo: Type::Integer.new, bar: Type::Float.new) + builder.build_from_database(foo: '1.1') + end + + test "freezing doesn't prevent the set from materializing" do + builder = AttributeSet::Builder.new(foo: Type::String.new) + attributes = builder.build_from_database(foo: "1") + + attributes.freeze + assert_equal({ foo: "1" }, attributes.to_hash) + end + end +end diff --git a/activerecord/test/cases/attribute_test.rb b/activerecord/test/cases/attribute_test.rb index 57dd2e9a5e..7b325abf1d 100644 --- a/activerecord/test/cases/attribute_test.rb +++ b/activerecord/test/cases/attribute_test.rb @@ -4,7 +4,7 @@ require 'minitest/mock' module ActiveRecord class AttributeTest < ActiveRecord::TestCase setup do - @type = MiniTest::Mock.new + @type = Minitest::Mock.new end teardown do @@ -13,7 +13,7 @@ module ActiveRecord test "from_database + read type casts from database" do @type.expect(:type_cast_from_database, 'type cast from database', ['a value']) - attribute = Attribute.from_database('a value', @type) + attribute = Attribute.from_database(nil, 'a value', @type) type_cast_value = attribute.value @@ -22,7 +22,7 @@ module ActiveRecord test "from_user + read type casts from user" do @type.expect(:type_cast_from_user, 'type cast from user', ['a value']) - attribute = Attribute.from_user('a value', @type) + attribute = Attribute.from_user(nil, 'a value', @type) type_cast_value = attribute.value @@ -31,7 +31,7 @@ module ActiveRecord test "reading memoizes the value" do @type.expect(:type_cast_from_database, 'from the database', ['whatever']) - attribute = Attribute.from_database('whatever', @type) + attribute = Attribute.from_database(nil, 'whatever', @type) type_cast_value = attribute.value second_read = attribute.value @@ -42,14 +42,14 @@ module ActiveRecord test "reading memoizes falsy values" do @type.expect(:type_cast_from_database, false, ['whatever']) - attribute = Attribute.from_database('whatever', @type) + attribute = Attribute.from_database(nil, 'whatever', @type) attribute.value attribute.value end test "read_before_typecast returns the given value" do - attribute = Attribute.from_database('raw value', @type) + attribute = Attribute.from_database(nil, 'raw value', @type) raw_value = attribute.value_before_type_cast @@ -59,7 +59,7 @@ module ActiveRecord test "from_database + read_for_database type casts to and from database" do @type.expect(:type_cast_from_database, 'read from database', ['whatever']) @type.expect(:type_cast_for_database, 'ready for database', ['read from database']) - attribute = Attribute.from_database('whatever', @type) + attribute = Attribute.from_database(nil, 'whatever', @type) type_cast_for_database = attribute.value_for_database @@ -69,7 +69,7 @@ module ActiveRecord test "from_user + read_for_database type casts from the user to the database" do @type.expect(:type_cast_from_user, 'read from user', ['whatever']) @type.expect(:type_cast_for_database, 'ready for database', ['read from user']) - attribute = Attribute.from_user('whatever', @type) + attribute = Attribute.from_user(nil, 'whatever', @type) type_cast_for_database = attribute.value_for_database @@ -78,7 +78,7 @@ module ActiveRecord test "duping dups the value" do @type.expect(:type_cast_from_database, 'type cast', ['a value']) - attribute = Attribute.from_database('a value', @type) + attribute = Attribute.from_database(nil, 'a value', @type) value_from_orig = attribute.value value_from_clone = attribute.dup.value @@ -90,14 +90,83 @@ module ActiveRecord test "duping does not dup the value if it is not dupable" do @type.expect(:type_cast_from_database, false, ['a value']) - attribute = Attribute.from_database('a value', @type) + attribute = Attribute.from_database(nil, 'a value', @type) assert_same attribute.value, attribute.dup.value end test "duping does not eagerly type cast if we have not yet type cast" do - attribute = Attribute.from_database('a value', @type) + attribute = Attribute.from_database(nil, 'a value', @type) attribute.dup end + + class MyType + def type_cast_from_user(value) + value + " from user" + end + + def type_cast_from_database(value) + value + " from database" + end + end + + test "with_value_from_user returns a new attribute with the value from the user" do + old = Attribute.from_database(nil, "old", MyType.new) + new = old.with_value_from_user("new") + + assert_equal "old from database", old.value + assert_equal "new from user", new.value + end + + test "with_value_from_database returns a new attribute with the value from the database" do + old = Attribute.from_user(nil, "old", MyType.new) + new = old.with_value_from_database("new") + + assert_equal "old from user", old.value + assert_equal "new from database", new.value + end + + test "uninitialized attributes yield their name if a block is given to value" do + block = proc { |name| name.to_s + "!" } + foo = Attribute.uninitialized(:foo, nil) + bar = Attribute.uninitialized(:bar, nil) + + assert_equal "foo!", foo.value(&block) + assert_equal "bar!", bar.value(&block) + end + + test "uninitialized attributes have no value" do + assert_nil Attribute.uninitialized(:foo, nil).value + end + + test "attributes equal other attributes with the same constructor arguments" do + first = Attribute.from_database(:foo, 1, Type::Integer.new) + second = Attribute.from_database(:foo, 1, Type::Integer.new) + assert_equal first, second + end + + test "attributes do not equal attributes with different names" do + first = Attribute.from_database(:foo, 1, Type::Integer.new) + second = Attribute.from_database(:bar, 1, Type::Integer.new) + assert_not_equal first, second + end + + test "attributes do not equal attributes with different types" do + first = Attribute.from_database(:foo, 1, Type::Integer.new) + second = Attribute.from_database(:foo, 1, Type::Float.new) + assert_not_equal first, second + end + + test "attributes do not equal attributes with different values" do + first = Attribute.from_database(:foo, 1, Type::Integer.new) + second = Attribute.from_database(:foo, 2, Type::Integer.new) + assert_not_equal first, second + end + + test "attributes do not equal attributes of other classes" do + first = Attribute.from_database(:foo, 1, Type::Integer.new) + second = Attribute.from_user(:foo, 1, Type::Integer.new) + assert_not_equal first, second + end end end diff --git a/activerecord/test/cases/attributes_test.rb b/activerecord/test/cases/attributes_test.rb index 79ef0502cb..dbe1eb48db 100644 --- a/activerecord/test/cases/attributes_test.rb +++ b/activerecord/test/cases/attributes_test.rb @@ -20,7 +20,7 @@ end module ActiveRecord class CustomPropertiesTest < ActiveRecord::TestCase - def test_overloading_types + test "overloading types" do data = OverloadedType.new data.overloaded_float = "1.1" @@ -30,7 +30,7 @@ module ActiveRecord assert_equal 1.1, data.unoverloaded_float end - def test_overloaded_properties_save + test "overloaded properties save" do data = OverloadedType.new data.overloaded_float = "2.2" @@ -43,18 +43,18 @@ module ActiveRecord assert_kind_of Float, UnoverloadedType.last.overloaded_float end - def test_properties_assigned_in_constructor + test "properties assigned in constructor" do data = OverloadedType.new(overloaded_float: '3.3') assert_equal 3, data.overloaded_float end - def test_overloaded_properties_with_limit + test "overloaded properties with limit" do assert_equal 50, OverloadedType.columns_hash['overloaded_string_with_limit'].limit assert_equal 255, UnoverloadedType.columns_hash['overloaded_string_with_limit'].limit end - def test_nonexistent_attribute + test "nonexistent attribute" do data = OverloadedType.new(non_existent_decimal: 1) assert_equal BigDecimal.new(1), data.non_existent_decimal @@ -63,7 +63,7 @@ module ActiveRecord end end - def test_changing_defaults + test "changing defaults" do data = OverloadedType.new unoverloaded_data = UnoverloadedType.new @@ -71,24 +71,28 @@ module ActiveRecord assert_equal 'the original default', unoverloaded_data.string_with_default end - def test_children_inherit_custom_properties + test "defaults are not touched on the columns" do + assert_equal 'the original default', OverloadedType.columns_hash['string_with_default'].default + end + + test "children inherit custom properties" do data = ChildOfOverloadedType.new(overloaded_float: '4.4') assert_equal 4, data.overloaded_float end - def test_children_can_override_parents + test "children can override parents" do data = GrandchildOfOverloadedType.new(overloaded_float: '4.4') assert_equal 4.4, data.overloaded_float end - def test_overloading_properties_does_not_change_column_order + test "overloading properties does not change column order" do column_names = OverloadedType.column_names assert_equal %w(id overloaded_float unoverloaded_float overloaded_string_with_limit string_with_default non_existent_decimal), column_names end - def test_caches_are_cleared + test "caches are cleared" do klass = Class.new(OverloadedType) assert_equal 6, klass.columns.length diff --git a/activerecord/test/cases/autosave_association_test.rb b/activerecord/test/cases/autosave_association_test.rb index 09892d50ba..04d5a2869c 100644 --- a/activerecord/test/cases/autosave_association_test.rb +++ b/activerecord/test/cases/autosave_association_test.rb @@ -1,8 +1,10 @@ require 'cases/helper' require 'models/bird' +require 'models/comment' require 'models/company' require 'models/customer' require 'models/developer' +require 'models/computer' require 'models/invoice' require 'models/line_item' require 'models/order' @@ -19,6 +21,9 @@ require 'models/treasure' require 'models/eye' require 'models/electron' require 'models/molecule' +require 'models/member' +require 'models/member_detail' +require 'models/organization' class TestAutosaveAssociationsInGeneral < ActiveRecord::TestCase def test_autosave_validation @@ -499,7 +504,7 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociation < ActiveRecord::TestCa def test_build_before_save company = companies(:first_firm) - new_client = assert_no_queries { company.clients_of_firm.build("name" => "Another Client") } + new_client = assert_no_queries(ignore_none: false) { company.clients_of_firm.build("name" => "Another Client") } assert !company.clients_of_firm.loaded? company.name += '-changed' @@ -510,7 +515,7 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociation < ActiveRecord::TestCa def test_build_many_before_save company = companies(:first_firm) - assert_no_queries { company.clients_of_firm.build([{"name" => "Another Client"}, {"name" => "Another Client II"}]) } + assert_no_queries(ignore_none: false) { company.clients_of_firm.build([{"name" => "Another Client"}, {"name" => "Another Client II"}]) } company.name += '-changed' assert_queries(3) { assert company.save } @@ -519,7 +524,7 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociation < ActiveRecord::TestCa def test_build_via_block_before_save company = companies(:first_firm) - new_client = assert_no_queries { company.clients_of_firm.build {|client| client.name = "Another Client" } } + new_client = assert_no_queries(ignore_none: false) { company.clients_of_firm.build {|client| client.name = "Another Client" } } assert !company.clients_of_firm.loaded? company.name += '-changed' @@ -530,7 +535,7 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociation < ActiveRecord::TestCa def test_build_many_via_block_before_save company = companies(:first_firm) - assert_no_queries do + assert_no_queries(ignore_none: false) do company.clients_of_firm.build([{"name" => "Another Client"}, {"name" => "Another Client II"}]) do |client| client.name = "changed" end @@ -613,6 +618,14 @@ class TestDefaultAutosaveAssociationOnNewRecord < ActiveRecord::TestCase firm.save! assert !account.persisted? end + + def test_autosave_new_record_with_after_create_callback + post = PostWithAfterCreateCallback.new(title: 'Captain Murphy', body: 'is back') + post.comments.build(body: 'foo') + post.save! + + assert_not_nil post.author_id + end end class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase @@ -761,13 +774,13 @@ class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase def test_should_destroy_has_many_as_part_of_the_save_transaction_if_they_were_marked_for_destruction 2.times { |i| @pirate.birds.create!(:name => "birds_#{i}") } - assert !@pirate.birds.any? { |child| child.marked_for_destruction? } + assert !@pirate.birds.any?(&:marked_for_destruction?) - @pirate.birds.each { |child| child.mark_for_destruction } + @pirate.birds.each(&:mark_for_destruction) klass = @pirate.birds.first.class ids = @pirate.birds.map(&:id) - assert @pirate.birds.all? { |child| child.marked_for_destruction? } + assert @pirate.birds.all?(&:marked_for_destruction?) ids.each { |id| assert klass.find_by_id(id) } @pirate.save @@ -801,14 +814,14 @@ class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase @pirate.birds.each { |bird| bird.name = '' } assert !@pirate.valid? - @pirate.birds.each { |bird| bird.destroy } + @pirate.birds.each(&:destroy) assert @pirate.valid? end def test_a_child_marked_for_destruction_should_not_be_destroyed_twice_while_saving_has_many @pirate.birds.create!(:name => "birds_1") - @pirate.birds.each { |bird| bird.mark_for_destruction } + @pirate.birds.each(&:mark_for_destruction) assert @pirate.save @pirate.birds.each { |bird| bird.expects(:destroy).never } @@ -875,7 +888,7 @@ class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase association_name_with_callbacks = "birds_with_#{callback_type}_callbacks" @pirate.send(association_name_with_callbacks).create!(:name => "Crowe the One-Eyed") - @pirate.send(association_name_with_callbacks).each { |c| c.mark_for_destruction } + @pirate.send(association_name_with_callbacks).each(&:mark_for_destruction) child_id = @pirate.send(association_name_with_callbacks).first.id @pirate.ship_log.clear @@ -893,8 +906,8 @@ class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase def test_should_destroy_habtm_as_part_of_the_save_transaction_if_they_were_marked_for_destruction 2.times { |i| @pirate.parrots.create!(:name => "parrots_#{i}") } - assert !@pirate.parrots.any? { |parrot| parrot.marked_for_destruction? } - @pirate.parrots.each { |parrot| parrot.mark_for_destruction } + assert !@pirate.parrots.any?(&:marked_for_destruction?) + @pirate.parrots.each(&:mark_for_destruction) assert_no_difference "Parrot.count" do @pirate.save @@ -927,14 +940,14 @@ class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase @pirate.parrots.each { |parrot| parrot.name = '' } assert !@pirate.valid? - @pirate.parrots.each { |parrot| parrot.destroy } + @pirate.parrots.each(&:destroy) assert @pirate.valid? end def test_a_child_marked_for_destruction_should_not_be_destroyed_twice_while_saving_habtm @pirate.parrots.create!(:name => "parrots_1") - @pirate.parrots.each { |parrot| parrot.mark_for_destruction } + @pirate.parrots.each(&:mark_for_destruction) assert @pirate.save Pirate.transaction do @@ -979,7 +992,7 @@ class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase association_name_with_callbacks = "parrots_with_#{callback_type}_callbacks" @pirate.send(association_name_with_callbacks).create!(:name => "Crowe the One-Eyed") - @pirate.send(association_name_with_callbacks).each { |c| c.mark_for_destruction } + @pirate.send(association_name_with_callbacks).each(&:mark_for_destruction) child_id = @pirate.send(association_name_with_callbacks).first.id @pirate.ship_log.clear @@ -1116,6 +1129,27 @@ class TestAutosaveAssociationOnAHasOneAssociation < ActiveRecord::TestCase end end +class TestAutosaveAssociationOnAHasOneThroughAssociation < ActiveRecord::TestCase + self.use_transactional_fixtures = false unless supports_savepoints? + + def setup + super + organization = Organization.create + @member = Member.create + MemberDetail.create(organization: organization, member: @member) + end + + def test_should_not_has_one_through_model + class << @member.organization + def save(*args) + super + raise 'Oh noes!' + end + end + assert_nothing_raised { @member.save } + end +end + class TestAutosaveAssociationOnABelongsToAssociation < ActiveRecord::TestCase self.use_transactional_fixtures = false unless supports_savepoints? diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb index 8f83cf7cb4..6acd9aa39f 100644 --- a/activerecord/test/cases/base_test.rb +++ b/activerecord/test/cases/base_test.rb @@ -10,6 +10,7 @@ require 'models/category' require 'models/company' require 'models/customer' require 'models/developer' +require 'models/computer' require 'models/project' require 'models/default' require 'models/auto_id' @@ -522,6 +523,10 @@ class BasicsTest < ActiveRecord::TestCase assert_equal Topic.find(['1-meowmeow', '2-hello']), Topic.find([1, 2]) end + def test_find_by_slug_with_range + assert_equal Topic.where(id: '1-meowmeow'..'2-hello'), Topic.where(id: 1..2) + end + def test_equality_of_new_records assert_not_equal Topic.new, Topic.new assert_equal false, Topic.new == Topic.new @@ -893,99 +898,11 @@ class BasicsTest < ActiveRecord::TestCase assert_equal 'a text field', default.char3 end end - - class Geometric < ActiveRecord::Base; end - def test_geometric_content - - # accepted format notes: - # ()'s aren't required - # values can be a mix of float or integer - - g = Geometric.new( - :a_point => '(5.0, 6.1)', - #:a_line => '((2.0, 3), (5.5, 7.0))' # line type is currently unsupported in postgresql - :a_line_segment => '(2.0, 3), (5.5, 7.0)', - :a_box => '2.0, 3, 5.5, 7.0', - :a_path => '[(2.0, 3), (5.5, 7.0), (8.5, 11.0)]', # [ ] is an open path - :a_polygon => '((2.0, 3), (5.5, 7.0), (8.5, 11.0))', - :a_circle => '<(5.3, 10.4), 2>' - ) - - assert g.save - - # Reload and check that we have all the geometric attributes. - h = Geometric.find(g.id) - - assert_equal [5.0, 6.1], h.a_point - assert_equal '[(2,3),(5.5,7)]', h.a_line_segment - assert_equal '(5.5,7),(2,3)', h.a_box # reordered to store upper right corner then bottom left corner - assert_equal '[(2,3),(5.5,7),(8.5,11)]', h.a_path - assert_equal '((2,3),(5.5,7),(8.5,11))', h.a_polygon - assert_equal '<(5.3,10.4),2>', h.a_circle - - # use a geometric function to test for an open path - objs = Geometric.find_by_sql ["select isopen(a_path) from geometrics where id = ?", g.id] - - assert_equal true, objs[0].isopen - - # test alternate formats when defining the geometric types - - g = Geometric.new( - :a_point => '5.0, 6.1', - #:a_line => '((2.0, 3), (5.5, 7.0))' # line type is currently unsupported in postgresql - :a_line_segment => '((2.0, 3), (5.5, 7.0))', - :a_box => '(2.0, 3), (5.5, 7.0)', - :a_path => '((2.0, 3), (5.5, 7.0), (8.5, 11.0))', # ( ) is a closed path - :a_polygon => '2.0, 3, 5.5, 7.0, 8.5, 11.0', - :a_circle => '((5.3, 10.4), 2)' - ) - - assert g.save - - # Reload and check that we have all the geometric attributes. - h = Geometric.find(g.id) - - assert_equal [5.0, 6.1], h.a_point - assert_equal '[(2,3),(5.5,7)]', h.a_line_segment - assert_equal '(5.5,7),(2,3)', h.a_box # reordered to store upper right corner then bottom left corner - assert_equal '((2,3),(5.5,7),(8.5,11))', h.a_path - assert_equal '((2,3),(5.5,7),(8.5,11))', h.a_polygon - assert_equal '<(5.3,10.4),2>', h.a_circle - - # use a geometric function to test for an closed path - objs = Geometric.find_by_sql ["select isclosed(a_path) from geometrics where id = ?", g.id] - - assert_equal true, objs[0].isclosed - - # test native ruby formats when defining the geometric types - g = Geometric.new( - :a_point => [5.0, 6.1], - #:a_line => '((2.0, 3), (5.5, 7.0))' # line type is currently unsupported in postgresql - :a_line_segment => '((2.0, 3), (5.5, 7.0))', - :a_box => '(2.0, 3), (5.5, 7.0)', - :a_path => '((2.0, 3), (5.5, 7.0), (8.5, 11.0))', # ( ) is a closed path - :a_polygon => '2.0, 3, 5.5, 7.0, 8.5, 11.0', - :a_circle => '((5.3, 10.4), 2)' - ) - - assert g.save - - # Reload and check that we have all the geometric attributes. - h = Geometric.find(g.id) - - assert_equal [5.0, 6.1], h.a_point - assert_equal '[(2,3),(5.5,7)]', h.a_line_segment - assert_equal '(5.5,7),(2,3)', h.a_box # reordered to store upper right corner then bottom left corner - assert_equal '((2,3),(5.5,7),(8.5,11))', h.a_path - assert_equal '((2,3),(5.5,7),(8.5,11))', h.a_polygon - assert_equal '<(5.3,10.4),2>', h.a_circle - end end class NumericData < ActiveRecord::Base self.table_name = 'numeric_data' - attribute :world_population, Type::Integer.new attribute :my_house_population, Type::Integer.new attribute :atoms_in_universe, Type::Integer.new end @@ -1347,14 +1264,32 @@ class BasicsTest < ActiveRecord::TestCase end def test_compute_type_no_method_error - ActiveSupport::Dependencies.stubs(:constantize).raises(NoMethodError) + ActiveSupport::Dependencies.stubs(:safe_constantize).raises(NoMethodError) assert_raises NoMethodError do ActiveRecord::Base.send :compute_type, 'InvalidModel' end end + def test_compute_type_on_undefined_method + error = nil + begin + Class.new(Author) do + alias_method :foo, :bar + end + rescue => e + error = e + end + + ActiveSupport::Dependencies.stubs(:safe_constantize).raises(e) + + exception = assert_raises NameError do + ActiveRecord::Base.send :compute_type, 'InvalidModel' + end + assert_equal error.message, exception.message + end + def test_compute_type_argument_error - ActiveSupport::Dependencies.stubs(:constantize).raises(ArgumentError) + ActiveSupport::Dependencies.stubs(:safe_constantize).raises(ArgumentError) assert_raises ArgumentError do ActiveRecord::Base.send :compute_type, 'InvalidModel' end @@ -1365,7 +1300,10 @@ class BasicsTest < ActiveRecord::TestCase c1 = Post.connection.schema_cache.columns('posts') ActiveRecord::Base.clear_cache! c2 = Post.connection.schema_cache.columns('posts') - assert_not_equal c1, c2 + c1.each_with_index do |v, i| + assert_not_same v, c2[i] + end + assert_equal c1, c2 end def test_current_scope_is_reset @@ -1522,20 +1460,6 @@ class BasicsTest < ActiveRecord::TestCase assert_equal "", Company.new.description end - ["find_by", "find_by!"].each do |meth| - test "#{meth} delegates to scoped" do - record = stub - - scope = mock - scope.expects(meth).with(:foo, :bar).returns(record) - - klass = Class.new(ActiveRecord::Base) - klass.stubs(:all => scope) - - assert_equal record, klass.public_send(meth, :foo, :bar) - end - end - test "scoped can take a values hash" do klass = Class.new(ActiveRecord::Base) assert_equal ['foo'], klass.all.merge!(select: 'foo').select_values @@ -1603,4 +1527,14 @@ class BasicsTest < ActiveRecord::TestCase test "records without an id have unique hashes" do assert_not_equal Post.new.hash, Post.new.hash end + + test "resetting column information doesn't remove attribute methods" do + topic = topics(:first) + + assert_not topic.id_changed? + + Topic.reset_column_information + + assert_not topic.id_changed? + end end diff --git a/activerecord/test/cases/binary_test.rb b/activerecord/test/cases/binary_test.rb index b41b95309b..ccf2be369d 100644 --- a/activerecord/test/cases/binary_test.rb +++ b/activerecord/test/cases/binary_test.rb @@ -21,7 +21,7 @@ unless current_adapter?(:DB2Adapter) name = binary.name - # Mysql adapter doesn't properly encode things, so we have to do it + # MySQL adapter doesn't properly encode things, so we have to do it if current_adapter?(:MysqlAdapter) name.force_encoding(Encoding::UTF_8) end diff --git a/activerecord/test/cases/bind_parameter_test.rb b/activerecord/test/cases/bind_parameter_test.rb index 0bc7ee6d64..c4634d11e2 100644 --- a/activerecord/test/cases/bind_parameter_test.rb +++ b/activerecord/test/cases/bind_parameter_test.rb @@ -31,9 +31,9 @@ module ActiveRecord if ActiveRecord::Base.connection.supports_statement_cache? def test_binds_are_logged - sub = @connection.substitute_at(@pk, 0) + sub = @connection.substitute_at(@pk) binds = [[@pk, 1]] - sql = "select * from topics where id = #{sub}" + sql = "select * from topics where id = #{sub.to_sql}" @connection.exec_query(sql, 'SQL', binds) @@ -42,9 +42,9 @@ module ActiveRecord end def test_binds_are_logged_after_type_cast - sub = @connection.substitute_at(@pk, 0) + sub = @connection.substitute_at(@pk) binds = [[@pk, "3"]] - sql = "select * from topics where id = #{sub}" + sql = "select * from topics where id = #{sub.to_sql}" @connection.exec_query(sql, 'SQL', binds) diff --git a/activerecord/test/cases/calculations_test.rb b/activerecord/test/cases/calculations_test.rb index 222b1505a8..299217214e 100644 --- a/activerecord/test/cases/calculations_test.rb +++ b/activerecord/test/cases/calculations_test.rb @@ -10,8 +10,7 @@ require 'models/reply' require 'models/minivan' require 'models/speedometer' require 'models/ship_part' - -Company.has_many :accounts +require 'models/treasure' class NumericData < ActiveRecord::Base self.table_name = 'numeric_data' @@ -22,7 +21,7 @@ class NumericData < ActiveRecord::Base end class CalculationsTest < ActiveRecord::TestCase - fixtures :companies, :accounts, :topics + fixtures :companies, :accounts, :topics, :speedometers, :minivans def test_should_sum_field assert_equal 318, Account.sum(:credit_limit) @@ -53,11 +52,6 @@ class CalculationsTest < ActiveRecord::TestCase assert_nil NumericData.average(:bank_balance) end - def test_type_cast_calculated_value_should_convert_db_averages_of_fixnum_class_to_decimal - assert_equal 0, NumericData.all.send(:type_cast_calculated_value, 0, nil, 'avg') - assert_equal 53.0, NumericData.all.send(:type_cast_calculated_value, 53, nil, 'avg') - end - def test_should_get_maximum_of_field assert_equal 60, Account.maximum(:credit_limit) end @@ -468,7 +462,6 @@ 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' assert_equal Edge.count(:all), Edge.from('edges USE INDEX(unique_edge_index)').count(:all) @@ -617,4 +610,25 @@ class CalculationsTest < ActiveRecord::TestCase .pluck('topics.title', 'replies_topics.title') assert_equal expected, actual end + + def test_calculation_with_polymorphic_relation + part = ShipPart.create!(name: "has trinket") + part.trinkets.create! + + assert_equal part.id, ShipPart.joins(:trinkets).sum(:id) + end + + def test_pluck_joined_with_polymorphic_relation + part = ShipPart.create!(name: "has trinket") + part.trinkets.create! + + assert_equal [part.id], ShipPart.joins(:trinkets).pluck(:id) + end + + def test_grouped_calculation_with_polymorphic_relation + part = ShipPart.create!(name: "has trinket") + part.trinkets.create! + + assert_equal({ "has trinket" => part.id }, ShipPart.joins(:trinkets).group("ship_parts.name").sum(:id)) + end end diff --git a/activerecord/test/cases/callbacks_test.rb b/activerecord/test/cases/callbacks_test.rb index c8f56e3c73..d4cc081f32 100644 --- a/activerecord/test/cases/callbacks_test.rb +++ b/activerecord/test/cases/callbacks_test.rb @@ -1,4 +1,6 @@ require "cases/helper" +require 'models/developer' +require 'models/computer' class CallbackDeveloper < ActiveRecord::Base self.table_name = 'developers' @@ -57,27 +59,6 @@ class ChildDeveloper < ParentDeveloper end -class RecursiveCallbackDeveloper < ActiveRecord::Base - self.table_name = 'developers' - - before_save :on_before_save - after_save :on_after_save - - attr_reader :on_before_save_called, :on_after_save_called - - def on_before_save - @on_before_save_called ||= 0 - @on_before_save_called += 1 - save unless @on_before_save_called > 1 - end - - def on_after_save - @on_after_save_called ||= 0 - @on_after_save_called += 1 - save unless @on_after_save_called > 1 - end -end - class ImmutableDeveloper < ActiveRecord::Base self.table_name = 'developers' @@ -86,37 +67,12 @@ class ImmutableDeveloper < ActiveRecord::Base before_save :cancel before_destroy :cancel - def cancelled? - @cancelled == true - end - private def cancel - @cancelled = true false end end -class ImmutableMethodDeveloper < ActiveRecord::Base - self.table_name = 'developers' - - validates_inclusion_of :salary, :in => 50000..200000 - - def cancelled? - @cancelled == true - end - - before_save do - @cancelled = true - false - end - - before_destroy do - @cancelled = true - false - end -end - class OnCallbacksDeveloper < ActiveRecord::Base self.table_name = 'developers' @@ -441,7 +397,8 @@ class CallbacksTest < ActiveRecord::TestCase david = ImmutableDeveloper.find(1) assert david.valid? assert !david.save - assert_raise(ActiveRecord::RecordNotSaved) { david.save! } + exc = assert_raise(ActiveRecord::RecordNotSaved) { david.save! } + assert_equal exc.record, david david = ImmutableDeveloper.find(1) david.salary = 10_000_000 @@ -475,7 +432,8 @@ class CallbacksTest < ActiveRecord::TestCase def test_before_destroy_returning_false david = ImmutableDeveloper.find(1) assert !david.destroy - assert_raise(ActiveRecord::RecordNotDestroyed) { david.destroy! } + exc = assert_raise(ActiveRecord::RecordNotDestroyed) { david.destroy! } + assert_equal exc.record, david assert_not_nil ImmutableDeveloper.find_by_id(1) someone = CallbackCancellationDeveloper.find(1) diff --git a/activerecord/test/cases/connection_adapters/merge_and_resolve_default_url_config_test.rb b/activerecord/test/cases/connection_adapters/merge_and_resolve_default_url_config_test.rb index e1b2804a18..37ad469476 100644 --- a/activerecord/test/cases/connection_adapters/merge_and_resolve_default_url_config_test.rb +++ b/activerecord/test/cases/connection_adapters/merge_and_resolve_default_url_config_test.rb @@ -5,10 +5,14 @@ module ActiveRecord class MergeAndResolveDefaultUrlConfigTest < ActiveRecord::TestCase def setup @previous_database_url = ENV.delete("DATABASE_URL") + @previous_rack_env = ENV.delete("RACK_ENV") + @previous_rails_env = ENV.delete("RAILS_ENV") end teardown do ENV["DATABASE_URL"] = @previous_database_url + ENV["RACK_ENV"] = @previous_rack_env + ENV["RAILS_ENV"] = @previous_rails_env end def resolve_config(config) @@ -27,6 +31,26 @@ module ActiveRecord assert_equal expected, actual end + def test_resolver_with_database_uri_and_current_env_symbol_key_and_rails_env + ENV['DATABASE_URL'] = "postgres://localhost/foo" + ENV['RAILS_ENV'] = "foo" + + config = { "not_production" => { "adapter" => "not_postgres", "database" => "not_foo" } } + actual = resolve_spec(:foo, config) + expected = { "adapter" => "postgresql", "database" => "foo", "host" => "localhost" } + assert_equal expected, actual + end + + def test_resolver_with_database_uri_and_current_env_symbol_key_and_rack_env + ENV['DATABASE_URL'] = "postgres://localhost/foo" + ENV['RACK_ENV'] = "foo" + + config = { "not_production" => { "adapter" => "not_postgres", "database" => "not_foo" } } + actual = resolve_spec(:foo, config) + expected = { "adapter" => "postgresql", "database" => "foo", "host" => "localhost" } + assert_equal expected, actual + end + def test_resolver_with_database_uri_and_and_current_env_string_key ENV['DATABASE_URL'] = "postgres://localhost/foo" config = { "default_env" => { "adapter" => "not_postgres", "database" => "not_foo" } } @@ -35,6 +59,26 @@ module ActiveRecord assert_equal expected, actual end + def test_resolver_with_database_uri_and_and_current_env_string_key_and_rails_env + ENV['DATABASE_URL'] = "postgres://localhost/foo" + ENV['RAILS_ENV'] = "foo" + + config = { "not_production" => {"adapter" => "not_postgres", "database" => "not_foo" } } + actual = assert_deprecated { resolve_spec("foo", config) } + expected = { "adapter" => "postgresql", "database" => "foo", "host" => "localhost" } + assert_equal expected, actual + end + + def test_resolver_with_database_uri_and_and_current_env_string_key_and_rack_env + ENV['DATABASE_URL'] = "postgres://localhost/foo" + ENV['RACK_ENV'] = "foo" + + config = { "not_production" => {"adapter" => "not_postgres", "database" => "not_foo" } } + actual = assert_deprecated { resolve_spec("foo", config) } + expected = { "adapter" => "postgresql", "database" => "foo", "host" => "localhost" } + assert_equal expected, actual + end + def test_resolver_with_database_uri_and_known_key ENV['DATABASE_URL'] = "postgres://localhost/foo" config = { "production" => { "adapter" => "not_postgres", "database" => "not_foo", "host" => "localhost" } } @@ -139,6 +183,51 @@ module ActiveRecord assert_equal nil, actual["production"] assert_equal nil, actual["development"] assert_equal nil, actual["test"] + assert_equal nil, actual[:default_env] + assert_equal nil, actual[:production] + assert_equal nil, actual[:development] + assert_equal nil, actual[:test] + end + + def test_blank_with_database_url_with_rails_env + ENV['RAILS_ENV'] = "not_production" + ENV['DATABASE_URL'] = "postgres://localhost/foo" + + config = {} + actual = resolve_config(config) + expected = { "adapter" => "postgresql", + "database" => "foo", + "host" => "localhost" } + + assert_equal expected, actual["not_production"] + assert_equal nil, actual["production"] + assert_equal nil, actual["default_env"] + assert_equal nil, actual["development"] + assert_equal nil, actual["test"] + assert_equal nil, actual[:default_env] + assert_equal nil, actual[:not_production] + assert_equal nil, actual[:production] + assert_equal nil, actual[:development] + assert_equal nil, actual[:test] + end + + def test_blank_with_database_url_with_rack_env + ENV['RACK_ENV'] = "not_production" + ENV['DATABASE_URL'] = "postgres://localhost/foo" + + config = {} + actual = resolve_config(config) + expected = { "adapter" => "postgresql", + "database" => "foo", + "host" => "localhost" } + + assert_equal expected, actual["not_production"] + assert_equal nil, actual["production"] + assert_equal nil, actual["default_env"] + assert_equal nil, actual["development"] + assert_equal nil, actual["test"] + assert_equal nil, actual[:default_env] + assert_equal nil, actual[:not_production] assert_equal nil, actual[:production] assert_equal nil, actual[:development] assert_equal nil, actual[:test] 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 d4d67487db..80244d1439 100644 --- a/activerecord/test/cases/connection_adapters/mysql_type_lookup_test.rb +++ b/activerecord/test/cases/connection_adapters/mysql_type_lookup_test.rb @@ -22,6 +22,10 @@ module ActiveRecord assert_lookup_type :string, "SET('one', 'two', 'three')" end + def test_enum_type_with_value_matching_other_type + assert_lookup_type :string, "ENUM('unicode', '8bit', 'none')" + end + def test_binary_types assert_lookup_type :binary, 'bit' assert_lookup_type :binary, 'BIT' diff --git a/activerecord/test/cases/connection_adapters/schema_cache_test.rb b/activerecord/test/cases/connection_adapters/schema_cache_test.rb index ecad7c942f..c7531f5418 100644 --- a/activerecord/test/cases/connection_adapters/schema_cache_test.rb +++ b/activerecord/test/cases/connection_adapters/schema_cache_test.rb @@ -45,8 +45,8 @@ module ActiveRecord @cache = Marshal.load(Marshal.dump(@cache)) - assert_equal 12, @cache.columns('posts').size - assert_equal 12, @cache.columns_hash('posts').size + assert_equal 11, @cache.columns('posts').size + assert_equal 11, @cache.columns_hash('posts').size assert @cache.tables('posts') assert_equal 'id', @cache.primary_keys('posts') end diff --git a/activerecord/test/cases/connection_management_test.rb b/activerecord/test/cases/connection_management_test.rb index 77d9ae9b8e..f53c496ecd 100644 --- a/activerecord/test/cases/connection_management_test.rb +++ b/activerecord/test/cases/connection_management_test.rb @@ -96,6 +96,14 @@ module ActiveRecord assert ActiveRecord::Base.connection_handler.active_connections? end + def test_connections_closed_if_exception_and_explicitly_not_test + @env['rack.test'] = false + app = Class.new(App) { def call(env); raise NotImplementedError; end }.new + explosive = ConnectionManagement.new(app) + assert_raises(NotImplementedError) { explosive.call(@env) } + assert !ActiveRecord::Base.connection_handler.active_connections? + end + test "doesn't clear active connections when running in a test case" do @env['rack.test'] = true @management.call(@env) diff --git a/activerecord/test/cases/counter_cache_test.rb b/activerecord/test/cases/counter_cache_test.rb index ab2a749ba8..07a182070b 100644 --- a/activerecord/test/cases/counter_cache_test.rb +++ b/activerecord/test/cases/counter_cache_test.rb @@ -19,6 +19,7 @@ class CounterCacheTest < ActiveRecord::TestCase class ::SpecialTopic < ::Topic has_many :special_replies, :foreign_key => 'parent_id' + has_many :lightweight_special_replies, -> { select('topics.id, topics.title') }, :foreign_key => 'parent_id', :class_name => 'SpecialReply' end class ::SpecialReply < ::Reply @@ -170,4 +171,13 @@ class CounterCacheTest < ActiveRecord::TestCase end assert_equal "'Topic' has no association called 'undefined_count'", e.message end + + test "reset counter works with select declared on association" do + special = SpecialTopic.create!(:title => 'Special') + SpecialTopic.increment_counter(:replies_count, special.id) + + assert_difference 'special.reload.replies_count', -1 do + SpecialTopic.reset_counters(special.id, :lightweight_special_replies) + end + end end diff --git a/activerecord/test/cases/date_time_test.rb b/activerecord/test/cases/date_time_test.rb index c0491bbee5..c2ec92c40d 100644 --- a/activerecord/test/cases/date_time_test.rb +++ b/activerecord/test/cases/date_time_test.rb @@ -3,6 +3,8 @@ require 'models/topic' require 'models/task' class DateTimeTest < ActiveRecord::TestCase + include InTimeZone + def test_saves_both_date_and_time with_env_tz 'America/New_York' do with_timezone_config default: :utc do @@ -29,6 +31,14 @@ class DateTimeTest < ActiveRecord::TestCase assert_nil task.ending end + def test_assign_bad_date_time_with_timezone + in_time_zone "Pacific Time (US & Canada)" do + task = Task.new + task.starting = '2014-07-01T24:59:59GMT' + assert_nil task.starting + end + end + def test_assign_empty_date topic = Topic.new topic.last_read = '' diff --git a/activerecord/test/cases/defaults_test.rb b/activerecord/test/cases/defaults_test.rb index 92144bc802..e9bc583bf4 100644 --- a/activerecord/test/cases/defaults_test.rb +++ b/activerecord/test/cases/defaults_test.rb @@ -18,25 +18,48 @@ class DefaultTest < ActiveRecord::TestCase end end - if current_adapter?(:PostgreSQLAdapter, :OracleAdapter) - def test_default_integers - default = Default.new - assert_instance_of Fixnum, default.positive_integer - assert_equal 1, default.positive_integer - assert_instance_of Fixnum, default.negative_integer - assert_equal(-1, default.negative_integer) - assert_instance_of BigDecimal, default.decimal_number - assert_equal BigDecimal.new("2.78"), default.decimal_number - end - end - if current_adapter?(:PostgreSQLAdapter) def test_multiline_default_text + record = Default.new # older postgres versions represent the default with escapes ("\\012" for a newline) - assert( "--- []\n\n" == Default.columns_hash['multiline_default'].default || - "--- []\\012\\012" == Default.columns_hash['multiline_default'].default) + assert("--- []\n\n" == record.multiline_default || "--- []\\012\\012" == record.multiline_default) + end + end +end + +class DefaultNumbersTest < ActiveRecord::TestCase + class DefaultNumber < ActiveRecord::Base; end + + setup do + @connection = ActiveRecord::Base.connection + @connection.create_table :default_numbers do |t| + t.integer :positive_integer, default: 7 + t.integer :negative_integer, default: -5 + t.decimal :decimal_number, default: "2.78", precision: 5, scale: 2 end end + + teardown do + @connection.drop_table "default_numbers" if @connection.table_exists? 'default_numbers' + end + + def test_default_positive_integer + record = DefaultNumber.new + assert_equal 7, record.positive_integer + assert_equal "7", record.positive_integer_before_type_cast + end + + def test_default_negative_integer + record = DefaultNumber.new + assert_equal (-5), record.negative_integer + assert_equal "-5", record.negative_integer_before_type_cast + end + + def test_default_decimal_number + record = DefaultNumber.new + assert_equal BigDecimal.new("2.78"), record.decimal_number + assert_equal "2.78", record.decimal_number_before_type_cast + end end class DefaultStringsTest < ActiveRecord::TestCase @@ -99,19 +122,21 @@ if current_adapter?(:MysqlAdapter, :Mysql2Adapter) def test_mysql_text_not_null_defaults_non_strict using_strict(false) do with_text_blob_not_null_table do |klass| - assert_equal '', klass.columns_hash['non_null_blob'].default - assert_equal '', klass.columns_hash['non_null_text'].default + record = klass.new + assert_equal '', record.non_null_blob + assert_equal '', record.non_null_text - assert_nil klass.columns_hash['null_blob'].default - assert_nil klass.columns_hash['null_text'].default + assert_nil record.null_blob + assert_nil record.null_text - instance = klass.create! + record.save! + record.reload - assert_equal '', instance.non_null_text - assert_equal '', instance.non_null_blob + assert_equal '', record.non_null_text + assert_equal '', record.non_null_blob - assert_nil instance.null_text - assert_nil instance.null_blob + assert_nil record.null_text + assert_nil record.null_blob end end end @@ -119,10 +144,11 @@ if current_adapter?(:MysqlAdapter, :Mysql2Adapter) def test_mysql_text_not_null_defaults_strict using_strict(true) do with_text_blob_not_null_table do |klass| - assert_nil klass.columns_hash['non_null_blob'].default - assert_nil klass.columns_hash['non_null_text'].default - assert_nil klass.columns_hash['null_blob'].default - assert_nil klass.columns_hash['null_text'].default + record = klass.new + assert_nil record.non_null_blob + assert_nil record.non_null_text + assert_nil record.null_blob + assert_nil record.null_text assert_raises(ActiveRecord::StatementInvalid) { klass.create } end @@ -154,7 +180,7 @@ if current_adapter?(:MysqlAdapter, :Mysql2Adapter) t.column :omit, :integer, :null => false end - assert_equal 0, klass.columns_hash['zero'].default + 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) @@ -172,48 +198,3 @@ if current_adapter?(:MysqlAdapter, :Mysql2Adapter) end end end - -if current_adapter?(:PostgreSQLAdapter) - class DefaultsUsingMultipleSchemasAndDomainTest < ActiveSupport::TestCase - def setup - @connection = ActiveRecord::Base.connection - - @old_search_path = @connection.schema_search_path - @connection.schema_search_path = "schema_1, pg_catalog" - @connection.create_table "defaults" do |t| - t.text "text_col", :default => "some value" - t.string "string_col", :default => "some value" - end - Default.reset_column_information - end - - def test_text_defaults_in_new_schema_when_overriding_domain - assert_equal "some value", Default.new.text_col, "Default of text column was not correctly parse" - end - - def test_string_defaults_in_new_schema_when_overriding_domain - assert_equal "some value", Default.new.string_col, "Default of string column was not correctly parse" - end - - def test_bpchar_defaults_in_new_schema_when_overriding_domain - @connection.execute "ALTER TABLE defaults ADD bpchar_col bpchar DEFAULT 'some value'" - Default.reset_column_information - assert_equal "some value", Default.new.bpchar_col, "Default of bpchar column was not correctly parse" - end - - def test_text_defaults_after_updating_column_default - @connection.execute "ALTER TABLE defaults ALTER COLUMN text_col SET DEFAULT 'some text'::schema_1.text" - assert_equal "some text", Default.new.text_col, "Default of text column was not correctly parse after updating default using '::text' since postgreSQL will add parens to the default in db" - end - - def test_default_containing_quote_and_colons - @connection.execute "ALTER TABLE defaults ALTER COLUMN string_col SET DEFAULT 'foo''::bar'" - assert_equal "foo'::bar", Default.new.string_col - end - - teardown do - @connection.schema_search_path = @old_search_path - Default.reset_column_information - end - end -end diff --git a/activerecord/test/cases/dirty_test.rb b/activerecord/test/cases/dirty_test.rb index 5d6601a881..98cf60a8c4 100644 --- a/activerecord/test/cases/dirty_test.rb +++ b/activerecord/test/cases/dirty_test.rb @@ -169,7 +169,19 @@ class DirtyTest < ActiveRecord::TestCase pirate = Pirate.create!(:catchphrase => 'Yar!') pirate.catchphrase = 'Ahoy!' - pirate.reset_catchphrase! + assert_deprecated do + pirate.reset_catchphrase! + end + assert_equal "Yar!", pirate.catchphrase + assert_equal Hash.new, pirate.changes + assert !pirate.catchphrase_changed? + end + + def test_restore_attribute! + pirate = Pirate.create!(:catchphrase => 'Yar!') + pirate.catchphrase = 'Ahoy!' + + pirate.restore_catchphrase! assert_equal "Yar!", pirate.catchphrase assert_equal Hash.new, pirate.changes assert !pirate.catchphrase_changed? @@ -309,16 +321,14 @@ class DirtyTest < ActiveRecord::TestCase def test_attribute_will_change! pirate = Pirate.create!(:catchphrase => 'arr') - pirate.catchphrase << ' matey' assert !pirate.catchphrase_changed? - assert pirate.catchphrase_will_change! assert pirate.catchphrase_changed? - assert_equal ['arr matey', 'arr matey'], pirate.catchphrase_change + assert_equal ['arr', 'arr'], pirate.catchphrase_change - pirate.catchphrase << '!' + pirate.catchphrase << ' matey!' assert pirate.catchphrase_changed? - assert_equal ['arr matey', 'arr matey!'], pirate.catchphrase_change + assert_equal ['arr', 'arr matey!'], pirate.catchphrase_change end def test_association_assignment_changes_foreign_key @@ -400,7 +410,7 @@ class DirtyTest < ActiveRecord::TestCase def test_dup_objects_should_not_copy_dirty_flag_from_creator pirate = Pirate.create!(:catchphrase => "shiver me timbers") pirate_dup = pirate.dup - pirate_dup.reset_catchphrase! + pirate_dup.restore_catchphrase! pirate.catchphrase = "I love Rum" assert pirate.catchphrase_changed? assert !pirate_dup.catchphrase_changed? @@ -651,6 +661,65 @@ class DirtyTest < ActiveRecord::TestCase assert_not model.foo_changed? end + test "in place mutation detection" do + pirate = Pirate.create!(catchphrase: "arrrr") + pirate.catchphrase << " matey!" + + assert pirate.catchphrase_changed? + expected_changes = { + "catchphrase" => ["arrrr", "arrrr matey!"] + } + assert_equal(expected_changes, pirate.changes) + assert_equal("arrrr", pirate.catchphrase_was) + assert pirate.catchphrase_changed?(from: "arrrr") + assert_not pirate.catchphrase_changed?(from: "anything else") + assert pirate.changed_attributes.include?(:catchphrase) + + pirate.save! + pirate.reload + + assert_equal "arrrr matey!", pirate.catchphrase + assert_not pirate.changed? + end + + test "in place mutation for binary" do + klass = Class.new(ActiveRecord::Base) do + self.table_name = :binaries + serialize :data + end + + binary = klass.create!(data: "\\\\foo") + + assert_not binary.changed? + + binary.data = binary.data.dup + + assert_not binary.changed? + + binary = klass.last + + assert_not binary.changed? + + binary.data << "bar" + + assert binary.changed? + end + + test "attribute_changed? doesn't compute in-place changes for unrelated attributes" do + test_type_class = Class.new(ActiveRecord::Type::Value) do + define_method(:changed_in_place?) do |*| + raise + end + end + klass = Class.new(ActiveRecord::Base) do + self.table_name = 'people' + attribute :foo, test_type_class.new + end + + model = klass.new(first_name: "Jim") + assert model.first_name_changed? + end + private def with_partial_writes(klass, on = true) old = klass.partial_writes? diff --git a/activerecord/test/cases/dup_test.rb b/activerecord/test/cases/dup_test.rb index 409d9a82e2..638cffe0e6 100644 --- a/activerecord/test/cases/dup_test.rb +++ b/activerecord/test/cases/dup_test.rb @@ -141,5 +141,17 @@ module ActiveRecord ensure Topic.default_scopes = prev_default_scopes end + + def test_dup_without_primary_key + klass = Class.new(ActiveRecord::Base) do + self.table_name = 'parrots_pirates' + end + + record = klass.create! + + assert_nothing_raised do + record.dup + end + end end end diff --git a/activerecord/test/cases/enum_test.rb b/activerecord/test/cases/enum_test.rb index 3b2f0dfe07..346fcab6ea 100644 --- a/activerecord/test/cases/enum_test.rb +++ b/activerecord/test/cases/enum_test.rb @@ -194,7 +194,8 @@ class EnumTest < ActiveRecord::TestCase :valid, # generates #valid?, which conflicts with an AR method :save, # generates #save!, which conflicts with an AR method :proposed, # same value as an existing enum - :public, :private, :protected, # generates a method that conflict with ruby words + :public, :private, :protected, # some important methods on Module and Class + :name, :parent, :superclass ] conflicts.each_with_index do |value, i| diff --git a/activerecord/test/cases/finder_test.rb b/activerecord/test/cases/finder_test.rb index bd77c412f6..02dc5d3ad3 100644 --- a/activerecord/test/cases/finder_test.rb +++ b/activerecord/test/cases/finder_test.rb @@ -4,18 +4,22 @@ require 'models/author' require 'models/categorization' require 'models/comment' require 'models/company' +require 'models/tagging' require 'models/topic' require 'models/reply' require 'models/entrant' require 'models/project' require 'models/developer' +require 'models/computer' require 'models/customer' require 'models/toy' require 'models/matey' require 'models/dog' +require 'models/car' +require 'models/tyre' class FinderTest < ActiveRecord::TestCase - fixtures :companies, :topics, :entrants, :developers, :developers_projects, :posts, :comments, :accounts, :authors, :customers, :categories, :categorizations + fixtures :companies, :topics, :entrants, :developers, :developers_projects, :posts, :comments, :accounts, :authors, :customers, :categories, :categorizations, :cars def test_find_by_id_with_hash assert_raises(ActiveRecord::StatementInvalid) do @@ -51,10 +55,13 @@ class FinderTest < ActiveRecord::TestCase end def test_symbols_table_ref - Post.first # warm up + gc_disabled = GC.disable if RUBY_VERSION >= '2.2.0' + Post.where("author_id" => nil) # warm up x = Symbol.all_symbols.count Post.where("title" => {"xxxqqqq" => "bar"}) assert_equal x, Symbol.all_symbols.count + ensure + GC.enable if gc_disabled == false end # find should handle strings that come from URLs @@ -78,6 +85,19 @@ class FinderTest < ActiveRecord::TestCase assert_raise(NoMethodError) { Topic.exists?([1,2]) } end + def test_exists_with_polymorphic_relation + post = Post.create!(title: 'Post', body: 'default', taggings: [Tagging.new(comment: 'tagging comment')]) + relation = Post.tagged_with_comment('tagging comment') + + assert_equal true, relation.exists?(title: ['Post']) + assert_equal true, relation.exists?(['title LIKE ?', 'Post%']) + assert_equal true, relation.exists? + assert_equal true, relation.exists?(post.id) + assert_equal true, relation.exists?(post.id.to_s) + + assert_equal false, relation.exists?(false) + end + def test_exists_passing_active_record_object_is_deprecated assert_deprecated do Topic.exists?(Topic.new) @@ -85,21 +105,10 @@ class FinderTest < ActiveRecord::TestCase end def test_exists_fails_when_parameter_has_invalid_type - if current_adapter?(:PostgreSQLAdapter, :MysqlAdapter) - assert_raises ActiveRecord::StatementInvalid do - Topic.exists?(("9"*53).to_i) # number that's bigger than int - end - else + assert_raises(RangeError) do assert_equal false, Topic.exists?(("9"*53).to_i) # number that's bigger than int end - - if current_adapter?(:PostgreSQLAdapter) - assert_raises ActiveRecord::StatementInvalid do - Topic.exists?("foo") - end - else - assert_equal false, Topic.exists?("foo") - end + assert_equal false, Topic.exists?("foo") end def test_exists_does_not_select_columns_without_alias @@ -144,8 +153,8 @@ class FinderTest < ActiveRecord::TestCase def test_exists_with_distinct_association_includes_limit_and_order author = Author.first - assert_equal false, author.unique_categorized_posts.includes(:special_comments).order('comments.taggings_count DESC').limit(0).exists? - assert_equal true, author.unique_categorized_posts.includes(:special_comments).order('comments.taggings_count DESC').limit(1).exists? + assert_equal false, author.unique_categorized_posts.includes(:special_comments).order('comments.tags_count DESC').limit(0).exists? + assert_equal true, author.unique_categorized_posts.includes(:special_comments).order('comments.tags_count DESC').limit(1).exists? end def test_exists_with_empty_table_and_no_args_given @@ -195,6 +204,28 @@ class FinderTest < ActiveRecord::TestCase assert_equal 2, last_devs.size end + def test_find_with_large_number + assert_raises(ActiveRecord::RecordNotFound) { Topic.find('9999999999999999999999999999999') } + end + + def test_find_by_with_large_number + assert_nil Topic.find_by(id: '9999999999999999999999999999999') + end + + def test_find_by_id_with_large_number + assert_nil Topic.find_by_id('9999999999999999999999999999999') + end + + def test_find_on_relation_with_large_number + assert_nil Topic.where('1=1').find_by(id: 9999999999999999999999999999999) + end + + def test_find_by_bang_on_relation_with_large_number + assert_raises(ActiveRecord::RecordNotFound) do + Topic.where('1=1').find_by!(id: 9999999999999999999999999999999) + end + end + def test_find_an_empty_array assert_equal [], Topic.find([]) end @@ -248,7 +279,7 @@ class FinderTest < ActiveRecord::TestCase end def test_take_bang_missing - assert_raises ActiveRecord::RecordNotFound do + assert_raises_with_message ActiveRecord::RecordNotFound, "Couldn't find Topic" do Topic.where("title = 'This title does not exist'").take! end end @@ -268,7 +299,7 @@ class FinderTest < ActiveRecord::TestCase end def test_first_bang_missing - assert_raises ActiveRecord::RecordNotFound do + assert_raises_with_message ActiveRecord::RecordNotFound, "Couldn't find Topic" do Topic.where("title = 'This title does not exist'").first! end end @@ -282,7 +313,7 @@ class FinderTest < ActiveRecord::TestCase def test_model_class_responds_to_first_bang assert Topic.first! Topic.delete_all - assert_raises ActiveRecord::RecordNotFound do + assert_raises_with_message ActiveRecord::RecordNotFound, "Couldn't find Topic" do Topic.first! end end @@ -304,7 +335,7 @@ class FinderTest < ActiveRecord::TestCase def test_model_class_responds_to_second_bang assert Topic.second! Topic.delete_all - assert_raises ActiveRecord::RecordNotFound do + assert_raises_with_message ActiveRecord::RecordNotFound, "Couldn't find Topic" do Topic.second! end end @@ -326,7 +357,7 @@ class FinderTest < ActiveRecord::TestCase def test_model_class_responds_to_third_bang assert Topic.third! Topic.delete_all - assert_raises ActiveRecord::RecordNotFound do + assert_raises_with_message ActiveRecord::RecordNotFound, "Couldn't find Topic" do Topic.third! end end @@ -348,7 +379,7 @@ class FinderTest < ActiveRecord::TestCase def test_model_class_responds_to_fourth_bang assert Topic.fourth! Topic.delete_all - assert_raises ActiveRecord::RecordNotFound do + assert_raises_with_message ActiveRecord::RecordNotFound, "Couldn't find Topic" do Topic.fourth! end end @@ -370,7 +401,7 @@ class FinderTest < ActiveRecord::TestCase def test_model_class_responds_to_fifth_bang assert Topic.fifth! Topic.delete_all - assert_raises ActiveRecord::RecordNotFound do + assert_raises_with_message ActiveRecord::RecordNotFound, "Couldn't find Topic" do Topic.fifth! end end @@ -382,14 +413,14 @@ class FinderTest < ActiveRecord::TestCase end def test_last_bang_missing - assert_raises ActiveRecord::RecordNotFound do + assert_raises_with_message ActiveRecord::RecordNotFound, "Couldn't find Topic" do Topic.where("title = 'This title does not exist'").last! end end def test_model_class_responds_to_last_bang assert_equal topics(:fifth), Topic.last! - assert_raises ActiveRecord::RecordNotFound do + assert_raises_with_message ActiveRecord::RecordNotFound, "Couldn't find Topic" do Topic.delete_all Topic.last! end @@ -463,6 +494,12 @@ class FinderTest < ActiveRecord::TestCase assert_raise(ActiveRecord::RecordNotFound) { Topic.where(topics: { approved: true }).find(1) } end + def test_find_on_combined_explicit_and_hashed_table_names + assert Topic.where('topics.approved' => false, topics: { author_name: "David" }).find(1) + assert_raise(ActiveRecord::RecordNotFound) { Topic.where('topics.approved' => true, topics: { author_name: "David" }).find(1) } + assert_raise(ActiveRecord::RecordNotFound) { Topic.where('topics.approved' => false, topics: { author_name: "Melanie" }).find(1) } + end + def test_find_with_hash_conditions_on_joined_table firms = Firm.joins(:account).where(:accounts => { :credit_limit => 50 }) assert_equal 1, firms.size @@ -507,6 +544,10 @@ class FinderTest < ActiveRecord::TestCase assert_equal [1,2,3,5,6,7,8,9], Comment.where(id: [1..2, 3, 5, 6..8, 9]).to_a.map(&:id).sort end + def test_find_on_hash_conditions_with_array_of_ranges + assert_equal [1,2,6,7,8], Comment.where(id: [1..2, 6..8]).to_a.map(&:id).sort + end + def test_find_on_multiple_hash_conditions assert Topic.where(author_name: "David", title: "The First Topic", replies_count: 1, approved: false).find(1) assert_raise(ActiveRecord::RecordNotFound) { Topic.where(author_name: "David", title: "The First Topic", replies_count: 1, approved: true).find(1) } @@ -760,7 +801,9 @@ class FinderTest < ActiveRecord::TestCase def test_find_by_one_attribute_bang assert_equal topics(:first), Topic.find_by_title!("The First Topic") - assert_raise(ActiveRecord::RecordNotFound) { Topic.find_by_title!("The First Topic!") } + assert_raises_with_message(ActiveRecord::RecordNotFound, "Couldn't find Topic") do + Topic.find_by_title!("The First Topic!") + end end def test_find_by_on_attribute_that_is_a_reserved_word @@ -878,7 +921,7 @@ class FinderTest < ActiveRecord::TestCase joins('LEFT JOIN developers_projects ON developers.id = developers_projects.developer_id'). where('project_id=1').to_a assert_equal 3, developers_on_project_one.length - developer_names = developers_on_project_one.map { |d| d.name } + developer_names = developers_on_project_one.map(&:name) assert developer_names.include?('David') assert developer_names.include?('Jamis') end @@ -933,7 +976,7 @@ class FinderTest < ActiveRecord::TestCase end def test_select_values - assert_equal ["1","2","3","4","5","6","7","8","9", "10", "11"], Company.connection.select_values("SELECT id FROM companies ORDER BY id").map! { |i| i.to_s } + assert_equal ["1","2","3","4","5","6","7","8","9", "10", "11"], Company.connection.select_values("SELECT id FROM companies ORDER BY id").map!(&:to_s) assert_equal ["37signals","Summit","Microsoft", "Flamboyant Software", "Ex Nihilo", "RailsCore", "Leetsoft", "Jadedpixel", "Odegy", "Ex Nihilo Part Deux", "Apex"], Company.connection.select_values("SELECT name FROM companies ORDER BY id") end @@ -959,7 +1002,7 @@ class FinderTest < ActiveRecord::TestCase where(client_of: [2, 1, nil], name: ['37signals', 'Summit', 'Microsoft']). order('client_of DESC'). - map { |x| x.client_of } + map(&:client_of) assert client_of.include?(nil) assert_equal [2, 1].sort, client_of.compact.sort @@ -969,7 +1012,7 @@ class FinderTest < ActiveRecord::TestCase client_of = Company. where(client_of: [nil]). order('client_of DESC'). - map { |x| x.client_of } + map(&:client_of) assert_equal [], client_of.compact end @@ -1013,6 +1056,73 @@ class FinderTest < ActiveRecord::TestCase assert_nothing_raised(ActiveRecord::StatementInvalid) { Topic.offset("3").to_a } end + test "find_by with hash conditions returns the first matching record" do + assert_equal posts(:eager_other), Post.find_by(id: posts(:eager_other).id) + end + + test "find_by with non-hash conditions returns the first matching record" do + assert_equal posts(:eager_other), Post.find_by("id = #{posts(:eager_other).id}") + end + + test "find_by with multi-arg conditions returns the first matching record" do + assert_equal posts(:eager_other), Post.find_by('id = ?', posts(:eager_other).id) + end + + test "find_by returns nil if the record is missing" do + assert_equal nil, Post.find_by("1 = 0") + end + + test "find_by with associations" do + assert_equal authors(:david), Post.find_by(author: authors(:david)).author + assert_equal authors(:mary) , Post.find_by(author: authors(:mary) ).author + end + + test "find_by doesn't have implicit ordering" do + assert_sql(/^((?!ORDER).)*$/) { Post.find_by(id: posts(:eager_other).id) } + end + + test "find_by! with hash conditions returns the first matching record" do + assert_equal posts(:eager_other), Post.find_by!(id: posts(:eager_other).id) + end + + test "find_by! with non-hash conditions returns the first matching record" do + assert_equal posts(:eager_other), Post.find_by!("id = #{posts(:eager_other).id}") + end + + test "find_by! with multi-arg conditions returns the first matching record" do + assert_equal posts(:eager_other), Post.find_by!('id = ?', posts(:eager_other).id) + end + + test "find_by! doesn't have implicit ordering" do + assert_sql(/^((?!ORDER).)*$/) { Post.find_by!(id: posts(:eager_other).id) } + end + + test "find_by! raises RecordNotFound if the record is missing" do + assert_raises(ActiveRecord::RecordNotFound) do + Post.find_by!("1 = 0") + end + end + + test "find on a scope does not perform statement caching" do + honda = cars(:honda) + zyke = cars(:zyke) + tyre = honda.tyres.create! + tyre2 = zyke.tyres.create! + + assert_equal tyre, honda.tyres.custom_find(tyre.id) + assert_equal tyre2, zyke.tyres.custom_find(tyre2.id) + end + + test "find_by on a scope does not perform statement caching" do + honda = cars(:honda) + zyke = cars(:zyke) + tyre = honda.tyres.create! + tyre2 = zyke.tyres.create! + + assert_equal tyre, honda.tyres.custom_find_by(id: tyre.id) + assert_equal tyre2, zyke.tyres.custom_find_by(id: tyre2.id) + end + protected def bind(statement, *vars) if vars.first.is_a?(Hash) @@ -1029,4 +1139,10 @@ class FinderTest < ActiveRecord::TestCase end end) end + + def assert_raises_with_message(exception_class, message, &block) + err = assert_raises(exception_class) { block.call } + assert_match message, err.message + end + end diff --git a/activerecord/test/cases/fixtures_test.rb b/activerecord/test/cases/fixtures_test.rb index 042fdaf0bb..9edeb8b47f 100644 --- a/activerecord/test/cases/fixtures_test.rb +++ b/activerecord/test/cases/fixtures_test.rb @@ -5,11 +5,13 @@ require 'models/admin/randomly_named_c1' require 'models/admin/user' require 'models/binary' require 'models/book' +require 'models/bulb' require 'models/category' require 'models/company' require 'models/computer' require 'models/course' require 'models/developer' +require 'models/computer' require 'models/joke' require 'models/matey' require 'models/parrot' @@ -644,6 +646,7 @@ class LoadAllFixturesWithPathnameTest < ActiveRecord::TestCase end class FasterFixturesTest < ActiveRecord::TestCase + self.use_transactional_fixtures = false fixtures :categories, :authors def load_extra_fixture(name) @@ -674,7 +677,7 @@ class FoxyFixturesTest < ActiveRecord::TestCase if ActiveRecord::Base.connection.adapter_name == 'PostgreSQL' require 'models/uuid_parent' require 'models/uuid_child' - fixtures :uuid_parents, :uuid_children + fixtures :uuid_parents, :uuid_children end def test_identifies_strings @@ -821,20 +824,6 @@ class ActiveSupportSubclassWithFixturesTest < ActiveRecord::TestCase end end -class FixtureLoadingTest < ActiveRecord::TestCase - def test_logs_message_for_failed_dependency_load - ActiveRecord::TestCase.expects(:require_dependency).with(:does_not_exist).raises(LoadError) - ActiveRecord::Base.logger.expects(:warn) - ActiveRecord::TestCase.try_to_load_dependency(:does_not_exist) - end - - def test_does_not_logs_message_for_successful_dependency_load - ActiveRecord::TestCase.expects(:require_dependency).with(:works_out_fine) - ActiveRecord::Base.logger.expects(:warn).never - ActiveRecord::TestCase.try_to_load_dependency(:works_out_fine) - end -end - class CustomNameForFixtureOrModelTest < ActiveRecord::TestCase ActiveRecord::FixtureSet.reset_cache @@ -865,3 +854,16 @@ class CustomNameForFixtureOrModelTest < ActiveRecord::TestCase assert_equal 'randomly_named_table', Admin::ClassNameThatDoesNotFollowCONVENTIONS.table_name end end + +class FixturesWithDefaultScopeTest < ActiveRecord::TestCase + fixtures :bulbs + + test "inserts fixtures excluded by a default scope" do + assert_equal 1, Bulb.count + assert_equal 2, Bulb.unscoped.count + end + + test "allows access to fixtures excluded by a default scope" do + assert_equal "special", bulbs(:special).name + end +end diff --git a/activerecord/test/cases/forbidden_attributes_protection_test.rb b/activerecord/test/cases/forbidden_attributes_protection_test.rb index 981a75faf6..f4e7646f03 100644 --- a/activerecord/test/cases/forbidden_attributes_protection_test.rb +++ b/activerecord/test/cases/forbidden_attributes_protection_test.rb @@ -66,4 +66,34 @@ class ForbiddenAttributesProtectionTest < ActiveRecord::TestCase person = Person.new assert_nil person.assign_attributes(ProtectedParams.new({})) end + + def test_create_with_checks_permitted + params = ProtectedParams.new(first_name: 'Guille', gender: 'm') + + assert_raises(ActiveModel::ForbiddenAttributesError) do + Person.create_with(params).create! + end + end + + def test_create_with_works_with_params_values + params = ProtectedParams.new(first_name: 'Guille') + + person = Person.create_with(first_name: params[:first_name]).create! + assert_equal 'Guille', person.first_name + end + + def test_where_checks_permitted + params = ProtectedParams.new(first_name: 'Guille', gender: 'm') + + assert_raises(ActiveModel::ForbiddenAttributesError) do + Person.where(params).create! + end + 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 end diff --git a/activerecord/test/cases/helper.rb b/activerecord/test/cases/helper.rb index 937646b09a..80ac57ec7c 100644 --- a/activerecord/test/cases/helper.rb +++ b/activerecord/test/cases/helper.rb @@ -24,6 +24,9 @@ ActiveSupport::Deprecation.debug = true # Disable available locale checks to avoid warnings running the test suite. I18n.enforce_available_locales = false +# Enable raise errors in after_commit and after_rollback. +ActiveRecord::Base.raise_in_transactional_callbacks = true + # Connect to the database ARTest.connect @@ -47,6 +50,10 @@ def mysql_56? ActiveRecord::Base.connection.send(:version).join(".") >= "5.6.0" end +def mysql_enforcing_gtid_consistency? + current_adapter?(:MysqlAdapter, :Mysql2Adapter) && 'ON' == ActiveRecord::Base.connection.show_variable('enforce_gtid_consistency') +end + def supports_savepoints? ActiveRecord::Base.connection.supports_savepoints? end @@ -88,7 +95,7 @@ EXPECTED_TIME_ZONE_AWARE_ATTRIBUTES = false def verify_default_timezone_config if Time.zone != EXPECTED_ZONE $stderr.puts <<-MSG -\n#{self.to_s} +\n#{self} Global state `Time.zone` was leaked. Expected: #{EXPECTED_ZONE} Got: #{Time.zone} @@ -96,7 +103,7 @@ def verify_default_timezone_config end if ActiveRecord::Base.default_timezone != EXPECTED_DEFAULT_TIMEZONE $stderr.puts <<-MSG -\n#{self.to_s} +\n#{self} Global state `ActiveRecord::Base.default_timezone` was leaked. Expected: #{EXPECTED_DEFAULT_TIMEZONE} Got: #{ActiveRecord::Base.default_timezone} @@ -104,7 +111,7 @@ def verify_default_timezone_config end if ActiveRecord::Base.time_zone_aware_attributes != EXPECTED_TIME_ZONE_AWARE_ATTRIBUTES $stderr.puts <<-MSG -\n#{self.to_s} +\n#{self} Global state `ActiveRecord::Base.time_zone_aware_attributes` was leaked. Expected: #{EXPECTED_TIME_ZONE_AWARE_ATTRIBUTES} Got: #{ActiveRecord::Base.time_zone_aware_attributes} @@ -112,26 +119,21 @@ def verify_default_timezone_config end end -def enable_uuid_ossp!(connection) +def enable_extension!(extension, connection) return false unless connection.supports_extensions? - return true if connection.extension_enabled?('uuid-ossp') + return connection.reconnect! if connection.extension_enabled?(extension) - connection.enable_extension 'uuid-ossp' + connection.enable_extension extension connection.commit_db_transaction connection.reconnect! end -unless ENV['FIXTURE_DEBUG'] - module ActiveRecord::TestFixtures::ClassMethods - def try_to_load_dependency_with_silence(*args) - old = ActiveRecord::Base.logger.level - ActiveRecord::Base.logger.level = ActiveSupport::Logger::ERROR - try_to_load_dependency_without_silence(*args) - ActiveRecord::Base.logger.level = old - end +def disable_extension!(extension, connection) + return false unless connection.supports_extensions? + return true unless connection.extension_enabled?(extension) - alias_method_chain :try_to_load_dependency, :silence - end + connection.disable_extension extension + connection.reconnect! end require "cases/validations_repair_helper" @@ -199,3 +201,10 @@ module InTimeZone ActiveRecord::Base.time_zone_aware_attributes = old_tz end end + +require 'mocha/setup' # FIXME: stop using mocha + +# FIXME: we have tests that depend on run order, we should fix that and +# remove this method call. +require 'active_support/test_case' +ActiveSupport::TestCase.test_order = :sorted diff --git a/activerecord/test/cases/inheritance_test.rb b/activerecord/test/cases/inheritance_test.rb index 792950d24d..fe6323ab02 100644 --- a/activerecord/test/cases/inheritance_test.rb +++ b/activerecord/test/cases/inheritance_test.rb @@ -22,7 +22,7 @@ class InheritanceTest < ActiveRecord::TestCase company = Company.first company = company.dup company.extend(Module.new { - def read_attribute(name) + def _read_attribute(name) return ' ' if name == 'type' super end @@ -304,7 +304,7 @@ class InheritanceTest < ActiveRecord::TestCase def test_eager_load_belongs_to_primary_key_quoting con = Account.connection - assert_sql(/#{con.quote_table_name('companies')}.#{con.quote_column_name('id')} IN \(1\)/) do + assert_sql(/#{con.quote_table_name('companies')}.#{con.quote_column_name('id')} = 1/) do Account.all.merge!(:includes => :firm).find(1) end end diff --git a/activerecord/test/cases/integration_test.rb b/activerecord/test/cases/integration_test.rb index dfb8a608cb..0021988083 100644 --- a/activerecord/test/cases/integration_test.rb +++ b/activerecord/test/cases/integration_test.rb @@ -3,6 +3,7 @@ require 'cases/helper' require 'models/company' require 'models/developer' +require 'models/computer' require 'models/owner' require 'models/pet' diff --git a/activerecord/test/cases/invertible_migration_test.rb b/activerecord/test/cases/invertible_migration_test.rb index 285172d33e..8144f3e5c5 100644 --- a/activerecord/test/cases/invertible_migration_test.rb +++ b/activerecord/test/cases/invertible_migration_test.rb @@ -122,12 +122,17 @@ module ActiveRecord end end + setup do + @verbose_was, ActiveRecord::Migration.verbose = ActiveRecord::Migration.verbose, false + end + teardown do %w[horses new_horses].each do |table| if ActiveRecord::Base.connection.table_exists?(table) ActiveRecord::Base.connection.drop_table(table) end end + ActiveRecord::Migration.verbose = @verbose_was end def test_no_reverse diff --git a/activerecord/test/cases/locking_test.rb b/activerecord/test/cases/locking_test.rb index c221430757..ee43f07dd7 100644 --- a/activerecord/test/cases/locking_test.rb +++ b/activerecord/test/cases/locking_test.rb @@ -5,6 +5,7 @@ require 'models/job' require 'models/reader' require 'models/ship' require 'models/legacy_thing' +require 'models/personal_legacy_thing' require 'models/reference' require 'models/string_key_object' require 'models/car' @@ -32,8 +33,6 @@ class OptimisticLockingTest < ActiveRecord::TestCase p1 = Person.find(1) assert_equal 0, p1.lock_version - Person.expects(:quote_value).with(0, Person.columns_hash[Person.locking_column]).returns('0').once - p1.first_name = 'anika2' p1.save! @@ -272,6 +271,13 @@ class OptimisticLockingTest < ActiveRecord::TestCase assert p.treasures.empty? assert RichPerson.connection.select_all("SELECT * FROM peoples_treasures WHERE rich_person_id = 1").empty? end + + def test_yaml_dumping_with_lock_column + t1 = LockWithoutDefault.new + t2 = YAML.load(YAML.dump(t1)) + + assert_equal t1.attributes, t2.attributes + end end class OptimisticLockingWithSchemaChangeTest < ActiveRecord::TestCase @@ -304,30 +310,24 @@ class OptimisticLockingWithSchemaChangeTest < ActiveRecord::TestCase # See Lighthouse ticket #1966 def test_destroy_dependents - # Establish dependent relationship between People and LegacyThing - add_counter_column_to(Person, 'legacy_things_count') - LegacyThing.connection.add_column LegacyThing.table_name, 'person_id', :integer - LegacyThing.reset_column_information - LegacyThing.class_eval do - belongs_to :person, :counter_cache => true - end - Person.class_eval do - has_many :legacy_things, :dependent => :destroy - end + # Establish dependent relationship between Person and PersonalLegacyThing + add_counter_column_to(Person, 'personal_legacy_things_count') + PersonalLegacyThing.reset_column_information # Make sure that counter incrementing doesn't cause problems p1 = Person.new(:first_name => 'fjord') p1.save! - t = LegacyThing.new(:person => p1) + t = PersonalLegacyThing.new(:person => p1) t.save! p1.reload - assert_equal 1, p1.legacy_things_count + assert_equal 1, p1.personal_legacy_things_count assert p1.destroy assert_equal true, p1.frozen? assert_raises(ActiveRecord::RecordNotFound) { Person.find(p1.id) } - assert_raises(ActiveRecord::RecordNotFound) { LegacyThing.find(t.id) } + assert_raises(ActiveRecord::RecordNotFound) { PersonalLegacyThing.find(t.id) } ensure - remove_counter_column_from(Person, 'legacy_things_count') + remove_counter_column_from(Person, 'personal_legacy_things_count') + PersonalLegacyThing.reset_column_information end private diff --git a/activerecord/test/cases/migration/change_schema_test.rb b/activerecord/test/cases/migration/change_schema_test.rb index 9b26c30d14..d774cfebc4 100644 --- a/activerecord/test/cases/migration/change_schema_test.rb +++ b/activerecord/test/cases/migration/change_schema_test.rb @@ -68,9 +68,9 @@ module ActiveRecord five = columns.detect { |c| c.name == "five" } unless mysql assert_equal "hello", one.default - assert_equal true, two.default - assert_equal false, three.default - assert_equal 1, four.default + assert_equal true, two.type_cast_from_database(two.default) + assert_equal false, three.type_cast_from_database(three.default) + assert_equal '1', four.default assert_equal "hello", five.default unless mysql end @@ -97,6 +97,25 @@ module ActiveRecord end end + def test_create_table_with_bigint + connection.create_table :testings do |t| + t.bigint :eight_int + end + columns = connection.columns(:testings) + eight = columns.detect { |c| c.name == "eight_int" } + + if current_adapter?(:OracleAdapter) + assert_equal 'NUMBER(8)', eight.sql_type + elsif current_adapter?(:SQLite3Adapter) + assert_equal 'bigint', eight.sql_type + else + assert_equal :integer, eight.type + assert_equal 8, eight.limit + end + ensure + connection.drop_table :testings + end + def test_create_table_with_limits connection.create_table :testings do |t| t.column :foo, :string, :limit => 255 @@ -176,8 +195,11 @@ module ActiveRecord end def test_create_table_with_timestamps_should_create_datetime_columns - connection.create_table table_name do |t| - t.timestamps + # FIXME: Remove the silence when we change the default `null` behavior + ActiveSupport::Deprecation.silence do + connection.create_table table_name do |t| + t.timestamps + end end created_columns = connection.columns(table_name) @@ -275,7 +297,7 @@ module ActiveRecord person_klass.connection.add_column "testings", "wealth", :integer, :null => false, :default => 99 person_klass.reset_column_information - assert_equal 99, person_klass.columns_hash["wealth"].default + assert_equal 99, person_klass.column_defaults["wealth"] assert_equal false, person_klass.columns_hash["wealth"].null # Oracle needs primary key value from sequence if current_adapter?(:OracleAdapter) @@ -287,20 +309,20 @@ module ActiveRecord # change column default to see that column doesn't lose its not null definition person_klass.connection.change_column_default "testings", "wealth", 100 person_klass.reset_column_information - assert_equal 100, person_klass.columns_hash["wealth"].default + assert_equal 100, person_klass.column_defaults["wealth"] assert_equal false, person_klass.columns_hash["wealth"].null # rename column to see that column doesn't lose its not null and/or default definition person_klass.connection.rename_column "testings", "wealth", "money" person_klass.reset_column_information assert_nil person_klass.columns_hash["wealth"] - assert_equal 100, person_klass.columns_hash["money"].default + assert_equal 100, person_klass.column_defaults["money"] assert_equal false, person_klass.columns_hash["money"].null # change column person_klass.connection.change_column "testings", "money", :integer, :null => false, :default => 1000 person_klass.reset_column_information - assert_equal 1000, person_klass.columns_hash["money"].default + assert_equal 1000, person_klass.column_defaults["money"] assert_equal false, person_klass.columns_hash["money"].null # change column, make it nullable and clear default @@ -393,5 +415,36 @@ module ActiveRecord yield end end + + if ActiveRecord::Base.connection.supports_foreign_keys? + class ChangeSchemaWithDependentObjectsTest < ActiveRecord::TestCase + self.use_transactional_fixtures = false + + setup do + @connection = ActiveRecord::Base.connection + @connection.create_table :trains + @connection.create_table(:wagons) { |t| t.references :train } + @connection.add_foreign_key :wagons, :trains + end + + teardown do + [:wagons, :trains].each do |table| + @connection.drop_table(table) if @connection.table_exists?(table) + end + 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) + # can't re-create table referenced by foreign key + assert_raises(ActiveRecord::StatementInvalid) do + @connection.create_table :trains, force: true + end + + # can recreate referenced table with force: :cascade + @connection.create_table :trains, force: :cascade + assert_equal [], @connection.foreign_keys(:wagons) + end + end + end end end diff --git a/activerecord/test/cases/migration/change_table_test.rb b/activerecord/test/cases/migration/change_table_test.rb index a6d506b04a..7010af5434 100644 --- a/activerecord/test/cases/migration/change_table_test.rb +++ b/activerecord/test/cases/migration/change_table_test.rb @@ -72,17 +72,31 @@ module ActiveRecord end end + def test_references_column_type_with_polymorphic_and_type + with_change_table do |t| + @connection.expect :add_reference, nil, [:delete_me, :taggable, polymorphic: true, type: :string] + t.references :taggable, polymorphic: true, type: :string + end + end + + def test_remove_references_column_type_with_polymorphic_and_type + with_change_table do |t| + @connection.expect :remove_reference, nil, [:delete_me, :taggable, polymorphic: true, type: :string] + t.remove_references :taggable, polymorphic: true, type: :string + end + end + def test_timestamps_creates_updated_at_and_created_at with_change_table do |t| - @connection.expect :add_timestamps, nil, [:delete_me] - t.timestamps + @connection.expect :add_timestamps, nil, [:delete_me, null: true] + t.timestamps null: true end end def test_remove_timestamps_creates_updated_at_and_created_at with_change_table do |t| - @connection.expect :remove_timestamps, nil, [:delete_me] - t.remove_timestamps + @connection.expect :remove_timestamps, nil, [:delete_me, { null: true }] + t.remove_timestamps({ null: true }) end end @@ -199,6 +213,12 @@ module ActiveRecord t.rename :bar, :baz end end + + def test_table_name_set + with_change_table do |t| + assert_equal :delete_me, t.name + end + end end end end diff --git a/activerecord/test/cases/migration/column_attributes_test.rb b/activerecord/test/cases/migration/column_attributes_test.rb index 984d1c2597..763aa88f72 100644 --- a/activerecord/test/cases/migration/column_attributes_test.rb +++ b/activerecord/test/cases/migration/column_attributes_test.rb @@ -51,46 +51,46 @@ module ActiveRecord end end - # We specifically do a manual INSERT here, and then test only the SELECT - # functionality. This allows us to more easily catch INSERT being broken, - # but SELECT actually working fine. - def test_native_decimal_insert_manual_vs_automatic - correct_value = '0012345678901234567890.0123456789'.to_d - - connection.add_column "test_models", "wealth", :decimal, :precision => '30', :scale => '10' - - # 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 - connection.execute "insert into test_models (wealth) values (12345678901234567890.0123456789)" - end + unless current_adapter?(:SQLite3Adapter) + # We specifically do a manual INSERT here, and then test only the SELECT + # functionality. This allows us to more easily catch INSERT being broken, + # but SELECT actually working fine. + def test_native_decimal_insert_manual_vs_automatic + correct_value = '0012345678901234567890.0123456789'.to_d + + connection.add_column "test_models", "wealth", :decimal, :precision => '30', :scale => '10' + + # 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 + connection.execute "insert into test_models (wealth) values (12345678901234567890.0123456789)" + end - # SELECT - row = TestModel.first - assert_kind_of BigDecimal, row.wealth + # SELECT + row = TestModel.first + assert_kind_of BigDecimal, row.wealth - # If this assert fails, that means the SELECT is broken! - unless current_adapter?(:SQLite3Adapter) - assert_equal correct_value, row.wealth - end + # If this assert fails, that means the SELECT is broken! + unless current_adapter?(:SQLite3Adapter) + assert_equal correct_value, row.wealth + end - # Reset to old state - TestModel.delete_all + # Reset to old state + TestModel.delete_all - # Now use the Rails insertion - TestModel.create :wealth => BigDecimal.new("12345678901234567890.0123456789") + # Now use the Rails insertion + TestModel.create :wealth => BigDecimal.new("12345678901234567890.0123456789") - # SELECT - row = TestModel.first - assert_kind_of BigDecimal, row.wealth + # SELECT + row = TestModel.first + assert_kind_of BigDecimal, row.wealth - # If these asserts fail, that means the INSERT (create function, or cast to SQL) is broken! - unless current_adapter?(:SQLite3Adapter) + # If these asserts fail, that means the INSERT (create function, or cast to SQL) is broken! assert_equal correct_value, row.wealth end end @@ -121,54 +121,54 @@ module ActiveRecord end end - def test_native_types - add_column "test_models", "first_name", :string - add_column "test_models", "last_name", :string - add_column "test_models", "bio", :text - add_column "test_models", "age", :integer - add_column "test_models", "height", :float - add_column "test_models", "wealth", :decimal, :precision => '30', :scale => '10' - add_column "test_models", "birthday", :datetime - add_column "test_models", "favorite_day", :date - add_column "test_models", "moment_of_truth", :datetime - add_column "test_models", "male", :boolean - - TestModel.create :first_name => 'bob', :last_name => 'bobsen', - :bio => "I was born ....", :age => 18, :height => 1.78, - :wealth => BigDecimal.new("12345678901234567890.0123456789"), - :birthday => 18.years.ago, :favorite_day => 10.days.ago, - :moment_of_truth => "1782-10-10 21:40:18", :male => true - - bob = TestModel.first - assert_equal 'bob', bob.first_name - assert_equal 'bobsen', bob.last_name - assert_equal "I was born ....", bob.bio - assert_equal 18, bob.age - - # Test for 30 significant digits (beyond the 16 of float), 10 of them - # after the decimal place. - - unless current_adapter?(:SQLite3Adapter) + unless current_adapter?(:SQLite3Adapter) + def test_native_types + add_column "test_models", "first_name", :string + add_column "test_models", "last_name", :string + add_column "test_models", "bio", :text + add_column "test_models", "age", :integer + add_column "test_models", "height", :float + add_column "test_models", "wealth", :decimal, :precision => '30', :scale => '10' + add_column "test_models", "birthday", :datetime + add_column "test_models", "favorite_day", :date + add_column "test_models", "moment_of_truth", :datetime + add_column "test_models", "male", :boolean + + TestModel.create :first_name => 'bob', :last_name => 'bobsen', + :bio => "I was born ....", :age => 18, :height => 1.78, + :wealth => BigDecimal.new("12345678901234567890.0123456789"), + :birthday => 18.years.ago, :favorite_day => 10.days.ago, + :moment_of_truth => "1782-10-10 21:40:18", :male => true + + bob = TestModel.first + assert_equal 'bob', bob.first_name + assert_equal 'bobsen', bob.last_name + assert_equal "I was born ....", bob.bio + assert_equal 18, bob.age + + # Test for 30 significant digits (beyond the 16 of float), 10 of them + # after the decimal place. + assert_equal BigDecimal.new("0012345678901234567890.0123456789"), bob.wealth - end - assert_equal true, bob.male? + assert_equal true, bob.male? - assert_equal String, bob.first_name.class - assert_equal String, bob.last_name.class - assert_equal String, bob.bio.class - assert_equal Fixnum, bob.age.class - assert_equal Time, bob.birthday.class + assert_equal String, bob.first_name.class + assert_equal String, bob.last_name.class + assert_equal String, bob.bio.class + assert_equal Fixnum, bob.age.class + assert_equal Time, bob.birthday.class - if current_adapter?(:OracleAdapter) - # Oracle doesn't differentiate between date/time - assert_equal Time, bob.favorite_day.class - else - assert_equal Date, bob.favorite_day.class - end + if current_adapter?(:OracleAdapter) + # Oracle doesn't differentiate between date/time + assert_equal Time, bob.favorite_day.class + else + assert_equal Date, bob.favorite_day.class + end - assert_instance_of TrueClass, bob.male? - assert_kind_of BigDecimal, bob.wealth + assert_instance_of TrueClass, bob.male? + assert_kind_of BigDecimal, bob.wealth + end end if current_adapter?(:MysqlAdapter, :Mysql2Adapter, :PostgreSQLAdapter) diff --git a/activerecord/test/cases/migration/column_positioning_test.rb b/activerecord/test/cases/migration/column_positioning_test.rb index 77a752f050..62186e13a5 100644 --- a/activerecord/test/cases/migration/column_positioning_test.rb +++ b/activerecord/test/cases/migration/column_positioning_test.rb @@ -25,30 +25,30 @@ module ActiveRecord if current_adapter?(:MysqlAdapter, :Mysql2Adapter) def test_column_positioning - assert_equal %w(first second third), conn.columns(:testings).map {|c| c.name } + assert_equal %w(first second third), conn.columns(:testings).map(&:name) end def test_add_column_with_positioning conn.add_column :testings, :new_col, :integer - assert_equal %w(first second third new_col), conn.columns(:testings).map {|c| c.name } + assert_equal %w(first second third new_col), conn.columns(:testings).map(&:name) end def test_add_column_with_positioning_first conn.add_column :testings, :new_col, :integer, :first => true - assert_equal %w(new_col first second third), conn.columns(:testings).map {|c| c.name } + assert_equal %w(new_col first second third), conn.columns(:testings).map(&:name) end def test_add_column_with_positioning_after conn.add_column :testings, :new_col, :integer, :after => :first - assert_equal %w(first new_col second third), conn.columns(:testings).map {|c| c.name } + assert_equal %w(first new_col second third), conn.columns(:testings).map(&:name) end def test_change_column_with_positioning conn.change_column :testings, :second, :integer, :first => true - assert_equal %w(second first third), conn.columns(:testings).map {|c| c.name } + assert_equal %w(second first third), conn.columns(:testings).map(&:name) conn.change_column :testings, :second, :integer, :after => :third - assert_equal %w(first third second), conn.columns(:testings).map {|c| c.name } + assert_equal %w(first third second), conn.columns(:testings).map(&:name) end end end diff --git a/activerecord/test/cases/migration/columns_test.rb b/activerecord/test/cases/migration/columns_test.rb index a7c287515d..e6aa901814 100644 --- a/activerecord/test/cases/migration/columns_test.rb +++ b/activerecord/test/cases/migration/columns_test.rb @@ -53,19 +53,22 @@ module ActiveRecord add_column 'test_models', 'salary', :integer, :default => 70000 default_before = connection.columns("test_models").find { |c| c.name == "salary" }.default - assert_equal 70000, default_before + assert_equal '70000', default_before rename_column "test_models", "salary", "annual_salary" assert TestModel.column_names.include?("annual_salary") default_after = connection.columns("test_models").find { |c| c.name == "annual_salary" }.default - assert_equal 70000, default_after + assert_equal '70000', default_after end if current_adapter?(:MysqlAdapter, :Mysql2Adapter) def test_mysql_rename_column_preserves_auto_increment rename_column "test_models", "id", "id_test" assert_equal "auto_increment", connection.columns("test_models").find { |c| c.name == "id_test" }.extra + TestModel.reset_column_information + ensure + rename_column "test_models", "id_test", "id" end end @@ -193,14 +196,21 @@ module ActiveRecord old_columns = connection.columns(TestModel.table_name) assert old_columns.find { |c| - c.name == 'approved' && c.type == :boolean && c.default == true + default = c.type_cast_from_database(c.default) + c.name == 'approved' && c.type == :boolean && default == true } change_column :test_models, :approved, :boolean, :default => false new_columns = connection.columns(TestModel.table_name) - assert_not new_columns.find { |c| c.name == 'approved' and c.type == :boolean and c.default == true } - assert new_columns.find { |c| c.name == 'approved' and c.type == :boolean and c.default == false } + assert_not new_columns.find { |c| + default = c.type_cast_from_database(c.default) + c.name == 'approved' and c.type == :boolean and default == true + } + assert new_columns.find { |c| + default = c.type_cast_from_database(c.default) + c.name == 'approved' and c.type == :boolean and default == false + } change_column :test_models, :approved, :boolean, :default => true end diff --git a/activerecord/test/cases/migration/command_recorder_test.rb b/activerecord/test/cases/migration/command_recorder_test.rb index a925cf4c05..8cba777fe2 100644 --- a/activerecord/test/cases/migration/command_recorder_test.rb +++ b/activerecord/test/cases/migration/command_recorder_test.rb @@ -157,6 +157,23 @@ module ActiveRecord assert_equal [:remove_column, [:table, :column, :type, {}], nil], remove end + def test_invert_change_column + assert_raises(ActiveRecord::IrreversibleMigration) do + @recorder.inverse_of :change_column, [:table, :column, :type, {}] + end + end + + def test_invert_change_column_default + assert_raises(ActiveRecord::IrreversibleMigration) do + @recorder.inverse_of :change_column_default, [:table, :column, 'default_value'] + end + end + + def test_invert_change_column_null + add = @recorder.inverse_of :change_column_null, [:table, :column, true] + assert_equal [:change_column_null, [:table, :column, false]], add + end + def test_invert_remove_column add = @recorder.inverse_of :remove_column, [:table, :column, :type, {}] assert_equal [:add_column, [:table, :column, :type, {}], nil], add @@ -220,8 +237,8 @@ module ActiveRecord end def test_invert_remove_timestamps - add = @recorder.inverse_of :remove_timestamps, [:table] - assert_equal [:add_timestamps, [:table], nil], add + add = @recorder.inverse_of :remove_timestamps, [:table, { null: true }] + assert_equal [:add_timestamps, [:table, {null: true }], nil], add end def test_invert_add_reference @@ -253,6 +270,31 @@ module ActiveRecord enable = @recorder.inverse_of :disable_extension, ['uuid-ossp'] assert_equal [:enable_extension, ['uuid-ossp'], nil], enable end + + def test_invert_add_foreign_key + enable = @recorder.inverse_of :add_foreign_key, [:dogs, :people] + assert_equal [:remove_foreign_key, [:dogs, :people]], enable + end + + def test_invert_add_foreign_key_with_column + enable = @recorder.inverse_of :add_foreign_key, [:dogs, :people, column: "owner_id"] + assert_equal [:remove_foreign_key, [:dogs, column: "owner_id"]], enable + end + + def test_invert_add_foreign_key_with_column_and_name + enable = @recorder.inverse_of :add_foreign_key, [:dogs, :people, column: "owner_id", name: "fk"] + assert_equal [:remove_foreign_key, [:dogs, name: "fk"]], enable + end + + def test_remove_foreign_key_is_irreversible + assert_raises ActiveRecord::IrreversibleMigration do + @recorder.inverse_of :remove_foreign_key, [:dogs, column: "owner_id"] + end + + assert_raises ActiveRecord::IrreversibleMigration do + @recorder.inverse_of :remove_foreign_key, [:dogs, name: "fk"] + 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 62b60f7f7b..bea9d6b2c9 100644 --- a/activerecord/test/cases/migration/create_join_table_test.rb +++ b/activerecord/test/cases/migration/create_join_table_test.rb @@ -119,6 +119,30 @@ module ActiveRecord assert !connection.tables.include?('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' + + connection.drop_join_table 'audio_artists', 'audio_musics' + assert !connection.tables.include?('audio_artists_musics'), "Should have dropped join table, but didn't" + end + end + + private + + def with_table_cleanup + tables_before = connection.tables + + yield + ensure + tables_after = connection.tables - tables_before + + tables_after.each do |table| + connection.execute "DROP TABLE #{table}" + end + end end end end diff --git a/activerecord/test/cases/migration/foreign_key_test.rb b/activerecord/test/cases/migration/foreign_key_test.rb new file mode 100644 index 0000000000..51e21528c2 --- /dev/null +++ b/activerecord/test/cases/migration/foreign_key_test.rb @@ -0,0 +1,250 @@ +require 'cases/helper' +require 'support/ddl_helper' +require 'support/schema_dumping_helper' + +if ActiveRecord::Base.connection.supports_foreign_keys? +module ActiveRecord + class Migration + class ForeignKeyTest < ActiveRecord::TestCase + include DdlHelper + include SchemaDumpingHelper + + class Rocket < ActiveRecord::Base + end + + class Astronaut < ActiveRecord::Base + end + + setup do + @connection = ActiveRecord::Base.connection + @connection.create_table "rockets", force: true do |t| + t.string :name + end + + @connection.create_table "astronauts", force: true do |t| + t.string :name + t.references :rocket + end + end + + teardown do + if defined?(@connection) + @connection.drop_table "astronauts" if @connection.table_exists? 'astronauts' + @connection.drop_table "rockets" if @connection.table_exists? 'rockets' + end + end + + def test_foreign_keys + foreign_keys = @connection.foreign_keys("fk_test_has_fk") + assert_equal 1, foreign_keys.size + + fk = foreign_keys.first + assert_equal "fk_test_has_fk", fk.from_table + assert_equal "fk_test_has_pk", fk.to_table + assert_equal "fk_id", fk.column + assert_equal "pk_id", fk.primary_key + assert_equal "fk_name", fk.name + end + + def test_add_foreign_key_inferes_column + @connection.add_foreign_key :astronauts, :rockets + + foreign_keys = @connection.foreign_keys("astronauts") + assert_equal 1, foreign_keys.size + + fk = foreign_keys.first + assert_equal "astronauts", fk.from_table + assert_equal "rockets", fk.to_table + assert_equal "rocket_id", fk.column + assert_equal "id", fk.primary_key + assert_match(/^fk_rails_.{10}$/, fk.name) + end + + def test_add_foreign_key_with_column + @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id" + + foreign_keys = @connection.foreign_keys("astronauts") + assert_equal 1, foreign_keys.size + + fk = foreign_keys.first + assert_equal "astronauts", fk.from_table + assert_equal "rockets", fk.to_table + assert_equal "rocket_id", fk.column + assert_equal "id", fk.primary_key + assert_match(/^fk_rails_.{10}$/, fk.name) + end + + def test_add_foreign_key_with_non_standard_primary_key + with_example_table @connection, "space_shuttles", "pk integer PRIMARY KEY" do + @connection.add_foreign_key(:astronauts, :space_shuttles, + column: "rocket_id", primary_key: "pk", name: "custom_pk") + + foreign_keys = @connection.foreign_keys("astronauts") + assert_equal 1, foreign_keys.size + + fk = foreign_keys.first + assert_equal "astronauts", fk.from_table + assert_equal "space_shuttles", fk.to_table + assert_equal "pk", fk.primary_key + + @connection.remove_foreign_key :astronauts, name: "custom_pk" + end + end + + def test_add_on_delete_restrict_foreign_key + @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", on_delete: :restrict + + foreign_keys = @connection.foreign_keys("astronauts") + assert_equal 1, foreign_keys.size + + fk = foreign_keys.first + if current_adapter?(:MysqlAdapter, :Mysql2Adapter) + # ON DELETE RESTRICT is the default on MySQL + assert_equal nil, fk.on_delete + else + assert_equal :restrict, fk.on_delete + end + end + + def test_add_on_delete_cascade_foreign_key + @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", on_delete: :cascade + + foreign_keys = @connection.foreign_keys("astronauts") + assert_equal 1, foreign_keys.size + + fk = foreign_keys.first + assert_equal :cascade, fk.on_delete + end + + def test_add_on_delete_nullify_foreign_key + @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", on_delete: :nullify + + foreign_keys = @connection.foreign_keys("astronauts") + assert_equal 1, foreign_keys.size + + fk = foreign_keys.first + assert_equal :nullify, fk.on_delete + end + + def test_on_update_and_on_delete_raises_with_invalid_values + assert_raises ArgumentError do + @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", on_delete: :invalid + end + + assert_raises ArgumentError do + @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", on_update: :invalid + end + end + + def test_add_foreign_key_with_on_update + @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", on_update: :nullify + + foreign_keys = @connection.foreign_keys("astronauts") + assert_equal 1, foreign_keys.size + + fk = foreign_keys.first + assert_equal :nullify, fk.on_update + end + + def test_remove_foreign_key_inferes_column + @connection.add_foreign_key :astronauts, :rockets + + assert_equal 1, @connection.foreign_keys("astronauts").size + @connection.remove_foreign_key :astronauts, :rockets + assert_equal [], @connection.foreign_keys("astronauts") + end + + def test_remove_foreign_key_by_column + @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id" + + assert_equal 1, @connection.foreign_keys("astronauts").size + @connection.remove_foreign_key :astronauts, column: "rocket_id" + assert_equal [], @connection.foreign_keys("astronauts") + end + + def test_remove_foreign_key_by_symbol_column + @connection.add_foreign_key :astronauts, :rockets, column: :rocket_id + + assert_equal 1, @connection.foreign_keys("astronauts").size + @connection.remove_foreign_key :astronauts, column: :rocket_id + assert_equal [], @connection.foreign_keys("astronauts") + end + + def test_remove_foreign_key_by_name + @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", name: "fancy_named_fk" + + assert_equal 1, @connection.foreign_keys("astronauts").size + @connection.remove_foreign_key :astronauts, name: "fancy_named_fk" + assert_equal [], @connection.foreign_keys("astronauts") + end + + def test_remove_foreign_non_existing_foreign_key_raises + assert_raises ArgumentError do + @connection.remove_foreign_key :astronauts, :rockets + end + end + + def test_schema_dumping + @connection.add_foreign_key :astronauts, :rockets + output = dump_table_schema "astronauts" + assert_match %r{\s+add_foreign_key "astronauts", "rockets"$}, output + end + + def test_schema_dumping_with_options + output = dump_table_schema "fk_test_has_fk" + assert_match %r{\s+add_foreign_key "fk_test_has_fk", "fk_test_has_pk", column: "fk_id", primary_key: "pk_id", name: "fk_name"$}, output + end + + def test_schema_dumping_on_delete_and_on_update_options + @connection.add_foreign_key :astronauts, :rockets, column: "rocket_id", on_delete: :nullify, on_update: :cascade + + output = dump_table_schema "astronauts" + assert_match %r{\s+add_foreign_key "astronauts",.+on_update: :cascade,.+on_delete: :nullify$}, output + end + + class CreateCitiesAndHousesMigration < ActiveRecord::Migration + def change + create_table("cities") { |t| } + + create_table("houses") do |t| + t.column :city_id, :integer + end + add_foreign_key :houses, :cities, column: "city_id" + end + end + + def test_add_foreign_key_is_reversible + migration = CreateCitiesAndHousesMigration.new + silence_stream($stdout) { migration.migrate(:up) } + assert_equal 1, @connection.foreign_keys("houses").size + ensure + silence_stream($stdout) { migration.migrate(:down) } + end + end + end +end +else +module ActiveRecord + class Migration + class NoForeignKeySupportTest < ActiveRecord::TestCase + setup do + @connection = ActiveRecord::Base.connection + end + + def test_add_foreign_key_should_be_noop + @connection.add_foreign_key :clubs, :categories + end + + def test_remove_foreign_key_should_be_noop + @connection.remove_foreign_key :clubs, :categories + end + + def test_foreign_keys_should_raise_not_implemented + assert_raises NotImplementedError do + @connection.foreign_keys("clubs") + end + end + end + end +end +end diff --git a/activerecord/test/cases/migration/helper.rb b/activerecord/test/cases/migration/helper.rb index e28feedcf9..5bc0898f33 100644 --- a/activerecord/test/cases/migration/helper.rb +++ b/activerecord/test/cases/migration/helper.rb @@ -5,10 +5,6 @@ module ActiveRecord class << self; attr_accessor :message_count; end self.message_count = 0 - def puts(text="") - ActiveRecord::Migration.message_count += 1 - end - module TestHelper attr_reader :connection, :table_name @@ -22,7 +18,7 @@ module ActiveRecord super @connection = ActiveRecord::Base.connection connection.create_table :test_models do |t| - t.timestamps + t.timestamps null: true end TestModel.reset_column_information diff --git a/activerecord/test/cases/migration/index_test.rb b/activerecord/test/cases/migration/index_test.rb index 93c3bfae7a..b23b9a679f 100644 --- a/activerecord/test/cases/migration/index_test.rb +++ b/activerecord/test/cases/migration/index_test.rb @@ -36,6 +36,20 @@ module ActiveRecord assert connection.index_name_exists?(table_name, 'new_idx', true) end + def test_rename_index_too_long + too_long_index_name = good_index_name + 'x' + # keep the names short to make Oracle and similar behave + connection.add_index(table_name, [:foo], :name => 'old_idx') + e = assert_raises(ArgumentError) { + connection.rename_index(table_name, 'old_idx', too_long_index_name) + } + assert_match(/too long; the limit is #{connection.allowed_index_name_length} characters/, e.message) + + # if the adapter doesn't support the indexes call, pick defaults that let the test pass + assert connection.index_name_exists?(table_name, 'old_idx', false) + end + + def test_double_add_index connection.add_index(table_name, [:foo], :name => 'some_idx') assert_raises(ArgumentError) { @@ -95,6 +109,12 @@ module ActiveRecord assert connection.index_exists?(:testings, [:foo, :bar]) end + def test_index_exists_with_custom_name_checks_columns + connection.add_index :testings, [:foo, :bar], name: "my_index" + assert connection.index_exists?(:testings, [:foo, :bar], name: "my_index") + assert_not connection.index_exists?(:testings, [:foo], name: "my_index") + end + def test_valid_index_options assert_raise ArgumentError do connection.add_index :testings, :foo, unqiue: true diff --git a/activerecord/test/cases/migration/logger_test.rb b/activerecord/test/cases/migration/logger_test.rb index 84224e6e4c..319d3e1af3 100644 --- a/activerecord/test/cases/migration/logger_test.rb +++ b/activerecord/test/cases/migration/logger_test.rb @@ -3,7 +3,7 @@ require "cases/helper" module ActiveRecord class Migration class LoggerTest < ActiveRecord::TestCase - # mysql can't roll back ddl changes + # MySQL can't roll back ddl changes self.use_transactional_fixtures = false Migration = Struct.new(:name, :version) do diff --git a/activerecord/test/cases/migration/pending_migrations_test.rb b/activerecord/test/cases/migration/pending_migrations_test.rb new file mode 100644 index 0000000000..7afac83bd2 --- /dev/null +++ b/activerecord/test/cases/migration/pending_migrations_test.rb @@ -0,0 +1,53 @@ +require 'cases/helper' +require "minitest/mock" + +module ActiveRecord + class Migration + class PendingMigrationsTest < ActiveRecord::TestCase + def setup + super + @connection = Minitest::Mock.new + @app = Minitest::Mock.new + conn = @connection + @pending = Class.new(CheckPending) { + define_method(:connection) { conn } + }.new(@app) + @pending.instance_variable_set :@last_check, -1 # Force checking + end + + def teardown + assert @connection.verify + assert @app.verify + super + end + + def test_errors_if_pending + @connection.expect :supports_migrations?, true + + ActiveRecord::Migrator.stub :needs_migration?, true do + assert_raise ActiveRecord::PendingMigrationError do + @pending.call(nil) + end + end + end + + def test_checks_if_supported + @connection.expect :supports_migrations?, true + @app.expect :call, nil, [:foo] + + ActiveRecord::Migrator.stub :needs_migration?, false do + @pending.call(:foo) + end + end + + def test_doesnt_check_if_unsupported + @connection.expect :supports_migrations?, false + @app.expect :call, nil, [:foo] + + ActiveRecord::Migrator.stub :needs_migration?, true do + @pending.call(:foo) + end + end + end + end +end diff --git a/activerecord/test/cases/migration/references_foreign_key_test.rb b/activerecord/test/cases/migration/references_foreign_key_test.rb new file mode 100644 index 0000000000..bba8897a0d --- /dev/null +++ b/activerecord/test/cases/migration/references_foreign_key_test.rb @@ -0,0 +1,101 @@ +require 'cases/helper' + +if ActiveRecord::Base.connection.supports_foreign_keys? +module ActiveRecord + class Migration + class ReferencesForeignKeyTest < ActiveRecord::TestCase + setup do + @connection = ActiveRecord::Base.connection + @connection.create_table(:testing_parents, force: true) + end + + teardown do + @connection.execute("drop table if exists testings") + @connection.execute("drop table if exists testing_parents") + end + + test "foreign keys can be created with the table" do + @connection.create_table :testings do |t| + t.references :testing_parent, foreign_key: true + end + + fk = @connection.foreign_keys("testings").first + assert_equal "testings", fk.from_table + assert_equal "testing_parents", fk.to_table + end + + test "no foreign key is created by default" do + @connection.create_table :testings do |t| + t.references :testing_parent + end + + assert_equal [], @connection.foreign_keys("testings") + end + + test "options hash can be passed" do + @connection.change_table :testing_parents do |t| + t.integer :other_id + t.index :other_id, unique: true + end + @connection.create_table :testings do |t| + t.references :testing_parent, foreign_key: { primary_key: :other_id } + end + + fk = @connection.foreign_keys("testings").find { |k| k.to_table == "testing_parents" } + assert_equal "other_id", fk.primary_key + end + + test "foreign keys cannot be added to polymorphic relations when creating the table" do + @connection.create_table :testings do |t| + assert_raises(ArgumentError) do + t.references :testing_parent, polymorphic: true, foreign_key: true + end + end + end + + test "foreign keys can be created while changing the table" do + @connection.create_table :testings + @connection.change_table :testings do |t| + t.references :testing_parent, foreign_key: true + end + + fk = @connection.foreign_keys("testings").first + assert_equal "testings", fk.from_table + assert_equal "testing_parents", fk.to_table + end + + test "foreign keys are not added by default when changing the table" do + @connection.create_table :testings + @connection.change_table :testings do |t| + t.references :testing_parent + end + + assert_equal [], @connection.foreign_keys("testings") + end + + test "foreign keys accept options when changing the table" do + @connection.change_table :testing_parents do |t| + t.integer :other_id + t.index :other_id, unique: true + end + @connection.create_table :testings + @connection.change_table :testings do |t| + t.references :testing_parent, foreign_key: { primary_key: :other_id } + end + + fk = @connection.foreign_keys("testings").find { |k| k.to_table == "testing_parents" } + assert_equal "other_id", fk.primary_key + end + + test "foreign keys cannot be added to polymorphic relations when changing the table" do + @connection.create_table :testings + @connection.change_table :testings do |t| + assert_raises(ArgumentError) do + t.references :testing_parent, polymorphic: true, foreign_key: true + end + end + end + end + end +end +end diff --git a/activerecord/test/cases/migration/references_index_test.rb b/activerecord/test/cases/migration/references_index_test.rb index 4485701a4e..ad6b828d0b 100644 --- a/activerecord/test/cases/migration/references_index_test.rb +++ b/activerecord/test/cases/migration/references_index_test.rb @@ -55,7 +55,7 @@ module ActiveRecord t.references :foo, :polymorphic => true, :index => true end - assert connection.index_exists?(table_name, [:foo_id, :foo_type], :name => :index_testings_on_foo_id_and_foo_type) + assert connection.index_exists?(table_name, [:foo_type, :foo_id], name: :index_testings_on_foo_type_and_foo_id) end end @@ -93,7 +93,7 @@ module ActiveRecord t.references :foo, :polymorphic => true, :index => true end - assert connection.index_exists?(table_name, [:foo_id, :foo_type], :name => :index_testings_on_foo_id_and_foo_type) + assert connection.index_exists?(table_name, [:foo_type, :foo_id], name: :index_testings_on_foo_type_and_foo_id) end end end diff --git a/activerecord/test/cases/migration/references_statements_test.rb b/activerecord/test/cases/migration/references_statements_test.rb index e9545f2cce..988bd9c89f 100644 --- a/activerecord/test/cases/migration/references_statements_test.rb +++ b/activerecord/test/cases/migration/references_statements_test.rb @@ -42,7 +42,7 @@ module ActiveRecord def test_creates_polymorphic_index add_reference table_name, :taggable, polymorphic: true, index: true - assert index_exists?(table_name, [:taggable_id, :taggable_type]) + assert index_exists?(table_name, [:taggable_type, :taggable_id]) end def test_creates_reference_type_column_with_default @@ -55,6 +55,11 @@ module ActiveRecord assert index_exists?(table_name, :tag_id, name: 'index_taggings_on_tag_id') end + def test_creates_reference_id_with_specified_type + add_reference table_name, :user, type: :string + assert column_exists?(table_name, :user_id, :string) + end + def test_deletes_reference_id_column remove_reference table_name, :supplier assert_not column_exists?(table_name, :supplier_id, :integer) diff --git a/activerecord/test/cases/migration/rename_table_test.rb b/activerecord/test/cases/migration/rename_table_test.rb index a52b58c4ac..c8b3f75e10 100644 --- a/activerecord/test/cases/migration/rename_table_test.rb +++ b/activerecord/test/cases/migration/rename_table_test.rb @@ -76,6 +76,17 @@ module ActiveRecord assert_equal ConnectionAdapters::PostgreSQL::Name.new("public", "octopi_#{pk}_seq"), seq 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 + ensure + disable_extension!('uuid-ossp', connection) + connection.drop_table :cats if connection.table_exists? :cats + connection.drop_table :felines if connection.table_exists? :felines + end end end end diff --git a/activerecord/test/cases/migration_test.rb b/activerecord/test/cases/migration_test.rb index 9855835e27..3192b797b4 100644 --- a/activerecord/test/cases/migration_test.rb +++ b/activerecord/test/cases/migration_test.rb @@ -5,6 +5,7 @@ require 'bigdecimal/util' require 'models/person' require 'models/topic' require 'models/developer' +require 'models/computer' require MIGRATIONS_ROOT + "/valid/2_we_need_reminders" require MIGRATIONS_ROOT + "/rename/1_we_need_things" @@ -15,7 +16,6 @@ class BigNumber < ActiveRecord::Base unless current_adapter?(:PostgreSQLAdapter, :SQLite3Adapter) attribute :value_of_e, Type::Integer.new end - attribute :world_population, Type::Integer.new attribute :my_house_population, Type::Integer.new end @@ -34,8 +34,7 @@ class MigrationTest < ActiveRecord::TestCase Reminder.connection.drop_table(table) rescue nil end Reminder.reset_column_information - ActiveRecord::Migration.verbose = true - ActiveRecord::Migration.message_count = 0 + @verbose_was, ActiveRecord::Migration.verbose = ActiveRecord::Migration.verbose, false ActiveRecord::Base.connection.schema_cache.clear! end @@ -63,8 +62,10 @@ class MigrationTest < ActiveRecord::TestCase end Person.connection.remove_column("people", "first_name") rescue nil Person.connection.remove_column("people", "middle_name") rescue nil - Person.connection.add_column("people", "first_name", :string, :limit => 40) + Person.connection.add_column("people", "first_name", :string) Person.reset_column_information + + ActiveRecord::Migration.verbose = @verbose_was end def test_migrator_versions @@ -81,6 +82,34 @@ class MigrationTest < ActiveRecord::TestCase assert_equal 0, ActiveRecord::Migrator.current_version assert_equal 3, ActiveRecord::Migrator.last_version assert_equal true, ActiveRecord::Migrator.needs_migration? + + ActiveRecord::SchemaMigration.create!(:version => ActiveRecord::Migrator.last_version) + assert_equal true, ActiveRecord::Migrator.needs_migration? + ensure + ActiveRecord::Migrator.migrations_paths = old_path + end + + def test_migration_detection_without_schema_migration_table + ActiveRecord::Base.connection.drop_table('schema_migrations') if ActiveRecord::Base.connection.table_exists?('schema_migrations') + + migrations_path = MIGRATIONS_ROOT + "/valid" + old_path = ActiveRecord::Migrator.migrations_paths + ActiveRecord::Migrator.migrations_paths = migrations_path + + assert_equal true, ActiveRecord::Migrator.needs_migration? + ensure + ActiveRecord::Migrator.migrations_paths = old_path + end + + def test_any_migrations + old_path = ActiveRecord::Migrator.migrations_paths + ActiveRecord::Migrator.migrations_paths = MIGRATIONS_ROOT + "/valid" + + assert ActiveRecord::Migrator.any_migrations? + + ActiveRecord::Migrator.migrations_paths = MIGRATIONS_ROOT + "/empty" + + assert_not ActiveRecord::Migrator.any_migrations? ensure ActiveRecord::Migrator.migrations_paths = old_path end @@ -414,30 +443,32 @@ class MigrationTest < ActiveRecord::TestCase Person.connection.drop_table :binary_testings rescue nil end - def test_create_table_with_query - Person.connection.drop_table :table_from_query_testings rescue nil - Person.connection.create_table(:person, force: true) + 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" + Person.connection.create_table :table_from_query_testings, as: "SELECT id FROM person" - columns = Person.connection.columns(:table_from_query_testings) - assert_equal 1, columns.length - assert_equal "id", columns.first.name + columns = Person.connection.columns(:table_from_query_testings) + assert_equal 1, columns.length + assert_equal "id", columns.first.name - Person.connection.drop_table :table_from_query_testings rescue nil - end + 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) + 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) + Person.connection.create_table :table_from_query_testings, as: Person.select(:id) - columns = Person.connection.columns(:table_from_query_testings) - assert_equal 1, columns.length - assert_equal "id", columns.first.name + columns = Person.connection.columns(:table_from_query_testings) + assert_equal 1, columns.length + assert_equal "id", columns.first.name - Person.connection.drop_table :table_from_query_testings rescue nil + Person.connection.drop_table :table_from_query_testings rescue nil + end end if current_adapter? :OracleAdapter @@ -561,13 +592,13 @@ if ActiveRecord::Base.connection.supports_bulk_alter? t.string :qualification, :experience t.integer :age, :default => 0 t.date :birthdate - t.timestamps + t.timestamps null: true end end assert_equal 8, columns.size [:name, :qualification, :experience].each {|s| assert_equal :string, column(s).type } - assert_equal 0, column(:age).default + assert_equal '0', column(:age).default end def test_removing_columns @@ -895,4 +926,14 @@ class CopyMigrationsTest < ActiveRecord::TestCase ensure ActiveRecord::Base.logger = old end + + private + + def quietly + silence_stream(STDOUT) do + silence_stream(STDERR) do + yield + end + end + end end diff --git a/activerecord/test/cases/migrator_test.rb b/activerecord/test/cases/migrator_test.rb index 9568aa2217..c0daa83e9c 100644 --- a/activerecord/test/cases/migrator_test.rb +++ b/activerecord/test/cases/migrator_test.rb @@ -1,377 +1,388 @@ require "cases/helper" require "cases/migration/helper" -module ActiveRecord - class MigratorTest < ActiveRecord::TestCase - self.use_transactional_fixtures = false +class MigratorTest < ActiveRecord::TestCase + self.use_transactional_fixtures = false - # Use this class to sense if migrations have gone - # up or down. - class Sensor < ActiveRecord::Migration - attr_reader :went_up, :went_down + # Use this class to sense if migrations have gone + # up or down. + class Sensor < ActiveRecord::Migration + attr_reader :went_up, :went_down - def initialize name = self.class.name, version = nil - super - @went_up = false - @went_down = false - end - - def up; @went_up = true; end - def down; @went_down = true; end - end - - def setup + def initialize name = self.class.name, version = nil super - ActiveRecord::SchemaMigration.create_table - ActiveRecord::SchemaMigration.delete_all rescue nil + @went_up = false + @went_down = false end - teardown do - ActiveRecord::SchemaMigration.delete_all rescue nil - ActiveRecord::Migration.verbose = true - end + def up; @went_up = true; end + def down; @went_down = true; end + end - def test_migrator_with_duplicate_names - assert_raises(ActiveRecord::DuplicateMigrationNameError, "Multiple migrations have the name Chunky") do - list = [Migration.new('Chunky'), Migration.new('Chunky')] - ActiveRecord::Migrator.new(:up, list) + def setup + super + ActiveRecord::SchemaMigration.create_table + ActiveRecord::SchemaMigration.delete_all rescue nil + @verbose_was = ActiveRecord::Migration.verbose + ActiveRecord::Migration.message_count = 0 + ActiveRecord::Migration.class_eval do + undef :puts + def puts(*) + ActiveRecord::Migration.message_count += 1 end end + end - def test_migrator_with_duplicate_versions - assert_raises(ActiveRecord::DuplicateMigrationVersionError) do - list = [Migration.new('Foo', 1), Migration.new('Bar', 1)] - ActiveRecord::Migrator.new(:up, list) + teardown do + ActiveRecord::SchemaMigration.delete_all rescue nil + ActiveRecord::Migration.verbose = @verbose_was + ActiveRecord::Migration.class_eval do + undef :puts + def puts(*) + super end end + end - def test_migrator_with_missing_version_numbers - assert_raises(ActiveRecord::UnknownMigrationVersionError) do - list = [Migration.new('Foo', 1), Migration.new('Bar', 2)] - ActiveRecord::Migrator.new(:up, list, 3).run - end + def test_migrator_with_duplicate_names + assert_raises(ActiveRecord::DuplicateMigrationNameError, "Multiple migrations have the name Chunky") do + list = [ActiveRecord::Migration.new('Chunky'), ActiveRecord::Migration.new('Chunky')] + ActiveRecord::Migrator.new(:up, list) end + end - def test_finds_migrations - migrations = ActiveRecord::Migrator.migrations(MIGRATIONS_ROOT + "/valid") + def test_migrator_with_duplicate_versions + assert_raises(ActiveRecord::DuplicateMigrationVersionError) do + list = [ActiveRecord::Migration.new('Foo', 1), ActiveRecord::Migration.new('Bar', 1)] + ActiveRecord::Migrator.new(:up, list) + end + end - [[1, 'ValidPeopleHaveLastNames'], [2, 'WeNeedReminders'], [3, 'InnocentJointable']].each_with_index do |pair, i| - assert_equal migrations[i].version, pair.first - assert_equal migrations[i].name, pair.last - end + def test_migrator_with_missing_version_numbers + assert_raises(ActiveRecord::UnknownMigrationVersionError) do + list = [ActiveRecord::Migration.new('Foo', 1), ActiveRecord::Migration.new('Bar', 2)] + ActiveRecord::Migrator.new(:up, list, 3).run end + end - def test_finds_migrations_in_subdirectories - migrations = ActiveRecord::Migrator.migrations(MIGRATIONS_ROOT + "/valid_with_subdirectories") + def test_finds_migrations + migrations = ActiveRecord::Migrator.migrations(MIGRATIONS_ROOT + "/valid") - [[1, 'ValidPeopleHaveLastNames'], [2, 'WeNeedReminders'], [3, 'InnocentJointable']].each_with_index do |pair, i| - assert_equal migrations[i].version, pair.first - assert_equal migrations[i].name, pair.last - end + [[1, 'ValidPeopleHaveLastNames'], [2, 'WeNeedReminders'], [3, 'InnocentJointable']].each_with_index do |pair, i| + assert_equal migrations[i].version, pair.first + assert_equal migrations[i].name, pair.last end + end - def test_finds_migrations_from_two_directories - directories = [MIGRATIONS_ROOT + '/valid_with_timestamps', MIGRATIONS_ROOT + '/to_copy_with_timestamps'] - migrations = ActiveRecord::Migrator.migrations directories - - [[20090101010101, "PeopleHaveHobbies"], - [20090101010202, "PeopleHaveDescriptions"], - [20100101010101, "ValidWithTimestampsPeopleHaveLastNames"], - [20100201010101, "ValidWithTimestampsWeNeedReminders"], - [20100301010101, "ValidWithTimestampsInnocentJointable"]].each_with_index do |pair, i| - assert_equal pair.first, migrations[i].version - assert_equal pair.last, migrations[i].name - end - end + def test_finds_migrations_in_subdirectories + migrations = ActiveRecord::Migrator.migrations(MIGRATIONS_ROOT + "/valid_with_subdirectories") - def test_finds_migrations_in_numbered_directory - migrations = ActiveRecord::Migrator.migrations [MIGRATIONS_ROOT + '/10_urban'] - assert_equal 9, migrations[0].version - assert_equal 'AddExpressions', migrations[0].name + [[1, 'ValidPeopleHaveLastNames'], [2, 'WeNeedReminders'], [3, 'InnocentJointable']].each_with_index do |pair, i| + assert_equal migrations[i].version, pair.first + assert_equal migrations[i].name, pair.last end + end - def test_relative_migrations - list = Dir.chdir(MIGRATIONS_ROOT) do - ActiveRecord::Migrator.migrations("valid") - end + def test_finds_migrations_from_two_directories + directories = [MIGRATIONS_ROOT + '/valid_with_timestamps', MIGRATIONS_ROOT + '/to_copy_with_timestamps'] + migrations = ActiveRecord::Migrator.migrations directories + + [[20090101010101, "PeopleHaveHobbies"], + [20090101010202, "PeopleHaveDescriptions"], + [20100101010101, "ValidWithTimestampsPeopleHaveLastNames"], + [20100201010101, "ValidWithTimestampsWeNeedReminders"], + [20100301010101, "ValidWithTimestampsInnocentJointable"]].each_with_index do |pair, i| + assert_equal pair.first, migrations[i].version + assert_equal pair.last, migrations[i].name + end + end - migration_proxy = list.find { |item| - item.name == 'ValidPeopleHaveLastNames' - } - assert migration_proxy, 'should find pending migration' + def test_finds_migrations_in_numbered_directory + migrations = ActiveRecord::Migrator.migrations [MIGRATIONS_ROOT + '/10_urban'] + assert_equal 9, migrations[0].version + assert_equal 'AddExpressions', migrations[0].name + end + + def test_relative_migrations + list = Dir.chdir(MIGRATIONS_ROOT) do + ActiveRecord::Migrator.migrations("valid") end - def test_finds_pending_migrations - ActiveRecord::SchemaMigration.create!(:version => '1') - migration_list = [ Migration.new('foo', 1), Migration.new('bar', 3) ] - migrations = ActiveRecord::Migrator.new(:up, migration_list).pending_migrations + migration_proxy = list.find { |item| + item.name == 'ValidPeopleHaveLastNames' + } + assert migration_proxy, 'should find pending migration' + end - assert_equal 1, migrations.size - assert_equal migration_list.last, migrations.first - end + def test_finds_pending_migrations + ActiveRecord::SchemaMigration.create!(:version => '1') + migration_list = [ActiveRecord::Migration.new('foo', 1), ActiveRecord::Migration.new('bar', 3)] + migrations = ActiveRecord::Migrator.new(:up, migration_list).pending_migrations - def test_migrator_interleaved_migrations - pass_one = [Sensor.new('One', 1)] + assert_equal 1, migrations.size + assert_equal migration_list.last, migrations.first + end - ActiveRecord::Migrator.new(:up, pass_one).migrate - assert pass_one.first.went_up - assert_not pass_one.first.went_down + def test_migrator_interleaved_migrations + pass_one = [Sensor.new('One', 1)] - pass_two = [Sensor.new('One', 1), Sensor.new('Three', 3)] - ActiveRecord::Migrator.new(:up, pass_two).migrate - assert_not pass_two[0].went_up - assert pass_two[1].went_up - assert pass_two.all? { |x| !x.went_down } + ActiveRecord::Migrator.new(:up, pass_one).migrate + assert pass_one.first.went_up + assert_not pass_one.first.went_down - pass_three = [Sensor.new('One', 1), - Sensor.new('Two', 2), - Sensor.new('Three', 3)] + pass_two = [Sensor.new('One', 1), Sensor.new('Three', 3)] + ActiveRecord::Migrator.new(:up, pass_two).migrate + assert_not pass_two[0].went_up + assert pass_two[1].went_up + assert pass_two.all? { |x| !x.went_down } - ActiveRecord::Migrator.new(:down, pass_three).migrate - assert pass_three[0].went_down - assert_not pass_three[1].went_down - assert pass_three[2].went_down - end + pass_three = [Sensor.new('One', 1), + Sensor.new('Two', 2), + Sensor.new('Three', 3)] - def test_up_calls_up - migrations = [Sensor.new(nil, 0), Sensor.new(nil, 1), Sensor.new(nil, 2)] - ActiveRecord::Migrator.new(:up, migrations).migrate - assert migrations.all? { |m| m.went_up } - assert migrations.all? { |m| !m.went_down } - assert_equal 2, ActiveRecord::Migrator.current_version - end + ActiveRecord::Migrator.new(:down, pass_three).migrate + assert pass_three[0].went_down + assert_not pass_three[1].went_down + assert pass_three[2].went_down + end - def test_down_calls_down - test_up_calls_up + def test_up_calls_up + migrations = [Sensor.new(nil, 0), Sensor.new(nil, 1), Sensor.new(nil, 2)] + ActiveRecord::Migrator.new(:up, migrations).migrate + assert migrations.all?(&:went_up) + assert migrations.all? { |m| !m.went_down } + assert_equal 2, ActiveRecord::Migrator.current_version + end - migrations = [Sensor.new(nil, 0), Sensor.new(nil, 1), Sensor.new(nil, 2)] - ActiveRecord::Migrator.new(:down, migrations).migrate - assert migrations.all? { |m| !m.went_up } - assert migrations.all? { |m| m.went_down } - assert_equal 0, ActiveRecord::Migrator.current_version - end + def test_down_calls_down + test_up_calls_up - def test_current_version - ActiveRecord::SchemaMigration.create!(:version => '1000') - assert_equal 1000, ActiveRecord::Migrator.current_version - end + migrations = [Sensor.new(nil, 0), Sensor.new(nil, 1), Sensor.new(nil, 2)] + ActiveRecord::Migrator.new(:down, migrations).migrate + assert migrations.all? { |m| !m.went_up } + assert migrations.all?(&:went_down) + assert_equal 0, ActiveRecord::Migrator.current_version + end - def test_migrator_one_up - calls, migrations = sensors(3) + def test_current_version + ActiveRecord::SchemaMigration.create!(:version => '1000') + assert_equal 1000, ActiveRecord::Migrator.current_version + end - ActiveRecord::Migrator.new(:up, migrations, 1).migrate - assert_equal [[:up, 1]], calls - calls.clear + def test_migrator_one_up + calls, migrations = sensors(3) - ActiveRecord::Migrator.new(:up, migrations, 2).migrate - assert_equal [[:up, 2]], calls - end + ActiveRecord::Migrator.new(:up, migrations, 1).migrate + assert_equal [[:up, 1]], calls + calls.clear - def test_migrator_one_down - calls, migrations = sensors(3) + ActiveRecord::Migrator.new(:up, migrations, 2).migrate + assert_equal [[:up, 2]], calls + end - ActiveRecord::Migrator.new(:up, migrations).migrate - assert_equal [[:up, 1], [:up, 2], [:up, 3]], calls - calls.clear + def test_migrator_one_down + calls, migrations = sensors(3) - ActiveRecord::Migrator.new(:down, migrations, 1).migrate + ActiveRecord::Migrator.new(:up, migrations).migrate + assert_equal [[:up, 1], [:up, 2], [:up, 3]], calls + calls.clear - assert_equal [[:down, 3], [:down, 2]], calls - end + ActiveRecord::Migrator.new(:down, migrations, 1).migrate - def test_migrator_one_up_one_down - calls, migrations = sensors(3) + assert_equal [[:down, 3], [:down, 2]], calls + end - ActiveRecord::Migrator.new(:up, migrations, 1).migrate - assert_equal [[:up, 1]], calls - calls.clear + def test_migrator_one_up_one_down + calls, migrations = sensors(3) - ActiveRecord::Migrator.new(:down, migrations, 0).migrate - assert_equal [[:down, 1]], calls - end + ActiveRecord::Migrator.new(:up, migrations, 1).migrate + assert_equal [[:up, 1]], calls + calls.clear - def test_migrator_double_up - calls, migrations = sensors(3) - assert_equal(0, ActiveRecord::Migrator.current_version) + ActiveRecord::Migrator.new(:down, migrations, 0).migrate + assert_equal [[:down, 1]], calls + end - ActiveRecord::Migrator.new(:up, migrations, 1).migrate - assert_equal [[:up, 1]], calls - calls.clear + def test_migrator_double_up + calls, migrations = sensors(3) + assert_equal(0, ActiveRecord::Migrator.current_version) - ActiveRecord::Migrator.new(:up, migrations, 1).migrate - assert_equal [], calls - end + ActiveRecord::Migrator.new(:up, migrations, 1).migrate + assert_equal [[:up, 1]], calls + calls.clear - def test_migrator_double_down - calls, migrations = sensors(3) + ActiveRecord::Migrator.new(:up, migrations, 1).migrate + assert_equal [], calls + end - assert_equal(0, ActiveRecord::Migrator.current_version) + def test_migrator_double_down + calls, migrations = sensors(3) - ActiveRecord::Migrator.new(:up, migrations, 1).run - assert_equal [[:up, 1]], calls - calls.clear + assert_equal(0, ActiveRecord::Migrator.current_version) - ActiveRecord::Migrator.new(:down, migrations, 1).run - assert_equal [[:down, 1]], calls - calls.clear + ActiveRecord::Migrator.new(:up, migrations, 1).run + assert_equal [[:up, 1]], calls + calls.clear - ActiveRecord::Migrator.new(:down, migrations, 1).run - assert_equal [], calls + ActiveRecord::Migrator.new(:down, migrations, 1).run + assert_equal [[:down, 1]], calls + calls.clear - assert_equal(0, ActiveRecord::Migrator.current_version) - end + ActiveRecord::Migrator.new(:down, migrations, 1).run + assert_equal [], calls - def test_migrator_verbosity - _, migrations = sensors(3) + assert_equal(0, ActiveRecord::Migrator.current_version) + end - ActiveRecord::Migrator.new(:up, migrations, 1).migrate - assert_not_equal 0, ActiveRecord::Migration.message_count + def test_migrator_verbosity + _, migrations = sensors(3) - ActiveRecord::Migration.message_count = 0 + ActiveRecord::Migrator.new(:up, migrations, 1).migrate + assert_not_equal 0, ActiveRecord::Migration.message_count - ActiveRecord::Migrator.new(:down, migrations, 0).migrate - assert_not_equal 0, ActiveRecord::Migration.message_count - ActiveRecord::Migration.message_count = 0 - end + ActiveRecord::Migration.message_count = 0 - def test_migrator_verbosity_off - _, migrations = sensors(3) + ActiveRecord::Migrator.new(:down, migrations, 0).migrate + assert_not_equal 0, ActiveRecord::Migration.message_count + end - ActiveRecord::Migration.message_count = 0 - ActiveRecord::Migration.verbose = false - ActiveRecord::Migrator.new(:up, migrations, 1).migrate - assert_equal 0, ActiveRecord::Migration.message_count - ActiveRecord::Migrator.new(:down, migrations, 0).migrate - assert_equal 0, ActiveRecord::Migration.message_count - end + def test_migrator_verbosity_off + _, migrations = sensors(3) - def test_target_version_zero_should_run_only_once - calls, migrations = sensors(3) + ActiveRecord::Migration.message_count = 0 + ActiveRecord::Migration.verbose = false + ActiveRecord::Migrator.new(:up, migrations, 1).migrate + assert_equal 0, ActiveRecord::Migration.message_count + ActiveRecord::Migrator.new(:down, migrations, 0).migrate + assert_equal 0, ActiveRecord::Migration.message_count + end - # migrate up to 1 - ActiveRecord::Migrator.new(:up, migrations, 1).migrate - assert_equal [[:up, 1]], calls - calls.clear + def test_target_version_zero_should_run_only_once + calls, migrations = sensors(3) - # migrate down to 0 - ActiveRecord::Migrator.new(:down, migrations, 0).migrate - assert_equal [[:down, 1]], calls - calls.clear + # migrate up to 1 + ActiveRecord::Migrator.new(:up, migrations, 1).migrate + assert_equal [[:up, 1]], calls + calls.clear - # migrate down to 0 again - ActiveRecord::Migrator.new(:down, migrations, 0).migrate - assert_equal [], calls - end + # migrate down to 0 + ActiveRecord::Migrator.new(:down, migrations, 0).migrate + assert_equal [[:down, 1]], calls + calls.clear - def test_migrator_going_down_due_to_version_target - calls, migrator = migrator_class(3) + # migrate down to 0 again + ActiveRecord::Migrator.new(:down, migrations, 0).migrate + assert_equal [], calls + end - migrator.up("valid", 1) - assert_equal [[:up, 1]], calls - calls.clear + def test_migrator_going_down_due_to_version_target + calls, migrator = migrator_class(3) - migrator.migrate("valid", 0) - assert_equal [[:down, 1]], calls - calls.clear + migrator.up("valid", 1) + assert_equal [[:up, 1]], calls + calls.clear - migrator.migrate("valid") - assert_equal [[:up, 1], [:up, 2], [:up, 3]], calls - end + migrator.migrate("valid", 0) + assert_equal [[:down, 1]], calls + calls.clear - def test_migrator_rollback - _, migrator = migrator_class(3) + migrator.migrate("valid") + assert_equal [[:up, 1], [:up, 2], [:up, 3]], calls + end - migrator.migrate("valid") - assert_equal(3, ActiveRecord::Migrator.current_version) + def test_migrator_rollback + _, migrator = migrator_class(3) - migrator.rollback("valid") - assert_equal(2, ActiveRecord::Migrator.current_version) + migrator.migrate("valid") + assert_equal(3, ActiveRecord::Migrator.current_version) - migrator.rollback("valid") - assert_equal(1, ActiveRecord::Migrator.current_version) + migrator.rollback("valid") + assert_equal(2, ActiveRecord::Migrator.current_version) - migrator.rollback("valid") - assert_equal(0, ActiveRecord::Migrator.current_version) + migrator.rollback("valid") + assert_equal(1, ActiveRecord::Migrator.current_version) - migrator.rollback("valid") - assert_equal(0, ActiveRecord::Migrator.current_version) - end + migrator.rollback("valid") + assert_equal(0, ActiveRecord::Migrator.current_version) - def test_migrator_db_has_no_schema_migrations_table - _, migrator = migrator_class(3) + migrator.rollback("valid") + assert_equal(0, ActiveRecord::Migrator.current_version) + end - ActiveRecord::Base.connection.execute("DROP TABLE schema_migrations") - assert_not ActiveRecord::Base.connection.table_exists?('schema_migrations') - migrator.migrate("valid", 1) - assert ActiveRecord::Base.connection.table_exists?('schema_migrations') - end + def test_migrator_db_has_no_schema_migrations_table + _, migrator = migrator_class(3) - def test_migrator_forward - _, migrator = migrator_class(3) - migrator.migrate("/valid", 1) - assert_equal(1, ActiveRecord::Migrator.current_version) + ActiveRecord::Base.connection.execute("DROP TABLE schema_migrations") + assert_not ActiveRecord::Base.connection.table_exists?('schema_migrations') + migrator.migrate("valid", 1) + assert ActiveRecord::Base.connection.table_exists?('schema_migrations') + end - migrator.forward("/valid", 2) - assert_equal(3, ActiveRecord::Migrator.current_version) + def test_migrator_forward + _, migrator = migrator_class(3) + migrator.migrate("/valid", 1) + assert_equal(1, ActiveRecord::Migrator.current_version) - migrator.forward("/valid") - assert_equal(3, ActiveRecord::Migrator.current_version) - end + migrator.forward("/valid", 2) + assert_equal(3, ActiveRecord::Migrator.current_version) - def test_only_loads_pending_migrations - # migrate up to 1 - ActiveRecord::SchemaMigration.create!(:version => '1') + migrator.forward("/valid") + assert_equal(3, ActiveRecord::Migrator.current_version) + end - calls, migrator = migrator_class(3) - migrator.migrate("valid", nil) + def test_only_loads_pending_migrations + # migrate up to 1 + ActiveRecord::SchemaMigration.create!(:version => '1') - assert_equal [[:up, 2], [:up, 3]], calls - end + calls, migrator = migrator_class(3) + migrator.migrate("valid", nil) - def test_get_all_versions - _, migrator = migrator_class(3) + assert_equal [[:up, 2], [:up, 3]], calls + end - migrator.migrate("valid") - assert_equal([1,2,3], ActiveRecord::Migrator.get_all_versions) + def test_get_all_versions + _, migrator = migrator_class(3) - migrator.rollback("valid") - assert_equal([1,2], ActiveRecord::Migrator.get_all_versions) + migrator.migrate("valid") + assert_equal([1,2,3], ActiveRecord::Migrator.get_all_versions) - migrator.rollback("valid") - assert_equal([1], ActiveRecord::Migrator.get_all_versions) + migrator.rollback("valid") + assert_equal([1,2], ActiveRecord::Migrator.get_all_versions) - migrator.rollback("valid") - assert_equal([], ActiveRecord::Migrator.get_all_versions) - end + migrator.rollback("valid") + assert_equal([1], ActiveRecord::Migrator.get_all_versions) - private - def m(name, version, &block) - x = Sensor.new name, version - x.extend(Module.new { - define_method(:up) { block.call(:up, x); super() } - define_method(:down) { block.call(:down, x); super() } - }) if block_given? - end + migrator.rollback("valid") + assert_equal([], ActiveRecord::Migrator.get_all_versions) + end + + private + def m(name, version) + x = Sensor.new name, version + x.extend(Module.new { + define_method(:up) { yield(:up, x); super() } + define_method(:down) { yield(:down, x); super() } + }) if block_given? + end - def sensors(count) - calls = [] - migrations = count.times.map { |i| - m(nil, i + 1) { |c,migration| - calls << [c, migration.version] - } + def sensors(count) + calls = [] + migrations = count.times.map { |i| + m(nil, i + 1) { |c,migration| + calls << [c, migration.version] } - [calls, migrations] - end + } + [calls, migrations] + end - def migrator_class(count) - calls, migrations = sensors(count) + def migrator_class(count) + calls, migrations = sensors(count) - migrator = Class.new(Migrator).extend(Module.new { - define_method(:migrations) { |paths| - migrations - } - }) - [calls, migrator] - end + migrator = Class.new(ActiveRecord::Migrator).extend(Module.new { + define_method(:migrations) { |paths| + migrations + } + }) + [calls, migrator] end end diff --git a/activerecord/test/cases/modules_test.rb b/activerecord/test/cases/modules_test.rb index e87773df94..6f65bf80eb 100644 --- a/activerecord/test/cases/modules_test.rb +++ b/activerecord/test/cases/modules_test.rb @@ -2,6 +2,7 @@ require "cases/helper" require 'models/company_in_module' require 'models/shop' require 'models/developer' +require 'models/computer' class ModulesTest < ActiveRecord::TestCase fixtures :accounts, :companies, :projects, :developers, :collections, :products, :variants diff --git a/activerecord/test/cases/nested_attributes_test.rb b/activerecord/test/cases/nested_attributes_test.rb index cf96c3fccf..5c7e8a65d2 100644 --- a/activerecord/test/cases/nested_attributes_test.rb +++ b/activerecord/test/cases/nested_attributes_test.rb @@ -13,7 +13,7 @@ require 'active_support/hash_with_indifferent_access' class TestNestedAttributesInGeneral < ActiveRecord::TestCase teardown do - Pirate.accepts_nested_attributes_for :ship, :allow_destroy => true, :reject_if => proc { |attributes| attributes.empty? } + Pirate.accepts_nested_attributes_for :ship, :allow_destroy => true, :reject_if => proc(&:empty?) end def test_base_should_have_an_empty_nested_attributes_options @@ -300,13 +300,13 @@ class TestNestedAttributesOnAHasOneAssociation < ActiveRecord::TestCase end def test_should_not_destroy_an_existing_record_if_allow_destroy_is_false - Pirate.accepts_nested_attributes_for :ship, :allow_destroy => false, :reject_if => proc { |attributes| attributes.empty? } + Pirate.accepts_nested_attributes_for :ship, :allow_destroy => false, :reject_if => proc(&:empty?) @pirate.update(ship_attributes: { id: @pirate.ship.id, _destroy: '1' }) assert_equal @ship, @pirate.reload.ship - Pirate.accepts_nested_attributes_for :ship, :allow_destroy => true, :reject_if => proc { |attributes| attributes.empty? } + Pirate.accepts_nested_attributes_for :ship, :allow_destroy => true, :reject_if => proc(&:empty?) end def test_should_also_work_with_a_HashWithIndifferentAccess @@ -494,12 +494,12 @@ class TestNestedAttributesOnABelongsToAssociation < ActiveRecord::TestCase end def test_should_not_destroy_an_existing_record_if_allow_destroy_is_false - Ship.accepts_nested_attributes_for :pirate, :allow_destroy => false, :reject_if => proc { |attributes| attributes.empty? } + 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 } ensure - Ship.accepts_nested_attributes_for :pirate, :allow_destroy => true, :reject_if => proc { |attributes| attributes.empty? } + Ship.accepts_nested_attributes_for :pirate, :allow_destroy => true, :reject_if => proc(&:empty?) end def test_should_work_with_update_as_well @@ -855,7 +855,7 @@ end module NestedAttributesLimitTests def teardown - Pirate.accepts_nested_attributes_for :parrots, :allow_destroy => true, :reject_if => proc { |attributes| attributes.empty? } + Pirate.accepts_nested_attributes_for :parrots, :allow_destroy => true, :reject_if => proc(&:empty?) end def test_limit_with_less_records diff --git a/activerecord/test/cases/persistence_test.rb b/activerecord/test/cases/persistence_test.rb index 28341d0b42..d6816041bc 100644 --- a/activerecord/test/cases/persistence_test.rb +++ b/activerecord/test/cases/persistence_test.rb @@ -8,6 +8,7 @@ require 'models/reply' require 'models/category' require 'models/company' require 'models/developer' +require 'models/computer' require 'models/project' require 'models/minimalistic' require 'models/warehouse_thing' @@ -20,7 +21,7 @@ require 'models/toy' require 'rexml/document' class PersistenceTest < ActiveRecord::TestCase - fixtures :topics, :companies, :developers, :projects, :computers, :accounts, :minimalistics, 'warehouse-things', :authors, :categorizations, :categories, :posts, :minivans, :pets, :toys + fixtures :topics, :companies, :developers, :projects, :computers, :accounts, :minimalistics, 'warehouse-things', :authors, :author_addresses, :categorizations, :categories, :posts, :minivans, :pets, :toys # Oracle UPDATE does not support ORDER BY unless current_adapter?(:OracleAdapter) @@ -126,7 +127,7 @@ class PersistenceTest < ActiveRecord::TestCase assert_difference('Topic.count', -topics_by_mary.size) do destroyed = Topic.destroy_all(conditions).sort_by(&:id) assert_equal topics_by_mary, destroyed - assert destroyed.all? { |topic| topic.frozen? }, "destroyed topics should be frozen" + assert destroyed.all?(&:frozen?), "destroyed topics should be frozen" end end @@ -136,7 +137,7 @@ class PersistenceTest < ActiveRecord::TestCase assert_difference('Client.count', -2) do destroyed = Client.destroy([2, 3]).sort_by(&:id) assert_equal clients, destroyed - assert destroyed.all? { |client| client.frozen? }, "destroyed clients should be frozen" + assert destroyed.all?(&:frozen?), "destroyed clients should be frozen" end end @@ -251,8 +252,10 @@ class PersistenceTest < ActiveRecord::TestCase def test_create_columns_not_equal_attributes topic = Topic.instantiate( - 'title' => 'Another New Topic', - 'does_not_exist' => 'test' + 'attributes' => { + 'title' => 'Another New Topic', + 'does_not_exist' => 'test' + } ) assert_nothing_raised { topic.save } end @@ -858,4 +861,54 @@ class PersistenceTest < ActiveRecord::TestCase post.body end end + + def test_reload_removes_custom_selects + post = Post.select('posts.*, 1 as wibble').last! + + assert_equal 1, post[:wibble] + assert_nil post.reload[:wibble] + end + + def test_find_via_reload + post = Post.new + + assert post.new_record? + + post.id = 1 + post.reload + + assert_equal "Welcome to the weblog", post.title + assert_not post.new_record? + end + + class SaveTest < ActiveRecord::TestCase + self.use_transactional_fixtures = false + + def test_save_touch_false + widget = Class.new(ActiveRecord::Base) do + connection.create_table :widgets, force: true do |t| + t.string :name + t.timestamps null: false + end + + self.table_name = :widgets + end + + instance = widget.create!({ + name: 'Bob', + created_at: 1.day.ago, + updated_at: 1.day.ago + }) + + created_at = instance.created_at + updated_at = instance.updated_at + + instance.name = 'Barb' + instance.save!(touch: false) + assert_equal instance.created_at, created_at + assert_equal instance.updated_at, updated_at + ensure + ActiveRecord::Base.connection.drop_table :widgets + end + end end diff --git a/activerecord/test/cases/pooled_connections_test.rb b/activerecord/test/cases/pooled_connections_test.rb index 8eea10143f..287a3f33ea 100644 --- a/activerecord/test/cases/pooled_connections_test.rb +++ b/activerecord/test/cases/pooled_connections_test.rb @@ -13,7 +13,7 @@ class PooledConnectionsTest < ActiveRecord::TestCase teardown do ActiveRecord::Base.clear_all_connections! ActiveRecord::Base.establish_connection(@connection) - @per_test_teardown.each {|td| td.call } + @per_test_teardown.each(&:call) end # Will deadlock due to lack of Monitor timeouts in 1.9 @@ -35,6 +35,22 @@ class PooledConnectionsTest < ActiveRecord::TestCase end end + def checkout_checkin_connections_loop(pool_size, loops) + ActiveRecord::Base.establish_connection(@connection.merge({:pool => pool_size, :checkout_timeout => 0.5})) + @connection_count = 0 + @timed_out = 0 + loops.times do + begin + conn = ActiveRecord::Base.connection_pool.checkout + ActiveRecord::Base.connection_pool.checkin conn + @connection_count += 1 + ActiveRecord::Base.connection.tables + rescue ActiveRecord::ConnectionTimeoutError + @timed_out += 1 + end + end + end + def test_pooled_connection_checkin_one checkout_checkin_connections 1, 2 assert_equal 2, @connection_count @@ -42,6 +58,20 @@ class PooledConnectionsTest < ActiveRecord::TestCase assert_equal 1, ActiveRecord::Base.connection_pool.connections.size end + def test_pooled_connection_checkin_two + checkout_checkin_connections_loop 2, 3 + assert_equal 3, @connection_count + assert_equal 0, @timed_out + assert_equal 2, ActiveRecord::Base.connection_pool.connections.size + end + + def test_pooled_connection_remove + ActiveRecord::Base.establish_connection(@connection.merge({:pool => 2, :checkout_timeout => 0.5})) + old_connection = ActiveRecord::Base.connection + extra_connection = ActiveRecord::Base.connection_pool.checkout + ActiveRecord::Base.connection_pool.remove(extra_connection) + assert_equal ActiveRecord::Base.connection, old_connection + end private diff --git a/activerecord/test/cases/primary_keys_test.rb b/activerecord/test/cases/primary_keys_test.rb index f43483d291..2dfc53f132 100644 --- a/activerecord/test/cases/primary_keys_test.rb +++ b/activerecord/test/cases/primary_keys_test.rb @@ -5,6 +5,7 @@ require 'models/subscriber' require 'models/movie' require 'models/keyboard' require 'models/mixed_case_monkey' +require 'models/dashboard' class PrimaryKeysTest < ActiveRecord::TestCase fixtures :topics, :subscribers, :movies, :mixed_case_monkeys @@ -134,14 +135,22 @@ class PrimaryKeysTest < ActiveRecord::TestCase 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? - assert_equal 'id', ActiveRecord::Base.connection.primary_key('developers') + 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 + if ActiveRecord::Base.connection.supports_primary_key? - assert_nil ActiveRecord::Base.connection.primary_key('developers_projects') + assert_nil klass.primary_key end end @@ -156,6 +165,15 @@ class PrimaryKeysTest < ActiveRecord::TestCase MixedCaseMonkey.reset_primary_key assert_equal "monkeyID", MixedCaseMonkey.primary_key end + + def test_primary_key_update_with_custom_key_name + dashboard = Dashboard.create!(dashboard_id: '1') + dashboard.id = '2' + dashboard.save! + + dashboard = Dashboard.first + assert_equal '2', dashboard.id + end end class PrimaryKeyWithNoConnectionTest < ActiveRecord::TestCase @@ -177,6 +195,30 @@ class PrimaryKeyWithNoConnectionTest < ActiveRecord::TestCase end end +class PrimaryKeyAnyTypeTest < ActiveRecord::TestCase + self.use_transactional_fixtures = false + + class Barcode < ActiveRecord::Base + end + + setup do + @connection = ActiveRecord::Base.connection + @connection.create_table(:barcodes, primary_key: "code", id: :string, limit: 42, force: true) + end + + teardown do + @connection.execute("DROP TABLE IF EXISTS barcodes") + end + + def test_any_type_primary_key + assert_equal "code", Barcode.primary_key + + column_type = Barcode.type_for_attribute(Barcode.primary_key) + assert_equal :string, column_type.type + assert_equal 42, column_type.limit + end +end + if current_adapter?(:MysqlAdapter, :Mysql2Adapter) class PrimaryKeyWithAnsiQuotesTest < ActiveRecord::TestCase self.use_transactional_fixtures = false @@ -191,7 +233,7 @@ if current_adapter?(:MysqlAdapter, :Mysql2Adapter) end end -if current_adapter?(:PostgreSQLAdapter) +if current_adapter?(:PostgreSQLAdapter, :MysqlAdapter, :Mysql2Adapter) class PrimaryKeyBigSerialTest < ActiveRecord::TestCase self.use_transactional_fixtures = false @@ -200,17 +242,24 @@ if current_adapter?(:PostgreSQLAdapter) setup do @connection = ActiveRecord::Base.connection - @connection.create_table(:widgets, id: :bigserial) { |t| } + if current_adapter?(:PostgreSQLAdapter) + @connection.create_table(:widgets, id: :bigserial, force: true) + else + @connection.create_table(:widgets, id: :bigint, force: true) + end end teardown do - @connection.drop_table :widgets + @connection.execute("DROP TABLE IF EXISTS widgets") end - def test_bigserial_primary_key - assert_equal "id", Widget.primary_key - assert_equal :integer, Widget.columns_hash[Widget.primary_key].type + test "primary key column type with bigserial" do + column_type = Widget.type_for_attribute(Widget.primary_key) + assert_equal :integer, column_type.type + assert_equal 8, column_type.limit + end + test "primary key with bigserial are automatically numbered" do widget = Widget.create! assert_not_nil widget.id end diff --git a/activerecord/test/cases/quoting_test.rb b/activerecord/test/cases/quoting_test.rb index bbd5298da1..1d6ae2f67f 100644 --- a/activerecord/test/cases/quoting_test.rb +++ b/activerecord/test/cases/quoting_test.rb @@ -83,69 +83,56 @@ module ActiveRecord def test_quote_with_quoted_id assert_equal 1, @quoter.quote(Struct.new(:quoted_id).new(1), nil) - assert_equal 1, @quoter.quote(Struct.new(:quoted_id).new(1), 'foo') end def test_quote_nil assert_equal 'NULL', @quoter.quote(nil, nil) - assert_equal 'NULL', @quoter.quote(nil, 'foo') end def test_quote_true assert_equal @quoter.quoted_true, @quoter.quote(true, nil) - assert_equal '1', @quoter.quote(true, Type::Integer.new) end def test_quote_false assert_equal @quoter.quoted_false, @quoter.quote(false, nil) - assert_equal '0', @quoter.quote(false, Type::Integer.new) end def test_quote_float float = 1.2 assert_equal float.to_s, @quoter.quote(float, nil) - assert_equal float.to_s, @quoter.quote(float, Object.new) end def test_quote_fixnum fixnum = 1 assert_equal fixnum.to_s, @quoter.quote(fixnum, nil) - assert_equal fixnum.to_s, @quoter.quote(fixnum, Object.new) end def test_quote_bignum bignum = 1 << 100 assert_equal bignum.to_s, @quoter.quote(bignum, nil) - assert_equal bignum.to_s, @quoter.quote(bignum, Object.new) end def test_quote_bigdecimal bigdec = BigDecimal.new((1 << 100).to_s) assert_equal bigdec.to_s('F'), @quoter.quote(bigdec, nil) - assert_equal bigdec.to_s('F'), @quoter.quote(bigdec, Object.new) end def test_dates_and_times @quoter.extend(Module.new { def quoted_date(value) 'lol' end }) assert_equal "'lol'", @quoter.quote(Date.today, nil) - assert_equal "'lol'", @quoter.quote(Date.today, Object.new) assert_equal "'lol'", @quoter.quote(Time.now, nil) - assert_equal "'lol'", @quoter.quote(Time.now, Object.new) assert_equal "'lol'", @quoter.quote(DateTime.now, nil) - assert_equal "'lol'", @quoter.quote(DateTime.now, Object.new) end def test_crazy_object crazy = Class.new.new expected = "'#{YAML.dump(crazy)}'" assert_equal expected, @quoter.quote(crazy, nil) - assert_equal expected, @quoter.quote(crazy, Object.new) end def test_crazy_object_calls_quote_string crazy = Class.new { def initialize; @lol = 'lo\l' end }.new assert_match "lo\\\\l", @quoter.quote(crazy, nil) - assert_match "lo\\\\l", @quoter.quote(crazy, Object.new) end def test_quote_string_no_column @@ -157,25 +144,6 @@ module ActiveRecord assert_equal "'lo\\\\l'", @quoter.quote(string, nil) end - def test_quote_string_int_column - assert_equal "1", @quoter.quote('1', Type::Integer.new) - assert_equal "1", @quoter.quote('1.2', Type::Integer.new) - end - - def test_quote_string_float_column - assert_equal "1.0", @quoter.quote('1', Type::Float.new) - assert_equal "1.2", @quoter.quote('1.2', Type::Float.new) - end - - def test_quote_as_mb_chars_binary_column - string = ActiveSupport::Multibyte::Chars.new('lo\l') - assert_equal "'lo\\\\l'", @quoter.quote(string, Type::Binary.new) - end - - def test_quote_binary_without_string_to_binary - assert_equal "'lo\\\\l'", @quoter.quote('lo\l', Type::Binary.new) - end - def test_string_with_crazy_column assert_equal "'lo\\\\l'", @quoter.quote('lo\l') end @@ -183,10 +151,6 @@ module ActiveRecord def test_quote_duration assert_equal "1800", @quoter.quote(30.minutes) end - - def test_quote_duration_int_column - assert_equal "7200", @quoter.quote(2.hours, Type::Integer.new) - end end end end diff --git a/activerecord/test/cases/readonly_test.rb b/activerecord/test/cases/readonly_test.rb index 2afd25c989..1c919f0b57 100644 --- a/activerecord/test/cases/readonly_test.rb +++ b/activerecord/test/cases/readonly_test.rb @@ -3,6 +3,7 @@ require 'models/author' require 'models/post' require 'models/comment' require 'models/developer' +require 'models/computer' require 'models/project' require 'models/reader' require 'models/person' @@ -22,9 +23,15 @@ class ReadOnlyTest < ActiveRecord::TestCase assert !dev.save dev.name = 'Forbidden.' end - assert_raise(ActiveRecord::ReadOnlyRecord) { dev.save } - assert_raise(ActiveRecord::ReadOnlyRecord) { dev.save! } - assert_raise(ActiveRecord::ReadOnlyRecord) { dev.destroy } + + e = assert_raise(ActiveRecord::ReadOnlyRecord) { dev.save } + assert_equal "Developer is marked as readonly", e.message + + e = assert_raise(ActiveRecord::ReadOnlyRecord) { dev.save! } + assert_equal "Developer is marked as readonly", e.message + + e = assert_raise(ActiveRecord::ReadOnlyRecord) { dev.destroy } + assert_equal "Developer is marked as readonly", e.message end diff --git a/activerecord/test/cases/reaper_test.rb b/activerecord/test/cases/reaper_test.rb index f52fd22489..cccfc6774e 100644 --- a/activerecord/test/cases/reaper_test.rb +++ b/activerecord/test/cases/reaper_test.rb @@ -60,7 +60,7 @@ module ActiveRecord def test_connection_pool_starts_reaper spec = ActiveRecord::Base.connection_pool.spec.dup - spec.config[:reaping_frequency] = 0.0001 + spec.config[:reaping_frequency] = '0.0001' pool = ConnectionPool.new spec diff --git a/activerecord/test/cases/reflection_test.rb b/activerecord/test/cases/reflection_test.rb index c6e73f78cc..e86b892a0a 100644 --- a/activerecord/test/cases/reflection_test.rb +++ b/activerecord/test/cases/reflection_test.rb @@ -50,13 +50,13 @@ class ReflectionTest < ActiveRecord::TestCase end def test_columns_are_returned_in_the_order_they_were_declared - column_names = Topic.columns.map { |column| column.name } + column_names = Topic.columns.map(&:name) assert_equal %w(id title author_name author_email_address written_on bonus_time last_read content important approved replies_count unique_replies_count parent_id parent_title type group created_at updated_at), column_names end def test_content_columns content_columns = Topic.content_columns - content_column_names = content_columns.map {|column| column.name} + content_column_names = content_columns.map(&:name) assert_equal 13, content_columns.length assert_equal %w(title author_name author_email_address written_on bonus_time last_read content important group approved parent_title created_at updated_at).sort, content_column_names.sort end @@ -80,28 +80,14 @@ class ReflectionTest < ActiveRecord::TestCase assert_equal :integer, @first.column_for_attribute("id").type end - def test_non_existent_columns_return_null_object - column = @first.column_for_attribute("attribute_that_doesnt_exist") - assert_instance_of ActiveRecord::ConnectionAdapters::NullColumn, column - assert_equal "attribute_that_doesnt_exist", column.name - assert_equal nil, column.sql_type - assert_equal nil, column.type - assert_not column.number? - assert_not column.text? - assert_not column.binary? - end - - def test_non_existent_columns_are_identity_types - column = @first.column_for_attribute("attribute_that_doesnt_exist") - object = Object.new - - assert_equal object, column.type_cast_from_database(object) - assert_equal object, column.type_cast_from_user(object) - assert_equal object, column.type_cast_for_database(object) + def test_non_existent_columns_return_nil + assert_deprecated do + assert_nil @first.column_for_attribute("attribute_that_doesnt_exist") + end end def test_reflection_klass_for_nested_class_name - reflection = MacroReflection.new(:company, nil, nil, { :class_name => 'MyApplication::Business::Company' }, ActiveRecord::Base) + reflection = ActiveRecord::Reflection.create(:has_many, nil, nil, { :class_name => 'MyApplication::Business::Company' }, ActiveRecord::Base) assert_nothing_raised do assert_equal MyApplication::Business::Company, reflection.klass end @@ -111,21 +97,21 @@ class ReflectionTest < ActiveRecord::TestCase ActiveSupport::Inflector.inflections do |inflect| inflect.irregular 'plural_irregular', 'plurales_irregulares' end - reflection = AssociationReflection.new(:has_many, 'plurales_irregulares', nil, {}, ActiveRecord::Base) + reflection = ActiveRecord::Reflection.create(:has_many, 'plurales_irregulares', nil, {}, ActiveRecord::Base) assert_equal 'PluralIrregular', reflection.class_name end def test_aggregation_reflection reflection_for_address = AggregateReflection.new( - :composed_of, :address, nil, { :mapping => [ %w(address_street street), %w(address_city city), %w(address_country country) ] }, Customer + :address, nil, { :mapping => [ %w(address_street street), %w(address_city city), %w(address_country country) ] }, Customer ) reflection_for_balance = AggregateReflection.new( - :composed_of, :balance, nil, { :class_name => "Money", :mapping => %w(balance amount) }, Customer + :balance, nil, { :class_name => "Money", :mapping => %w(balance amount) }, Customer ) reflection_for_gps_location = AggregateReflection.new( - :composed_of, :gps_location, nil, { }, Customer + :gps_location, nil, { }, Customer ) assert Customer.reflect_on_all_aggregations.include?(reflection_for_gps_location) @@ -149,7 +135,7 @@ class ReflectionTest < ActiveRecord::TestCase end def test_has_many_reflection - reflection_for_clients = AssociationReflection.new(:has_many, :clients, nil, { :order => "id", :dependent => :destroy }, Firm) + reflection_for_clients = ActiveRecord::Reflection.create(:has_many, :clients, nil, { :order => "id", :dependent => :destroy }, Firm) assert_equal reflection_for_clients, Firm.reflect_on_association(:clients) @@ -161,7 +147,7 @@ class ReflectionTest < ActiveRecord::TestCase end def test_has_one_reflection - reflection_for_account = AssociationReflection.new(:has_one, :account, nil, { :foreign_key => "firm_id", :dependent => :destroy }, Firm) + reflection_for_account = ActiveRecord::Reflection.create(:has_one, :account, nil, { :foreign_key => "firm_id", :dependent => :destroy }, Firm) assert_equal reflection_for_account, Firm.reflect_on_association(:account) assert_equal Account, Firm.reflect_on_association(:account).klass @@ -223,6 +209,10 @@ class ReflectionTest < ActiveRecord::TestCase assert_not_equal Object.new, Firm._reflections['clients'] end + def test_reflections_should_return_keys_as_strings + assert Category.reflections.keys.all? { |key| key.is_a? String }, "Model.reflections is expected to return string for keys" + end + def test_has_and_belongs_to_many_reflection assert_equal :has_and_belongs_to_many, Category.reflections['posts'].macro assert_equal :posts, Category.reflect_on_all_associations(:has_and_belongs_to_many).first.name @@ -298,12 +288,12 @@ class ReflectionTest < ActiveRecord::TestCase end def test_association_primary_key_raises_when_missing_primary_key - reflection = ActiveRecord::Reflection::AssociationReflection.new(:fuu, :edge, nil, {}, Author) + reflection = ActiveRecord::Reflection.create(:has_many, :edge, nil, {}, Author) assert_raises(ActiveRecord::UnknownPrimaryKey) { reflection.association_primary_key } through = Class.new(ActiveRecord::Reflection::ThroughReflection) { define_method(:source_reflection) { reflection } - }.new(:fuu, :edge, nil, {}, Author) + }.new(reflection) assert_raises(ActiveRecord::UnknownPrimaryKey) { through.association_primary_key } end @@ -313,7 +303,7 @@ class ReflectionTest < ActiveRecord::TestCase end def test_active_record_primary_key_raises_when_missing_primary_key - reflection = ActiveRecord::Reflection::AssociationReflection.new(:fuu, :author, nil, {}, Edge) + reflection = ActiveRecord::Reflection.create(:has_many, :author, nil, {}, Edge) assert_raises(ActiveRecord::UnknownPrimaryKey) { reflection.active_record_primary_key } end @@ -331,32 +321,28 @@ class ReflectionTest < ActiveRecord::TestCase end def test_default_association_validation - assert AssociationReflection.new(:has_many, :clients, nil, {}, Firm).validate? + assert ActiveRecord::Reflection.create(:has_many, :clients, nil, {}, Firm).validate? - assert !AssociationReflection.new(:has_one, :client, nil, {}, Firm).validate? - assert !AssociationReflection.new(:belongs_to, :client, nil, {}, Firm).validate? - assert !AssociationReflection.new(:has_and_belongs_to_many, :clients, nil, {}, Firm).validate? + assert !ActiveRecord::Reflection.create(:has_one, :client, nil, {}, Firm).validate? + assert !ActiveRecord::Reflection.create(:belongs_to, :client, nil, {}, Firm).validate? end def test_always_validate_association_if_explicit - assert AssociationReflection.new(:has_one, :client, nil, { :validate => true }, Firm).validate? - assert AssociationReflection.new(:belongs_to, :client, nil, { :validate => true }, Firm).validate? - assert AssociationReflection.new(:has_many, :clients, nil, { :validate => true }, Firm).validate? - assert AssociationReflection.new(:has_and_belongs_to_many, :clients, nil, { :validate => true }, Firm).validate? + assert ActiveRecord::Reflection.create(:has_one, :client, nil, { :validate => true }, Firm).validate? + assert ActiveRecord::Reflection.create(:belongs_to, :client, nil, { :validate => true }, Firm).validate? + assert ActiveRecord::Reflection.create(:has_many, :clients, nil, { :validate => true }, Firm).validate? end def test_validate_association_if_autosave - assert AssociationReflection.new(:has_one, :client, nil, { :autosave => true }, Firm).validate? - assert AssociationReflection.new(:belongs_to, :client, nil, { :autosave => true }, Firm).validate? - assert AssociationReflection.new(:has_many, :clients, nil, { :autosave => true }, Firm).validate? - assert AssociationReflection.new(:has_and_belongs_to_many, :clients, nil, { :autosave => true }, Firm).validate? + assert ActiveRecord::Reflection.create(:has_one, :client, nil, { :autosave => true }, Firm).validate? + assert ActiveRecord::Reflection.create(:belongs_to, :client, nil, { :autosave => true }, Firm).validate? + assert ActiveRecord::Reflection.create(:has_many, :clients, nil, { :autosave => true }, Firm).validate? end def test_never_validate_association_if_explicit - assert !AssociationReflection.new(:has_one, :client, nil, { :autosave => true, :validate => false }, Firm).validate? - assert !AssociationReflection.new(:belongs_to, :client, nil, { :autosave => true, :validate => false }, Firm).validate? - assert !AssociationReflection.new(:has_many, :clients, nil, { :autosave => true, :validate => false }, Firm).validate? - assert !AssociationReflection.new(:has_and_belongs_to_many, :clients, nil, { :autosave => true, :validate => false }, Firm).validate? + assert !ActiveRecord::Reflection.create(:has_one, :client, nil, { :autosave => true, :validate => false }, Firm).validate? + assert !ActiveRecord::Reflection.create(:belongs_to, :client, nil, { :autosave => true, :validate => false }, Firm).validate? + assert !ActiveRecord::Reflection.create(:has_many, :clients, nil, { :autosave => true, :validate => false }, Firm).validate? end def test_foreign_key @@ -378,11 +364,11 @@ class ReflectionTest < ActiveRecord::TestCase category = Struct.new(:table_name, :pluralize_table_names).new('categories', true) product = Struct.new(:table_name, :pluralize_table_names).new('products', true) - reflection = AssociationReflection.new(:has_and_belongs_to_many, :categories, nil, {}, product) + reflection = ActiveRecord::Reflection.create(:has_many, :categories, nil, {}, product) reflection.stubs(:klass).returns(category) assert_equal 'categories_products', reflection.join_table - reflection = AssociationReflection.new(:has_and_belongs_to_many, :products, nil, {}, category) + reflection = ActiveRecord::Reflection.create(:has_many, :products, nil, {}, category) reflection.stubs(:klass).returns(product) assert_equal 'categories_products', reflection.join_table end @@ -391,11 +377,11 @@ class ReflectionTest < ActiveRecord::TestCase category = Struct.new(:table_name, :pluralize_table_names).new('catalog_categories', true) product = Struct.new(:table_name, :pluralize_table_names).new('catalog_products', true) - reflection = AssociationReflection.new(:has_and_belongs_to_many, :categories, nil, {}, product) + reflection = ActiveRecord::Reflection.create(:has_many, :categories, nil, {}, product) reflection.stubs(:klass).returns(category) assert_equal 'catalog_categories_products', reflection.join_table - reflection = AssociationReflection.new(:has_and_belongs_to_many, :products, nil, {}, category) + reflection = ActiveRecord::Reflection.create(:has_many, :products, nil, {}, category) reflection.stubs(:klass).returns(product) assert_equal 'catalog_categories_products', reflection.join_table end @@ -404,11 +390,11 @@ class ReflectionTest < ActiveRecord::TestCase category = Struct.new(:table_name, :pluralize_table_names).new('catalog_categories', true) page = Struct.new(:table_name, :pluralize_table_names).new('content_pages', true) - reflection = AssociationReflection.new(:has_and_belongs_to_many, :categories, nil, {}, page) + reflection = ActiveRecord::Reflection.create(:has_many, :categories, nil, {}, page) reflection.stubs(:klass).returns(category) assert_equal 'catalog_categories_content_pages', reflection.join_table - reflection = AssociationReflection.new(:has_and_belongs_to_many, :pages, nil, {}, category) + reflection = ActiveRecord::Reflection.create(:has_many, :pages, nil, {}, category) reflection.stubs(:klass).returns(page) assert_equal 'catalog_categories_content_pages', reflection.join_table end @@ -417,15 +403,47 @@ class ReflectionTest < ActiveRecord::TestCase category = Struct.new(:table_name, :pluralize_table_names).new('categories', true) product = Struct.new(:table_name, :pluralize_table_names).new('products', true) - reflection = AssociationReflection.new(:has_and_belongs_to_many, :categories, nil, { :join_table => 'product_categories' }, product) + reflection = ActiveRecord::Reflection.create(:has_many, :categories, nil, { :join_table => 'product_categories' }, product) reflection.stubs(:klass).returns(category) assert_equal 'product_categories', reflection.join_table - reflection = AssociationReflection.new(:has_and_belongs_to_many, :products, nil, { :join_table => 'product_categories' }, category) + reflection = ActiveRecord::Reflection.create(:has_many, :products, nil, { :join_table => 'product_categories' }, category) reflection.stubs(:klass).returns(product) assert_equal 'product_categories', reflection.join_table end + def test_includes_accepts_symbols + hotel = Hotel.create! + department = hotel.departments.create! + department.chefs.create! + + assert_nothing_raised do + assert_equal department.chefs, Hotel.includes([departments: :chefs]).first.chefs + end + end + + def test_includes_accepts_strings + hotel = Hotel.create! + department = hotel.departments.create! + department.chefs.create! + + assert_nothing_raised do + assert_equal department.chefs, Hotel.includes(['departments' => 'chefs']).first.chefs + end + end + + def test_reflect_on_association_accepts_symbols + assert_nothing_raised do + assert_equal Hotel.reflect_on_association(:departments).name, :departments + end + end + + def test_reflect_on_association_accepts_strings + assert_nothing_raised do + assert_equal Hotel.reflect_on_association("departments").name, :departments + end + end + private def assert_reflection(klass, association, options) assert reflection = klass.reflect_on_association(association) diff --git a/activerecord/test/cases/relation/merging_test.rb b/activerecord/test/cases/relation/merging_test.rb index 2b5c2fd5a4..eb76ef6328 100644 --- a/activerecord/test/cases/relation/merging_test.rb +++ b/activerecord/test/cases/relation/merging_test.rb @@ -2,8 +2,10 @@ require 'cases/helper' require 'models/author' require 'models/comment' require 'models/developer' +require 'models/computer' require 'models/post' require 'models/project' +require 'models/rating' class RelationMergingTest < ActiveRecord::TestCase fixtures :developers, :comments, :authors, :posts @@ -116,7 +118,7 @@ class RelationMergingTest < ActiveRecord::TestCase end class MergingDifferentRelationsTest < ActiveRecord::TestCase - fixtures :posts, :authors + fixtures :posts, :authors, :developers test "merging where relations" do hello_by_bob = Post.where(body: "hello").joins(:author). @@ -144,4 +146,16 @@ class MergingDifferentRelationsTest < ActiveRecord::TestCase assert_equal ["Mary", "Mary", "Mary", "David"], posts_by_author_name end + + test "relation merging (using a proc argument)" do + dev = Developer.where(name: "Jamis").first + + comment_1 = dev.comments.create!(body: "I'm Jamis", post: Post.first) + rating_1 = comment_1.ratings.create! + + comment_2 = dev.comments.create!(body: "I'm John", post: Post.first) + comment_2.ratings.create! + + assert_equal dev.ratings, [rating_1] + end end diff --git a/activerecord/test/cases/relation/mutation_test.rb b/activerecord/test/cases/relation/mutation_test.rb index 1da5c36e1c..2443f10269 100644 --- a/activerecord/test/cases/relation/mutation_test.rb +++ b/activerecord/test/cases/relation/mutation_test.rb @@ -18,10 +18,14 @@ module ActiveRecord def attribute_alias?(name) false end + + def sanitize_sql(sql) + sql + end end def relation - @relation ||= Relation.new FakeKlass.new('posts'), Post.arel_table + @relation ||= Relation.new FakeKlass.new('posts'), Post.arel_table, Post.predicate_builder end (Relation::MULTI_VALUE_METHODS - [:references, :extending, :order, :unscope, :select]).each do |method| @@ -95,7 +99,7 @@ module ActiveRecord end test '#reorder!' do - relation = self.relation.order('foo') + @relation = self.relation.order('foo') assert relation.reorder!('bar').equal?(relation) assert_equal ['bar'], relation.order_values @@ -112,7 +116,7 @@ module ActiveRecord end test 'reverse_order!' do - relation = Post.order('title ASC, comments_count DESC') + @relation = Post.order('title ASC, comments_count DESC') relation.reverse_order! diff --git a/activerecord/test/cases/relation/predicate_builder_test.rb b/activerecord/test/cases/relation/predicate_builder_test.rb index 4057835688..0cc081fced 100644 --- a/activerecord/test/cases/relation/predicate_builder_test.rb +++ b/activerecord/test/cases/relation/predicate_builder_test.rb @@ -4,11 +4,13 @@ require 'models/topic' module ActiveRecord class PredicateBuilderTest < ActiveRecord::TestCase def test_registering_new_handlers - PredicateBuilder.register_handler(Regexp, proc do |column, value| + Topic.predicate_builder.register_handler(Regexp, proc do |column, value| Arel::Nodes::InfixOperation.new('~', column, Arel.sql(value.source)) end) assert_match %r{["`]topics["`].["`]title["`] ~ rails}i, Topic.where(title: /rails/).to_sql + ensure + Topic.reset_column_information end end end diff --git a/activerecord/test/cases/relation/where_chain_test.rb b/activerecord/test/cases/relation/where_chain_test.rb index b9e69bdb08..619055f1e7 100644 --- a/activerecord/test/cases/relation/where_chain_test.rb +++ b/activerecord/test/cases/relation/where_chain_test.rb @@ -149,5 +149,33 @@ module ActiveRecord assert_bound_ast value, Post.arel_table['title'], Arel::Nodes::Equality assert_equal 'alone', bind.last end + + def test_rewhere_with_range + relation = Post.where(comments_count: 1..3).rewhere(comments_count: 3..5) + + assert_equal 1, relation.where_values.size + assert_equal Post.where(comments_count: 3..5), relation + end + + def test_rewhere_with_infinite_upper_bound_range + relation = Post.where(comments_count: 1..Float::INFINITY).rewhere(comments_count: 3..5) + + assert_equal 1, relation.where_values.size + assert_equal Post.where(comments_count: 3..5), relation + end + + def test_rewhere_with_infinite_lower_bound_range + relation = Post.where(comments_count: -Float::INFINITY..1).rewhere(comments_count: 3..5) + + assert_equal 1, relation.where_values.size + assert_equal Post.where(comments_count: 3..5), relation + end + + def test_rewhere_with_infinite_range + relation = Post.where(comments_count: -Float::INFINITY..Float::INFINITY).rewhere(comments_count: 3..5) + + assert_equal 1, relation.where_values.size + assert_equal Post.where(comments_count: 3..5), relation + end end end diff --git a/activerecord/test/cases/relation/where_test.rb b/activerecord/test/cases/relation/where_test.rb index 937f226b1d..d675953da6 100644 --- a/activerecord/test/cases/relation/where_test.rb +++ b/activerecord/test/cases/relation/where_test.rb @@ -6,10 +6,12 @@ require 'models/post' require 'models/comment' require 'models/edge' require 'models/topic' +require 'models/binary' +require 'models/vertex' module ActiveRecord class WhereTest < ActiveRecord::TestCase - fixtures :posts, :edges, :authors + fixtures :posts, :edges, :authors, :binaries def test_where_copies_bind_params author = authors(:david) @@ -60,6 +62,15 @@ module ActiveRecord assert_equal expected.to_sql, actual.to_sql end + def test_belongs_to_nested_where_with_relation + author = authors(:david) + + expected = Author.where(id: author ).joins(:posts) + actual = Author.where(posts: { author_id: Author.where(id: author.id) }).joins(:posts) + + assert_equal expected.to_a, actual.to_a + end + def test_polymorphic_shallow_where treasure = Treasure.new treasure.id = 1 @@ -179,5 +190,35 @@ module ActiveRecord assert_equal 4, Edge.where(blank).order("sink_id").to_a.size end end + + def test_where_with_integer_for_string_column + count = Post.where(:title => 0).count + assert_equal 0, count + end + + def test_where_with_float_for_string_column + count = Post.where(:title => 0.0).count + assert_equal 0, count + end + + def test_where_with_boolean_for_string_column + count = Post.where(:title => false).count + assert_equal 0, count + end + + def test_where_with_decimal_for_string_column + count = Post.where(:title => BigDecimal.new(0)).count + assert_equal 0, count + end + + def test_where_with_duration_for_string_column + count = Post.where(:title => 0.seconds).count + assert_equal 0, count + end + + def test_where_with_integer_for_binary_column + count = Binary.where(:data => 0).count + assert_equal 0, count + end end end diff --git a/activerecord/test/cases/relation_test.rb b/activerecord/test/cases/relation_test.rb index fb0b906c07..f7cb471984 100644 --- a/activerecord/test/cases/relation_test.rb +++ b/activerecord/test/cases/relation_test.rb @@ -23,19 +23,19 @@ module ActiveRecord end def test_construction - relation = Relation.new FakeKlass, :b + relation = Relation.new(FakeKlass, :b, nil) assert_equal FakeKlass, relation.klass assert_equal :b, relation.table assert !relation.loaded, 'relation is not loaded' end def test_responds_to_model_and_returns_klass - relation = Relation.new FakeKlass, :b + relation = Relation.new(FakeKlass, :b, nil) assert_equal FakeKlass, relation.model end def test_initialize_single_values - relation = Relation.new FakeKlass, :b + relation = Relation.new(FakeKlass, :b, nil) (Relation::SINGLE_VALUE_METHODS - [:create_with]).each do |method| assert_nil relation.send("#{method}_value"), method.to_s end @@ -43,19 +43,19 @@ module ActiveRecord end def test_multi_value_initialize - relation = Relation.new FakeKlass, :b + relation = Relation.new(FakeKlass, :b, nil) Relation::MULTI_VALUE_METHODS.each do |method| assert_equal [], relation.send("#{method}_values"), method.to_s end end def test_extensions - relation = Relation.new FakeKlass, :b + relation = Relation.new(FakeKlass, :b, nil) assert_equal [], relation.extensions end def test_empty_where_values_hash - relation = Relation.new FakeKlass, :b + relation = Relation.new(FakeKlass, :b, nil) assert_equal({}, relation.where_values_hash) relation.where! :hello @@ -63,19 +63,20 @@ module ActiveRecord end def test_has_values - relation = Relation.new Post, Post.arel_table + relation = Relation.new(Post, Post.arel_table, Post.predicate_builder) relation.where! relation.table[:id].eq(10) assert_equal({:id => 10}, relation.where_values_hash) end def test_values_wrong_table - relation = Relation.new Post, Post.arel_table + relation = Relation.new(Post, Post.arel_table, Post.predicate_builder) relation.where! Comment.arel_table[:id].eq(10) assert_equal({}, relation.where_values_hash) end def test_tree_is_not_traversed - relation = Relation.new Post, Post.arel_table + 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 @@ -84,24 +85,25 @@ module ActiveRecord end def test_table_name_delegates_to_klass - relation = Relation.new FakeKlass.new('posts'), :b + relation = Relation.new(FakeKlass.new('posts'), :b, Post.predicate_builder) assert_equal 'posts', relation.table_name end def test_scope_for_create - relation = Relation.new FakeKlass, :b + relation = Relation.new(FakeKlass, :b, nil) assert_equal({}, relation.scope_for_create) end def test_create_with_value - relation = Relation.new Post, Post.arel_table + relation = Relation.new(Post, Post.arel_table, Post.predicate_builder) hash = { :hello => 'world' } relation.create_with_value = hash assert_equal hash, relation.scope_for_create end def test_create_with_value_with_wheres - relation = Relation.new Post, Post.arel_table + 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) @@ -109,9 +111,10 @@ module ActiveRecord # FIXME: is this really wanted or expected behavior? def test_scope_for_create_is_cached - relation = Relation.new Post, Post.arel_table + 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) @@ -126,31 +129,31 @@ module ActiveRecord end def test_empty_eager_loading? - relation = Relation.new FakeKlass, :b + relation = Relation.new(FakeKlass, :b, nil) assert !relation.eager_loading? end def test_eager_load_values - relation = Relation.new FakeKlass, :b + relation = Relation.new(FakeKlass, :b, nil) relation.eager_load! :b assert relation.eager_loading? end def test_references_values - relation = Relation.new FakeKlass, :b + relation = Relation.new(FakeKlass, :b, nil) assert_equal [], relation.references_values relation = relation.references(:foo).references(:omg, :lol) assert_equal ['foo', 'omg', 'lol'], relation.references_values end def test_references_values_dont_duplicate - relation = Relation.new FakeKlass, :b + relation = Relation.new(FakeKlass, :b, nil) relation = relation.references(:foo).references(:foo) assert_equal ['foo'], relation.references_values end test 'merging a hash into a relation' do - relation = Relation.new FakeKlass, :b + relation = Relation.new(FakeKlass, :b, nil) relation = relation.merge where: :lol, readonly: true assert_equal [:lol], relation.where_values @@ -158,7 +161,7 @@ module ActiveRecord end test 'merging an empty hash into a relation' do - assert_equal [], Relation.new(FakeKlass, :b).merge({}).where_values + assert_equal [], Relation.new(FakeKlass, :b, nil).merge({}).where_values end test 'merging a hash with unknown keys raises' do @@ -166,7 +169,7 @@ module ActiveRecord end test '#values returns a dup of the values' do - relation = Relation.new(FakeKlass, :b).where! :foo + relation = Relation.new(FakeKlass, :b, nil).where! :foo values = relation.values values[:where] = nil @@ -174,12 +177,12 @@ module ActiveRecord end test 'relations can be created with a values hash' do - relation = Relation.new(FakeKlass, :b, where: [:foo]) + relation = Relation.new(FakeKlass, :b, nil, where: [:foo]) assert_equal [:foo], relation.where_values end test 'merging a single where value' do - relation = Relation.new(FakeKlass, :b) + relation = Relation.new(FakeKlass, :b, nil) relation.merge!(where: :foo) assert_equal [:foo], relation.where_values end @@ -192,13 +195,13 @@ module ActiveRecord end end - relation = Relation.new(klass, :b) + relation = Relation.new(klass, :b, nil) relation.merge!(where: ['foo = ?', 'bar']) assert_equal ['foo = bar'], relation.where_values end def test_merging_readonly_false - relation = Relation.new FakeKlass, :b + relation = Relation.new(FakeKlass, :b, nil) readonly_false_relation = relation.readonly(false) # test merging in both directions assert_equal false, relation.merge(readonly_false_relation).readonly_value @@ -235,5 +238,33 @@ module ActiveRecord posts_with_special_comments_with_ratings = Post.group("posts.id").joins(:special_comments).merge(special_comments_with_ratings) assert_equal 3, authors(:david).posts.merge(posts_with_special_comments_with_ratings).count.length end + + class EnsureRoundTripTypeCasting < ActiveRecord::Type::Value + def type + :string + end + + def type_cast_from_database(value) + raise value unless value == "type cast for database" + "type cast from database" + end + + def type_cast_for_database(value) + raise value unless value == "value from user" + "type cast for database" + end + end + + class UpdateAllTestModel < ActiveRecord::Base + self.table_name = 'posts' + + attribute :body, EnsureRoundTripTypeCasting.new + end + + def test_update_all_goes_through_normal_type_casting + UpdateAllTestModel.update_all(body: "value from user", type: nil) # No STI + + assert_equal "type cast from database", UpdateAllTestModel.first.body + end end end diff --git a/activerecord/test/cases/relations_test.rb b/activerecord/test/cases/relations_test.rb index 88df997a2f..0210ae6487 100644 --- a/activerecord/test/cases/relations_test.rb +++ b/activerecord/test/cases/relations_test.rb @@ -7,6 +7,7 @@ require 'models/comment' require 'models/author' require 'models/entrant' require 'models/developer' +require 'models/computer' require 'models/reply' require 'models/company' require 'models/bird' @@ -21,6 +22,11 @@ class RelationTest < ActiveRecord::TestCase fixtures :authors, :topics, :entrants, :developers, :companies, :developers_projects, :accounts, :categories, :categorizations, :posts, :comments, :tags, :taggings, :cars, :minivans + class TopicWithCallbacks < ActiveRecord::Base + self.table_name = :topics + before_update { |topic| topic.author_name = 'David' if topic.author_name.blank? } + end + def test_do_not_double_quote_string_id van = Minivan.last assert van @@ -248,7 +254,7 @@ class RelationTest < ActiveRecord::TestCase def test_finding_with_reorder topics = Topic.order('author_name').order('title').reorder('id').to_a - topics_titles = topics.map{ |t| t.title } + topics_titles = topics.map(&:title) assert_equal ['The First Topic', 'The Second Topic of the day', 'The Third Topic of the day', 'The Fourth Topic of the day', 'The Fifth Topic of the day'], topics_titles end @@ -311,26 +317,26 @@ class RelationTest < ActiveRecord::TestCase end def test_none - assert_no_queries do + assert_no_queries(ignore_none: false) do assert_equal [], Developer.none assert_equal [], Developer.all.none end end def test_none_chainable - assert_no_queries do + assert_no_queries(ignore_none: false) do assert_equal [], Developer.none.where(:name => 'David') end end def test_none_chainable_to_existing_scope_extension_method - assert_no_queries do + assert_no_queries(ignore_none: false) do assert_equal 1, Topic.anonymous_extension.none.one end end def test_none_chained_to_methods_firing_queries_straight_to_db - assert_no_queries do + assert_no_queries(ignore_none: false) do assert_equal [], Developer.none.pluck(:id, :name) assert_equal 0, Developer.none.delete_all assert_equal 0, Developer.none.update_all(:name => 'David') @@ -340,7 +346,7 @@ class RelationTest < ActiveRecord::TestCase end def test_null_relation_content_size_methods - assert_no_queries do + assert_no_queries(ignore_none: false) do assert_equal 0, Developer.none.size assert_equal 0, Developer.none.count assert_equal true, Developer.none.empty? @@ -350,7 +356,7 @@ class RelationTest < ActiveRecord::TestCase end def test_null_relation_calculations_methods - assert_no_queries do + assert_no_queries(ignore_none: false) do assert_equal 0, Developer.none.count assert_equal 0, Developer.none.calculate(:count, nil, {}) assert_equal nil, Developer.none.calculate(:average, 'salary') @@ -420,6 +426,11 @@ class RelationTest < ActiveRecord::TestCase assert_equal nil, ac.engines.maximum(:id) end + def test_null_relation_in_where_condition + assert_operator Comment.count, :>, 0 # precondition, make sure there are comments. + assert_equal 0, Comment.where(post_id: Post.none).to_a.size + end + def test_joins_with_nil_argument assert_nothing_raised { DependentFirm.joins(nil).first } end @@ -435,7 +446,7 @@ class RelationTest < ActiveRecord::TestCase where('project_id=1').to_a assert_equal 3, developers_on_project_one.length - developer_names = developers_on_project_one.map { |d| d.name } + developer_names = developers_on_project_one.map(&:name) assert developer_names.include?('David') assert developer_names.include?('Jamis') end @@ -646,8 +657,8 @@ class RelationTest < ActiveRecord::TestCase expected_taggings = taggings(:welcome_general, :thinking_general) assert_no_queries do - assert_equal expected_taggings, author.taggings.distinct.sort_by { |t| t.id } - assert_equal expected_taggings, author.taggings.uniq.sort_by { |t| t.id } + assert_equal expected_taggings, author.taggings.distinct.sort_by(&:id) + assert_equal expected_taggings, author.taggings.uniq.sort_by(&:id) end authors = Author.all @@ -706,7 +717,9 @@ class RelationTest < ActiveRecord::TestCase def test_find_by_classname Author.create!(:name => Mary.name) - assert_equal 1, Author.where(:name => Mary).size + assert_deprecated do + assert_equal 1, Author.where(:name => Mary).size + end end def test_find_by_id_with_list_of_ar @@ -1421,6 +1434,19 @@ class RelationTest < ActiveRecord::TestCase assert_equal posts(:welcome), comments(:greetings).post end + def test_update_on_relation + topic1 = TopicWithCallbacks.create! title: 'arel', author_name: nil + topic2 = TopicWithCallbacks.create! title: 'activerecord', author_name: nil + topics = TopicWithCallbacks.where(id: [topic1.id, topic2.id]) + topics.update(title: 'adequaterecord') + + assert_equal 'adequaterecord', topic1.reload.title + assert_equal 'adequaterecord', topic2.reload.title + # Testing that the before_update callbacks have run + assert_equal 'David', topic1.reload.author_name + assert_equal 'David', topic2.reload.author_name + end + def test_distinct tag1 = Tag.create(:name => 'Foo') tag2 = Tag.create(:name => 'Foo') @@ -1570,7 +1596,7 @@ class RelationTest < ActiveRecord::TestCase end test "find_by doesn't have implicit ordering" do - assert_sql(/^((?!ORDER).)*$/) { Post.find_by(author_id: 2) } + assert_sql(/^((?!ORDER).)*$/) { Post.all.find_by(author_id: 2) } end test "find_by! with hash conditions returns the first matching record" do @@ -1586,7 +1612,7 @@ class RelationTest < ActiveRecord::TestCase end test "find_by! doesn't have implicit ordering" do - assert_sql(/^((?!ORDER).)*$/) { Post.find_by!(author_id: 2) } + assert_sql(/^((?!ORDER).)*$/) { Post.all.find_by!(author_id: 2) } end test "find_by! raises RecordNotFound if the record is missing" do @@ -1655,7 +1681,9 @@ class RelationTest < ActiveRecord::TestCase test 'using a custom table affects the wheres' do table_alias = Post.arel_table.alias('omg_posts') - relation = ActiveRecord::Relation.new Post, table_alias + table_metadata = ActiveRecord::TableMetadata.new(Post, table_alias) + predicate_builder = ActiveRecord::PredicateBuilder.new(table_metadata) + relation = ActiveRecord::Relation.new(Post, table_alias, predicate_builder) relation.where!(:foo => "bar") node = relation.arel.constraints.first.grep(Arel::Attributes::Attribute).first @@ -1697,7 +1725,7 @@ class RelationTest < ActiveRecord::TestCase end def test_unscope_removes_binds - left = Post.where(id: Arel::Nodes::BindParam.new('?')) + left = Post.where(id: Arel::Nodes::BindParam.new) column = Post.columns_hash['id'] left.bind_values += [[column, 20]] diff --git a/activerecord/test/cases/result_test.rb b/activerecord/test/cases/result_test.rb index 2131b32a0c..dec01dfa76 100644 --- a/activerecord/test/cases/result_test.rb +++ b/activerecord/test/cases/result_test.rb @@ -10,7 +10,11 @@ module ActiveRecord ]) end - def test_to_hash_returns_row_hashes + test "length" do + assert_equal 3, result.length + end + + test "to_hash returns row_hashes" do assert_equal [ {'col_1' => 'row 1 col 1', 'col_2' => 'row 1 col 2'}, {'col_1' => 'row 2 col 1', 'col_2' => 'row 2 col 2'}, @@ -18,13 +22,13 @@ module ActiveRecord ], result.to_hash end - def test_each_with_block_returns_row_hashes + test "each with block returns row hashes" do result.each do |row| assert_equal ['col_1', 'col_2'], row.keys end end - def test_each_without_block_returns_an_enumerator + test "each without block returns an enumerator" do result.each.with_index do |row, index| assert_equal ['col_1', 'col_2'], row.keys assert_kind_of Integer, index @@ -32,9 +36,45 @@ module ActiveRecord end if Enumerator.method_defined? :size - def test_each_without_block_returns_a_sized_enumerator + test "each without block returns a sized enumerator" do assert_equal 3, result.each.size end end + + test "cast_values returns rows after type casting" do + values = [["1.1", "2.2"], ["3.3", "4.4"]] + columns = ["col1", "col2"] + types = { "col1" => Type::Integer.new, "col2" => Type::Float.new } + result = Result.new(columns, values, types) + + assert_equal [[1, 2.2], [3, 4.4]], result.cast_values + end + + test "cast_values uses identity type for unknown types" do + values = [["1.1", "2.2"], ["3.3", "4.4"]] + columns = ["col1", "col2"] + types = { "col1" => Type::Integer.new } + result = Result.new(columns, values, types) + + assert_equal [[1, "2.2"], [3, "4.4"]], result.cast_values + end + + test "cast_values returns single dimensional array if single column" do + values = [["1.1"], ["3.3"]] + columns = ["col1"] + types = { "col1" => Type::Integer.new } + result = Result.new(columns, values, types) + + assert_equal [1, 3], result.cast_values + end + + test "cast_values can receive types to use instead" do + values = [["1.1", "2.2"], ["3.3", "4.4"]] + columns = ["col1", "col2"] + types = { "col1" => Type::Integer.new, "col2" => Type::Float.new } + result = Result.new(columns, values, types) + + assert_equal [[1.1, 2.2], [3.3, 4.4]], result.cast_values("col1" => Type::Float.new) + end end end diff --git a/activerecord/test/cases/sanitize_test.rb b/activerecord/test/cases/sanitize_test.rb index dca85fb5eb..f477cf7d92 100644 --- a/activerecord/test/cases/sanitize_test.rb +++ b/activerecord/test/cases/sanitize_test.rb @@ -13,7 +13,9 @@ class SanitizeTest < ActiveRecord::TestCase quoted_table_name = ActiveRecord::Base.connection.quote_table_name("adorable_animals") expected_value = "#{quoted_table_name}.#{quoted_column_name} = #{quoted_bambi}" - assert_equal expected_value, Binary.send(:sanitize_sql_hash, {adorable_animals: {name: 'Bambi'}}) + assert_deprecated do + assert_equal expected_value, Binary.send(:sanitize_sql_hash, {adorable_animals: {name: 'Bambi'}}) + end end def test_sanitize_sql_array_handles_string_interpolation diff --git a/activerecord/test/cases/schema_dumper_test.rb b/activerecord/test/cases/schema_dumper_test.rb index 5f02d39e32..a094136766 100644 --- a/activerecord/test/cases/schema_dumper_test.rb +++ b/activerecord/test/cases/schema_dumper_test.rb @@ -2,30 +2,35 @@ require "cases/helper" require 'support/schema_dumping_helper' class SchemaDumperTest < ActiveRecord::TestCase + include SchemaDumpingHelper + self.use_transactional_fixtures = false + setup do ActiveRecord::SchemaMigration.create_table end def standard_dump - @stream = StringIO.new - ActiveRecord::SchemaDumper.ignore_tables = [] - ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, @stream) - @stream.string + @@standard_dump ||= perform_schema_dump + end + + def perform_schema_dump + dump_all_table_schema [] end def test_dump_schema_information_outputs_lexically_ordered_versions versions = %w{ 20100101010101 20100201010101 20100301010101 } - versions.reverse.each do |v| + versions.reverse_each do |v| ActiveRecord::SchemaMigration.create!(:version => v) end schema_info = ActiveRecord::Base.connection.dump_schema_information assert_match(/20100201010101.*20100301010101/m, schema_info) + ensure + ActiveRecord::SchemaMigration.delete_all end def test_magic_comment - output = standard_dump - assert_match "# encoding: #{@stream.external_encoding.name}", output + assert_match "# encoding: #{Encoding.default_external.name}", standard_dump end def test_schema_dump @@ -35,6 +40,11 @@ class SchemaDumperTest < ActiveRecord::TestCase assert_no_match %r{create_table "schema_migrations"}, output end + def test_schema_dump_uses_force_cascade_on_create_table + output = dump_table_schema "authors" + assert_match %r{create_table "authors", force: :cascade}, output + end + def test_schema_dump_excludes_sqlite_sequence output = standard_dump assert_no_match %r{create_table "sqlite_sequence"}, output @@ -86,22 +96,18 @@ class SchemaDumperTest < ActiveRecord::TestCase end def test_schema_dump_includes_not_null_columns - stream = StringIO.new - - ActiveRecord::SchemaDumper.ignore_tables = [/^[^r]/] - ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, stream) - output = stream.string + output = dump_all_table_schema([/^[^r]/]) assert_match %r{null: false}, output end def test_schema_dump_includes_limit_constraint_for_integer_columns - stream = StringIO.new + output = dump_all_table_schema([/^(?!integer_limits)/]) - ActiveRecord::SchemaDumper.ignore_tables = [/^(?!integer_limits)/] - ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, stream) - output = stream.string + assert_match %r{c_int_without_limit}, output if current_adapter?(:PostgreSQLAdapter) + assert_no_match %r{c_int_without_limit.*limit:}, output + assert_match %r{c_int_1.*limit: 2}, output assert_match %r{c_int_2.*limit: 2}, output @@ -112,6 +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 + assert_match %r{c_int_1.*limit: 1}, output assert_match %r{c_int_2.*limit: 2}, output assert_match %r{c_int_3.*limit: 3}, output @@ -119,13 +127,13 @@ class SchemaDumperTest < ActiveRecord::TestCase assert_match %r{c_int_4.*}, output assert_no_match %r{c_int_4.*:limit}, output elsif current_adapter?(:SQLite3Adapter) + assert_no_match %r{c_int_without_limit.*limit:}, output + assert_match %r{c_int_1.*limit: 1}, output assert_match %r{c_int_2.*limit: 2}, output assert_match %r{c_int_3.*limit: 3}, output assert_match %r{c_int_4.*limit: 4}, output end - assert_match %r{c_int_without_limit.*}, output - assert_no_match %r{c_int_without_limit.*limit:}, output if current_adapter?(:SQLite3Adapter) assert_match %r{c_int_5.*limit: 5}, output @@ -146,35 +154,19 @@ class SchemaDumperTest < ActiveRecord::TestCase end def test_schema_dump_with_string_ignored_table - stream = StringIO.new - - ActiveRecord::SchemaDumper.ignore_tables = ['accounts'] - ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, stream) - output = stream.string + output = dump_all_table_schema(['accounts']) assert_no_match %r{create_table "accounts"}, output assert_match %r{create_table "authors"}, output assert_no_match %r{create_table "schema_migrations"}, output end def test_schema_dump_with_regexp_ignored_table - stream = StringIO.new - - ActiveRecord::SchemaDumper.ignore_tables = [/^account/] - ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, stream) - output = stream.string + output = dump_all_table_schema([/^account/]) assert_no_match %r{create_table "accounts"}, output assert_match %r{create_table "authors"}, output assert_no_match %r{create_table "schema_migrations"}, output end - def test_schema_dump_illegal_ignored_table_value - stream = StringIO.new - ActiveRecord::SchemaDumper.ignore_tables = [5] - assert_raise(StandardError) do - ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, stream) - end - end - def test_schema_dumps_index_columns_in_right_order index_definition = standard_dump.split(/\n/).grep(/add_index.*companies/).first.strip if current_adapter?(:MysqlAdapter, :Mysql2Adapter, :PostgreSQLAdapter) @@ -210,9 +202,9 @@ class SchemaDumperTest < ActiveRecord::TestCase end if current_adapter?(:MysqlAdapter, :Mysql2Adapter) - def test_schema_dump_should_not_add_default_value_for_mysql_text_field + def test_schema_dump_should_add_default_value_for_mysql_text_field output = standard_dump - assert_match %r{t.text\s+"body",\s+null: false$}, output + assert_match %r{t.text\s+"body",\s+limit: 65535,\s+null: false$}, output end def test_schema_dump_includes_length_for_mysql_binary_fields @@ -224,13 +216,13 @@ class SchemaDumperTest < ActiveRecord::TestCase def test_schema_dump_includes_length_for_mysql_blob_and_text_fields output = standard_dump assert_match %r{t.binary\s+"tiny_blob",\s+limit: 255$}, output - assert_match %r{t.binary\s+"normal_blob"$}, output + assert_match %r{t.binary\s+"normal_blob",\s+limit: 65535$}, output assert_match %r{t.binary\s+"medium_blob",\s+limit: 16777215$}, output - assert_match %r{t.binary\s+"long_blob",\s+limit: 2147483647$}, output + assert_match %r{t.binary\s+"long_blob",\s+limit: 4294967295$}, output assert_match %r{t.text\s+"tiny_text",\s+limit: 255$}, output - assert_match %r{t.text\s+"normal_text"$}, output + assert_match %r{t.text\s+"normal_text",\s+limit: 65535$}, output assert_match %r{t.text\s+"medium_text",\s+limit: 16777215$}, output - assert_match %r{t.text\s+"long_text",\s+limit: 2147483647$}, output + assert_match %r{t.text\s+"long_text",\s+limit: 4294967295$}, output end def test_schema_dumps_index_type @@ -241,10 +233,7 @@ class SchemaDumperTest < ActiveRecord::TestCase end def test_schema_dump_includes_decimal_options - stream = StringIO.new - ActiveRecord::SchemaDumper.ignore_tables = [/^[^n]/] - ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, stream) - output = stream.string + output = dump_all_table_schema([/^[^n]/]) assert_match %r{precision: 3,[[:space:]]+scale: 2,[[:space:]]+default: 2.78}, output end @@ -259,94 +248,16 @@ class SchemaDumperTest < ActiveRecord::TestCase connection = ActiveRecord::Base.connection connection.stubs(:extensions).returns(['hstore']) - output = standard_dump + output = perform_schema_dump assert_match "# These are extensions that must be enabled", output assert_match %r{enable_extension "hstore"}, output connection.stubs(:extensions).returns([]) - output = standard_dump + output = perform_schema_dump assert_no_match "# These are extensions that must be enabled", output assert_no_match %r{enable_extension}, output end end - - def test_schema_dump_includes_xml_shorthand_definition - output = standard_dump - if %r{create_table "postgresql_xml_data_type"} =~ output - assert_match %r{t.xml "data"}, output - end - end - - def test_schema_dump_includes_json_shorthand_definition - output = standard_dump - if %r{create_table "postgresql_json_data_type"} =~ output - assert_match %r|t.json "json_data", default: {}|, output - end - end - - def test_schema_dump_includes_inet_shorthand_definition - output = standard_dump - if %r{create_table "postgresql_network_addresses"} =~ output - assert_match %r{t.inet\s+"inet_address",\s+default: "192.168.1.1"}, output - end - end - - def test_schema_dump_includes_cidr_shorthand_definition - output = standard_dump - if %r{create_table "postgresql_network_addresses"} =~ output - assert_match %r{t.cidr\s+"cidr_address",\s+default: "192.168.1.0/24"}, output - end - end - - def test_schema_dump_includes_macaddr_shorthand_definition - output = standard_dump - if %r{create_table "postgresql_network_addresses"} =~ output - assert_match %r{t.macaddr\s+"mac_address",\s+default: "ff:ff:ff:ff:ff:ff"}, output - end - end - - def test_schema_dump_includes_uuid_shorthand_definition - output = standard_dump - if %r{create_table "postgresql_uuids"} =~ output - assert_match %r{t.uuid "guid"}, output - end - end - - def test_schema_dump_includes_hstores_shorthand_definition - output = standard_dump - if %r{create_table "postgresql_hstores"} =~ output - assert_match %r[t.hstore "hash_store", default: {}], output - end - end - - def test_schema_dump_includes_citext_shorthand_definition - output = standard_dump - if %r{create_table "postgresql_citext"} =~ output - assert_match %r[t.citext "text_citext"], output - end - end - - def test_schema_dump_includes_ltrees_shorthand_definition - output = standard_dump - if %r{create_table "postgresql_ltrees"} =~ output - assert_match %r[t.ltree "path"], output - end - end - - def test_schema_dump_includes_arrays_shorthand_definition - output = standard_dump - if %r{create_table "postgresql_arrays"} =~ output - assert_match %r[t.text\s+"nicknames",\s+array: true], output - assert_match %r[t.integer\s+"commission_by_quarter",\s+array: true], output - end - end - - def test_schema_dump_includes_tsvector_shorthand_definition - output = standard_dump - if %r{create_table "postgresql_tsvectors"} =~ output - assert_match %r{t.tsvector "text_vector"}, output - end - end end def test_schema_dump_keeps_large_precision_integer_columns_as_decimal @@ -364,7 +275,7 @@ class SchemaDumperTest < ActiveRecord::TestCase match = output.match(%r{create_table "goofy_string_id"(.*)do.*\n(.*)\n}) assert_not_nil(match, "goofy_string_id table not found") assert_match %r(id: false), match[1], "no table id not preserved" - assert_match %r{t.string[[:space:]]+"id",[[:space:]]+null: false$}, match[2], "non-primary key id column not preserved" + assert_match %r{t.string\s+"id",.*?null: false$}, match[2], "non-primary key id column not preserved" end def test_schema_dump_keeps_id_false_when_id_is_false_and_unique_not_null_column_added @@ -372,15 +283,33 @@ class SchemaDumperTest < ActiveRecord::TestCase assert_match %r{create_table "subscribers", id: false}, output end + if ActiveRecord::Base.connection.supports_foreign_keys? + def test_foreign_keys_are_dumped_at_the_bottom_to_circumvent_dependency_issues + output = standard_dump + assert_match(/^\s+add_foreign_key "fk_test_has_fk"[^\n]+\n\s+add_foreign_key "lessons_students"/, output) + end + + def test_do_not_dump_foreign_keys_for_ignored_tables + output = dump_table_schema "authors" + assert_equal ["authors"], output.scan(/^\s*add_foreign_key "([^"]+)".+$/).flatten + end + end + class CreateDogMigration < ActiveRecord::Migration def up + create_table("dog_owners") do |t| + end + create_table("dogs") do |t| t.column :name, :string + t.column :owner_id, :integer end add_index "dogs", [:name] + add_foreign_key :dogs, :dog_owners, column: "owner_id" if supports_foreign_keys? end def down drop_table("dogs") + drop_table("dog_owners") end end @@ -392,17 +321,21 @@ class SchemaDumperTest < ActiveRecord::TestCase migration = CreateDogMigration.new migration.migrate(:up) - output = standard_dump + output = perform_schema_dump assert_no_match %r{create_table "foo_.+_bar"}, output assert_no_match %r{add_index "foo_.+_bar"}, output assert_no_match %r{create_table "schema_migrations"}, output + + if ActiveRecord::Base.connection.supports_foreign_keys? + assert_no_match %r{add_foreign_key "foo_.+_bar"}, output + assert_no_match %r{add_foreign_key "[^"]+", "foo_.+_bar"}, output + end ensure migration.migrate(:down) ActiveRecord::Base.table_name_suffix = ActiveRecord::Base.table_name_prefix = '' $stdout = original end - end class SchemaDumperDefaultsTest < ActiveRecord::TestCase @@ -420,13 +353,13 @@ class SchemaDumperDefaultsTest < ActiveRecord::TestCase teardown do return unless @connection - @connection.execute 'DROP TABLE IF EXISTS defaults' + @connection.execute 'DROP TABLE defaults' if @connection.table_exists? 'defaults' end def test_schema_dump_defaults_with_universally_supported_types output = dump_table_schema('defaults') - assert_match %r{t\.string\s+"string_with_default",\s+default: "Hello!"}, output + assert_match %r{t\.string\s+"string_with_default",.*?default: "Hello!"}, output assert_match %r{t\.date\s+"date_with_default",\s+default: '2014-06-05'}, output assert_match %r{t\.datetime\s+"datetime_with_default",\s+default: '2014-06-05 07:17:04'}, output assert_match %r{t\.time\s+"time_with_default",\s+default: '2000-01-01 07:17:04'}, output diff --git a/activerecord/test/cases/scoping/default_scoping_test.rb b/activerecord/test/cases/scoping/default_scoping_test.rb index 9a4d8c6740..0738df1b54 100644 --- a/activerecord/test/cases/scoping/default_scoping_test.rb +++ b/activerecord/test/cases/scoping/default_scoping_test.rb @@ -1,13 +1,15 @@ require 'cases/helper' require 'models/post' +require 'models/comment' require 'models/developer' +require 'models/computer' class DefaultScopingTest < ActiveRecord::TestCase - fixtures :developers, :posts + fixtures :developers, :posts, :comments def test_default_scope - expected = Developer.all.merge!(:order => 'salary DESC').to_a.collect { |dev| dev.salary } - received = DeveloperOrderedBySalary.all.collect { |dev| dev.salary } + expected = Developer.all.merge!(:order => 'salary DESC').to_a.collect(&:salary) + received = DeveloperOrderedBySalary.all.collect(&:salary) assert_equal expected, received end @@ -84,14 +86,14 @@ class DefaultScopingTest < ActiveRecord::TestCase end def test_scope_overwrites_default - expected = Developer.all.merge!(order: 'salary DESC, name DESC').to_a.collect { |dev| dev.name } - received = DeveloperOrderedBySalary.by_name.to_a.collect { |dev| dev.name } + expected = Developer.all.merge!(order: 'salary DESC, name DESC').to_a.collect(&:name) + received = DeveloperOrderedBySalary.by_name.to_a.collect(&:name) assert_equal expected, received end def test_reorder_overrides_default_scope_order - expected = Developer.order('name DESC').collect { |dev| dev.name } - received = DeveloperOrderedBySalary.reorder('name DESC').collect { |dev| dev.name } + expected = Developer.order('name DESC').collect(&:name) + received = DeveloperOrderedBySalary.reorder('name DESC').collect(&:name) assert_equal expected, received end @@ -141,37 +143,45 @@ class DefaultScopingTest < ActiveRecord::TestCase expected_5 = Developer.order('salary DESC').collect(&:name) received_5 = DeveloperOrderedBySalary.where.not("name" => ["Jamis", "David"]).unscope(where: :name).collect(&:name) assert_equal expected_5, received_5 + + expected_6 = Developer.order('salary DESC').collect(&:name) + received_6 = DeveloperOrderedBySalary.where(Developer.arel_table['name'].eq('David')).unscope(where: :name).collect(&:name) + assert_equal expected_6, received_6 + + expected_7 = Developer.order('salary DESC').collect(&:name) + received_7 = DeveloperOrderedBySalary.where(Developer.arel_table[:name].eq('David')).unscope(where: :name).collect(&:name) + assert_equal expected_7, received_7 end def test_unscope_multiple_where_clauses - expected = Developer.order('salary DESC').collect { |dev| dev.name } - received = DeveloperOrderedBySalary.where(name: 'Jamis').where(id: 1).unscope(where: [:name, :id]).collect { |dev| dev.name } + expected = Developer.order('salary DESC').collect(&:name) + received = DeveloperOrderedBySalary.where(name: 'Jamis').where(id: 1).unscope(where: [:name, :id]).collect(&:name) assert_equal expected, received end def test_unscope_string_where_clauses_involved dev_relation = Developer.order('salary DESC').where("created_at > ?", 1.year.ago) - expected = dev_relation.collect { |dev| dev.name } + expected = dev_relation.collect(&:name) dev_ordered_relation = DeveloperOrderedBySalary.where(name: 'Jamis').where("created_at > ?", 1.year.ago) - received = dev_ordered_relation.unscope(where: [:name]).collect { |dev| dev.name } + received = dev_ordered_relation.unscope(where: [:name]).collect(&:name) assert_equal expected, received end def test_unscope_with_grouping_attributes - expected = Developer.order('salary DESC').collect { |dev| dev.name } - received = DeveloperOrderedBySalary.group(:name).unscope(:group).collect { |dev| dev.name } + expected = Developer.order('salary DESC').collect(&:name) + received = DeveloperOrderedBySalary.group(:name).unscope(:group).collect(&:name) assert_equal expected, received - expected_2 = Developer.order('salary DESC').collect { |dev| dev.name } - received_2 = DeveloperOrderedBySalary.group("name").unscope(:group).collect { |dev| dev.name } + expected_2 = Developer.order('salary DESC').collect(&:name) + received_2 = DeveloperOrderedBySalary.group("name").unscope(:group).collect(&:name) assert_equal expected_2, received_2 end def test_unscope_with_limit_in_query - expected = Developer.order('salary DESC').collect { |dev| dev.name } - received = DeveloperOrderedBySalary.limit(1).unscope(:limit).collect { |dev| dev.name } + expected = Developer.order('salary DESC').collect(&:name) + received = DeveloperOrderedBySalary.limit(1).unscope(:limit).collect(&:name) assert_equal expected, received end @@ -181,42 +191,42 @@ class DefaultScopingTest < ActiveRecord::TestCase end def test_unscope_reverse_order - expected = Developer.all.collect { |dev| dev.name } - received = Developer.order('salary DESC').reverse_order.unscope(:order).collect { |dev| dev.name } + expected = Developer.all.collect(&:name) + received = Developer.order('salary DESC').reverse_order.unscope(:order).collect(&:name) assert_equal expected, received end def test_unscope_select - expected = Developer.order('salary ASC').collect { |dev| dev.name } - received = Developer.order('salary DESC').reverse_order.select(:name).unscope(:select).collect { |dev| dev.name } + expected = Developer.order('salary ASC').collect(&:name) + received = Developer.order('salary DESC').reverse_order.select(:name).unscope(:select).collect(&:name) assert_equal expected, received - expected_2 = Developer.all.collect { |dev| dev.id } - received_2 = Developer.select(:name).unscope(:select).collect { |dev| dev.id } + expected_2 = Developer.all.collect(&:id) + received_2 = Developer.select(:name).unscope(:select).collect(&:id) assert_equal expected_2, received_2 end def test_unscope_offset - expected = Developer.all.collect { |dev| dev.name } - received = Developer.offset(5).unscope(:offset).collect { |dev| dev.name } + expected = Developer.all.collect(&:name) + received = Developer.offset(5).unscope(:offset).collect(&:name) assert_equal expected, received end def test_unscope_joins_and_select_on_developers_projects - expected = Developer.all.collect { |dev| dev.name } - received = Developer.joins('JOIN developers_projects ON id = developer_id').select(:id).unscope(:joins, :select).collect { |dev| dev.name } + expected = Developer.all.collect(&:name) + received = Developer.joins('JOIN developers_projects ON id = developer_id').select(:id).unscope(:joins, :select).collect(&:name) assert_equal expected, received end def test_unscope_includes - expected = Developer.all.collect { |dev| dev.name } - received = Developer.includes(:projects).select(:id).unscope(:includes, :select).collect { |dev| dev.name } + expected = Developer.all.collect(&:name) + received = Developer.includes(:projects).select(:id).unscope(:includes, :select).collect(&:name) assert_equal expected, received end def test_unscope_having - expected = DeveloperOrderedBySalary.all.collect { |dev| dev.name } - received = DeveloperOrderedBySalary.having("name IN ('Jamis', 'David')").unscope(:having).collect { |dev| dev.name } + expected = DeveloperOrderedBySalary.all.collect(&:name) + received = DeveloperOrderedBySalary.having("name IN ('Jamis', 'David')").unscope(:having).collect(&:name) assert_equal expected, received end @@ -279,8 +289,8 @@ class DefaultScopingTest < ActiveRecord::TestCase end def test_order_in_default_scope_should_not_prevail - expected = Developer.all.merge!(order: 'salary desc').to_a.collect { |dev| dev.salary } - received = DeveloperOrderedBySalary.all.merge!(order: 'salary').to_a.collect { |dev| dev.salary } + expected = Developer.all.merge!(order: 'salary desc').to_a.collect(&:salary) + received = DeveloperOrderedBySalary.all.merge!(order: 'salary').to_a.collect(&:salary) assert_equal expected, received end @@ -378,6 +388,24 @@ class DefaultScopingTest < ActiveRecord::TestCase assert_equal 1, DeveloperWithIncludes.where(:audit_logs => { :message => 'foo' }).count end + def test_default_scope_with_references_works_through_collection_association + post = PostWithCommentWithDefaultScopeReferencesAssociation.create!(title: "Hello World", body: "Here we go.") + comment = post.comment_with_default_scope_references_associations.create!(body: "Great post.", developer_id: Developer.first.id) + assert_equal comment, post.comment_with_default_scope_references_associations.to_a.first + end + + def test_default_scope_with_references_works_through_association + post = PostWithCommentWithDefaultScopeReferencesAssociation.create!(title: "Hello World", body: "Here we go.") + comment = post.comment_with_default_scope_references_associations.create!(body: "Great post.", developer_id: Developer.first.id) + assert_equal comment, post.first_comment + end + + def test_default_scope_with_references_works_with_find_by + post = PostWithCommentWithDefaultScopeReferencesAssociation.create!(title: "Hello World", body: "Here we go.") + comment = post.comment_with_default_scope_references_associations.create!(body: "Great post.", developer_id: Developer.first.id) + assert_equal comment, CommentWithDefaultScopeReferencesAssociation.find_by(id: comment.id) + end + unless in_memory_db? def test_default_scope_is_threadsafe threads = [] diff --git a/activerecord/test/cases/scoping/named_scoping_test.rb b/activerecord/test/cases/scoping/named_scoping_test.rb index 59ec2dd6a4..41f3449828 100644 --- a/activerecord/test/cases/scoping/named_scoping_test.rb +++ b/activerecord/test/cases/scoping/named_scoping_test.rb @@ -5,6 +5,7 @@ require 'models/comment' require 'models/reply' require 'models/author' require 'models/developer' +require 'models/computer' class NamedScopingTest < ActiveRecord::TestCase fixtures :posts, :authors, :topics, :comments, :author_addresses @@ -132,6 +133,13 @@ class NamedScopingTest < ActiveRecord::TestCase assert_equal Post.ranked_by_comments.limit_by(5), Post.top(5) end + def test_scopes_body_is_a_callable + e = assert_raises ArgumentError do + Class.new(Post).class_eval { scope :containing_the_letter_z, where("body LIKE '%z%'") } + end + assert_equal "The scope body needs to be callable.", e.message + end + def test_active_records_have_scope_named__all__ assert !Topic.all.empty? @@ -291,9 +299,12 @@ class NamedScopingTest < ActiveRecord::TestCase :relation, # private class method on AR::Base :new, # redefined class method on AR::Base :all, # a default scope - :public, + :public, # some imporant methods on Module and Class :protected, - :private + :private, + :name, + :parent, + :superclass ] non_conflicts = [ diff --git a/activerecord/test/cases/scoping/relation_scoping_test.rb b/activerecord/test/cases/scoping/relation_scoping_test.rb index d8a467ec4d..d7bcbf6203 100644 --- a/activerecord/test/cases/scoping/relation_scoping_test.rb +++ b/activerecord/test/cases/scoping/relation_scoping_test.rb @@ -2,6 +2,7 @@ require "cases/helper" require 'models/post' require 'models/author' require 'models/developer' +require 'models/computer' require 'models/project' require 'models/comment' require 'models/category' @@ -11,6 +12,30 @@ require 'models/reference' class RelationScopingTest < ActiveRecord::TestCase fixtures :authors, :developers, :projects, :comments, :posts, :developers_projects + setup do + developers(:david) + end + + def test_unscoped_breaks_caching + author = authors :mary + assert_nil author.first_post + post = FirstPost.unscoped do + author.reload.first_post + end + assert post + end + + def test_scope_breaks_caching_on_collections + author = authors :david + ids = author.reload.special_posts_with_default_scope.map(&:id) + assert_equal [1,5,6], ids.sort + scoped_posts = SpecialPostWithDefaultScope.unscoped do + author = authors :david + author.reload.special_posts_with_default_scope.to_a + end + assert_equal author.posts.map(&:id).sort, scoped_posts.map(&:id).sort + end + def test_reverse_order assert_equal Developer.order("id DESC").to_a.reverse, Developer.order("id DESC").reverse_order end @@ -260,7 +285,7 @@ class NestedRelationScopingTest < ActiveRecord::TestCase end end -class HasManyScopingTest< ActiveRecord::TestCase +class HasManyScopingTest < ActiveRecord::TestCase fixtures :comments, :posts, :people, :references def setup @@ -306,7 +331,7 @@ class HasManyScopingTest< ActiveRecord::TestCase end end -class HasAndBelongsToManyScopingTest< ActiveRecord::TestCase +class HasAndBelongsToManyScopingTest < ActiveRecord::TestCase fixtures :posts, :categories, :categories_posts def setup diff --git a/activerecord/test/cases/serialization_test.rb b/activerecord/test/cases/serialization_test.rb index 3f52e80e11..35b13ea247 100644 --- a/activerecord/test/cases/serialization_test.rb +++ b/activerecord/test/cases/serialization_test.rb @@ -2,6 +2,8 @@ require "cases/helper" require 'models/contact' require 'models/topic' require 'models/book' +require 'models/author' +require 'models/post' class SerializationTest < ActiveRecord::TestCase fixtures :books @@ -92,4 +94,11 @@ class SerializationTest < ActiveRecord::TestCase book = klazz.find(books(:awdr).id) assert_equal 'paperback', book.read_attribute_for_serialization(:format) end + + def test_find_records_by_serialized_attributes_through_join + author = Author.create!(name: "David") + author.serialized_posts.create!(title: "Hello") + + assert_equal 1, Author.joins(:serialized_posts).where(name: "David", serialized_posts: { title: "Hello" }).length + end end diff --git a/activerecord/test/cases/serialized_attribute_test.rb b/activerecord/test/cases/serialized_attribute_test.rb index 186a1a2ade..c261a5b1c0 100644 --- a/activerecord/test/cases/serialized_attribute_test.rb +++ b/activerecord/test/cases/serialized_attribute_test.rb @@ -3,10 +3,11 @@ require 'models/topic' require 'models/reply' require 'models/person' require 'models/traffic_light' +require 'models/post' require 'bcrypt' class SerializedAttributeTest < ActiveRecord::TestCase - fixtures :topics + fixtures :topics, :posts MyObject = Struct.new :attribute1, :attribute2 @@ -15,8 +16,8 @@ class SerializedAttributeTest < ActiveRecord::TestCase end def test_serialize_does_not_eagerly_load_columns + Topic.reset_column_information assert_no_queries do - Topic.reset_column_information Topic.serialize(:content) end end @@ -67,6 +68,40 @@ class SerializedAttributeTest < ActiveRecord::TestCase assert_equal(orig.content, clone.content) end + def test_serialized_json_attribute_returns_unserialized_value + Topic.serialize :content, JSON + my_post = posts(:welcome) + + t = Topic.new(content: my_post) + t.save! + t.reload + + assert_instance_of(Hash, t.content) + assert_equal(my_post.id, t.content["id"]) + assert_equal(my_post.title, t.content["title"]) + end + + def test_json_read_legacy_null + Topic.serialize :content, JSON + + # Force a row to have a JSON "null" instead of a database NULL (this is how + # null values are saved on 4.1 and before) + id = Topic.connection.insert "INSERT INTO topics (content) VALUES('null')" + t = Topic.find(id) + + assert_nil t.content + end + + def test_json_read_db_null + Topic.serialize :content, JSON + + # Force a row to have a database NULL instead of a JSON "null" + id = Topic.connection.insert "INSERT INTO topics (content) VALUES(NULL)" + t = Topic.find(id) + + assert_nil t.content + end + def test_serialized_attribute_declared_in_subclass hash = { 'important1' => 'value1', 'important2' => 'value2' } important_topic = ImportantTopic.create("important" => hash) @@ -105,11 +140,10 @@ class SerializedAttributeTest < ActiveRecord::TestCase assert_equal 1, Topic.where(:content => nil).count end - def test_serialized_attribute_should_raise_exception_on_save_with_wrong_type + def test_serialized_attribute_should_raise_exception_on_assignment_with_wrong_type Topic.serialize(:content, Hash) assert_raise(ActiveRecord::SerializationTypeMismatch) do - topic = Topic.new(content: 'string') - topic.save + Topic.new(content: 'string') end end @@ -209,8 +243,9 @@ class SerializedAttributeTest < ActiveRecord::TestCase t = Topic.create(content: "first") assert_equal("first", t.content) - t.update_column(:content, Topic.type_for_attribute('content').type_cast_for_database("second")) - assert_equal("second", t.content) + t.update_column(:content, ["second"]) + assert_equal(["second"], t.content) + assert_equal(["second"], t.reload.content) end def test_serialized_column_should_unserialize_after_update_attribute @@ -219,5 +254,20 @@ class SerializedAttributeTest < ActiveRecord::TestCase t.update_attribute(:content, "second") assert_equal("second", t.content) + assert_equal("second", t.reload.content) + end + + def test_nil_is_not_changed_when_serialized_with_a_class + Topic.serialize(:content, Array) + + topic = Topic.new(content: nil) + + assert_not topic.content_changed? + end + + def test_classes_without_no_arg_constructors_are_not_supported + assert_raises(ArgumentError) do + Topic.serialize(:content, Regexp) + end end end diff --git a/activerecord/test/cases/tasks/database_tasks_test.rb b/activerecord/test/cases/tasks/database_tasks_test.rb index bf9e14fa4f..2fa033ed45 100644 --- a/activerecord/test/cases/tasks/database_tasks_test.rb +++ b/activerecord/test/cases/tasks/database_tasks_test.rb @@ -205,7 +205,7 @@ module ActiveRecord ActiveRecord::Tasks::DatabaseTasks.drop_all end - def test_creates_configurations_with_local_ip + def test_drops_configurations_with_local_ip @configurations[:development].merge!('host' => '127.0.0.1') ActiveRecord::Tasks::DatabaseTasks.expects(:drop) @@ -213,7 +213,7 @@ module ActiveRecord ActiveRecord::Tasks::DatabaseTasks.drop_all end - def test_creates_configurations_with_local_host + def test_drops_configurations_with_local_host @configurations[:development].merge!('host' => 'localhost') ActiveRecord::Tasks::DatabaseTasks.expects(:drop) @@ -221,7 +221,7 @@ module ActiveRecord ActiveRecord::Tasks::DatabaseTasks.drop_all end - def test_creates_configurations_with_blank_hosts + def test_drops_configurations_with_blank_hosts @configurations[:development].merge!('host' => nil) ActiveRecord::Tasks::DatabaseTasks.expects(:drop) @@ -241,7 +241,7 @@ module ActiveRecord ActiveRecord::Base.stubs(:configurations).returns(@configurations) end - def test_creates_current_environment_database + def test_drops_current_environment_database ActiveRecord::Tasks::DatabaseTasks.expects(:drop). with('database' => 'prod-db') @@ -273,6 +273,19 @@ module ActiveRecord end end + class DatabaseTasksMigrateTest < ActiveRecord::TestCase + def test_migrate_receives_correct_env_vars + verbose, version = ENV['VERBOSE'], ENV['VERSION'] + + ENV['VERBOSE'] = 'false' + ENV['VERSION'] = '4' + + ActiveRecord::Migrator.expects(:migrate).with(ActiveRecord::Migrator.migrations_paths, 4) + ActiveRecord::Tasks::DatabaseTasks.migrate + ensure + ENV['VERBOSE'], ENV['VERSION'] = verbose, version + end + end class DatabaseTasksPurgeTest < ActiveRecord::TestCase include DatabaseTasksSetupper @@ -285,6 +298,35 @@ module ActiveRecord end end + class DatabaseTasksPurgeCurrentTest < ActiveRecord::TestCase + def test_purges_current_environment_database + configurations = { + 'development' => {'database' => 'dev-db'}, + 'test' => {'database' => 'test-db'}, + 'production' => {'database' => 'prod-db'} + } + ActiveRecord::Base.stubs(:configurations).returns(configurations) + + ActiveRecord::Tasks::DatabaseTasks.expects(:purge). + with('database' => 'prod-db') + ActiveRecord::Base.expects(:establish_connection).with(:production) + + ActiveRecord::Tasks::DatabaseTasks.purge_current('production') + end + end + + class DatabaseTasksPurgeAllTest < ActiveRecord::TestCase + def test_purge_all_local_configurations + configurations = {:development => {'database' => 'my-db'}} + ActiveRecord::Base.stubs(:configurations).returns(configurations) + + ActiveRecord::Tasks::DatabaseTasks.expects(:purge). + with('database' => 'my-db') + + ActiveRecord::Tasks::DatabaseTasks.purge_all + end + end + class DatabaseTasksCharsetTest < ActiveRecord::TestCase include DatabaseTasksSetupper diff --git a/activerecord/test/cases/tasks/mysql_rake_test.rb b/activerecord/test/cases/tasks/mysql_rake_test.rb index 3e3a2828f3..f58535f044 100644 --- a/activerecord/test/cases/tasks/mysql_rake_test.rb +++ b/activerecord/test/cases/tasks/mysql_rake_test.rb @@ -1,5 +1,6 @@ require 'cases/helper' +if current_adapter?(:MysqlAdapter, :Mysql2Adapter) module ActiveRecord class MysqlDBCreateTest < ActiveRecord::TestCase def setup @@ -196,8 +197,8 @@ module ActiveRecord ActiveRecord::Base.stubs(:establish_connection).returns(true) end - def test_establishes_connection_to_test_database - ActiveRecord::Base.expects(:establish_connection).with(:test) + def test_establishes_connection_to_the_appropriate_database + ActiveRecord::Base.expects(:establish_connection).with(@configuration) ActiveRecord::Tasks::DatabaseTasks.purge @configuration end @@ -307,3 +308,4 @@ module ActiveRecord end end +end diff --git a/activerecord/test/cases/tasks/postgresql_rake_test.rb b/activerecord/test/cases/tasks/postgresql_rake_test.rb index 6ea225178f..0d574d071c 100644 --- a/activerecord/test/cases/tasks/postgresql_rake_test.rb +++ b/activerecord/test/cases/tasks/postgresql_rake_test.rb @@ -1,5 +1,6 @@ require 'cases/helper' +if current_adapter?(:PostgreSQLAdapter) module ActiveRecord class PostgreSQLDBCreateTest < ActiveRecord::TestCase def setup @@ -241,3 +242,4 @@ module ActiveRecord end end +end diff --git a/activerecord/test/cases/tasks/sqlite_rake_test.rb b/activerecord/test/cases/tasks/sqlite_rake_test.rb index da3471adf9..750d5e42dc 100644 --- a/activerecord/test/cases/tasks/sqlite_rake_test.rb +++ b/activerecord/test/cases/tasks/sqlite_rake_test.rb @@ -1,6 +1,7 @@ require 'cases/helper' require 'pathname' +if current_adapter?(:SQLite3Adapter) module ActiveRecord class SqliteDBCreateTest < ActiveRecord::TestCase def setup @@ -189,3 +190,4 @@ module ActiveRecord end end end +end diff --git a/activerecord/test/cases/test_case.rb b/activerecord/test/cases/test_case.rb index b6c5511849..5ba17359f0 100644 --- a/activerecord/test/cases/test_case.rb +++ b/activerecord/test/cases/test_case.rb @@ -13,6 +13,23 @@ module ActiveRecord assert_equal expected.to_s, actual.to_s, message end + def capture(stream) + stream = stream.to_s + captured_stream = Tempfile.new(stream) + stream_io = eval("$#{stream}") + origin_stream = stream_io.dup + stream_io.reopen(captured_stream) + + yield + + stream_io.rewind + return captured_stream.read + ensure + captured_stream.close + captured_stream.unlink + stream_io.reopen(origin_stream) + end + def capture_sql SQLCounter.clear_log yield @@ -26,7 +43,7 @@ module ActiveRecord patterns_to_match.each do |pattern| failed_patterns << pattern unless SQLCounter.log_all.any?{ |sql| pattern === sql } end - assert failed_patterns.empty?, "Query pattern(s) #{failed_patterns.map{ |p| p.inspect }.join(', ')} not found.#{SQLCounter.log.size == 0 ? '' : "\nQueries:\n#{SQLCounter.log.join("\n")}"}" + assert failed_patterns.empty?, "Query pattern(s) #{failed_patterns.map(&:inspect).join(', ')} not found.#{SQLCounter.log.size == 0 ? '' : "\nQueries:\n#{SQLCounter.log.join("\n")}"}" end def assert_queries(num = 1, options = {}) @@ -76,8 +93,8 @@ module ActiveRecord # ignored SQL, or better yet, use a different notification for the queries # instead examining the SQL content. oracle_ignored = [/^select .*nextval/i, /^SAVEPOINT/, /^ROLLBACK TO/, /^\s*select .* from all_triggers/im, /^\s*select .* from all_constraints/im, /^\s*select .* from all_tab_cols/im] - mysql_ignored = [/^SHOW TABLES/i, /^SHOW FULL FIELDS/, /^SHOW CREATE TABLE /i] - postgresql_ignored = [/^\s*select\b.*\bfrom\b.*pg_namespace\b/im, /^\s*select\b.*\battname\b.*\bfrom\b.*\bpg_attribute\b/im, /^SHOW search_path/i] + mysql_ignored = [/^SHOW TABLES/i, /^SHOW FULL FIELDS/, /^SHOW CREATE TABLE /i, /^SHOW VARIABLES /] + postgresql_ignored = [/^\s*select\b.*\bfrom\b.*pg_namespace\b/im, /^\s*select tablename\b.*from pg_tables\b/im, /^\s*select\b.*\battname\b.*\bfrom\b.*\bpg_attribute\b/im, /^SHOW search_path/i] sqlite3_ignored = [/^\s*SELECT name\b.*\bFROM sqlite_master/im] [oracle_ignored, mysql_ignored, postgresql_ignored, sqlite3_ignored].each do |db_ignored_sql| diff --git a/activerecord/test/cases/timestamp_test.rb b/activerecord/test/cases/timestamp_test.rb index 0472246f71..db474c63a4 100644 --- a/activerecord/test/cases/timestamp_test.rb +++ b/activerecord/test/cases/timestamp_test.rb @@ -1,5 +1,7 @@ require 'cases/helper' +require 'support/ddl_helper' require 'models/developer' +require 'models/computer' require 'models/owner' require 'models/pet' require 'models/toy' @@ -424,3 +426,21 @@ class TimestampTest < ActiveRecord::TestCase assert_equal [:created_at, :updated_at], toy.send(:all_timestamp_attributes_in_model) end end + +class TimestampsWithoutTransactionTest < ActiveRecord::TestCase + include DdlHelper + self.use_transactional_fixtures = false + + class TimestampAttributePost < ActiveRecord::Base + attr_accessor :created_at, :updated_at + end + + def test_do_not_write_timestamps_on_save_if_they_are_not_attributes + with_example_table ActiveRecord::Base.connection, "timestamp_attribute_posts", "id integer primary key" do + post = TimestampAttributePost.new(id: 1) + post.save! # should not try to assign and persist created_at, updated_at + assert_nil post.created_at + assert_nil post.updated_at + end + end +end diff --git a/activerecord/test/cases/transaction_callbacks_test.rb b/activerecord/test/cases/transaction_callbacks_test.rb index 3d64ecb464..0f5caa52e3 100644 --- a/activerecord/test/cases/transaction_callbacks_test.rb +++ b/activerecord/test/cases/transaction_callbacks_test.rb @@ -129,6 +129,19 @@ class TransactionCallbacksTest < ActiveRecord::TestCase assert_equal [:commit_on_update], @first.history end + def test_only_call_after_commit_on_top_level_transactions + @first.after_commit_block{|r| r.history << :after_commit} + assert @first.history.empty? + + @first.transaction do + @first.transaction(requires_new: true) do + @first.touch + end + assert @first.history.empty? + end + assert_equal [:after_commit], @first.history + end + def test_call_after_rollback_after_transaction_rollsback @first.after_commit_block{|r| r.history << :after_commit} @first.after_rollback_block{|r| r.history << :after_rollback} @@ -254,6 +267,9 @@ class TransactionCallbacksTest < ActiveRecord::TestCase end def test_after_transaction_callbacks_should_prevent_callbacks_from_being_called + old_transaction_config = ActiveRecord::Base.raise_in_transactional_callbacks + ActiveRecord::Base.raise_in_transactional_callbacks = false + def @first.last_after_transaction_error=(e); @last_transaction_error = e; end def @first.last_after_transaction_error; @last_transaction_error; end @first.after_commit_block{|r| r.last_after_transaction_error = :commit; raise "fail!";} @@ -278,14 +294,91 @@ class TransactionCallbacksTest < ActiveRecord::TestCase end assert_equal :rollback, @first.last_after_transaction_error assert_equal [:after_rollback], second.history + ensure + ActiveRecord::Base.raise_in_transactional_callbacks = old_transaction_config + end + + def test_after_commit_should_not_raise_when_raise_in_transactional_callbacks_false + old_transaction_config = ActiveRecord::Base.raise_in_transactional_callbacks + ActiveRecord::Base.raise_in_transactional_callbacks = false + @first.after_commit_block{ fail "boom" } + Topic.transaction { @first.save! } + ensure + ActiveRecord::Base.raise_in_transactional_callbacks = old_transaction_config + end + + def test_after_commit_callback_should_not_swallow_errors + @first.after_commit_block{ fail "boom" } + assert_raises(RuntimeError) do + Topic.transaction do + @first.save! + end + end + end + + def test_after_commit_callback_when_raise_should_not_restore_state + first = TopicWithCallbacks.new + second = TopicWithCallbacks.new + first.after_commit_block{ fail "boom" } + second.after_commit_block{ fail "boom" } + + begin + Topic.transaction do + first.save! + assert_not_nil first.id + second.save! + assert_not_nil second.id + end + rescue + end + assert_not_nil first.id + assert_not_nil second.id + assert first.reload + end + + def test_after_rollback_callback_should_not_swallow_errors_when_set_to_raise + error_class = Class.new(StandardError) + @first.after_rollback_block{ raise error_class } + assert_raises(error_class) do + Topic.transaction do + @first.save! + raise ActiveRecord::Rollback + end + end + end + + def test_after_rollback_callback_when_raise_should_restore_state + error_class = Class.new(StandardError) + + first = TopicWithCallbacks.new + second = TopicWithCallbacks.new + first.after_rollback_block{ raise error_class } + second.after_rollback_block{ raise error_class } + + begin + Topic.transaction do + first.save! + assert_not_nil first.id + second.save! + assert_not_nil second.id + raise ActiveRecord::Rollback + end + rescue error_class + end + assert_nil first.id + assert_nil second.id end def test_after_rollback_callbacks_should_validate_on_condition assert_raise(ArgumentError) { Topic.after_rollback(on: :save) } + e = assert_raise(ArgumentError) { Topic.after_rollback(on: 'create') } + assert_match(/:on conditions for after_commit and after_rollback callbacks have to be one of \[:create, :destroy, :update\]/, e.message) end def test_after_commit_callbacks_should_validate_on_condition assert_raise(ArgumentError) { Topic.after_commit(on: :save) } + e = assert_raise(ArgumentError) { Topic.after_commit(on: 'create') } + assert_match(/:on conditions for after_commit and after_rollback callbacks have to be one of \[:create, :destroy, :update\]/, e.message) end def test_saving_a_record_with_a_belongs_to_that_specifies_touching_the_parent_should_call_callbacks_on_the_parent_object diff --git a/activerecord/test/cases/transactions_test.rb b/activerecord/test/cases/transactions_test.rb index de1f624191..0bbb4bb79e 100644 --- a/activerecord/test/cases/transactions_test.rb +++ b/activerecord/test/cases/transactions_test.rb @@ -2,6 +2,7 @@ require "cases/helper" require 'models/topic' require 'models/reply' require 'models/developer' +require 'models/computer' require 'models/book' require 'models/author' require 'models/post' @@ -12,7 +13,7 @@ class TransactionTest < ActiveRecord::TestCase fixtures :topics, :developers, :authors, :posts def setup - @first, @second = Topic.find(1, 2).sort_by { |t| t.id } + @first, @second = Topic.find(1, 2).sort_by(&:id) end def test_persisted_in_a_model_with_custom_primary_key_after_failed_save @@ -80,6 +81,30 @@ class TransactionTest < ActiveRecord::TestCase end end + def test_number_of_transactions_in_commit + num = nil + + Topic.connection.class_eval do + alias :real_commit_db_transaction :commit_db_transaction + define_method(:commit_db_transaction) do + num = transaction_manager.open_transactions + real_commit_db_transaction + end + end + + Topic.transaction do + @first.approved = true + @first.save! + end + + assert_equal 0, num + ensure + Topic.connection.class_eval do + remove_method :commit_db_transaction + alias :commit_db_transaction :real_commit_db_transaction rescue nil + end + end + def test_successful_with_instance_method @first.transaction do @first.approved = true @@ -424,6 +449,26 @@ class TransactionTest < ActiveRecord::TestCase end end + def test_savepoints_name + Topic.transaction do + assert_nil Topic.connection.current_savepoint_name + assert_nil Topic.connection.current_transaction.savepoint_name + + Topic.transaction(requires_new: true) do + assert_equal "active_record_1", Topic.connection.current_savepoint_name + assert_equal "active_record_1", Topic.connection.current_transaction.savepoint_name + + Topic.transaction(requires_new: true) do + assert_equal "active_record_2", Topic.connection.current_savepoint_name + assert_equal "active_record_2", Topic.connection.current_transaction.savepoint_name + end + + assert_equal "active_record_1", Topic.connection.current_savepoint_name + assert_equal "active_record_1", Topic.connection.current_transaction.savepoint_name + end + end + end + def test_rollback_when_commit_raises Topic.connection.expects(:begin_db_transaction) Topic.connection.expects(:commit_db_transaction).raises('OH NOES') @@ -448,6 +493,37 @@ class TransactionTest < ActiveRecord::TestCase assert topic.frozen?, 'not frozen' end + # The behavior of killed threads having a status of "aborting" was changed + # in Ruby 2.0, so Thread#kill on 1.9 will prematurely commit the transaction + # and there's nothing we can do about it. + if !RUBY_VERSION.start_with?('1.9') && !in_memory_db? + def test_rollback_when_thread_killed + queue = Queue.new + thread = Thread.new do + Topic.transaction do + @first.approved = true + @second.approved = false + @first.save + + queue.push nil + sleep + + @second.save + end + end + + queue.pop + thread.kill + thread.join + + assert @first.approved?, "First should still be changed in the objects" + assert !@second.approved?, "Second should still be changed in the objects" + + assert !Topic.find(1).approved?, "First shouldn't have been approved" + assert Topic.find(2).approved?, "Second should still be approved" + end + end + def test_restore_active_record_state_for_all_records_in_a_transaction topic_without_callbacks = Class.new(ActiveRecord::Base) do self.table_name = 'topics' @@ -486,6 +562,21 @@ class TransactionTest < ActiveRecord::TestCase assert !@second.destroyed?, 'not destroyed' end + def test_restore_frozen_state_after_double_destroy + topic = Topic.create + reply = topic.replies.create + + Topic.transaction do + topic.destroy # calls #destroy on reply (since dependent: destroy) + reply.destroy + + raise ActiveRecord::Rollback + end + + assert_not reply.frozen? + assert_not topic.frozen? + end + def test_sqlite_add_column_in_transaction return true unless current_adapter?(:SQLite3Adapter) @@ -526,13 +617,13 @@ class TransactionTest < ActiveRecord::TestCase def test_transactions_state_from_rollback connection = Topic.connection - transaction = ActiveRecord::ConnectionAdapters::ClosedTransaction.new(connection).begin + transaction = ActiveRecord::ConnectionAdapters::TransactionManager.new(connection).begin_transaction assert transaction.open? assert !transaction.state.rolledback? assert !transaction.state.committed? - transaction.perform_rollback + transaction.rollback assert transaction.state.rolledback? assert !transaction.state.committed? @@ -540,13 +631,13 @@ class TransactionTest < ActiveRecord::TestCase def test_transactions_state_from_commit connection = Topic.connection - transaction = ActiveRecord::ConnectionAdapters::ClosedTransaction.new(connection).begin + transaction = ActiveRecord::ConnectionAdapters::TransactionManager.new(connection).begin_transaction assert transaction.open? assert !transaction.state.rolledback? assert !transaction.state.committed? - transaction.perform_commit + transaction.commit assert !transaction.state.rolledback? assert transaction.state.committed? @@ -623,7 +714,7 @@ if current_adapter?(:PostgreSQLAdapter) end end - threads.each { |t| t.join } + threads.each(&:join) end end @@ -671,7 +762,7 @@ if current_adapter?(:PostgreSQLAdapter) Developer.connection.close end - threads.each { |t| t.join } + threads.each(&:join) end assert_equal original_salary, Developer.find(1).salary diff --git a/activerecord/test/cases/type/decimal_test.rb b/activerecord/test/cases/type/decimal_test.rb new file mode 100644 index 0000000000..34ed1d7b19 --- /dev/null +++ b/activerecord/test/cases/type/decimal_test.rb @@ -0,0 +1,51 @@ +require "cases/helper" + +module ActiveRecord + module Type + class DecimalTest < ActiveRecord::TestCase + def test_type_cast_decimal + type = Decimal.new + assert_equal BigDecimal.new("0"), type.type_cast_from_user(BigDecimal.new("0")) + assert_equal BigDecimal.new("123"), type.type_cast_from_user(123.0) + assert_equal BigDecimal.new("1"), type.type_cast_from_user(:"1") + end + + def test_type_cast_decimal_from_float_with_large_precision + type = Decimal.new(precision: ::Float::DIG + 2) + assert_equal BigDecimal.new("123.0"), type.type_cast_from_user(123.0) + end + + def test_type_cast_from_float_with_unspecified_precision + type = Decimal.new + assert_equal 22.68.to_d, type.type_cast_from_user(22.68) + end + + def test_type_cast_decimal_from_rational_with_precision + type = Decimal.new(precision: 2) + assert_equal BigDecimal("0.33"), type.type_cast_from_user(Rational(1, 3)) + end + + def test_type_cast_decimal_from_rational_without_precision_defaults_to_18_36 + type = Decimal.new + assert_equal BigDecimal("0.333333333333333333E0"), type.type_cast_from_user(Rational(1, 3)) + end + + def test_type_cast_decimal_from_object_responding_to_d + value = Object.new + def value.to_d + BigDecimal.new("1") + end + type = Decimal.new + assert_equal BigDecimal("1"), type.type_cast_from_user(value) + end + + def test_changed? + type = Decimal.new + + assert type.changed?(5.0, 5.0, '5.0wibble') + assert_not type.changed?(5.0, 5.0, '5.0') + assert_not type.changed?(-5.0, -5.0, '-5.0') + end + end + end +end diff --git a/activerecord/test/cases/type/integer_test.rb b/activerecord/test/cases/type/integer_test.rb new file mode 100644 index 0000000000..ff956b7680 --- /dev/null +++ b/activerecord/test/cases/type/integer_test.rb @@ -0,0 +1,114 @@ +require "cases/helper" +require "models/company" + +module ActiveRecord + module Type + class IntegerTest < ActiveRecord::TestCase + test "simple values" do + type = Type::Integer.new + assert_equal 1, type.type_cast_from_user(1) + assert_equal 1, type.type_cast_from_user('1') + assert_equal 1, type.type_cast_from_user('1ignore') + assert_equal 0, type.type_cast_from_user('bad1') + assert_equal 0, type.type_cast_from_user('bad') + assert_equal 1, type.type_cast_from_user(1.7) + assert_equal 0, type.type_cast_from_user(false) + assert_equal 1, type.type_cast_from_user(true) + assert_nil type.type_cast_from_user(nil) + end + + test "random objects cast to nil" do + type = Type::Integer.new + assert_nil type.type_cast_from_user([1,2]) + assert_nil type.type_cast_from_user({1 => 2}) + assert_nil type.type_cast_from_user((1..2)) + end + + test "casting ActiveRecord models" do + type = Type::Integer.new + firm = Firm.create(:name => 'Apple') + assert_nil type.type_cast_from_user(firm) + end + + test "casting objects without to_i" do + type = Type::Integer.new + assert_nil type.type_cast_from_user(::Object.new) + end + + test "casting nan and infinity" do + type = Type::Integer.new + assert_nil type.type_cast_from_user(::Float::NAN) + assert_nil type.type_cast_from_user(1.0/0.0) + end + + test "casting booleans for database" do + type = Type::Integer.new + assert_equal 1, type.type_cast_for_database(true) + assert_equal 0, type.type_cast_for_database(false) + end + + test "changed?" do + type = Type::Integer.new + + assert type.changed?(5, 5, '5wibble') + assert_not type.changed?(5, 5, '5') + assert_not type.changed?(5, 5, '5.0') + assert_not type.changed?(-5, -5, '-5') + assert_not type.changed?(-5, -5, '-5.0') + assert_not type.changed?(nil, nil, nil) + end + + test "values below int min value are out of range" do + assert_raises(::RangeError) do + Integer.new.type_cast_from_user("-2147483649") + end + end + + test "values above int max value are out of range" do + assert_raises(::RangeError) do + Integer.new.type_cast_from_user("2147483648") + end + end + + test "very small numbers are out of range" do + assert_raises(::RangeError) do + Integer.new.type_cast_from_user("-9999999999999999999999999999999") + end + end + + test "very large numbers are out of range" do + assert_raises(::RangeError) do + Integer.new.type_cast_from_user("9999999999999999999999999999999") + end + end + + test "normal numbers are in range" do + type = Integer.new + assert_equal(0, type.type_cast_from_user("0")) + assert_equal(-1, type.type_cast_from_user("-1")) + assert_equal(1, type.type_cast_from_user("1")) + end + + test "int max value is in range" do + assert_equal(2147483647, Integer.new.type_cast_from_user("2147483647")) + end + + test "int min value is in range" do + assert_equal(-2147483648, Integer.new.type_cast_from_user("-2147483648")) + end + + test "columns with a larger limit have larger ranges" do + type = Integer.new(limit: 8) + + assert_equal(9223372036854775807, type.type_cast_from_user("9223372036854775807")) + assert_equal(-9223372036854775808, type.type_cast_from_user("-9223372036854775808")) + assert_raises(::RangeError) do + type.type_cast_from_user("-9999999999999999999999999999999") + end + assert_raises(::RangeError) do + type.type_cast_from_user("9999999999999999999999999999999") + end + end + end + end +end diff --git a/activerecord/test/cases/type/string_test.rb b/activerecord/test/cases/type/string_test.rb new file mode 100644 index 0000000000..4d78f287f1 --- /dev/null +++ b/activerecord/test/cases/type/string_test.rb @@ -0,0 +1,36 @@ +require 'cases/helper' + +module ActiveRecord + class StringTypeTest < ActiveRecord::TestCase + test "type casting" do + type = Type::String.new + assert_equal "t", type.type_cast_from_user(true) + assert_equal "f", type.type_cast_from_user(false) + assert_equal "123", type.type_cast_from_user(123) + end + + test "values are duped coming out" do + s = "foo" + type = Type::String.new + assert_not_same s, type.type_cast_from_user(s) + assert_not_same s, type.type_cast_from_database(s) + end + + test "string mutations are detected" do + klass = Class.new(Base) + klass.table_name = 'authors' + + author = klass.create!(name: 'Sean') + assert_not author.changed? + + author.name << ' Griffin' + assert author.name_changed? + + author.save! + author.reload + + assert_equal 'Sean Griffin', author.name + assert_not author.changed? + end + end +end diff --git a/activerecord/test/cases/type/type_map_test.rb b/activerecord/test/cases/type/type_map_test.rb index 4e32f92dd0..172c6dfc4c 100644 --- a/activerecord/test/cases/type/type_map_test.rb +++ b/activerecord/test/cases/type/type_map_test.rb @@ -124,6 +124,53 @@ module ActiveRecord assert_equal mapping.lookup(3), 'string' assert_kind_of Type::Value, mapping.lookup(4) end + + def test_fetch + mapping = TypeMap.new + mapping.register_type(1, "string") + + assert_equal "string", mapping.fetch(1) { "int" } + assert_equal "int", mapping.fetch(2) { "int" } + end + + def test_fetch_yields_args + mapping = TypeMap.new + + assert_equal "foo-1-2-3", mapping.fetch("foo", 1, 2, 3) { |*args| args.join("-") } + assert_equal "bar-1-2-3", mapping.fetch("bar", 1, 2, 3) { |*args| args.join("-") } + end + + def test_fetch_memoizes + mapping = TypeMap.new + + looked_up = false + mapping.register_type(1) do + fail if looked_up + looked_up = true + "string" + end + + assert_equal "string", mapping.fetch(1) + assert_equal "string", mapping.fetch(1) + end + + def test_fetch_memoizes_on_args + mapping = TypeMap.new + mapping.register_type("foo") { |*args| args.join("-") } + + assert_equal "foo-1-2-3", mapping.fetch("foo", 1, 2, 3) { |*args| args.join("-") } + assert_equal "foo-2-3-4", mapping.fetch("foo", 2, 3, 4) { |*args| args.join("-") } + end + + def test_register_clears_cache + mapping = TypeMap.new + + mapping.register_type(1, "string") + mapping.lookup(1) + mapping.register_type(1, "int") + + assert_equal "int", mapping.lookup(1) + end end end end diff --git a/activerecord/test/cases/type/unsigned_integer_test.rb b/activerecord/test/cases/type/unsigned_integer_test.rb new file mode 100644 index 0000000000..b6f673109e --- /dev/null +++ b/activerecord/test/cases/type/unsigned_integer_test.rb @@ -0,0 +1,17 @@ +require "cases/helper" + +module ActiveRecord + module Type + class UnsignedIntegerTest < ActiveRecord::TestCase + test "unsigned int max value is in range" do + assert_equal(4294967295, UnsignedInteger.new.type_cast_from_user("4294967295")) + end + + test "minus value is out of range" do + assert_raises(::RangeError) do + UnsignedInteger.new.type_cast_from_user("-1") + end + end + end + end +end diff --git a/activerecord/test/cases/types_test.rb b/activerecord/test/cases/types_test.rb index 731f8cfba3..b0979cbe1f 100644 --- a/activerecord/test/cases/types_test.rb +++ b/activerecord/test/cases/types_test.rb @@ -1,5 +1,4 @@ require "cases/helper" -require 'models/company' module ActiveRecord module ConnectionAdapters @@ -29,54 +28,12 @@ module ActiveRecord assert_equal false, type.type_cast_from_user('FALSE') assert_equal false, type.type_cast_from_user('off') assert_equal false, type.type_cast_from_user('OFF') - assert_equal false, type.type_cast_from_user(' ') - assert_equal false, type.type_cast_from_user("\u3000\r\n") - assert_equal false, type.type_cast_from_user("\u0000") - assert_equal false, type.type_cast_from_user('SOMETHING RANDOM') - end - - def test_type_cast_string - type = Type::String.new - assert_equal "1", type.type_cast_from_user(true) - assert_equal "0", type.type_cast_from_user(false) - assert_equal "123", type.type_cast_from_user(123) - end - - def test_type_cast_integer - type = Type::Integer.new - assert_equal 1, type.type_cast_from_user(1) - assert_equal 1, type.type_cast_from_user('1') - assert_equal 1, type.type_cast_from_user('1ignore') - assert_equal 0, type.type_cast_from_user('bad1') - assert_equal 0, type.type_cast_from_user('bad') - assert_equal 1, type.type_cast_from_user(1.7) - assert_equal 0, type.type_cast_from_user(false) - assert_equal 1, type.type_cast_from_user(true) - assert_nil type.type_cast_from_user(nil) - end - - def test_type_cast_non_integer_to_integer - type = Type::Integer.new - assert_nil type.type_cast_from_user([1,2]) - assert_nil type.type_cast_from_user({1 => 2}) - assert_nil type.type_cast_from_user((1..2)) - end - - def test_type_cast_activerecord_to_integer - type = Type::Integer.new - firm = Firm.create(:name => 'Apple') - assert_nil type.type_cast_from_user(firm) - end - - def test_type_cast_object_without_to_i_to_integer - type = Type::Integer.new - assert_nil type.type_cast_from_user(Object.new) - end - - def test_type_cast_nan_and_infinity_to_integer - type = Type::Integer.new - assert_nil type.type_cast_from_user(Float::NAN) - assert_nil type.type_cast_from_user(1.0/0.0) + assert_deprecated do + assert_equal false, type.type_cast_from_user(' ') + assert_equal false, type.type_cast_from_user("\u3000\r\n") + assert_equal false, type.type_cast_from_user("\u0000") + assert_equal false, type.type_cast_from_user('SOMETHING RANDOM') + end end def test_type_cast_float @@ -84,11 +41,13 @@ module ActiveRecord assert_equal 1.0, type.type_cast_from_user("1") end - def test_type_cast_decimal - type = Type::Decimal.new - assert_equal BigDecimal.new("0"), type.type_cast_from_user(BigDecimal.new("0")) - assert_equal BigDecimal.new("123"), type.type_cast_from_user(123.0) - assert_equal BigDecimal.new("1"), type.type_cast_from_user(:"1") + def test_changing_float + type = Type::Float.new + + assert type.changed?(5.0, 5.0, '5wibble') + assert_not type.changed?(5.0, 5.0, '5') + assert_not type.changed?(5.0, 5.0, '5.0') + assert_not type.changed?(nil, nil, nil) end def test_type_cast_binary @@ -145,6 +104,12 @@ module ActiveRecord end end + def test_type_equality + assert_equal Type::Value.new, Type::Value.new + assert_not_equal Type::Value.new, Type::Integer.new + assert_not_equal Type::Value.new(precision: 1), Type::Value.new(precision: 2) + end + if current_adapter?(:SQLite3Adapter) def test_binary_encoding type = SQLite3Binary.new diff --git a/activerecord/test/cases/validations/i18n_validation_test.rb b/activerecord/test/cases/validations/i18n_validation_test.rb index 3db742c15b..268d7914b5 100644 --- a/activerecord/test/cases/validations/i18n_validation_test.rb +++ b/activerecord/test/cases/validations/i18n_validation_test.rb @@ -6,6 +6,7 @@ class I18nValidationTest < ActiveRecord::TestCase repair_validations(Topic, Reply) def setup + repair_validations(Topic, Reply) Reply.validates_presence_of(:title) @topic = Topic.new @old_load_path, @old_backend = I18n.load_path.dup, I18n.backend diff --git a/activerecord/test/cases/validations/length_validation_test.rb b/activerecord/test/cases/validations/length_validation_test.rb index 4a92da38ce..2c0e282761 100644 --- a/activerecord/test/cases/validations/length_validation_test.rb +++ b/activerecord/test/cases/validations/length_validation_test.rb @@ -2,6 +2,7 @@ require "cases/helper" require 'models/owner' require 'models/pet' +require 'models/person' class LengthValidationTest < ActiveRecord::TestCase fixtures :owners @@ -44,4 +45,21 @@ class LengthValidationTest < ActiveRecord::TestCase assert o.valid? end end + + def test_validates_size_of_reprects_records_marked_for_destruction + assert_nothing_raised { Owner.validates_size_of :pets, minimum: 1 } + owner = Owner.new + assert_not owner.save + assert owner.errors[:pets].any? + pet = owner.pets.build + assert owner.valid? + assert owner.save + + pet_count = Pet.count + assert_not owner.update_attributes pets_attributes: [ {_destroy: 1, id: pet.id} ] + assert_not owner.valid? + assert owner.errors[:pets].any? + assert_equal pet_count, Pet.count + end + end diff --git a/activerecord/test/cases/validations/presence_validation_test.rb b/activerecord/test/cases/validations/presence_validation_test.rb index 3790d3c8cf..4f38849131 100644 --- a/activerecord/test/cases/validations/presence_validation_test.rb +++ b/activerecord/test/cases/validations/presence_validation_test.rb @@ -52,14 +52,15 @@ class PresenceValidationTest < ActiveRecord::TestCase end def test_validates_presence_doesnt_convert_to_array - Speedometer.validates_presence_of :dashboard + speedometer = Class.new(Speedometer) + speedometer.validates_presence_of :dashboard dash = Dashboard.new # dashboard has to_a method def dash.to_a; ['(/)', '(\)']; end - s = Speedometer.new + s = speedometer.new s.dashboard = dash assert_nothing_raised { s.valid? } diff --git a/activerecord/test/cases/validations/uniqueness_validation_test.rb b/activerecord/test/cases/validations/uniqueness_validation_test.rb index 18221cc73d..524f59876e 100644 --- a/activerecord/test/cases/validations/uniqueness_validation_test.rb +++ b/activerecord/test/cases/validations/uniqueness_validation_test.rb @@ -30,18 +30,13 @@ class ReplyWithTitleObject < Reply def title; ReplyTitle.new; end end -class Employee < ActiveRecord::Base - self.table_name = 'postgresql_arrays' - validates_uniqueness_of :nicknames -end - class TopicWithUniqEvent < Topic belongs_to :event, foreign_key: :parent_id validates :event, uniqueness: true end class UniquenessValidationTest < ActiveRecord::TestCase - fixtures :topics, 'warehouse-things', :developers + fixtures :topics, 'warehouse-things' repair_validations(Topic, Reply) @@ -378,18 +373,6 @@ class UniquenessValidationTest < ActiveRecord::TestCase } end - if current_adapter? :PostgreSQLAdapter - def test_validate_uniqueness_with_array_column - e1 = Employee.create("nicknames" => ["john", "johnny"], "commission_by_quarter" => [1000, 1200]) - assert e1.persisted?, "Saving e1" - - e2 = Employee.create("nicknames" => ["john", "johnny"], "commission_by_quarter" => [2200]) - assert !e2.persisted?, "e2 shouldn't be valid" - assert e2.errors[:nicknames].any?, "Should have errors for nicknames" - assert_equal ["has already been taken"], e2.errors[:nicknames], "Should have uniqueness message for nicknames" - end - end - def test_validate_uniqueness_on_existing_relation event = Event.create assert TopicWithUniqEvent.create(event: event).valid? diff --git a/activerecord/test/cases/validations_repair_helper.rb b/activerecord/test/cases/validations_repair_helper.rb index c02b3241cd..b30666d876 100644 --- a/activerecord/test/cases/validations_repair_helper.rb +++ b/activerecord/test/cases/validations_repair_helper.rb @@ -5,19 +5,15 @@ module ActiveRecord module ClassMethods def repair_validations(*model_classes) teardown do - model_classes.each do |k| - k.clear_validators! - end + model_classes.each(&:clear_validators!) end end end def repair_validations(*model_classes) - yield + yield if block_given? ensure - model_classes.each do |k| - k.clear_validators! - end + model_classes.each(&:clear_validators!) end end end diff --git a/activerecord/test/cases/validations_test.rb b/activerecord/test/cases/validations_test.rb index a6e1dc72e5..959c58aa85 100644 --- a/activerecord/test/cases/validations_test.rb +++ b/activerecord/test/cases/validations_test.rb @@ -4,6 +4,7 @@ require 'models/topic' require 'models/reply' require 'models/person' require 'models/developer' +require 'models/computer' require 'models/parrot' require 'models/company' @@ -78,6 +79,20 @@ class ValidationsTest < ActiveRecord::TestCase assert_equal r, invalid.record end + def test_validate_with_bang + assert_raise(ActiveRecord::RecordInvalid) do + WrongReply.new.validate! + end + end + + def test_validate_with_bang_and_context + assert_raise(ActiveRecord::RecordInvalid) do + WrongReply.new.validate!(:special_case) + end + r = WrongReply.new(:title => "Valid title", :author_name => "secret", :content => "Good") + assert r.validate!(:special_case) + end + def test_exception_on_create_bang_many assert_raise(ActiveRecord::RecordInvalid) do WrongReply.create!([ { "title" => "OK" }, { "title" => "Wrong Create" }]) @@ -134,4 +149,17 @@ class ValidationsTest < ActiveRecord::TestCase assert_equal 1, Company.validators_on(:name).size end + def test_numericality_validation_with_mutation + Topic.class_eval do + attribute :wibble, ActiveRecord::Type::String.new + validates_numericality_of :wibble, only_integer: true + end + + topic = Topic.new(wibble: '123-4567') + topic.wibble.gsub!('-', '') + + assert topic.valid? + ensure + Topic.reset_column_information + end end diff --git a/activerecord/test/cases/view_test.rb b/activerecord/test/cases/view_test.rb new file mode 100644 index 0000000000..3aed90ba36 --- /dev/null +++ b/activerecord/test/cases/view_test.rb @@ -0,0 +1,113 @@ +require "cases/helper" +require "models/book" + +module ViewBehavior + extend ActiveSupport::Concern + + included do + fixtures :books + end + + class Ebook < ActiveRecord::Base + self.primary_key = "id" + end + + def setup + super + @connection = ActiveRecord::Base.connection + create_view "ebooks", <<-SQL + SELECT id, name, status FROM books WHERE format = 'ebook' + SQL + end + + def teardown + super + drop_view "ebooks" + end + + def test_reading + books = Ebook.all + assert_equal [books(:rfr).id], books.map(&:id) + assert_equal ["Ruby for Rails"], books.map(&:name) + end + + def test_table_exists + view_name = Ebook.table_name + assert @connection.table_exists?(view_name), "'#{view_name}' table should exist" + end + + def test_column_definitions + assert_equal([["id", :integer], + ["name", :string], + ["status", :integer]], Ebook.columns.map { |c| [c.name, c.type] }) + end + + def test_attributes + assert_equal({"id" => 2, "name" => "Ruby for Rails", "status" => 0}, + Ebook.first.attributes) + end + + def test_does_not_assume_id_column_as_primary_key + model = Class.new(ActiveRecord::Base) do + self.table_name = "ebooks" + end + assert_nil model.primary_key + end +end + +if ActiveRecord::Base.connection.supports_views? +class ViewWithPrimaryKeyTest < ActiveRecord::TestCase + include ViewBehavior + + private + def create_view(name, query) + @connection.execute "CREATE VIEW #{name} AS #{query}" + end + + def drop_view(name) + @connection.execute "DROP VIEW #{name}" if @connection.table_exists? name + end +end + +class ViewWithoutPrimaryKeyTest < ActiveRecord::TestCase + fixtures :books + + class Paperback < ActiveRecord::Base; end + + setup do + @connection = ActiveRecord::Base.connection + @connection.execute <<-SQL + CREATE VIEW paperbacks + AS SELECT name, status FROM books WHERE format = 'paperback' + SQL + end + + teardown do + @connection.execute "DROP VIEW paperbacks" if @connection.table_exists? "paperbacks" + end + + def test_reading + books = Paperback.all + assert_equal ["Agile Web Development with Rails"], books.map(&:name) + end + + def test_table_exists + view_name = Paperback.table_name + assert @connection.table_exists?(view_name), "'#{view_name}' table should exist" + end + + def test_column_definitions + assert_equal([["name", :string], + ["status", :integer]], Paperback.columns.map { |c| [c.name, c.type] }) + end + + def test_attributes + assert_equal({"name" => "Agile Web Development with Rails", "status" => 0}, + Paperback.first.attributes) + end + + def test_does_not_have_a_primary_key + assert_nil Paperback.primary_key + end +end +end diff --git a/activerecord/test/cases/yaml_serialization_test.rb b/activerecord/test/cases/yaml_serialization_test.rb index 9f1d110ddb..bce59b4fcd 100644 --- a/activerecord/test/cases/yaml_serialization_test.rb +++ b/activerecord/test/cases/yaml_serialization_test.rb @@ -1,5 +1,6 @@ require 'cases/helper' require 'models/topic' +require 'models/reply' require 'models/post' require 'models/author' diff --git a/activerecord/test/config.example.yml b/activerecord/test/config.example.yml index a54914c372..e3b55d640e 100644 --- a/activerecord/test/config.example.yml +++ b/activerecord/test/config.example.yml @@ -55,6 +55,7 @@ connections: arunit: username: rails encoding: utf8 + collation: utf8_unicode_ci arunit2: username: rails encoding: utf8 @@ -63,16 +64,11 @@ connections: arunit: username: rails encoding: utf8 + collation: utf8_unicode_ci arunit2: username: rails encoding: utf8 - openbase: - arunit: - username: admin - arunit2: - username: admin - oracle: arunit: adapter: oracle_enhanced diff --git a/activerecord/test/fixtures/bulbs.yml b/activerecord/test/fixtures/bulbs.yml new file mode 100644 index 0000000000..e5ce2b796c --- /dev/null +++ b/activerecord/test/fixtures/bulbs.yml @@ -0,0 +1,5 @@ +defaulty: + name: defaulty + +special: + name: special diff --git a/activerecord/test/fixtures/computers.yml b/activerecord/test/fixtures/computers.yml index 7281a4d768..ad5ae2ec71 100644 --- a/activerecord/test/fixtures/computers.yml +++ b/activerecord/test/fixtures/computers.yml @@ -3,3 +3,8 @@ workstation: system: 'Linux' developer: 1 extendedWarranty: 1 + +laptop: + system: 'MacOS 1' + developer: 1 + extendedWarranty: 1 diff --git a/activerecord/test/fixtures/developers.yml b/activerecord/test/fixtures/developers.yml index 3656564f63..54b74e7a74 100644 --- a/activerecord/test/fixtures/developers.yml +++ b/activerecord/test/fixtures/developers.yml @@ -2,6 +2,7 @@ david: id: 1 name: David salary: 80000 + shared_computers: laptop jamis: id: 2 @@ -18,4 +19,4 @@ dev_<%= digit %>: poor_jamis: id: 11 name: Jamis - salary: 9000
\ No newline at end of file + salary: 9000 diff --git a/activerecord/test/fixtures/fk_test_has_pk.yml b/activerecord/test/fixtures/fk_test_has_pk.yml index c93952180b..73882bac41 100644 --- a/activerecord/test/fixtures/fk_test_has_pk.yml +++ b/activerecord/test/fixtures/fk_test_has_pk.yml @@ -1,2 +1,2 @@ first: - id: 1
\ No newline at end of file + pk_id: 1
\ No newline at end of file diff --git a/activerecord/test/fixtures/naked/csv/accounts.csv b/activerecord/test/fixtures/naked/csv/accounts.csv deleted file mode 100644 index 8b13789179..0000000000 --- a/activerecord/test/fixtures/naked/csv/accounts.csv +++ /dev/null @@ -1 +0,0 @@ - diff --git a/activerecord/test/fixtures/posts.yml b/activerecord/test/fixtures/posts.yml index 7298096025..86d46f753a 100644 --- a/activerecord/test/fixtures/posts.yml +++ b/activerecord/test/fixtures/posts.yml @@ -4,7 +4,6 @@ welcome: title: Welcome to the weblog body: Such a lovely day comments_count: 2 - taggings_count: 1 tags_count: 1 type: Post @@ -14,7 +13,6 @@ thinking: title: So I was thinking body: Like I hopefully always am comments_count: 1 - taggings_count: 1 tags_count: 1 type: SpecialPost diff --git a/activerecord/test/models/author.rb b/activerecord/test/models/author.rb index 8949cf5826..8c1f14bd36 100644 --- a/activerecord/test/models/author.rb +++ b/activerecord/test/models/author.rb @@ -1,5 +1,6 @@ class Author < ActiveRecord::Base has_many :posts + has_many :serialized_posts has_one :post has_many :very_special_comments, :through => :posts has_many :posts_with_comments, -> { includes(:comments) }, :class_name => "Post" @@ -44,13 +45,14 @@ class Author < ActiveRecord::Base has_many :special_posts has_many :special_post_comments, :through => :special_posts, :source => :comments + has_many :special_posts_with_default_scope, :class_name => 'SpecialPostWithDefaultScope' has_many :sti_posts, :class_name => 'StiPost' has_many :sti_post_comments, :through => :sti_posts, :source => :comments - has_many :special_nonexistant_posts, -> { where("posts.body = 'nonexistant'") }, :class_name => "SpecialPost" - has_many :special_nonexistant_post_comments, -> { where('comments.post_id' => 0) }, :through => :special_nonexistant_posts, :source => :comments - has_many :nonexistant_comments, :through => :posts + has_many :special_nonexistent_posts, -> { where("posts.body = 'nonexistent'") }, :class_name => "SpecialPost" + has_many :special_nonexistent_post_comments, -> { where('comments.post_id' => 0) }, :through => :special_nonexistent_posts, :source => :comments + has_many :nonexistent_comments, :through => :posts has_many :hello_posts, -> { where "posts.body = 'hello'" }, :class_name => "Post" has_many :hello_post_comments, :through => :hello_posts, :source => :comments diff --git a/activerecord/test/models/club.rb b/activerecord/test/models/club.rb index a762ad4bb5..6ceafe5858 100644 --- a/activerecord/test/models/club.rb +++ b/activerecord/test/models/club.rb @@ -14,3 +14,10 @@ class Club < ActiveRecord::Base "I'm sorry sir, this is a *private* club, not a *pirate* club" end end + +class SuperClub < ActiveRecord::Base + self.table_name = "clubs" + + has_many :memberships, class_name: 'SuperMembership', foreign_key: 'club_id' + has_many :members, through: :memberships +end diff --git a/activerecord/test/models/comment.rb b/activerecord/test/models/comment.rb index 15970758db..b38b17e90e 100644 --- a/activerecord/test/models/comment.rb +++ b/activerecord/test/models/comment.rb @@ -9,6 +9,7 @@ class Comment < ActiveRecord::Base belongs_to :post, :counter_cache => true belongs_to :author, polymorphic: true belongs_to :resource, polymorphic: true + belongs_to :developer has_many :ratings @@ -51,3 +52,8 @@ class CommentThatAutomaticallyAltersPostBody < Comment comment.post.update_attributes(body: "Automatically altered") end end + +class CommentWithDefaultScopeReferencesAssociation < Comment + default_scope ->{ includes(:developer).order('developers.name').references(:developer) } + belongs_to :developer +end diff --git a/activerecord/test/models/company.rb b/activerecord/test/models/company.rb index 76411ecb37..5a56616eb9 100644 --- a/activerecord/test/models/company.rb +++ b/activerecord/test/models/company.rb @@ -10,6 +10,7 @@ class Company < AbstractCompany has_one :dummy_account, :foreign_key => "firm_id", :class_name => "Account" has_many :contracts has_many :developers, :through => :contracts + has_many :accounts scope :of_first_firm, lambda { joins(:account => :firm). @@ -71,6 +72,7 @@ class Firm < Company # Oracle tests were failing because of that as the second fixture was selected has_one :account_using_primary_key, -> { order('id') }, :primary_key => "firm_id", :class_name => "Account" has_one :account_using_foreign_and_primary_keys, :foreign_key => "firm_name", :primary_key => "name", :class_name => "Account" + has_one :account_with_inexistent_foreign_key, class_name: 'Account', foreign_key: "inexistent" has_one :deletable_account, :foreign_key => "firm_id", :class_name => "Account", :dependent => :delete has_one :account_limit_500_with_hash_conditions, -> { where :credit_limit => 500 }, :foreign_key => "firm_id", :class_name => "Account" diff --git a/activerecord/test/models/contact.rb b/activerecord/test/models/contact.rb index a1cb8d62b6..3ea17c3abf 100644 --- a/activerecord/test/models/contact.rb +++ b/activerecord/test/models/contact.rb @@ -8,6 +8,7 @@ module ContactFakeColumns table_name => 'id' } + column :id, :integer column :name, :string column :age, :integer column :avatar, :binary diff --git a/activerecord/test/models/customer.rb b/activerecord/test/models/customer.rb index 7e8e82542f..afe4b3d707 100644 --- a/activerecord/test/models/customer.rb +++ b/activerecord/test/models/customer.rb @@ -2,7 +2,7 @@ class Customer < ActiveRecord::Base cattr_accessor :gps_conversion_was_run composed_of :address, :mapping => [ %w(address_street street), %w(address_city city), %w(address_country country) ], :allow_nil => true - composed_of :balance, :class_name => "Money", :mapping => %w(balance amount), :converter => Proc.new { |balance| balance.to_money } + composed_of :balance, :class_name => "Money", :mapping => %w(balance amount), :converter => Proc.new(&:to_money) composed_of :gps_location, :allow_nil => true composed_of :non_blank_gps_location, :class_name => "GpsLocation", :allow_nil => true, :mapping => %w(gps_location gps_location), :converter => lambda { |gps| self.gps_conversion_was_run = true; gps.blank? ? nil : GpsLocation.new(gps)} diff --git a/activerecord/test/models/developer.rb b/activerecord/test/models/developer.rb index 5bd2f00129..d2a5a7fc49 100644 --- a/activerecord/test/models/developer.rb +++ b/activerecord/test/models/developer.rb @@ -15,6 +15,8 @@ class Developer < ActiveRecord::Base accepts_nested_attributes_for :projects + has_and_belongs_to_many :shared_computers, class_name: "Computer" + has_and_belongs_to_many :projects_extended_by_name, -> { extending(DeveloperProjectsAssociationExtension) }, :class_name => "Project", @@ -46,6 +48,8 @@ class Developer < ActiveRecord::Base has_many :audit_logs has_many :contracts has_many :firms, :through => :contracts, :source => :firm + has_many :comments, ->(developer) { where(body: "I'm #{developer.name}") } + has_many :ratings, through: :comments scope :jamises, -> { where(:name => 'Jamis') } diff --git a/activerecord/test/models/face.rb b/activerecord/test/models/face.rb index 3d7f0626e2..91e46f83e5 100644 --- a/activerecord/test/models/face.rb +++ b/activerecord/test/models/face.rb @@ -1,7 +1,8 @@ class Face < ActiveRecord::Base belongs_to :man, :inverse_of => :face belongs_to :polymorphic_man, :polymorphic => true, :inverse_of => :polymorphic_face - belongs_to :polymorphic_man_without_inverse, :polymorphic => true + # Oracle identifier lengh is limited to 30 bytes or less, `polymorphic` renamed `poly` + belongs_to :poly_man_without_inverse, :polymorphic => true # These is a "broken" inverse_of for the purposes of testing belongs_to :horrible_man, :class_name => 'Man', :inverse_of => :horrible_face belongs_to :horrible_polymorphic_man, :polymorphic => true, :inverse_of => :horrible_polymorphic_face diff --git a/activerecord/test/models/image.rb b/activerecord/test/models/image.rb new file mode 100644 index 0000000000..7ae8e4a7f6 --- /dev/null +++ b/activerecord/test/models/image.rb @@ -0,0 +1,3 @@ +class Image < ActiveRecord::Base + belongs_to :imageable, foreign_key: :imageable_identifier, foreign_type: :imageable_class +end diff --git a/activerecord/test/models/man.rb b/activerecord/test/models/man.rb index a26491ce61..4fbb6b226b 100644 --- a/activerecord/test/models/man.rb +++ b/activerecord/test/models/man.rb @@ -1,7 +1,7 @@ class Man < ActiveRecord::Base has_one :face, :inverse_of => :man has_one :polymorphic_face, :class_name => 'Face', :as => :polymorphic_man, :inverse_of => :polymorphic_man - has_one :polymorphic_face_without_inverse, :class_name => 'Face', :as => :polymorphic_man_without_inverse + has_one :polymorphic_face_without_inverse, :class_name => 'Face', :as => :poly_man_without_inverse has_many :interests, :inverse_of => :man has_many :polymorphic_interests, :class_name => 'Interest', :as => :polymorphic_man, :inverse_of => :polymorphic_man # These are "broken" inverse_of associations for the purposes of testing diff --git a/activerecord/test/models/member.rb b/activerecord/test/models/member.rb index 72095f9236..dc0566d8a7 100644 --- a/activerecord/test/models/member.rb +++ b/activerecord/test/models/member.rb @@ -27,6 +27,9 @@ class Member < ActiveRecord::Base has_many :clubs, :through => :current_memberships has_one :club_through_many, :through => :current_memberships, :source => :club + + belongs_to :admittable, polymorphic: true + has_one :premium_club, through: :admittable end class SelfMember < ActiveRecord::Base diff --git a/activerecord/test/models/owner.rb b/activerecord/test/models/owner.rb index 2e3a9a3681..cedb774b10 100644 --- a/activerecord/test/models/owner.rb +++ b/activerecord/test/models/owner.rb @@ -17,6 +17,8 @@ class Owner < ActiveRecord::Base after_commit :execute_blocks + accepts_nested_attributes_for :pets, allow_destroy: true + def blocks @blocks ||= [] end diff --git a/activerecord/test/models/parrot.rb b/activerecord/test/models/parrot.rb index e76e83f314..8c83de573f 100644 --- a/activerecord/test/models/parrot.rb +++ b/activerecord/test/models/parrot.rb @@ -19,7 +19,7 @@ class LiveParrot < Parrot end class DeadParrot < Parrot - belongs_to :killer, :class_name => 'Pirate' + belongs_to :killer, :class_name => 'Pirate', foreign_key: :killer_id end class FunkyParrot < Parrot diff --git a/activerecord/test/models/person.rb b/activerecord/test/models/person.rb index c7e54e7b63..ad12f00d42 100644 --- a/activerecord/test/models/person.rb +++ b/activerecord/test/models/person.rb @@ -30,6 +30,8 @@ class Person < ActiveRecord::Base has_many :agents_of_agents, :through => :agents, :source => :agents belongs_to :number1_fan, :class_name => 'Person' + has_many :personal_legacy_things, :dependent => :destroy + has_many :agents_posts, :through => :agents, :source => :posts has_many :agents_posts_authors, :through => :agents_posts, :source => :author has_many :essays, primary_key: "first_name", foreign_key: "writer_id" diff --git a/activerecord/test/models/personal_legacy_thing.rb b/activerecord/test/models/personal_legacy_thing.rb new file mode 100644 index 0000000000..a7ee3a0bca --- /dev/null +++ b/activerecord/test/models/personal_legacy_thing.rb @@ -0,0 +1,4 @@ +class PersonalLegacyThing < ActiveRecord::Base + self.locking_column = :version + belongs_to :person, :counter_cache => true +end diff --git a/activerecord/test/models/pirate.rb b/activerecord/test/models/pirate.rb index 90a3c3ecee..641a33f9be 100644 --- a/activerecord/test/models/pirate.rb +++ b/activerecord/test/models/pirate.rb @@ -36,8 +36,8 @@ class Pirate < ActiveRecord::Base has_one :foo_bulb, -> { where :name => 'foo' }, :foreign_key => :car_id, :class_name => "Bulb" - accepts_nested_attributes_for :parrots, :birds, :allow_destroy => true, :reject_if => proc { |attributes| attributes.empty? } - accepts_nested_attributes_for :ship, :allow_destroy => true, :reject_if => proc { |attributes| attributes.empty? } + accepts_nested_attributes_for :parrots, :birds, :allow_destroy => true, :reject_if => proc(&:empty?) + accepts_nested_attributes_for :ship, :allow_destroy => true, :reject_if => proc(&:empty?) accepts_nested_attributes_for :update_only_ship, :update_only => true accepts_nested_attributes_for :parrots_with_method_callbacks, :parrots_with_proc_callbacks, :birds_with_method_callbacks, :birds_with_proc_callbacks, :allow_destroy => true diff --git a/activerecord/test/models/post.rb b/activerecord/test/models/post.rb index 5f01ab0a82..56073cc588 100644 --- a/activerecord/test/models/post.rb +++ b/activerecord/test/models/post.rb @@ -41,6 +41,7 @@ class Post < ActiveRecord::Base scope :with_tags, -> { preload(:taggings) } scope :tagged_with, ->(id) { joins(:taggings).where(taggings: { tag_id: id }) } + scope :tagged_with_comment, ->(comment) { joins(:taggings).where(taggings: { comment: comment }) } has_many :comments do def find_most_recent @@ -77,8 +78,9 @@ class Post < ActiveRecord::Base has_one :very_special_comment has_one :very_special_comment_with_post, -> { includes(:post) }, :class_name => "VerySpecialComment" + has_one :very_special_comment_with_post_with_joins, -> { joins(:post).order('posts.id') }, class_name: "VerySpecialComment" has_many :special_comments - has_many :nonexistant_comments, -> { where 'comments.id < 0' }, :class_name => 'Comment' + has_many :nonexistent_comments, -> { where 'comments.id < 0' }, :class_name => 'Comment' has_many :special_comments_ratings, :through => :special_comments, :source => :ratings has_many :special_comments_ratings_taggings, :through => :special_comments_ratings, :source => :taggings @@ -88,7 +90,7 @@ class Post < ActiveRecord::Base has_and_belongs_to_many :categories has_and_belongs_to_many :special_categories, :join_table => "categories_posts", :association_foreign_key => 'category_id' - has_many :taggings, :as => :taggable + has_many :taggings, :as => :taggable, :counter_cache => :tags_count has_many :tags, :through => :taggings do def add_joins_and_select select('tags.*, authors.id as author_id') @@ -126,6 +128,9 @@ class Post < ActiveRecord::Base has_many :taggings_using_author_id, :primary_key => :author_id, :as => :taggable, :class_name => 'Tagging' has_many :tags_using_author_id, :through => :taggings_using_author_id, :source => :tag + has_many :images, :as => :imageable, :foreign_key => :imageable_identifier, :foreign_type => :imageable_class + has_one :main_image, :as => :imageable, :foreign_key => :imageable_identifier, :foreign_type => :imageable_class, :class_name => 'Image' + has_many :standard_categorizations, :class_name => 'Categorization', :foreign_key => :post_id has_many :author_using_custom_pk, :through => :standard_categorizations has_many :authors_using_custom_pk, :through => :standard_categorizations @@ -217,3 +222,22 @@ class PostThatLoadsCommentsInAnAfterSaveHook < ActiveRecord::Base post.comments.load end end + +class PostWithAfterCreateCallback < ActiveRecord::Base + self.table_name = 'posts' + has_many :comments, foreign_key: :post_id + + after_create do |post| + update_attribute(:author_id, comments.first.id) + end +end + +class PostWithCommentWithDefaultScopeReferencesAssociation < ActiveRecord::Base + self.table_name = 'posts' + has_many :comment_with_default_scope_references_associations, foreign_key: :post_id + has_one :first_comment, class_name: "CommentWithDefaultScopeReferencesAssociation", foreign_key: :post_id +end + +class SerializedPost < ActiveRecord::Base + serialize :title +end diff --git a/activerecord/test/models/publisher/article.rb b/activerecord/test/models/publisher/article.rb index 03a277bbdd..d73a8eb936 100644 --- a/activerecord/test/models/publisher/article.rb +++ b/activerecord/test/models/publisher/article.rb @@ -1,3 +1,4 @@ class Publisher::Article < ActiveRecord::Base has_and_belongs_to_many :magazines + has_and_belongs_to_many :tags end diff --git a/activerecord/test/models/ship.rb b/activerecord/test/models/ship.rb index 77a4728d0b..5f618a50d2 100644 --- a/activerecord/test/models/ship.rb +++ b/activerecord/test/models/ship.rb @@ -6,7 +6,7 @@ class Ship < ActiveRecord::Base has_many :parts, :class_name => 'ShipPart' accepts_nested_attributes_for :parts, :allow_destroy => true - accepts_nested_attributes_for :pirate, :allow_destroy => true, :reject_if => proc { |attributes| attributes.empty? } + accepts_nested_attributes_for :pirate, :allow_destroy => true, :reject_if => proc(&:empty?) accepts_nested_attributes_for :update_only_pirate, :update_only => true validates_presence_of :name diff --git a/activerecord/test/models/tagging.rb b/activerecord/test/models/tagging.rb index f91f2ad2e9..a6c05da26a 100644 --- a/activerecord/test/models/tagging.rb +++ b/activerecord/test/models/tagging.rb @@ -8,6 +8,6 @@ class Tagging < ActiveRecord::Base belongs_to :invalid_tag, :class_name => 'Tag', :foreign_key => 'tag_id' belongs_to :blue_tag, -> { where :tags => { :name => 'Blue' } }, :class_name => 'Tag', :foreign_key => :tag_id belongs_to :tag_with_primary_key, :class_name => 'Tag', :foreign_key => :tag_id, :primary_key => :custom_primary_key - belongs_to :taggable, :polymorphic => true, :counter_cache => true + belongs_to :taggable, :polymorphic => true, :counter_cache => :tags_count has_many :things, :through => :taggable end diff --git a/activerecord/test/models/tyre.rb b/activerecord/test/models/tyre.rb index bc3444aa7d..e50a21ca68 100644 --- a/activerecord/test/models/tyre.rb +++ b/activerecord/test/models/tyre.rb @@ -1,3 +1,11 @@ class Tyre < ActiveRecord::Base belongs_to :car + + def self.custom_find(id) + find(id) + end + + def self.custom_find_by(*args) + find_by(*args) + end end diff --git a/activerecord/test/schema/oracle_specific_schema.rb b/activerecord/test/schema/oracle_specific_schema.rb index a7817772f4..264d9b8910 100644 --- a/activerecord/test/schema/oracle_specific_schema.rb +++ b/activerecord/test/schema/oracle_specific_schema.rb @@ -32,10 +32,7 @@ create sequence test_oracle_defaults_seq minvalue 10000 fixed_time date default TO_DATE('2004-01-01 00:00:00', 'YYYY-MM-DD HH24:MI:SS'), char1 varchar2(1) default 'Y', char2 varchar2(50) default 'a varchar field', - char3 clob default 'a text field', - positive_integer integer default 1, - negative_integer integer default -1, - decimal_number number(3,2) default 2.78 + char3 clob default 'a text field' ) SQL execute "create sequence defaults_seq minvalue 10000" diff --git a/activerecord/test/schema/postgresql_specific_schema.rb b/activerecord/test/schema/postgresql_specific_schema.rb index e9294a11b9..55360b9aa2 100644 --- a/activerecord/test/schema/postgresql_specific_schema.rb +++ b/activerecord/test/schema/postgresql_specific_schema.rb @@ -1,7 +1,7 @@ ActiveRecord::Schema.define do - %w(postgresql_tsvectors postgresql_hstores postgresql_arrays postgresql_moneys postgresql_numbers postgresql_times postgresql_network_addresses postgresql_uuids postgresql_ltrees - postgresql_oids postgresql_xml_data_type defaults geometrics postgresql_timestamp_with_zones postgresql_partitioned_table postgresql_partitioned_table_parent postgresql_json_data_type postgresql_citext).each do |table_name| + %w(postgresql_times postgresql_oids defaults postgresql_timestamp_with_zones + postgresql_partitioned_table postgresql_partitioned_table_parent).each do |table_name| execute "DROP TABLE IF EXISTS #{quote_table_name table_name}" end @@ -12,8 +12,6 @@ ActiveRecord::Schema.define do execute 'DROP FUNCTION IF EXISTS partitioned_insert_trigger()' - execute "DROP SCHEMA IF EXISTS schema_1 CASCADE" - %w(accounts_id_seq developers_id_seq projects_id_seq topics_id_seq customers_id_seq orders_id_seq).each do |seq_name| execute "SELECT setval('#{seq_name}', 100)" end @@ -30,101 +28,13 @@ ActiveRecord::Schema.define do char1 char(1) default 'Y', char2 character varying(50) default 'a varchar field', char3 text default 'a text field', - positive_integer integer default 1, - negative_integer integer default -1, bigint_default bigint default 0::bigint, - decimal_number decimal(3,2) default 2.78, multiline_default text DEFAULT '--- [] '::text ); _SQL - execute "CREATE SCHEMA schema_1" - execute "CREATE DOMAIN schema_1.text AS text" - execute "CREATE DOMAIN schema_1.varchar AS varchar" - execute "CREATE DOMAIN schema_1.bpchar AS bpchar" - - execute <<_SQL - CREATE TABLE geometrics ( - id serial primary key, - a_point point, - -- a_line line, (the line type is currently not implemented in postgresql) - a_line_segment lseg, - a_box box, - a_path path, - a_polygon polygon, - a_circle circle - ); -_SQL - - execute <<_SQL - CREATE TABLE postgresql_arrays ( - id SERIAL PRIMARY KEY, - commission_by_quarter INTEGER[], - nicknames TEXT[] - ); -_SQL - - execute <<_SQL - CREATE TABLE postgresql_uuids ( - id SERIAL PRIMARY KEY, - guid uuid, - compact_guid uuid - ); -_SQL - - execute <<_SQL - CREATE TABLE postgresql_tsvectors ( - id SERIAL PRIMARY KEY, - text_vector tsvector - ); -_SQL - - if 't' == select_value("select 'hstore'=ANY(select typname from pg_type)") - execute <<_SQL - CREATE TABLE postgresql_hstores ( - id SERIAL PRIMARY KEY, - hash_store hstore default ''::hstore - ); -_SQL - end - - if 't' == select_value("select 'ltree'=ANY(select typname from pg_type)") - execute <<_SQL - CREATE TABLE postgresql_ltrees ( - id SERIAL PRIMARY KEY, - path ltree - ); -_SQL - end - - if 't' == select_value("select 'citext'=ANY(select typname from pg_type)") - execute <<_SQL - CREATE TABLE postgresql_citext ( - id SERIAL PRIMARY KEY, - text_citext citext default ''::citext - ); -_SQL - end - - if 't' == select_value("select 'json'=ANY(select typname from pg_type)") - execute <<_SQL - CREATE TABLE postgresql_json_data_type ( - id SERIAL PRIMARY KEY, - json_data json default '{}'::json - ); -_SQL - end - - execute <<_SQL - CREATE TABLE postgresql_numbers ( - id SERIAL PRIMARY KEY, - single REAL, - double DOUBLE PRECISION - ); -_SQL - execute <<_SQL CREATE TABLE postgresql_times ( id SERIAL PRIMARY KEY, @@ -134,15 +44,6 @@ _SQL _SQL execute <<_SQL - CREATE TABLE postgresql_network_addresses ( - id SERIAL PRIMARY KEY, - cidr_address CIDR default '192.168.1.0/24', - inet_address INET default '192.168.1.1', - mac_address MACADDR default 'ff:ff:ff:ff:ff:ff' - ); -_SQL - - execute <<_SQL CREATE TABLE postgresql_oids ( id SERIAL PRIMARY KEY, obj_id OID diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb index a7c7fc70bc..a9c2b1d112 100644 --- a/activerecord/test/schema/schema.rb +++ b/activerecord/test/schema/schema.rb @@ -10,7 +10,7 @@ ActiveRecord::Schema.define do #put adapter specific setup here case adapter_name when "PostgreSQL" - enable_uuid_ossp!(ActiveRecord::Base.connection) + enable_extension!('uuid-ossp', ActiveRecord::Base.connection) create_table :uuid_parents, id: :uuid, force: true do |t| t.string :name end @@ -62,6 +62,11 @@ ActiveRecord::Schema.define do t.references :magazine end + create_table :articles_tags, force: true do |t| + t.references :article + t.references :tag + end + create_table :audit_logs, force: true do |t| t.column :message, :string, null: false t.column :developer_id, :integer, null: false @@ -79,6 +84,8 @@ ActiveRecord::Schema.define do create_table :author_addresses, force: true do |t| end + add_foreign_key :authors, :author_addresses + create_table :author_favorites, force: true do |t| t.column :author_id, :integer t.column :favorite_author_id, :integer @@ -131,7 +138,7 @@ ActiveRecord::Schema.define do t.integer :engines_count t.integer :wheels_count t.column :lock_version, :integer, null: false, default: 0 - t.timestamps + t.timestamps null: false end create_table :categories, force: true do |t| @@ -185,12 +192,13 @@ ActiveRecord::Schema.define do t.text :body, null: false end t.string :type - t.integer :taggings_count, default: 0 + t.integer :tags_count, default: 0 t.integer :children_count, default: 0 t.integer :parent_id t.references :author, polymorphic: true t.string :resource_id t.string :resource_type + t.integer :developer_id end create_table :companies, force: true do |t| @@ -220,6 +228,11 @@ ActiveRecord::Schema.define do t.integer :extendedWarranty, null: false end + create_table :computers_developers, id: false, force: true do |t| + t.references :computer + t.references :developer + end + create_table :contracts, force: true do |t| t.integer :developer_id t.integer :company_id @@ -303,7 +316,7 @@ ActiveRecord::Schema.define do end create_table :cold_jokes, force: true do |t| - t.string :name + t.string :cold_name end create_table :friendships, force: true do |t| @@ -530,7 +543,7 @@ ActiveRecord::Schema.define do t.references :best_friend_of t.integer :insures, null: false, default: 0 t.timestamp :born_at - t.timestamps + t.timestamps null: false end create_table :peoples_treasures, id: false, force: true do |t| @@ -538,10 +551,16 @@ ActiveRecord::Schema.define do t.column :treasure_id, :integer end + create_table :personal_legacy_things, force: true do |t| + t.integer :tps_report_number + t.integer :person_id + t.integer :version, null: false, default: 0 + end + create_table :pets, primary_key: :pet_id, force: true do |t| t.string :name t.integer :owner_id, :integer - t.timestamps + t.timestamps null: false end create_table :pirates, force: true do |t| @@ -564,7 +583,6 @@ ActiveRecord::Schema.define do end t.string :type t.integer :comments_count, default: 0 - t.integer :taggings_count, default: 0 t.integer :taggings_with_delete_all_count, default: 0 t.integer :taggings_with_destroy_count, default: 0 t.integer :tags_count, default: 0 @@ -572,6 +590,16 @@ ActiveRecord::Schema.define do t.integer :tags_with_nullify_count, default: 0 end + create_table :serialized_posts, force: true do |t| + t.integer :author_id + t.string :title, null: false + end + + create_table :images, force: true do |t| + t.integer :imageable_identifier + t.string :imageable_class + end + create_table :price_estimates, force: true do |t| t.string :estimate_of_type t.integer :estimate_of_id @@ -720,13 +748,13 @@ ActiveRecord::Schema.define do t.string :parent_title t.string :type t.string :group - t.timestamps + t.timestamps null: true end create_table :toys, primary_key: :toy_id, force: true do |t| t.string :name t.integer :pet_id, :integer - t.timestamps + t.timestamps null: false end create_table :traffic_lights, force: true do |t| @@ -775,8 +803,8 @@ ActiveRecord::Schema.define do t.integer :man_id t.integer :polymorphic_man_id t.string :polymorphic_man_type - t.integer :polymorphic_man_without_inverse_id - t.string :polymorphic_man_without_inverse_type + t.integer :poly_man_without_inverse_id + t.string :poly_man_without_inverse_type t.integer :horrible_polymorphic_man_id t.string :horrible_polymorphic_man_type end @@ -851,12 +879,11 @@ ActiveRecord::Schema.define do t.integer :fk_id, null: false end - create_table :fk_test_has_pk, force: true do |t| + create_table :fk_test_has_pk, force: true, primary_key: "pk_id" do |t| end - execute "ALTER TABLE fk_test_has_fk ADD CONSTRAINT fk_name FOREIGN KEY (#{quote_column_name 'fk_id'}) REFERENCES #{quote_table_name 'fk_test_has_pk'} (#{quote_column_name 'id'})" - - execute "ALTER TABLE lessons_students ADD CONSTRAINT student_id_fk FOREIGN KEY (#{quote_column_name 'student_id'}) REFERENCES #{quote_table_name 'students'} (#{quote_column_name 'id'})" + add_foreign_key :fk_test_has_fk, :fk_test_has_pk, column: "fk_id", name: "fk_name", primary_key: "pk_id" + add_foreign_key :lessons_students, :students end create_table :overloaded_types, force: true do |t| diff --git a/activerecord/test/schema/sqlite_specific_schema.rb b/activerecord/test/schema/sqlite_specific_schema.rb index b7aff4f47d..b5552c2755 100644 --- a/activerecord/test/schema/sqlite_specific_schema.rb +++ b/activerecord/test/schema/sqlite_specific_schema.rb @@ -7,7 +7,7 @@ ActiveRecord::Schema.define do execute "DROP TABLE fk_test_has_pk" rescue nil execute <<_SQL CREATE TABLE 'fk_test_has_pk' ( - 'id' INTEGER NOT NULL PRIMARY KEY + 'pk_id' INTEGER NOT NULL PRIMARY KEY ); _SQL @@ -16,7 +16,7 @@ _SQL 'id' INTEGER NOT NULL PRIMARY KEY, 'fk_id' INTEGER NOT NULL, - FOREIGN KEY ('fk_id') REFERENCES 'fk_test_has_pk'('id') + FOREIGN KEY ('fk_id') REFERENCES 'fk_test_has_pk'('pk_id') ); _SQL -end
\ No newline at end of file +end diff --git a/activerecord/test/support/ddl_helper.rb b/activerecord/test/support/ddl_helper.rb index 0107babaaf..43cb235e01 100644 --- a/activerecord/test/support/ddl_helper.rb +++ b/activerecord/test/support/ddl_helper.rb @@ -1,8 +1,8 @@ module DdlHelper def with_example_table(connection, table_name, definition = nil) - connection.exec_query("CREATE TABLE #{table_name}(#{definition})") + connection.execute("CREATE TABLE #{table_name}(#{definition})") yield ensure - connection.exec_query("DROP TABLE #{table_name}") + connection.execute("DROP TABLE #{table_name}") end end diff --git a/activerecord/test/support/schema_dumping_helper.rb b/activerecord/test/support/schema_dumping_helper.rb index 2ae8d299e5..2d1651454d 100644 --- a/activerecord/test/support/schema_dumping_helper.rb +++ b/activerecord/test/support/schema_dumping_helper.rb @@ -8,4 +8,13 @@ module SchemaDumpingHelper ensure ActiveRecord::SchemaDumper.ignore_tables = old_ignore_tables end + + def dump_all_table_schema(ignore_tables) + old_ignore_tables, ActiveRecord::SchemaDumper.ignore_tables = ActiveRecord::SchemaDumper.ignore_tables, ignore_tables + stream = StringIO.new + ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, stream) + stream.string + ensure + ActiveRecord::SchemaDumper.ignore_tables = old_ignore_tables + end end |