diff options
Diffstat (limited to 'activerecord')
80 files changed, 1762 insertions, 507 deletions
diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index e526343a22..78c0ab1cfd 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -8,6 +8,245 @@ *Jonathan Baudanza* +* Parsing PostgreSQL arrays with empty strings now works correctly. + + Previously, if you tried to parse `{"1","","2","","3"}` the result + would be `["1","2","3"]`, removing the empty strings from the array, + which would be incorrect. Now it will correctly produce `["1","","2","","3"]` + as the result of parsing the above PostgreSQL array. + + Fixes #13907. + + *Maurício Linhares* + +* Associations now raise `ArgumentError` on name conflicts. + + Dangerous association names conflicts include instance or class methods already + defined by `ActiveRecord::Base`. + + Example: + + class Car < ActiveRecord::Base + has_many :errors + end + # Will raise ArgumentError. + + Fixes #13217. + + *Lauro Caetano* + +* Fix regressions on `select_*` methods. + When `select_*` methods receive a `Relation` object, they should be able to get the arel/binds from it. + Also fix regressions on `select_rows` that was ignoring the binds. + + Fixes #7538, #12017, #13731, #12056. + + *arthurnn* + +* Active Record objects can now be correctly dumped, loaded and dumped again without issues. + + Previously, if you did `YAML.dump`, `YAML.load` and then `YAML.dump` again + in an Active Record model that used serialization it would fail at the last + dump due to the fields not being correctly serialized before being dumped + to YAML. Now it is possible to dump and load the same object as many times + as needed without any issues. + + Fixes #13861. + + *Maurício Linhares* + +* `find_in_batches` now returns an `Enumerator` when called without a block, so that it + can be chained with other `Enumerable` methods. + + *Marc-André Lafortune* + +* `enum` now raises on "dangerous" name conflicts. + + Dangerous name conflicts includes instance or class method conflicts + with methods defined within `ActiveRecord::Base` but not its ancestors, + as well as conflicts with methods generated by other enums on the same + class. + + Fixes #13389. + + *Godfrey Chan* + +* `scope` now raises on "dangerous" name conflicts. + + Similar to dangerous attribute methods, a scope name conflict is + dangerous if it conflicts with an existing class method defined within + `ActiveRecord::Base` but not its ancestors. + + See also #13389. + + *Godfrey Chan*, *Philippe Creux* + +* Correctly send an user provided statement to a `lock!()` call. + + person.lock! 'FOR SHARE NOWAIT' + # Before: SELECT * ... LIMIT 1 FOR UPDATE + # After: SELECT * ... LIMIT 1 FOR SHARE NOWAIT + + Fixes #13788. + + *Maurício Linhares* + +* Handle aliased attributes `select()`, `order()` and `reorder()`. + + *Tsutomu Kuroda* + +* Reset the collection association when calling `reset` on it. + + Before: + + post.comments.loaded? # => true + post.comments.reset + post.comments.loaded? # => true + + After: + + post.comments.loaded? # => true + post.comments.reset + post.comments.loaded? # => false + + Fixes #13777. + + *Kelsey Schlarman* + +* Make enum fields work as expected with the `ActiveModel::Dirty` API. + + Before this change, using the dirty API would have surprising results: + + conversation = Conversation.new + conversation.status = :active + conversation.status = :archived + conversation.status_was # => 0 + + After this change, the same code would result in: + + conversation = Conversation.new + conversation.status = :active + conversation.status = :archived + conversation.status_was # => "active" + + *Rafael Mendonça França* + +* `has_one` and `belongs_to` accessors don't add ORDER BY to the queries anymore. + + Since Rails 4.0, we add an ORDER BY in the `first` method to ensure consistent results + among different database engines. But for singular associations this behavior is not needed + since we will have one record to return. As this ORDER BY option can lead some performance + issues we are removing it for singular associations accessors. + + Fixes #12623. + + *Rafael Mendonça França* + +* Prepend table name for column names passed to `Relation#select`. + + Example: + + Post.select(:id) + # Before: => SELECT id FROM "posts" + # After: => SELECT "posts"."id" FROM "posts" + + *Yves Senn* + +* Fail early with "Primary key not included in the custom select clause" + in `find_in_batches`. + + Before this patch, the exception was raised after the first batch was + yielded to the block. This means that you only get it, when you hit the + `batch_size` treshold. This could shadow the issue in development. + + *Alexander Balashov* + +* Ensure `second` through `fifth` methods act like the `first` finder. + + The famous ordinal Array instance methods defined in ActiveSupport + (`first`, `second`, `third`, `fourth`, and `fifth`) are now available as + full-fledged finders in ActiveRecord. The biggest benefit of this is ordering + of the records returned now defaults to the table's primary key in ascending order. + + Fixes #13743. + + Example: + + User.all.second + + # Before + # => 'SELECT "users".* FROM "users"' + + # After + # => SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT 1 OFFSET 1' + + User.offset(3).second + + # Before + # => 'SELECT "users".* FROM "users" LIMIT -1 OFFSET 3' # sqlite3 gem + # => 'SELECT "users".* FROM "users" OFFSET 3' # pg gem + # => 'SELECT `users`.* FROM `users` LIMIT 18446744073709551615 OFFSET 3' # mysql2 gem + + # After + # => SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT 1 OFFSET 4' + + *Jason Meller* + +* ActiveRecord states are now correctly restored after a rollback for + models that did not define any transactional callbacks (i.e. + `after_commit`, `after_rollback` or `after_create`). + + Fixes #13744. + + *Godfrey Chan* + +* Make `touch` fire the `after_commit` and `after_rollback` callbacks. + + *Harry Brundage* + +* Enable partial indexes for sqlite >= 3.8.0 + + See http://www.sqlite.org/partialindex.html + + *Cody Cutrer* + +* Don't try to get the subclass if the inheritance column doesn't exist + + The `subclass_from_attrs` method is called even if the column specified by + the `inheritance_column` setting doesn't exist. This prevents setting associations + via the attributes hash if the association name clashes with the value of the setting, + typically `:type`. This worked previously in Rails 3.2. + + *Ujjwal Thaakar* + +* Enum mappings are now exposed via class methods instead of constants. + + Example: + + class Conversation < ActiveRecord::Base + enum status: [ :active, :archived ] + end + + Before: + + Conversation::STATUS # => { "active" => 0, "archived" => 1 } + + After: + + Conversation.statuses # => { "active" => 0, "archived" => 1 } + + *Godfrey Chan* + +* Set `NameError#name` when STI-class-lookup fails. + + *Chulki Lee* + +* Fix bug in `becomes!` when changing from the base model to a STI sub-class. + + Fixes #13272. + + *the-web-dev*, *Yves Senn* + * Currently Active Record can be configured via the environment variable `DATABASE_URL` or by manually injecting a hash of values which is what Rails does, reading in `database.yml` and setting Active Record appropriately. Active Record @@ -65,7 +304,7 @@ class: `ActiveRecord::ConnectionHandling::MergeAndResolveDefaultUrlConfig`. To understand the exact behavior of this class, it is best to review the - behavior in `activerecord/test/cases/connection_adapters/connection_handler_test.rb` + behavior in `activerecord/test/cases/connection_adapters/connection_handler_test.rb`. *Richard Schneeman* @@ -411,7 +650,7 @@ *kostya*, *Lauro Caetano* * `type_to_sql` returns a `String` for unmapped columns. This fixes an error - when using unmapped array types in PG + when using unmapped PostgreSQL array types. Example: @@ -450,7 +689,7 @@ * Update counter cache on a `has_many` relationship regardless of default scope. - Fix #12952. + Fixes #12952. *Uku Taht* @@ -461,9 +700,10 @@ *Cody Cutrer*, *Yves Senn* -* Raise `ActiveRecord::RecordNotDestroyed` when a replaced child marked with `dependent: destroy` fails to be destroyed. +* Raise `ActiveRecord::RecordNotDestroyed` when a replaced child + marked with `dependent: destroy` fails to be destroyed. - Fix #12812 + Fixex #12812. *Brian Thomas Storti* @@ -1369,6 +1609,7 @@ *Yves Senn* * Fix the `:primary_key` option for `has_many` associations. + Fixes #10693. *Yves Senn* @@ -1493,7 +1734,7 @@ * Trigger a save on `has_one association=(associate)` when the associate contents have changed. - Fix #8856. + Fixes #8856. *Chris Thompson* diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index 714f623af3..f3f77e21c0 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -1213,7 +1213,8 @@ module ActiveRecord # Returns the associated object. +nil+ is returned if none is found. # [association=(associate)] # Assigns the associate object, extracts the primary key, sets it as the foreign key, - # and saves the associate object. + # and saves the associate object. To avoid database inconsistencies, permanently deletes an existing + # associated object when assigning a new one, even if the new one isn't saved to database. # [build_association(attributes = {})] # Returns a new object of the associated type that has been instantiated # with +attributes+ and linked to this object through a foreign key, but has not diff --git a/activerecord/lib/active_record/associations/association_scope.rb b/activerecord/lib/active_record/associations/association_scope.rb index 17f056e764..5a0ba9e6b1 100644 --- a/activerecord/lib/active_record/associations/association_scope.rb +++ b/activerecord/lib/active_record/associations/association_scope.rb @@ -1,3 +1,5 @@ +require 'active_record/associations/join_helper' + module ActiveRecord module Associations class AssociationScope #:nodoc: diff --git a/activerecord/lib/active_record/associations/builder/association.rb b/activerecord/lib/active_record/associations/builder/association.rb index 3911d1b520..f085fd1cfd 100644 --- a/activerecord/lib/active_record/associations/builder/association.rb +++ b/activerecord/lib/active_record/associations/builder/association.rb @@ -26,6 +26,12 @@ module ActiveRecord::Associations::Builder attr_reader :name, :scope, :options def self.build(model, name, scope, options, &block) + if model.dangerous_attribute_method?(name) + raise ArgumentError, "You tried to define an association named #{name} on the model #{model.name}, but " \ + "this will conflict with a method #{name} already defined by Active Record. " \ + "Please choose a different association name." + end + builder = create_builder model, name, scope, options, &block reflection = builder.build(model) define_accessors model, reflection diff --git a/activerecord/lib/active_record/associations/collection_association.rb b/activerecord/lib/active_record/associations/collection_association.rb index 52531a3520..89b7945c78 100644 --- a/activerecord/lib/active_record/associations/collection_association.rb +++ b/activerecord/lib/active_record/associations/collection_association.rb @@ -96,11 +96,31 @@ module ActiveRecord end def first(*args) - first_or_last(:first, *args) + first_nth_or_last(:first, *args) + end + + def second(*args) + first_nth_or_last(:second, *args) + end + + def third(*args) + first_nth_or_last(:third, *args) + end + + def fourth(*args) + first_nth_or_last(:fourth, *args) + end + + def fifth(*args) + first_nth_or_last(:fifth, *args) + end + + def forty_two(*args) + first_nth_or_last(:forty_two, *args) end def last(*args) - first_or_last(:last, *args) + first_nth_or_last(:last, *args) end def build(attributes = {}, &block) @@ -526,7 +546,7 @@ module ActiveRecord # * target already loaded # * owner is new record # * target contains new or changed record(s) - def fetch_first_or_last_using_find?(args) + def fetch_first_nth_or_last_using_find?(args) if args.first.is_a?(Hash) true else @@ -564,10 +584,10 @@ module ActiveRecord end # Fetches the first/last using SQL if possible, otherwise from the target array. - def first_or_last(type, *args) + def first_nth_or_last(type, *args) args.shift if args.first.is_a?(Hash) && args.first.empty? - collection = fetch_first_or_last_using_find?(args) ? scope : load_target + collection = fetch_first_nth_or_last_using_find?(args) ? scope : load_target collection.send(type, *args).tap do |record| set_inverse_instance record if record.is_a? ActiveRecord::Base end diff --git a/activerecord/lib/active_record/associations/collection_proxy.rb b/activerecord/lib/active_record/associations/collection_proxy.rb index e3fc908444..eba688866c 100644 --- a/activerecord/lib/active_record/associations/collection_proxy.rb +++ b/activerecord/lib/active_record/associations/collection_proxy.rb @@ -84,7 +84,7 @@ module ActiveRecord # # Be careful because this also means you're initializing a model # object with only the fields that you've selected. If you attempt - # to access a field that is not in the initialized record you'll + # to access a field except +id+ that is not in the initialized record you'll # receive: # # person.pets.select(:name).first.person_id @@ -170,6 +170,32 @@ module ActiveRecord @association.first(*args) end + # Same as +first+ except returns only the second record. + def second(*args) + @association.second(*args) + end + + # Same as +first+ except returns only the third record. + def third(*args) + @association.third(*args) + end + + # Same as +first+ except returns only the fourth record. + def fourth(*args) + @association.fourth(*args) + end + + # Same as +first+ except returns only the fifth record. + def fifth(*args) + @association.fifth(*args) + end + + # Same as +first+ except returns only the forty second record. + # Also known as accessing "the reddit". + def forty_two(*args) + @association.forty_two(*args) + end + # Returns the last record, or the last +n+ records, from the collection. # If the collection is empty, the first form returns +nil+, and the second # form returns an empty array. @@ -978,6 +1004,28 @@ module ActiveRecord proxy_association.reload self end + + # Unloads the association. Returns +self+. + # + # class Person < ActiveRecord::Base + # has_many :pets + # end + # + # person.pets # fetches pets from the database + # # => [#<Pet id: 1, name: "Snoop", group: "dogs", person_id: 1>] + # + # person.pets # uses the pets cache + # # => [#<Pet id: 1, name: "Snoop", group: "dogs", person_id: 1>] + # + # person.pets.reset # clears the pets cache + # + # person.pets # fetches pets from the database + # # => [#<Pet id: 1, name: "Snoop", group: "dogs", person_id: 1>] + def reset + proxy_association.reset + proxy_association.reset_scope + self + end end end 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 84e18684d8..0cd2e1a816 100644 --- a/activerecord/lib/active_record/associations/join_dependency/join_association.rb +++ b/activerecord/lib/active_record/associations/join_dependency/join_association.rb @@ -52,18 +52,16 @@ module ActiveRecord end end - if reflection.type - scope_chain_items << - ActiveRecord::Relation.create(klass, table) - .where(reflection.type => foreign_klass.base_class.name) - end - scope_chain_items.concat [klass.send(:build_default_scope)].compact rel = scope_chain_items.inject(scope_chain_items.shift) do |left, right| left.merge right end + if reflection.type + constraint = constraint.and table[reflection.type].eq foreign_klass.base_class.name + end + if rel && !rel.arel.constraints.empty? constraint = constraint.and rel.arel.constraints end diff --git a/activerecord/lib/active_record/associations/singular_association.rb b/activerecord/lib/active_record/associations/singular_association.rb index e4500af5b2..399aff378a 100644 --- a/activerecord/lib/active_record/associations/singular_association.rb +++ b/activerecord/lib/active_record/associations/singular_association.rb @@ -39,7 +39,7 @@ module ActiveRecord end def find_target - if record = scope.first + if record = scope.take set_inverse_instance record end end diff --git a/activerecord/lib/active_record/attribute_methods.rb b/activerecord/lib/active_record/attribute_methods.rb index 73761520f7..9326c9c117 100644 --- a/activerecord/lib/active_record/attribute_methods.rb +++ b/activerecord/lib/active_record/attribute_methods.rb @@ -110,16 +110,34 @@ module ActiveRecord end end - # A method name is 'dangerous' if it is already defined by Active Record, but + # A method name is 'dangerous' if it is already (re)defined by Active Record, but # not by any ancestors. (So 'puts' is not dangerous but 'save' is.) def dangerous_attribute_method?(name) # :nodoc: method_defined_within?(name, Base) end - def method_defined_within?(name, klass, sup = klass.superclass) # :nodoc: + def method_defined_within?(name, klass, superklass = klass.superclass) # :nodoc: if klass.method_defined?(name) || klass.private_method_defined?(name) - if sup.method_defined?(name) || sup.private_method_defined?(name) - klass.instance_method(name).owner != sup.instance_method(name).owner + if superklass.method_defined?(name) || superklass.private_method_defined?(name) + klass.instance_method(name).owner != superklass.instance_method(name).owner + else + true + end + else + false + end + end + + # A class method is 'dangerous' if it is already (re)defined by Active Record, but + # not by any ancestors. (So 'puts' is not dangerous but 'new' is.) + def dangerous_class_method?(method_name) + class_method_defined_within?(method_name, Base) + end + + 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 else true end @@ -260,6 +278,11 @@ module ActiveRecord } end + # Placeholder so it can be overriden when needed by serialization + def attributes_for_coder # :nodoc: + attributes + end + # Returns an <tt>#inspect</tt>-like string for the value of the # attribute +attr_name+. String attributes are truncated upto 50 # characters, Date and Time attributes are returned in the diff --git a/activerecord/lib/active_record/attribute_methods/dirty.rb b/activerecord/lib/active_record/attribute_methods/dirty.rb index 19e81abba5..8a1b199997 100644 --- a/activerecord/lib/active_record/attribute_methods/dirty.rb +++ b/activerecord/lib/active_record/attribute_methods/dirty.rb @@ -38,11 +38,37 @@ module ActiveRecord end end + def initialize_dup(other) # :nodoc: + super + init_changed_attributes + end + private + def initialize_internals_callback + super + init_changed_attributes + end + + def init_changed_attributes + @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, @attributes[attr]) + end + end + # Wrap write_attribute to remember original attribute value. def write_attribute(attr, value) attr = attr.to_s + save_changed_attribute(attr, value) + + super(attr, value) + end + + def save_changed_attribute(attr, value) # The attribute already has an unsaved change. if attribute_changed?(attr) old = changed_attributes[attr] @@ -51,9 +77,6 @@ module ActiveRecord old = clone_attribute_value(:read_attribute, attr) changed_attributes[attr] = old if _field_changed?(attr, old, value) end - - # Carry on. - super(attr, value) end def update_record(*) @@ -67,7 +90,7 @@ module ActiveRecord # Serialized attributes should always be written in case they've been # changed in place. def keys_for_partial_write - changed | (attributes.keys & self.class.serialized_attributes.keys) + changed end def _field_changed?(attr, old, value) diff --git a/activerecord/lib/active_record/attribute_methods/serialization.rb b/activerecord/lib/active_record/attribute_methods/serialization.rb index d484659190..67abbbc2a0 100644 --- a/activerecord/lib/active_record/attribute_methods/serialization.rb +++ b/activerecord/lib/active_record/attribute_methods/serialization.rb @@ -115,6 +115,14 @@ module ActiveRecord end end + def should_record_timestamps? + super || (self.record_timestamps && (attributes.keys & self.class.serialized_attributes.keys).present?) + end + + def keys_for_partial_write + super | (attributes.keys & self.class.serialized_attributes.keys) + end + def type_cast_attribute_for_write(column, value) if column && coder = self.class.serialized_attributes[column.name] Attribute.new(coder, value, :unserialized) @@ -156,6 +164,16 @@ module ActiveRecord super end end + + def attributes_for_coder + attribute_names.each_with_object({}) do |name, attrs| + attrs[name] = if self.class.serialized_attributes.include?(name) + @attributes[name].serialized_value + else + read_attribute(name) + end + end + end end end end diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index 1d3ec75aa1..9ec1feea97 100644 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -294,6 +294,7 @@ module ActiveRecord #:nodoc: extend Enum extend Delegation::DelegateCache + include Core include Persistence include NoTouching include ReadonlyAttributes @@ -320,7 +321,6 @@ module ActiveRecord #:nodoc: include Reflection include Serialization include Store - include Core end ActiveSupport.run_load_hooks(:active_record, Base) 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 c90915c509..6eb59cc398 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb @@ -20,6 +20,14 @@ module ActiveRecord # Returns an ActiveRecord::Result instance. def select_all(arel, name = nil, binds = []) + if arel.is_a?(Relation) + relation = arel + arel = relation.arel + if !binds || binds.empty? + binds = relation.bind_values + end + end + select(to_sql(arel, binds), name, binds) end @@ -39,13 +47,16 @@ module ActiveRecord # Returns an array of the values of the first column in a select: # select_values("SELECT id FROM companies LIMIT 3") => [1,2,3] def select_values(arel, name = nil) - select_rows(to_sql(arel, []), name) - .map { |v| v[0] } + binds = [] + if arel.is_a?(Relation) + arel, binds = arel.arel, arel.bind_values + end + select_rows(to_sql(arel, binds), name, binds).map(&:first) end # Returns an array of arrays containing the field values. # Order is the same as that returned by +columns+. - def select_rows(sql, name = nil) + def select_rows(sql, name = nil, binds = []) end undef_method :select_rows diff --git a/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb b/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb index 2b6685499a..bc4884b538 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb @@ -23,6 +23,10 @@ module ActiveRecord @parent = nil end + def finalized? + @state + end + def committed? @state == :committed end diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb index 8aa1ce5c04..3c94bad208 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb @@ -349,6 +349,14 @@ module ActiveRecord protected + def translate_exception_class(e, sql) + message = "#{e.class.name}: #{e.message}: #{sql}" + @logger.error message if @logger + exception = translate_exception(e, message) + exception.set_backtrace e.backtrace + exception + end + def log(sql, name = "SQL", binds = [], statement_name = nil) @instrumenter.instrument( "sql.active_record", @@ -358,11 +366,7 @@ module ActiveRecord :statement_name => statement_name, :binds => binds) { yield } rescue => e - message = "#{e.class.name}: #{e.message}: #{sql}" - @logger.error message if @logger - exception = translate_exception(e, message) - exception.set_backtrace e.backtrace - raise exception + raise translate_exception_class(e, sql) end def translate_exception(exception, message) diff --git a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb index 6d8e994654..b07b0cb826 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb @@ -213,7 +213,7 @@ module ActiveRecord # Returns an array of arrays containing the field values. # Order is the same as that returned by +columns+. - def select_rows(sql, name = nil) + def select_rows(sql, name = nil, binds = []) execute(sql, name).to_a end diff --git a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb index 7dbaa272a3..49f0bfbcde 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb @@ -213,9 +213,9 @@ module ActiveRecord # DATABASE STATEMENTS ====================================== - def select_rows(sql, name = nil) + def select_rows(sql, name = nil, binds = []) @connection.query_with_result = true - rows = exec_query(sql, name).rows + rows = exec_query(sql, name, binds).rows @connection.more_results && @connection.next_result # invoking stored procedures with CLIENT_MULTI_RESULTS requires this to tidy up else connection will be dropped rows end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/array_parser.rb b/activerecord/lib/active_record/connection_adapters/postgresql/array_parser.rb index 20de8d1982..0b218f2bfd 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/array_parser.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/array_parser.rb @@ -91,8 +91,9 @@ module ActiveRecord end def add_item_to_array(array, current_item, quoted) - if current_item.length == 0 - elsif !quoted && current_item == 'NULL' + return if !quoted && current_item.length == 0 + + if !quoted && current_item == 'NULL' array.push nil else array.push current_item 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 f349c37724..51ee2829b2 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb @@ -46,8 +46,8 @@ module ActiveRecord # Executes a SELECT query and returns an array of rows. Each row is an # array of field values. - def select_rows(sql, name = nil) - select_raw(sql, name).last + def select_rows(sql, name = nil, binds = []) + exec_query(sql, name, binds).rows end # Executes an INSERT query and returns the new record's ID diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index 7e188907e1..9618ba4087 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -853,7 +853,11 @@ module ActiveRecord sql_key = sql_key(sql) unless @statements.key? sql_key nextkey = @statements.next_key - @connection.prepare nextkey, sql + begin + @connection.prepare nextkey, sql + rescue => e + raise translate_exception_class(e, sql) + end # Clear the queue @connection.get_last_result @statements[sql_key] = nextkey @@ -938,14 +942,6 @@ module ActiveRecord exec_query(sql, name, binds) end - def select_raw(sql, name = nil) - res = execute(sql, name) - results = result_as_array(res) - fields = res.fields - res.clear - return fields, results - 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/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb index 92bb70ba53..3c5f7a981e 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb @@ -155,6 +155,10 @@ module ActiveRecord true end + def supports_partial_index? + sqlite_version >= '3.8.0' + end + # Returns true, since this connection adapter supports prepared statement # caching. def supports_statement_cache? @@ -343,8 +347,8 @@ module ActiveRecord end alias :create :insert_sql - def select_rows(sql, name = nil) - exec_query(sql, name).rows + def select_rows(sql, name = nil, binds = []) + exec_query(sql, name, binds).rows end def begin_db_transaction #:nodoc: @@ -397,13 +401,25 @@ module ActiveRecord # Returns an array of indexes for the given table. def indexes(table_name, name = nil) #:nodoc: exec_query("PRAGMA index_list(#{quote_table_name(table_name)})", 'SCHEMA').map do |row| + sql = <<-SQL + SELECT sql + FROM sqlite_master + WHERE name=#{quote(row['name'])} AND type='index' + UNION ALL + SELECT sql + FROM sqlite_temp_master + WHERE name=#{quote(row['name'])} AND type='index' + SQL + index_sql = exec_query(sql).first['sql'] + match = /\sWHERE\s+(.+)$/i.match(index_sql) + where = match[1] if match IndexDefinition.new( table_name, row['name'], row['unique'] != 0, exec_query("PRAGMA index_info('#{row['name']}')", "SCHEMA").map { |col| col['name'] - }) + }, nil, nil, where) end end diff --git a/activerecord/lib/active_record/core.rb b/activerecord/lib/active_record/core.rb index cd8690d500..6303fe5ee4 100644 --- a/activerecord/lib/active_record/core.rb +++ b/activerecord/lib/active_record/core.rb @@ -182,9 +182,7 @@ module ActiveRecord @column_types = self.class.column_types init_internals - init_changed_attributes - ensure_proper_type - populate_with_current_scope_attributes + initialize_internals_callback # +options+ argument is only needed to make protected_attributes gem easier to hook. # Remove it when we drop support to this gem. @@ -255,16 +253,12 @@ module ActiveRecord run_callbacks(:initialize) unless _initialize_callbacks.empty? - @changed_attributes = {} - init_changed_attributes - @aggregation_cache = {} @association_cache = {} @attributes_cache = {} @new_record = true - ensure_proper_type super end @@ -281,7 +275,7 @@ module ActiveRecord # Post.new.encode_with(coder) # coder # => {"attributes" => {"id" => nil, ... }} def encode_with(coder) - coder['attributes'] = attributes + coder['attributes'] = attributes_for_coder end # Returns true if +comparison_object+ is the same exact object, or +comparison_object+ @@ -397,13 +391,10 @@ module ActiveRecord end def update_attributes_from_transaction_state(transaction_state, depth) - if transaction_state && !has_transactional_callbacks? + if transaction_state && transaction_state.finalized? && !has_transactional_callbacks? unless @reflects_state[depth] - if transaction_state.committed? - committed! - elsif transaction_state.rolledback? - rolledback! - end + restore_transaction_record_state if transaction_state.rolledback? + clear_transaction_record_state @reflects_state[depth] = true end @@ -443,13 +434,7 @@ module ActiveRecord @reflects_state = [false] end - def init_changed_attributes - # 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, @attributes[attr]) - end + def initialize_internals_callback end # This method is needed to make protected_attributes gem easier to hook. diff --git a/activerecord/lib/active_record/dynamic_matchers.rb b/activerecord/lib/active_record/dynamic_matchers.rb index 5caab09038..e94b74063e 100644 --- a/activerecord/lib/active_record/dynamic_matchers.rb +++ b/activerecord/lib/active_record/dynamic_matchers.rb @@ -6,8 +6,12 @@ module ActiveRecord # then we can remove the indirection. def respond_to?(name, include_private = false) - match = Method.match(self, name) - match && match.valid? || super + if self == Base + super + else + match = Method.match(self, name) + match && match.valid? || super + end end private diff --git a/activerecord/lib/active_record/enum.rb b/activerecord/lib/active_record/enum.rb index 9ad718a02a..059bfe9a0f 100644 --- a/activerecord/lib/active_record/enum.rb +++ b/activerecord/lib/active_record/enum.rb @@ -54,23 +54,35 @@ module ActiveRecord # remove unused values, the explicit +Hash+ syntax should be used. # # In rare circumstances you might need to access the mapping directly. - # The mappings are exposed through a constant with the attributes name: + # The mappings are exposed through a class method with the pluralized attribute + # name: # - # Conversation::STATUS # => { "active" => 0, "archived" => 1 } + # Conversation.statuses # => { "active" => 0, "archived" => 1 } # - # Use that constant when you need to know the ordinal value of an enum: + # Use that class method when you need to know the ordinal value of an enum: # - # Conversation.where("status <> ?", Conversation::STATUS[:archived]) + # Conversation.where("status <> ?", Conversation.statuses[:archived]) module Enum + DEFINED_ENUMS = {} # :nodoc: + + def enum_mapping_for(attr_name) # :nodoc: + DEFINED_ENUMS[attr_name.to_s] + end + def enum(definitions) klass = self definitions.each do |name, values| - # STATUS = { } - enum_values = _enum_methods_module.const_set name.to_s.upcase, ActiveSupport::HashWithIndifferentAccess.new + # statuses = { } + enum_values = ActiveSupport::HashWithIndifferentAccess.new name = name.to_sym + # def self.statuses statuses end + detect_enum_conflict!(name, name.to_s.pluralize, true) + klass.singleton_class.send(:define_method, name.to_s.pluralize) { enum_values } + _enum_methods_module.module_eval do - # def status=(value) self[:status] = STATUS[value] end + # def status=(value) self[:status] = statuses[value] end + klass.send(:detect_enum_conflict!, name, "#{name}=") define_method("#{name}=") { |value| if enum_values.has_key?(value) || value.blank? self[name] = enum_values[value] @@ -84,22 +96,32 @@ module ActiveRecord end } - # def status() STATUS.key self[:status] end + # def status() statuses.key self[:status] end + klass.send(:detect_enum_conflict!, name, name) define_method(name) { enum_values.key self[name] } + # def status_before_type_cast() statuses.key self[:status] end + klass.send(:detect_enum_conflict!, name, "#{name}_before_type_cast") + define_method("#{name}_before_type_cast") { enum_values.key self[name] } + pairs = values.respond_to?(:each_pair) ? values.each_pair : values.each_with_index pairs.each do |value, i| enum_values[value] = i - # scope :active, -> { where status: 0 } - klass.scope value, -> { klass.where name => i } - # def active?() status == 0 end + klass.send(:detect_enum_conflict!, name, "#{value}?") define_method("#{value}?") { self[name] == i } # def active!() update! status: :active end + klass.send(:detect_enum_conflict!, name, "#{value}!") define_method("#{value}!") { update! name => value } + + # scope :active, -> { where status: 0 } + klass.send(:detect_enum_conflict!, name, value, true) + klass.scope value, -> { klass.where name => i } end + + DEFINED_ENUMS[name.to_s] = enum_values end end end @@ -107,10 +129,64 @@ module ActiveRecord private def _enum_methods_module @_enum_methods_module ||= begin - mod = Module.new + mod = Module.new do + private + def save_changed_attribute(attr_name, value) + if (mapping = self.class.enum_mapping_for(attr_name)) + if attribute_changed?(attr_name) + old = changed_attributes[attr_name] + + if mapping[old] == value + changed_attributes.delete(attr_name) + end + else + old = clone_attribute_value(:read_attribute, attr_name) + + if old != value + changed_attributes[attr_name] = mapping.key old + end + end + else + super + end + end + end include mod mod end end + + ENUM_CONFLICT_MESSAGE = \ + "You tried to define an enum named \"%{enum}\" on the model \"%{klass}\", but " \ + "this will generate a %{type} method \"%{method}\", which is already defined " \ + "by %{source}." + + def detect_enum_conflict!(enum_name, method_name, klass_method = false) + if klass_method && dangerous_class_method?(method_name) + raise ArgumentError, ENUM_CONFLICT_MESSAGE % { + enum: enum_name, + klass: self.name, + type: 'class', + method: method_name, + source: 'Active Record' + } + elsif !klass_method && dangerous_attribute_method?(method_name) + raise ArgumentError, ENUM_CONFLICT_MESSAGE % { + enum: enum_name, + klass: self.name, + type: 'instance', + method: method_name, + source: 'Active Record' + } + elsif !klass_method && method_defined_within?(method_name, _enum_methods_module, Module) + raise ArgumentError, ENUM_CONFLICT_MESSAGE % { + enum: enum_name, + klass: self.name, + type: 'instance', + method: method_name, + source: 'another enum' + } + end + end end end diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb index fee3f51d9e..297792aeec 100644 --- a/activerecord/lib/active_record/fixtures.rb +++ b/activerecord/lib/active_record/fixtures.rb @@ -462,7 +462,7 @@ module ActiveRecord @class_names.delete_if { |k,klass| unless klass.is_a? Class klass = klass.safe_constantize - ActiveSupport::Deprecation.warn("The ability to pass in strings as a class name will be removed in Rails 4.2, consider using the class itself instead.") + ActiveSupport::Deprecation.warn("The ability to pass in strings as a class name to `set_fixture_class` will be removed in Rails 4.2. Use the class itself instead.") end !insert_class(@class_names, k, klass) } @@ -568,7 +568,7 @@ module ActiveRecord @model_class = nil if class_name.is_a?(String) - ActiveSupport::Deprecation.warn("The ability to pass in strings as a class name will be removed in Rails 4.2, consider using the class itself instead.") + ActiveSupport::Deprecation.warn("The ability to pass in strings as a class name to `FixtureSet.new` will be removed in Rails 4.2. Use the class itself instead.") end if class_name.is_a?(Class) # TODO: Should be an AR::Base type class, or any? diff --git a/activerecord/lib/active_record/inheritance.rb b/activerecord/lib/active_record/inheritance.rb index 949e7678a5..08fc91c9df 100644 --- a/activerecord/lib/active_record/inheritance.rb +++ b/activerecord/lib/active_record/inheritance.rb @@ -18,13 +18,17 @@ module ActiveRecord if abstract_class? || self == Base raise NotImplementedError, "#{self} is an abstract class and cannot be instantiated." end - if (attrs = args.first).is_a?(Hash) - if subclass = subclass_from_attrs(attrs) - return subclass.new(*args, &block) - end + + attrs = args.first + if subclass_from_attributes?(attrs) + subclass = subclass_from_attributes(attrs) + end + + if subclass + subclass.new(*args, &block) + else + super end - # Delegate to the original .new - super end # Returns +true+ if this does not need STI type condition. Returns @@ -126,7 +130,7 @@ module ActiveRecord end end - raise NameError, "uninitialized constant #{candidates.first}" + raise NameError.new("uninitialized constant #{candidates.first}", candidates.first) end end @@ -172,7 +176,11 @@ module ActiveRecord # is not self or a valid subclass, raises ActiveRecord::SubclassNotFound # If this is a StrongParameters hash, and access to inheritance_column is not permitted, # this will ignore the inheritance column and return nil - def subclass_from_attrs(attrs) + def subclass_from_attributes?(attrs) + columns_hash.include?(inheritance_column) && attrs.is_a?(Hash) + end + + def subclass_from_attributes(attrs) subclass_name = attrs.with_indifferent_access[inheritance_column] if subclass_name.present? && subclass_name != self.name @@ -187,8 +195,18 @@ module ActiveRecord end end + def initialize_dup(other) + super + ensure_proper_type + end + private + def initialize_internals_callback + super + ensure_proper_type + end + # Sets the attribute used for single table inheritance to this class name if this is not the # ActiveRecord::Base descendant. # Considering the hierarchy Reply < Message < ActiveRecord::Base, this makes it possible to diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb index 4669c072a1..b1b35ed940 100644 --- a/activerecord/lib/active_record/persistence.rb +++ b/activerecord/lib/active_record/persistence.rb @@ -196,7 +196,11 @@ module ActiveRecord # share the same set of attributes. def becomes!(klass) became = becomes(klass) - became.public_send("#{klass.inheritance_column}=", klass.sti_name) unless self.class.descends_from_active_record? + sti_type = nil + if !klass.descends_from_active_record? + sti_type = klass.sti_name + end + became.public_send("#{klass.inheritance_column}=", sti_type) became end @@ -385,7 +389,7 @@ module ActiveRecord fresh_object = if options && options[:lock] - self.class.unscoped { self.class.lock.find(id) } + self.class.unscoped { self.class.lock(options[:lock]).find(id) } else self.class.unscoped { self.class.find(id) } end diff --git a/activerecord/lib/active_record/querying.rb b/activerecord/lib/active_record/querying.rb index fd4c973504..ef138c6f80 100644 --- a/activerecord/lib/active_record/querying.rb +++ b/activerecord/lib/active_record/querying.rb @@ -1,6 +1,7 @@ module ActiveRecord module Querying delegate :find, :take, :take!, :first, :first!, :last, :last!, :exists?, :any?, :many?, to: :all + delegate :second, :second!, :third, :third!, :fourth, :fourth!, :fifth, :fifth!, :forty_two, :forty_two!, to: :all delegate :first_or_create, :first_or_create!, :first_or_initialize, to: :all delegate :find_or_create_by, :find_or_create_by!, :find_or_initialize_by, to: :all delegate :find_by, :find_by!, to: :all diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb index 745c6cf349..fb213dc6f7 100644 --- a/activerecord/lib/active_record/relation.rb +++ b/activerecord/lib/active_record/relation.rb @@ -24,6 +24,7 @@ module ActiveRecord @klass = klass @table = table @values = values + @offsets = {} @loaded = false end @@ -495,9 +496,10 @@ module ActiveRecord end def reset - @first = @last = @to_sql = @order_clause = @scope_for_create = @arel = @loaded = nil + @last = @to_sql = @order_clause = @scope_for_create = @arel = @loaded = nil @should_eager_load = @join_dependency = nil @records = [] + @offsets = {} self end @@ -533,7 +535,6 @@ module ActiveRecord } binds = Hash[bind_values.find_all(&:first).map { |column, v| [column.name, v] }] - binds.merge!(Hash[bind_values.find_all(&:first).map { |column, v| [column.name, v] }]) Hash[equalities.map { |where| name = where.left.name diff --git a/activerecord/lib/active_record/relation/batches.rb b/activerecord/lib/active_record/relation/batches.rb index 49b01909c6..666cef80a9 100644 --- a/activerecord/lib/active_record/relation/batches.rb +++ b/activerecord/lib/active_record/relation/batches.rb @@ -64,6 +64,16 @@ module ActiveRecord # group.each { |person| person.party_all_night! } # end # + # If you do not provide a block to #find_in_batches, it will return an Enumerator + # for chaining with other methods: + # + # Person.find_in_batches.with_index do |group, batch| + # puts "Processing group ##{batch}" + # group.each(&:recover_from_last_night!) + # end + # + # To be yielded each record one by one, use #find_each instead. + # # ==== Options # * <tt>:batch_size</tt> - Specifies the size of the batch. Default to 1000. # * <tt>:start</tt> - Specifies the starting point for the batch processing. @@ -86,6 +96,7 @@ module ActiveRecord # the batch sizes. def find_in_batches(options = {}) options.assert_valid_keys(:start, :batch_size) + return to_enum(:find_in_batches, options) unless block_given? relation = self @@ -93,8 +104,8 @@ module ActiveRecord logger.warn("Scoped order and limit are ignored, it's forced to be batch order and batch size") end - start = options.delete(:start) - batch_size = options.delete(:batch_size) || 1000 + start = options[:start] + batch_size = options[:batch_size] || 1000 relation = relation.reorder(batch_order).limit(batch_size) records = start ? relation.where(table[primary_key].gteq(start)).to_a : relation.to_a @@ -102,16 +113,13 @@ module ActiveRecord while records.any? records_size = records.size primary_key_offset = records.last.id + raise "Primary key not included in the custom select clause" unless primary_key_offset yield records break if records_size < batch_size - if primary_key_offset - records = relation.where(table[primary_key].gt(primary_key_offset)).to_a - else - raise "Primary key not included in the custom select clause" - end + records = relation.where(table[primary_key].gt(primary_key_offset)).to_a end end diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb index 4984dbd277..01d46f7676 100644 --- a/activerecord/lib/active_record/relation/finder_methods.rb +++ b/activerecord/lib/active_record/relation/finder_methods.rb @@ -127,9 +127,9 @@ module ActiveRecord # def first(limit = nil) if limit - find_first_with_limit(limit) + find_nth_with_limit(offset_value, limit) else - find_first + find_nth(:first, offset_value) end end @@ -172,6 +172,86 @@ module ActiveRecord last or raise RecordNotFound end + # Find the second record. + # If no order is defined it will order by primary key. + # + # Person.second # returns the second object fetched by SELECT * FROM people + # Person.offset(3).second # returns the second object from OFFSET 3 (which is OFFSET 4) + # Person.where(["user_name = :u", { u: user_name }]).second + def second + find_nth(:second, offset_value ? offset_value + 1 : 1) + end + + # Same as +second+ but raises <tt>ActiveRecord::RecordNotFound</tt> if no record + # is found. + def second! + second or raise RecordNotFound + end + + # Find the third record. + # If no order is defined it will order by primary key. + # + # Person.third # returns the third object fetched by SELECT * FROM people + # Person.offset(3).third # returns the third object from OFFSET 3 (which is OFFSET 5) + # Person.where(["user_name = :u", { u: user_name }]).third + def third + find_nth(:third, offset_value ? offset_value + 2 : 2) + end + + # Same as +third+ but raises <tt>ActiveRecord::RecordNotFound</tt> if no record + # is found. + def third! + third or raise RecordNotFound + end + + # Find the fourth record. + # If no order is defined it will order by primary key. + # + # Person.fourth # returns the fourth object fetched by SELECT * FROM people + # Person.offset(3).fourth # returns the fourth object from OFFSET 3 (which is OFFSET 6) + # Person.where(["user_name = :u", { u: user_name }]).fourth + def fourth + find_nth(:fourth, offset_value ? offset_value + 3 : 3) + end + + # Same as +fourth+ but raises <tt>ActiveRecord::RecordNotFound</tt> if no record + # is found. + def fourth! + fourth or raise RecordNotFound + end + + # Find the fifth record. + # If no order is defined it will order by primary key. + # + # Person.fifth # returns the fifth object fetched by SELECT * FROM people + # Person.offset(3).fifth # returns the fifth object from OFFSET 3 (which is OFFSET 7) + # Person.where(["user_name = :u", { u: user_name }]).fifth + def fifth + find_nth(:fifth, offset_value ? offset_value + 4 : 4) + end + + # Same as +fifth+ but raises <tt>ActiveRecord::RecordNotFound</tt> if no record + # is found. + def fifth! + fifth or raise RecordNotFound + end + + # Find the forty-second record. Also known as accessing "the reddit". + # If no order is defined it will order by primary key. + # + # Person.forty_two # returns the forty-second object fetched by SELECT * FROM people + # Person.offset(3).forty_two # returns the fifth object from OFFSET 3 (which is OFFSET 44) + # Person.where(["user_name = :u", { u: user_name }]).forty_two + def forty_two + find_nth(:forty_two, offset_value ? offset_value + 41 : 41) + end + + # Same as +forty_two+ but raises <tt>ActiveRecord::RecordNotFound</tt> if no record + # is found. + def forty_two! + forty_two or raise RecordNotFound + end + # Returns +true+ if a record exists in the table that matches the +id+ or # conditions given, or +false+ otherwise. The argument can take six forms: # @@ -231,9 +311,9 @@ module ActiveRecord conditions = " [#{conditions}]" if conditions if Array(ids).size == 1 - error = "Couldn't find #{@klass.name} with #{primary_key}=#{ids}#{conditions}" + error = "Couldn't find #{@klass.name} with '#{primary_key}'=#{ids}#{conditions}" else - error = "Couldn't find all #{@klass.name.pluralize} with IDs " + error = "Couldn't find all #{@klass.name.pluralize} with '#{primary_key}': " error << "(#{ids.join(", ")})#{conditions} (found #{result_size} results, but was looking for #{expected_size})" end @@ -364,19 +444,19 @@ module ActiveRecord end end - def find_first + def find_nth(ordinal, offset) if loaded? - @records.first + @records.send(ordinal) else - @first ||= find_first_with_limit(1).first + @offsets[offset] ||= find_nth_with_limit(offset, 1).first end end - def find_first_with_limit(limit) + def find_nth_with_limit(offset, limit) if order_values.empty? && primary_key - order(arel_table[primary_key].asc).limit(limit).to_a + order(arel_table[primary_key].asc).limit(limit).offset(offset).to_a else - limit(limit).to_a + limit(limit).offset(offset).to_a end end diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb index 78da6a83ec..860063426a 100644 --- a/activerecord/lib/active_record/relation/query_methods.rb +++ b/activerecord/lib/active_record/relation/query_methods.rb @@ -120,6 +120,9 @@ module ActiveRecord # Will throw an error, but this will work: # # User.includes(:posts).where('posts.name = ?', 'example').references(:posts) + # + # Note that +includes+ works with association names while +references+ needs + # the actual table name. def includes(*args) check_if_method_has_arguments!(:includes, args) spawn.includes!(*args) @@ -163,24 +166,26 @@ module ActiveRecord self end - # Used to indicate that an association is referenced by an SQL string, and should - # therefore be JOINed in any query rather than loaded separately. + # Use to indicate that the given +table_names+ are referenced by an SQL string, + # and should therefore be JOINed in any query rather than loaded separately. + # This method only works in conjuction with +includes+. + # See #includes for more details. # # User.includes(:posts).where("posts.name = 'foo'") # # => Doesn't JOIN the posts table, resulting in an error. # # User.includes(:posts).where("posts.name = 'foo'").references(:posts) # # => Query now knows the string references posts, so adds a JOIN - def references(*args) - check_if_method_has_arguments!(:references, args) - spawn.references!(*args) + def references(*table_names) + check_if_method_has_arguments!(:references, table_names) + spawn.references!(*table_names) end - def references!(*args) # :nodoc: - args.flatten! - args.map!(&:to_s) + def references!(*table_names) # :nodoc: + table_names.flatten! + table_names.map!(&:to_s) - self.references_values |= args + self.references_values |= table_names self end @@ -234,7 +239,9 @@ module ActiveRecord def select!(*fields) # :nodoc: fields.flatten! - + fields.map! do |field| + klass.attribute_alias?(field) ? klass.attribute_alias(field).to_sym : field + end self.select_values += fields self end @@ -855,11 +862,11 @@ module ActiveRecord end single_val_method = Relation::SINGLE_VALUE_METHODS.include?(scope) - unscope_code = :"#{scope}_value#{'s' unless single_val_method}=" + unscope_code = "#{scope}_value#{'s' unless single_val_method}=" case scope when :order - self.send(:reverse_order_value=, false) + self.reverse_order_value = false result = [] else result = [] unless single_val_method @@ -869,17 +876,17 @@ module ActiveRecord end def where_unscoping(target_value) - target_value_sym = target_value.to_sym + target_value = target_value.to_s where_values.reject! do |rel| case rel when Arel::Nodes::In, Arel::Nodes::NotIn, Arel::Nodes::Equality, Arel::Nodes::NotEqual subrelation = (rel.left.kind_of?(Arel::Attributes::Attribute) ? rel.left : rel.right) - subrelation.name.to_sym == target_value_sym - else - raise "unscope(where: #{target_value.inspect}) failed: unscoping #{rel.class} is unimplemented." + subrelation.name == target_value end end + + bind_values.reject! { |col,_| col.name == target_value } end def custom_join_ast(table, joins) @@ -985,7 +992,10 @@ module ActiveRecord def build_select(arel, selects) if !selects.empty? - arel.project(*selects) + expanded_select = selects.map do |field| + columns_hash.key?(field.to_s) ? arel_table[field] : field + end + arel.project(*expanded_select) elsif from_value arel.project(Arel.star) else @@ -1043,9 +1053,11 @@ module ActiveRecord order_args.map! do |arg| case arg when Symbol + arg = klass.attribute_alias(arg).to_sym if klass.attribute_alias?(arg) table[arg].asc when Hash arg.map { |field, dir| + field = klass.attribute_alias(field).to_sym if klass.attribute_alias?(field) table[field].send(dir) } else diff --git a/activerecord/lib/active_record/scoping.rb b/activerecord/lib/active_record/scoping.rb index 0cf3d59985..3e43591672 100644 --- a/activerecord/lib/active_record/scoping.rb +++ b/activerecord/lib/active_record/scoping.rb @@ -27,6 +27,11 @@ module ActiveRecord end end + def initialize_internals_callback + super + populate_with_current_scope_attributes + end + # This class stores the +:current_scope+ and +:ignore_default_scope+ values # for different classes. The registry is stored as a thread local, which is # accessed through +ScopeRegistry.current+. diff --git a/activerecord/lib/active_record/scoping/named.rb b/activerecord/lib/active_record/scoping/named.rb index 2a5718f388..49cadb66d0 100644 --- a/activerecord/lib/active_record/scoping/named.rb +++ b/activerecord/lib/active_record/scoping/named.rb @@ -139,6 +139,12 @@ module ActiveRecord # Article.published.featured.latest_article # Article.featured.titles def scope(name, body, &block) + 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 " \ + "a class method with the same name." + end + extension = Module.new(&block) if block singleton_class.send(:define_method, name) do |*args| diff --git a/activerecord/lib/active_record/timestamp.rb b/activerecord/lib/active_record/timestamp.rb index e0541b7681..7178bed560 100644 --- a/activerecord/lib/active_record/timestamp.rb +++ b/activerecord/lib/active_record/timestamp.rb @@ -37,8 +37,8 @@ module ActiveRecord end def initialize_dup(other) # :nodoc: - clear_timestamp_attributes super + clear_timestamp_attributes end private @@ -71,7 +71,7 @@ module ActiveRecord end def should_record_timestamps? - self.record_timestamps && (!partial_writes? || changed? || (attributes.keys & self.class.serialized_attributes.keys).present?) + self.record_timestamps && (!partial_writes? || changed?) end def timestamp_attributes_for_create_in_model diff --git a/activerecord/lib/active_record/transactions.rb b/activerecord/lib/active_record/transactions.rb index 45313b5e75..c33ffeece0 100644 --- a/activerecord/lib/active_record/transactions.rb +++ b/activerecord/lib/active_record/transactions.rb @@ -6,9 +6,6 @@ module ActiveRecord extend ActiveSupport::Concern ACTIONS = [:create, :destroy, :update] - class TransactionError < ActiveRecordError # :nodoc: - end - included do define_callbacks :commit, :rollback, terminator: ->(_, result) { result == false }, @@ -243,15 +240,14 @@ module ActiveRecord def set_options_for_callbacks!(args) options = args.last if options.is_a?(Hash) && options[:on] - assert_valid_transaction_action(options[:on]) - options[:if] = Array(options[:if]) fire_on = Array(options[:on]) + assert_valid_transaction_action(fire_on) + options[:if] = Array(options[:if]) options[:if] << "transaction_include_any_action?(#{fire_on})" end end def assert_valid_transaction_action(actions) - actions = Array(actions) if (actions - ACTIONS).any? raise ArgumentError, ":on conditions for after_commit and after_rollback callbacks have to be one of #{ACTIONS.join(",")}" end @@ -277,6 +273,10 @@ module ActiveRecord with_transaction_returning_status { super } end + def touch(*) #:nodoc: + with_transaction_returning_status { super } + end + # Reset id and @new_record if the transaction rolls back. def rollback_active_record_state! remember_transaction_record_state diff --git a/activerecord/test/cases/adapter_test.rb b/activerecord/test/cases/adapter_test.rb index b67e70ec7e..0eb1231c79 100644 --- a/activerecord/test/cases/adapter_test.rb +++ b/activerecord/test/cases/adapter_test.rb @@ -1,5 +1,7 @@ require "cases/helper" require "models/book" +require "models/post" +require "models/author" module ActiveRecord class AdapterTest < ActiveRecord::TestCase @@ -179,6 +181,27 @@ module ActiveRecord assert result.is_a?(ActiveRecord::Result) end + 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) + 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) + assert_equal "foo", @connection.select_value(query) + assert_equal ["foo"], @connection.select_values(query) + end + + def test_select_methods_passing_a_relation + Post.create!(title: 'foo', body: 'bar') + query = Post.where(title: 'foo').select(:title) + assert_equal({"title" => "foo"}, @connection.select_one(query.arel, nil, query.bind_values)) + assert_equal({"title" => "foo"}, @connection.select_one(query)) + assert @connection.select_all(query).is_a?(ActiveRecord::Result) + assert_equal "foo", @connection.select_value(query) + assert_equal ["foo"], @connection.select_values(query) + end + test "type_to_sql returns a String for unmapped types" do assert_equal "special_db_type", @connection.type_to_sql(:special_db_type) end diff --git a/activerecord/test/cases/adapters/postgresql/array_test.rb b/activerecord/test/cases/adapters/postgresql/array_test.rb index d71e2aa2bb..3090f4478f 100644 --- a/activerecord/test/cases/adapters/postgresql/array_test.rb +++ b/activerecord/test/cases/adapters/postgresql/array_test.rb @@ -93,6 +93,18 @@ class PostgresqlArrayTest < ActiveRecord::TestCase assert_cycle(:tags, [[['1'], ['2']], [['2'], ['3']]]) end + def test_with_empty_strings + assert_cycle(:tags, [ '1', '2', '', '4', '', '5' ]) + end + + def test_with_multi_dimensional_empty_strings + assert_cycle(:tags, [[['1', '2'], ['', '4'], ['', '5']]]) + end + + def test_with_arbitrary_whitespace + assert_cycle(:tags, [[['1', '2'], [' ', '4'], [' ', '5']]]) + end + def test_multi_dimensional_with_integers assert_cycle(:ratings, [[[1], [7]], [[8], [10]]]) end diff --git a/activerecord/test/cases/adapters/postgresql/json_test.rb b/activerecord/test/cases/adapters/postgresql/json_test.rb index 01e7334aad..3daef399d8 100644 --- a/activerecord/test/cases/adapters/postgresql/json_test.rb +++ b/activerecord/test/cases/adapters/postgresql/json_test.rb @@ -21,7 +21,7 @@ class PostgresqlJSONTest < ActiveRecord::TestCase end end rescue ActiveRecord::StatementInvalid - return skip "do not test on PG without json" + skip "do not test on PG without json" end @column = JsonDataType.columns.find { |c| c.name == 'payload' } end diff --git a/activerecord/test/cases/adapters/postgresql/range_test.rb b/activerecord/test/cases/adapters/postgresql/range_test.rb index a56b8ac791..5c2d8e1c1d 100644 --- a/activerecord/test/cases/adapters/postgresql/range_test.rb +++ b/activerecord/test/cases/adapters/postgresql/range_test.rb @@ -26,7 +26,7 @@ if ActiveRecord::Base.connection.supports_ranges? end end rescue ActiveRecord::StatementInvalid - return skip "do not test on PG without range" + skip "do not test on PG without range" end insert_range(id: 101, diff --git a/activerecord/test/cases/adapters/postgresql/schema_test.rb b/activerecord/test/cases/adapters/postgresql/schema_test.rb index c67997c5a1..3f7009c1d1 100644 --- a/activerecord/test/cases/adapters/postgresql/schema_test.rb +++ b/activerecord/test/cases/adapters/postgresql/schema_test.rb @@ -115,6 +115,12 @@ class SchemaTest < ActiveRecord::TestCase end end + def test_raise_wraped_exception_on_bad_prepare + assert_raises(ActiveRecord::StatementInvalid) do + @connection.exec_query "select * from developers where id = ?", 'sql', [[nil, 1]] + end + end + def test_schema_change_with_prepared_stmt altered = false @connection.exec_query "select * from developers where id = $1", 'sql', [[nil, 1]] diff --git a/activerecord/test/cases/adapters/postgresql/xml_test.rb b/activerecord/test/cases/adapters/postgresql/xml_test.rb index bf14b378d8..dd2a727afe 100644 --- a/activerecord/test/cases/adapters/postgresql/xml_test.rb +++ b/activerecord/test/cases/adapters/postgresql/xml_test.rb @@ -18,7 +18,7 @@ class PostgresqlXMLTest < ActiveRecord::TestCase end end rescue ActiveRecord::StatementInvalid - return skip "do not test on PG without xml" + skip "do not test on PG without xml" end @column = XmlDataType.columns.find { |c| c.name == 'payload' } end diff --git a/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb b/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb index 0598ff25f8..02834edf7b 100644 --- a/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb +++ b/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb @@ -34,24 +34,30 @@ module ActiveRecord end def test_connect_with_url - original_connection = ActiveRecord::Base.remove_connection - tf = Tempfile.open 'whatever' - url = "sqlite3://#{tf.path}" - ActiveRecord::Base.establish_connection(url) - assert ActiveRecord::Base.connection - ensure - tf.close - tf.unlink - ActiveRecord::Base.establish_connection(original_connection) + skip "can't establish new connection when using memory db" if in_memory_db? + begin + original_connection = ActiveRecord::Base.remove_connection + tf = Tempfile.open 'whatever' + url = "sqlite3://#{tf.path}" + ActiveRecord::Base.establish_connection(url) + assert ActiveRecord::Base.connection + ensure + tf.close + tf.unlink + ActiveRecord::Base.establish_connection(original_connection) + end end def test_connect_memory_with_url - original_connection = ActiveRecord::Base.remove_connection - url = "sqlite3:///:memory:" - ActiveRecord::Base.establish_connection(url) - assert ActiveRecord::Base.connection - ensure - ActiveRecord::Base.establish_connection(original_connection) + skip "can't establish new connection when using memory db" if in_memory_db? + begin + original_connection = ActiveRecord::Base.remove_connection + url = "sqlite3:///:memory:" + ActiveRecord::Base.establish_connection(url) + assert ActiveRecord::Base.connection + ensure + ActiveRecord::Base.establish_connection(original_connection) + end end def test_valid_column @@ -183,7 +189,7 @@ module ActiveRecord DualEncoding.connection.execute(<<-eosql) CREATE TABLE dual_encodings ( id integer PRIMARY KEY AUTOINCREMENT, - name string, + name varchar(255), data binary ) eosql diff --git a/activerecord/test/cases/associations/belongs_to_associations_test.rb b/activerecord/test/cases/associations/belongs_to_associations_test.rb index 3205d0c28b..9340bc0a83 100644 --- a/activerecord/test/cases/associations/belongs_to_associations_test.rb +++ b/activerecord/test/cases/associations/belongs_to_associations_test.rb @@ -28,6 +28,13 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase assert_equal companies(:first_firm).name, firm.name end + def test_belongs_to_does_not_use_order_by + ActiveRecord::SQLCounter.clear_log + Client.find(3).firm + ensure + assert ActiveRecord::SQLCounter.log_all.all? { |sql| /order by/i !~ sql }, 'ORDER BY was used in the query' + end + def test_belongs_to_with_primary_key client = Client.create(:name => "Primary key client", :firm_name => companies(:first_firm).name) assert_equal companies(:first_firm).name, client.firm_with_primary_key.name @@ -846,4 +853,14 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase assert post.save assert_equal post.author_id, author2.id 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 + Class.new(ActiveRecord::Base) do + belongs_to name + end + end + end + end end diff --git a/activerecord/test/cases/associations/eager_singularization_test.rb b/activerecord/test/cases/associations/eager_singularization_test.rb index 634f6b63ba..669569a774 100644 --- a/activerecord/test/cases/associations/eager_singularization_test.rb +++ b/activerecord/test/cases/associations/eager_singularization_test.rb @@ -1,128 +1,133 @@ require "cases/helper" -class Virus < ActiveRecord::Base - belongs_to :octopus -end -class Octopus < ActiveRecord::Base - has_one :virus -end -class Pass < ActiveRecord::Base - belongs_to :bus -end -class Bus < ActiveRecord::Base - has_many :passes -end -class Mess < ActiveRecord::Base - has_and_belongs_to_many :crises -end -class Crisis < ActiveRecord::Base - has_and_belongs_to_many :messes - has_many :analyses, :dependent => :destroy - has_many :successes, :through => :analyses - has_many :dresses, :dependent => :destroy - has_many :compresses, :through => :dresses -end -class Analysis < ActiveRecord::Base - belongs_to :crisis - belongs_to :success -end -class Success < ActiveRecord::Base - has_many :analyses, :dependent => :destroy - has_many :crises, :through => :analyses -end -class Dress < ActiveRecord::Base - belongs_to :crisis - has_many :compresses -end -class Compress < ActiveRecord::Base - belongs_to :dress -end - class EagerSingularizationTest < ActiveRecord::TestCase + class Virus < ActiveRecord::Base + belongs_to :octopus + end + + class Octopus < ActiveRecord::Base + has_one :virus + end + + class Pass < ActiveRecord::Base + belongs_to :bus + end + + class Bus < ActiveRecord::Base + has_many :passes + end + + class Mess < ActiveRecord::Base + has_and_belongs_to_many :crises + end + + class Crisis < ActiveRecord::Base + has_and_belongs_to_many :messes + has_many :analyses, :dependent => :destroy + has_many :successes, :through => :analyses + has_many :dresses, :dependent => :destroy + has_many :compresses, :through => :dresses + end + + class Analysis < ActiveRecord::Base + belongs_to :crisis + belongs_to :success + end + + class Success < ActiveRecord::Base + has_many :analyses, :dependent => :destroy + has_many :crises, :through => :analyses + end + + class Dress < ActiveRecord::Base + belongs_to :crisis + has_many :compresses + end + + class Compress < ActiveRecord::Base + belongs_to :dress + end def setup - if ActiveRecord::Base.connection.supports_migrations? - ActiveRecord::Base.connection.create_table :viri do |t| - t.column :octopus_id, :integer - t.column :species, :string - end - ActiveRecord::Base.connection.create_table :octopi do |t| - t.column :species, :string - end - ActiveRecord::Base.connection.create_table :passes do |t| - t.column :bus_id, :integer - t.column :rides, :integer - end - ActiveRecord::Base.connection.create_table :buses do |t| - t.column :name, :string - end - ActiveRecord::Base.connection.create_table :crises_messes, :id => false do |t| - t.column :crisis_id, :integer - t.column :mess_id, :integer - end - ActiveRecord::Base.connection.create_table :messes do |t| - t.column :name, :string - end - ActiveRecord::Base.connection.create_table :crises do |t| - t.column :name, :string - end - ActiveRecord::Base.connection.create_table :successes do |t| - t.column :name, :string - end - ActiveRecord::Base.connection.create_table :analyses do |t| - t.column :crisis_id, :integer - t.column :success_id, :integer - end - ActiveRecord::Base.connection.create_table :dresses do |t| - t.column :crisis_id, :integer - end - ActiveRecord::Base.connection.create_table :compresses do |t| - t.column :dress_id, :integer - end - @have_tables = true - else - @have_tables = false + skip 'Does not support migrations' unless connection.supports_migrations? + + connection.create_table :viri do |t| + t.column :octopus_id, :integer + t.column :species, :string + end + connection.create_table :octopi do |t| + t.column :species, :string + end + connection.create_table :passes do |t| + t.column :bus_id, :integer + t.column :rides, :integer + end + connection.create_table :buses do |t| + t.column :name, :string + end + connection.create_table :crises_messes, :id => false do |t| + t.column :crisis_id, :integer + t.column :mess_id, :integer + end + connection.create_table :messes do |t| + t.column :name, :string + end + connection.create_table :crises do |t| + t.column :name, :string + end + connection.create_table :successes do |t| + t.column :name, :string + end + connection.create_table :analyses do |t| + t.column :crisis_id, :integer + t.column :success_id, :integer + end + connection.create_table :dresses do |t| + t.column :crisis_id, :integer + end + connection.create_table :compresses do |t| + t.column :dress_id, :integer end end def teardown - ActiveRecord::Base.connection.drop_table :viri - ActiveRecord::Base.connection.drop_table :octopi - ActiveRecord::Base.connection.drop_table :passes - ActiveRecord::Base.connection.drop_table :buses - ActiveRecord::Base.connection.drop_table :crises_messes - ActiveRecord::Base.connection.drop_table :messes - ActiveRecord::Base.connection.drop_table :crises - ActiveRecord::Base.connection.drop_table :successes - ActiveRecord::Base.connection.drop_table :analyses - ActiveRecord::Base.connection.drop_table :dresses - ActiveRecord::Base.connection.drop_table :compresses + connection.drop_table :viri + connection.drop_table :octopi + connection.drop_table :passes + connection.drop_table :buses + connection.drop_table :crises_messes + connection.drop_table :messes + connection.drop_table :crises + connection.drop_table :successes + connection.drop_table :analyses + connection.drop_table :dresses + connection.drop_table :compresses + end + + def connection + ActiveRecord::Base.connection end def test_eager_no_extra_singularization_belongs_to - return unless @have_tables assert_nothing_raised do Virus.all.merge!(:includes => :octopus).to_a end end def test_eager_no_extra_singularization_has_one - return unless @have_tables assert_nothing_raised do Octopus.all.merge!(:includes => :virus).to_a end end def test_eager_no_extra_singularization_has_many - return unless @have_tables assert_nothing_raised do Bus.all.merge!(:includes => :passes).to_a end end def test_eager_no_extra_singularization_has_and_belongs_to_many - return unless @have_tables assert_nothing_raised do Crisis.all.merge!(:includes => :messes).to_a Mess.all.merge!(:includes => :crises).to_a @@ -130,14 +135,12 @@ class EagerSingularizationTest < ActiveRecord::TestCase end def test_eager_no_extra_singularization_has_many_through_belongs_to - return unless @have_tables assert_nothing_raised do Crisis.all.merge!(:includes => :successes).to_a end end def test_eager_no_extra_singularization_has_many_through_has_many - return unless @have_tables assert_nothing_raised do Crisis.all.merge!(:includes => :compresses).to_a end diff --git a/activerecord/test/cases/associations/has_many_associations_test.rb b/activerecord/test/cases/associations/has_many_associations_test.rb index e45efb0161..a86fb15719 100644 --- a/activerecord/test/cases/associations/has_many_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_associations_test.rb @@ -216,6 +216,31 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end assert_no_queries do + bulbs.second() + bulbs.second({}) + end + + assert_no_queries do + bulbs.third() + bulbs.third({}) + end + + assert_no_queries do + bulbs.fourth() + bulbs.fourth({}) + end + + assert_no_queries do + bulbs.fifth() + bulbs.fifth({}) + end + + assert_no_queries do + bulbs.forty_two() + bulbs.forty_two({}) + end + + assert_no_queries do bulbs.last() bulbs.last({}) end @@ -242,11 +267,11 @@ class HasManyAssociationsTest < ActiveRecord::TestCase # sometimes tests on Oracle fail if ORDER BY is not provided therefore add always :order with :first def test_counting_with_counter_sql - assert_equal 2, Firm.all.merge!(:order => "id").first.clients.count + assert_equal 3, Firm.all.merge!(:order => "id").first.clients.count end def test_counting - assert_equal 2, Firm.all.merge!(:order => "id").first.plain_clients.count + assert_equal 3, Firm.all.merge!(:order => "id").first.plain_clients.count end def test_counting_with_single_hash @@ -254,7 +279,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end def test_counting_with_column_name_and_hash - assert_equal 2, Firm.all.merge!(:order => "id").first.plain_clients.count(:name) + assert_equal 3, Firm.all.merge!(:order => "id").first.plain_clients.count(:name) end def test_counting_with_association_limit @@ -264,17 +289,17 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end def test_finding - assert_equal 2, Firm.all.merge!(:order => "id").first.clients.length + assert_equal 3, Firm.all.merge!(:order => "id").first.clients.length end def test_finding_array_compatibility - assert_equal 2, Firm.order(:id).find{|f| f.id > 0}.clients.length + assert_equal 3, Firm.order(:id).find{|f| f.id > 0}.clients.length end def test_find_many_with_merged_options assert_equal 1, companies(:first_firm).limited_clients.size assert_equal 1, companies(:first_firm).limited_clients.to_a.size - assert_equal 2, companies(:first_firm).limited_clients.limit(nil).to_a.size + assert_equal 3, companies(:first_firm).limited_clients.limit(nil).to_a.size end def test_find_should_append_to_association_order @@ -283,8 +308,8 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end def test_dynamic_find_should_respect_association_order - assert_equal companies(:second_client), companies(:first_firm).clients_sorted_desc.where("type = 'Client'").first - assert_equal companies(:second_client), companies(:first_firm).clients_sorted_desc.find_by_type('Client') + assert_equal companies(:another_first_firm_client), companies(:first_firm).clients_sorted_desc.where("type = 'Client'").first + assert_equal companies(:another_first_firm_client), companies(:first_firm).clients_sorted_desc.find_by_type('Client') end def test_cant_save_has_many_readonly_association @@ -297,7 +322,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end def test_finding_with_different_class_name_and_order - assert_equal "Microsoft", Firm.all.merge!(:order => "id").first.clients_sorted_desc.first.name + assert_equal "Apex", Firm.all.merge!(:order => "id").first.clients_sorted_desc.first.name end def test_finding_with_foreign_key @@ -355,7 +380,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_find_all firm = Firm.all.merge!(:order => "id").first - assert_equal 2, firm.clients.where("#{QUOTED_TYPE} = 'Client'").to_a.length + assert_equal 3, firm.clients.where("#{QUOTED_TYPE} = 'Client'").to_a.length assert_equal 1, firm.clients.where("name = 'Summit'").to_a.length end @@ -364,7 +389,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert ! firm.clients.loaded? - assert_queries(3) do + assert_queries(4) do firm.clients.find_each(:batch_size => 1) {|c| assert_equal firm.id, c.firm_id } end @@ -434,15 +459,15 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_find_grouped all_clients_of_firm1 = Client.all.merge!(:where => "firm_id = 1").to_a grouped_clients_of_firm1 = Client.all.merge!(:where => "firm_id = 1", :group => "firm_id", :select => 'firm_id, count(id) as clients_count').to_a - assert_equal 2, all_clients_of_firm1.size + assert_equal 3, all_clients_of_firm1.size assert_equal 1, grouped_clients_of_firm1.size end def test_find_scoped_grouped assert_equal 1, companies(:first_firm).clients_grouped_by_firm_id.size assert_equal 1, companies(:first_firm).clients_grouped_by_firm_id.length - assert_equal 2, companies(:first_firm).clients_grouped_by_name.size - assert_equal 2, companies(:first_firm).clients_grouped_by_name.length + assert_equal 3, companies(:first_firm).clients_grouped_by_name.size + assert_equal 3, companies(:first_firm).clients_grouped_by_name.length end def test_find_scoped_grouped_having @@ -462,25 +487,25 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_equal [1], posts(:welcome).comments.select { |c| c.id == 1 }.map(&:id) end - def test_select_without_foreign_key + def test_select_without_foreign_key assert_equal companies(:first_firm).accounts.first.credit_limit, companies(:first_firm).accounts.select(:credit_limit).first.credit_limit - end + end def test_adding force_signal37_to_load_all_clients_of_firm natural = Client.new("name" => "Natural Company") companies(:first_firm).clients_of_firm << natural - assert_equal 2, companies(:first_firm).clients_of_firm.size # checking via the collection - assert_equal 2, companies(:first_firm).clients_of_firm(true).size # checking using the db + assert_equal 3, companies(:first_firm).clients_of_firm.size # checking via the collection + assert_equal 3, companies(:first_firm).clients_of_firm(true).size # checking using the db assert_equal natural, companies(:first_firm).clients_of_firm.last end def test_adding_using_create first_firm = companies(:first_firm) - assert_equal 2, first_firm.plain_clients.size - first_firm.plain_clients.create(:name => "Natural Company") - assert_equal 3, first_firm.plain_clients.length assert_equal 3, first_firm.plain_clients.size + first_firm.plain_clients.create(:name => "Natural Company") + assert_equal 4, first_firm.plain_clients.length + assert_equal 4, first_firm.plain_clients.size end def test_create_with_bang_on_has_many_when_parent_is_new_raises @@ -519,8 +544,8 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_adding_a_collection force_signal37_to_load_all_clients_of_firm companies(:first_firm).clients_of_firm.concat([Client.new("name" => "Natural Company"), Client.new("name" => "Apple")]) - assert_equal 3, companies(:first_firm).clients_of_firm.size - assert_equal 3, companies(:first_firm).clients_of_firm(true).size + assert_equal 4, companies(:first_firm).clients_of_firm.size + assert_equal 4, companies(:first_firm).clients_of_firm(true).size end def test_transactions_when_adding_to_persisted @@ -573,7 +598,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase company = companies(:first_firm) # company already has one client company.clients_of_firm.build("name" => "Another Client") company.clients_of_firm.build("name" => "Yet Another Client") - assert_equal 3, company.clients_of_firm.size + assert_equal 4, company.clients_of_firm.size end def test_collection_not_empty_after_building @@ -649,14 +674,14 @@ class HasManyAssociationsTest < ActiveRecord::TestCase Firm.column_names Client.column_names - assert_equal 1, first_firm.clients_of_firm.size + assert_equal 2, first_firm.clients_of_firm.size first_firm.clients_of_firm.reset assert_queries(1) do first_firm.clients_of_firm.create(:name => "Superstars") end - assert_equal 2, first_firm.clients_of_firm.size + assert_equal 3, first_firm.clients_of_firm.size end def test_create @@ -669,7 +694,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_create_many companies(:first_firm).clients_of_firm.create([{"name" => "Another Client"}, {"name" => "Another Client II"}]) - assert_equal 3, companies(:first_firm).clients_of_firm(true).size + assert_equal 4, companies(:first_firm).clients_of_firm(true).size end def test_create_followed_by_save_does_not_load_target @@ -681,8 +706,8 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_deleting force_signal37_to_load_all_clients_of_firm companies(:first_firm).clients_of_firm.delete(companies(:first_firm).clients_of_firm.first) - assert_equal 0, companies(:first_firm).clients_of_firm.size - assert_equal 0, companies(:first_firm).clients_of_firm(true).size + assert_equal 1, companies(:first_firm).clients_of_firm.size + assert_equal 1, companies(:first_firm).clients_of_firm(true).size end def test_deleting_before_save @@ -779,8 +804,8 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_deleting_a_collection force_signal37_to_load_all_clients_of_firm companies(:first_firm).clients_of_firm.create("name" => "Another Client") - assert_equal 2, companies(:first_firm).clients_of_firm.size - companies(:first_firm).clients_of_firm.delete([companies(:first_firm).clients_of_firm[0], companies(:first_firm).clients_of_firm[1]]) + assert_equal 3, companies(:first_firm).clients_of_firm.size + companies(:first_firm).clients_of_firm.delete([companies(:first_firm).clients_of_firm[0], companies(:first_firm).clients_of_firm[1], companies(:first_firm).clients_of_firm[2]]) assert_equal 0, companies(:first_firm).clients_of_firm.size assert_equal 0, companies(:first_firm).clients_of_firm(true).size end @@ -789,7 +814,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase force_signal37_to_load_all_clients_of_firm companies(:first_firm).dependent_clients_of_firm.create("name" => "Another Client") clients = companies(:first_firm).dependent_clients_of_firm.to_a - assert_equal 2, clients.count + assert_equal 3, clients.count assert_difference "Client.count", -(clients.count) do companies(:first_firm).dependent_clients_of_firm.delete_all @@ -799,7 +824,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_delete_all_with_not_yet_loaded_association_collection force_signal37_to_load_all_clients_of_firm companies(:first_firm).clients_of_firm.create("name" => "Another Client") - assert_equal 2, companies(:first_firm).clients_of_firm.size + assert_equal 3, companies(:first_firm).clients_of_firm.size companies(:first_firm).clients_of_firm.reset companies(:first_firm).clients_of_firm.delete_all assert_equal 0, companies(:first_firm).clients_of_firm.size @@ -832,7 +857,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_clearing_an_association_collection firm = companies(:first_firm) client_id = firm.clients_of_firm.first.id - assert_equal 1, firm.clients_of_firm.size + assert_equal 2, firm.clients_of_firm.size firm.clients_of_firm.clear @@ -866,7 +891,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_clearing_a_dependent_association_collection firm = companies(:first_firm) client_id = firm.dependent_clients_of_firm.first.id - assert_equal 1, firm.dependent_clients_of_firm.size + assert_equal 2, firm.dependent_clients_of_firm.size assert_equal 1, Client.find_by_id(client_id).client_of # :delete_all is called on each client since the dependent options is :destroy @@ -897,7 +922,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_clearing_an_exclusively_dependent_association_collection firm = companies(:first_firm) client_id = firm.exclusively_dependent_clients_of_firm.first.id - assert_equal 1, firm.exclusively_dependent_clients_of_firm.size + assert_equal 2, firm.exclusively_dependent_clients_of_firm.size assert_equal [], Client.destroyed_client_ids[firm.id] @@ -953,10 +978,10 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_delete_all_association_with_primary_key_deletes_correct_records firm = Firm.first # break the vanilla firm_id foreign key - assert_equal 2, firm.clients.count + assert_equal 3, firm.clients.count firm.clients.first.update_columns(firm_id: nil) - assert_equal 1, firm.clients(true).count - assert_equal 1, firm.clients_using_primary_key_with_delete_all.count + assert_equal 2, firm.clients(true).count + assert_equal 2, firm.clients_using_primary_key_with_delete_all.count old_record = firm.clients_using_primary_key_with_delete_all.first firm = Firm.first firm.destroy @@ -988,8 +1013,8 @@ class HasManyAssociationsTest < ActiveRecord::TestCase force_signal37_to_load_all_clients_of_firm summit = Client.find_by_name('Summit') companies(:first_firm).clients_of_firm.delete(summit) - assert_equal 1, companies(:first_firm).clients_of_firm.size - assert_equal 1, companies(:first_firm).clients_of_firm(true).size + assert_equal 2, companies(:first_firm).clients_of_firm.size + assert_equal 2, companies(:first_firm).clients_of_firm(true).size assert_equal 2, summit.client_of end @@ -1026,8 +1051,8 @@ class HasManyAssociationsTest < ActiveRecord::TestCase companies(:first_firm).clients_of_firm.destroy(companies(:first_firm).clients_of_firm.first) end - assert_equal 0, companies(:first_firm).reload.clients_of_firm.size - assert_equal 0, companies(:first_firm).clients_of_firm(true).size + assert_equal 1, companies(:first_firm).reload.clients_of_firm.size + assert_equal 1, companies(:first_firm).clients_of_firm(true).size end def test_destroying_by_fixnum_id @@ -1037,8 +1062,8 @@ class HasManyAssociationsTest < ActiveRecord::TestCase companies(:first_firm).clients_of_firm.destroy(companies(:first_firm).clients_of_firm.first.id) end - assert_equal 0, companies(:first_firm).reload.clients_of_firm.size - assert_equal 0, companies(:first_firm).clients_of_firm(true).size + assert_equal 1, companies(:first_firm).reload.clients_of_firm.size + assert_equal 1, companies(:first_firm).clients_of_firm(true).size end def test_destroying_by_string_id @@ -1048,21 +1073,21 @@ class HasManyAssociationsTest < ActiveRecord::TestCase companies(:first_firm).clients_of_firm.destroy(companies(:first_firm).clients_of_firm.first.id.to_s) end - assert_equal 0, companies(:first_firm).reload.clients_of_firm.size - assert_equal 0, companies(:first_firm).clients_of_firm(true).size + assert_equal 1, companies(:first_firm).reload.clients_of_firm.size + assert_equal 1, companies(:first_firm).clients_of_firm(true).size end def test_destroying_a_collection force_signal37_to_load_all_clients_of_firm companies(:first_firm).clients_of_firm.create("name" => "Another Client") - assert_equal 2, companies(:first_firm).clients_of_firm.size + assert_equal 3, companies(:first_firm).clients_of_firm.size assert_difference "Client.count", -2 do companies(:first_firm).clients_of_firm.destroy([companies(:first_firm).clients_of_firm[0], companies(:first_firm).clients_of_firm[1]]) end - assert_equal 0, companies(:first_firm).reload.clients_of_firm.size - assert_equal 0, companies(:first_firm).clients_of_firm(true).size + assert_equal 1, companies(:first_firm).reload.clients_of_firm.size + assert_equal 1, companies(:first_firm).clients_of_firm(true).size end def test_destroy_all @@ -1078,7 +1103,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_dependence firm = companies(:first_firm) - assert_equal 2, firm.clients.size + assert_equal 3, firm.clients.size firm.destroy assert Client.all.merge!(:where => "firm_id=#{firm.id}").to_a.empty? end @@ -1091,14 +1116,14 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_destroy_dependent_when_deleted_from_association # sometimes tests on Oracle fail if ORDER BY is not provided therefore add always :order with :first firm = Firm.all.merge!(:order => "id").first - assert_equal 2, firm.clients.size + assert_equal 3, firm.clients.size client = firm.clients.first firm.clients.delete(client) assert_raise(ActiveRecord::RecordNotFound) { Client.find(client.id) } assert_raise(ActiveRecord::RecordNotFound) { firm.clients.find(client.id) } - assert_equal 1, firm.clients.size + assert_equal 2, firm.clients.size end def test_three_levels_of_dependence @@ -1113,12 +1138,12 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_dependence_with_transaction_support_on_failure firm = companies(:first_firm) clients = firm.clients - assert_equal 2, clients.length + assert_equal 3, clients.length clients.last.instance_eval { def overwrite_to_raise() raise "Trigger rollback" end } firm.destroy rescue "do nothing" - assert_equal 2, Client.all.merge!(:where => "firm_id=#{firm.id}").to_a.size + assert_equal 3, Client.all.merge!(:where => "firm_id=#{firm.id}").to_a.size end def test_dependence_on_account @@ -1239,7 +1264,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end def test_get_ids - assert_equal [companies(:first_client).id, companies(:second_client).id], companies(:first_firm).client_ids + assert_equal [companies(:first_client).id, companies(:second_client).id, companies(:another_first_firm_client).id], companies(:first_firm).client_ids end def test_get_ids_for_loaded_associations @@ -1254,7 +1279,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_get_ids_for_unloaded_associations_does_not_load_them company = companies(:first_firm) assert !company.clients.loaded? - assert_equal [companies(:first_client).id, companies(:second_client).id], company.client_ids + assert_equal [companies(:first_client).id, companies(:second_client).id, companies(:another_first_firm_client).id], company.client_ids assert !company.clients.loaded? end @@ -1263,7 +1288,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end def test_get_ids_for_ordered_association - assert_equal [companies(:second_client).id, companies(:first_client).id], companies(:first_firm).clients_ordered_by_name_ids + assert_equal [companies(:another_first_firm_client).id, companies(:second_client).id, companies(:first_client).id], companies(:first_firm).clients_ordered_by_name_ids end def test_get_ids_for_association_on_new_record_does_not_try_to_find_records @@ -1357,9 +1382,10 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_equal false, firm.clients.include?(client) end - def test_calling_first_or_last_on_association_should_not_load_association + def test_calling_first_nth_or_last_on_association_should_not_load_association firm = companies(:first_firm) firm.clients.first + firm.clients.second firm.clients.last assert !firm.clients.loaded? end @@ -1384,30 +1410,33 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_queries 1 do firm.clients.first + firm.clients.second firm.clients.last end assert firm.clients.loaded? end - def test_calling_first_or_last_on_existing_record_with_create_should_not_load_association + def test_calling_first_nth_or_last_on_existing_record_with_create_should_not_load_association firm = companies(:first_firm) firm.clients.create(:name => 'Foo') assert !firm.clients.loaded? - assert_queries 2 do + assert_queries 3 do firm.clients.first + firm.clients.second firm.clients.last end assert !firm.clients.loaded? end - def test_calling_first_or_last_on_new_record_should_not_run_queries + def test_calling_first_nth_or_last_on_new_record_should_not_run_queries firm = Firm.new assert_no_queries do firm.clients.first + firm.clients.second firm.clients.last end end @@ -1494,7 +1523,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase def test_calling_many_should_return_true_if_more_than_one firm = companies(:first_firm) assert firm.clients.many? - assert_equal 2, firm.clients.size + assert_equal 3, firm.clients.size end def test_joins_with_namespaced_model_should_use_correct_type @@ -1791,4 +1820,14 @@ class HasManyAssociationsTest < ActiveRecord::TestCase topic.approved_replies.create! end 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 + Class.new(ActiveRecord::Base) do + has_many name + end + end + end + end end diff --git a/activerecord/test/cases/associations/has_one_associations_test.rb b/activerecord/test/cases/associations/has_one_associations_test.rb index 5a41461edf..a4650ccdf2 100644 --- a/activerecord/test/cases/associations/has_one_associations_test.rb +++ b/activerecord/test/cases/associations/has_one_associations_test.rb @@ -22,6 +22,13 @@ class HasOneAssociationsTest < ActiveRecord::TestCase assert_equal Account.find(1).credit_limit, companies(:first_firm).account.credit_limit end + def test_has_one_does_not_use_order_by + ActiveRecord::SQLCounter.clear_log + companies(:first_firm).account + ensure + assert ActiveRecord::SQLCounter.log_all.all? { |sql| /order by/i !~ sql }, 'ORDER BY was used in the query' + end + def test_has_one_cache_nils firm = companies(:another_firm) assert_queries(1) { assert_nil firm.account } @@ -557,4 +564,14 @@ class HasOneAssociationsTest < ActiveRecord::TestCase end end 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 + Class.new(ActiveRecord::Base) do + has_one name + end + end + end + end end diff --git a/activerecord/test/cases/associations_test.rb b/activerecord/test/cases/associations_test.rb index 48e6fc5cd4..f663b5490c 100644 --- a/activerecord/test/cases/associations_test.rb +++ b/activerecord/test/cases/associations_test.rb @@ -255,6 +255,15 @@ class AssociationProxyTest < ActiveRecord::TestCase assert_equal man, man.interests.where("1=1").first.man end end + + def test_reset_unloads_target + david = authors(:david) + david.posts.reload + + assert david.posts.loaded? + david.posts.reset + assert !david.posts.loaded? + end end class OverridingAssociationsTest < ActiveRecord::TestCase diff --git a/activerecord/test/cases/autosave_association_test.rb b/activerecord/test/cases/autosave_association_test.rb index 517d2674a7..703f805188 100644 --- a/activerecord/test/cases/autosave_association_test.rb +++ b/activerecord/test/cases/autosave_association_test.rb @@ -401,7 +401,7 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociation < ActiveRecord::TestCa assert_equal new_client, companies(:first_firm).clients_of_firm.last assert !companies(:first_firm).save assert !new_client.persisted? - assert_equal 1, companies(:first_firm).clients_of_firm(true).size + assert_equal 2, companies(:first_firm).clients_of_firm(true).size end def test_adding_before_save @@ -455,7 +455,7 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociation < ActiveRecord::TestCa company.name += '-changed' assert_queries(2) { assert company.save } assert new_client.persisted? - assert_equal 2, company.clients_of_firm(true).size + assert_equal 3, company.clients_of_firm(true).size end def test_build_many_before_save @@ -464,7 +464,7 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociation < ActiveRecord::TestCa company.name += '-changed' assert_queries(3) { assert company.save } - assert_equal 3, company.clients_of_firm(true).size + assert_equal 4, company.clients_of_firm(true).size end def test_build_via_block_before_save @@ -475,7 +475,7 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociation < ActiveRecord::TestCa company.name += '-changed' assert_queries(2) { assert company.save } assert new_client.persisted? - assert_equal 2, company.clients_of_firm(true).size + assert_equal 3, company.clients_of_firm(true).size end def test_build_many_via_block_before_save @@ -488,7 +488,7 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociation < ActiveRecord::TestCa company.name += '-changed' assert_queries(3) { assert company.save } - assert_equal 3, company.clients_of_firm(true).size + assert_equal 4, company.clients_of_firm(true).size end def test_replace_on_new_object @@ -574,6 +574,13 @@ class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase @ship = @pirate.create_ship(:name => 'Nights Dirty Lightning') end + def teardown + # We are running without transactional fixtures and need to cleanup. + Bird.delete_all + @ship.delete + @pirate.delete + end + # reload def test_a_marked_for_destruction_record_should_not_be_be_marked_after_reload @pirate.mark_for_destruction diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb index cb8e564da1..8a0b0b9589 100644 --- a/activerecord/test/cases/base_test.rb +++ b/activerecord/test/cases/base_test.rb @@ -321,7 +321,7 @@ class BasicsTest < ActiveRecord::TestCase def test_load topics = Topic.all.merge!(:order => 'id').to_a - assert_equal(4, topics.size) + assert_equal(5, topics.size) assert_equal(topics(:first).title, topics.first.title) end @@ -626,6 +626,7 @@ class BasicsTest < ActiveRecord::TestCase assert_equal ["EUC-JP"], Weird.columns.map {|c| c.name.encoding.name }.uniq ensure silence_warnings { Encoding.default_internal = old_default_internal } + Weird.reset_column_information end end @@ -1129,7 +1130,7 @@ class BasicsTest < ActiveRecord::TestCase k = Class.new(ak) k.table_name = "projects" orig_name = k.sequence_name - return skip "sequences not supported by db" unless orig_name + skip "sequences not supported by db" unless orig_name assert_equal k.reset_sequence_name, orig_name end @@ -1301,9 +1302,11 @@ class BasicsTest < ActiveRecord::TestCase end def test_compute_type_nonexistent_constant - assert_raises NameError do + e = assert_raises NameError do ActiveRecord::Base.send :compute_type, 'NonexistentModel' end + assert_equal 'uninitialized constant ActiveRecord::Base::NonexistentModel', e.message + assert_equal 'ActiveRecord::Base::NonexistentModel', e.name end def test_compute_type_no_method_error @@ -1377,6 +1380,8 @@ class BasicsTest < ActiveRecord::TestCase }) rd, wr = IO.pipe + rd.binmode + wr.binmode ActiveRecord::Base.connection_handler.clear_all_connections! diff --git a/activerecord/test/cases/batches_test.rb b/activerecord/test/cases/batches_test.rb index 38c2560d69..8216d74cb3 100644 --- a/activerecord/test/cases/batches_test.rb +++ b/activerecord/test/cases/batches_test.rb @@ -46,7 +46,9 @@ class EachTest < ActiveRecord::TestCase def test_each_should_raise_if_select_is_set_without_id assert_raise(RuntimeError) do - Post.select(:title).find_each(:batch_size => 1) { |post| post } + Post.select(:title).find_each(batch_size: 1) { |post| + flunk "should not call this block" + } end end @@ -151,6 +153,12 @@ class EachTest < ActiveRecord::TestCase assert_equal special_posts_ids, posts.map(&:id) end + def test_find_in_batches_should_not_modify_passed_options + assert_nothing_raised do + Post.find_in_batches({ batch_size: 42, start: 1 }.freeze){} + end + end + def test_find_in_batches_should_use_any_column_as_primary_key nick_order_subscribers = Subscriber.order('nick asc') start_nick = nick_order_subscribers.second.nick @@ -170,4 +178,17 @@ class EachTest < ActiveRecord::TestCase end end end + + def test_find_in_batches_should_return_an_enumerator + enum = nil + assert_queries(0) do + enum = Post.find_in_batches(:batch_size => 1) + end + assert_queries(4) do + enum.first(4) do |batch| + assert_kind_of Array, batch + assert_kind_of Post, batch.first + end + end + end end diff --git a/activerecord/test/cases/calculations_test.rb b/activerecord/test/cases/calculations_test.rb index 2f6913167d..db999f90ab 100644 --- a/activerecord/test/cases/calculations_test.rb +++ b/activerecord/test/cases/calculations_test.rb @@ -278,7 +278,7 @@ class CalculationsTest < ActiveRecord::TestCase c = Company.group("UPPER(#{QUOTED_TYPE})").count(:all) assert_equal 2, c[nil] assert_equal 1, c['DEPENDENTFIRM'] - assert_equal 4, c['CLIENT'] + assert_equal 5, c['CLIENT'] assert_equal 2, c['FIRM'] end @@ -286,7 +286,7 @@ class CalculationsTest < ActiveRecord::TestCase c = Company.group("UPPER(companies.#{QUOTED_TYPE})").count(:all) assert_equal 2, c[nil] assert_equal 1, c['DEPENDENTFIRM'] - assert_equal 4, c['CLIENT'] + assert_equal 5, c['CLIENT'] assert_equal 2, c['FIRM'] end @@ -466,14 +466,14 @@ class CalculationsTest < ActiveRecord::TestCase def test_distinct_is_honored_when_used_with_count_operation_after_group # Count the number of authors for approved topics approved_topics_count = Topic.group(:approved).count(:author_name)[true] - assert_equal approved_topics_count, 3 + assert_equal approved_topics_count, 4 # Count the number of distinct authors for approved Topics distinct_authors_for_approved_count = Topic.group(:approved).distinct.count(:author_name)[true] - assert_equal distinct_authors_for_approved_count, 2 + assert_equal distinct_authors_for_approved_count, 3 end def test_pluck - assert_equal [1,2,3,4], Topic.order(:id).pluck(:id) + assert_equal [1,2,3,4,5], Topic.order(:id).pluck(:id) end def test_pluck_without_column_names @@ -509,7 +509,7 @@ class CalculationsTest < ActiveRecord::TestCase end def test_pluck_with_qualified_column_name - assert_equal [1,2,3,4], Topic.order(:id).pluck("topics.id") + assert_equal [1,2,3,4,5], Topic.order(:id).pluck("topics.id") end def test_pluck_auto_table_name_prefix @@ -557,11 +557,13 @@ class CalculationsTest < ActiveRecord::TestCase def test_pluck_multiple_columns assert_equal [ [1, "The First Topic"], [2, "The Second Topic of the day"], - [3, "The Third Topic of the day"], [4, "The Fourth Topic of the day"] + [3, "The Third Topic of the day"], [4, "The Fourth Topic of the day"], + [5, "The Fifth Topic of the day"] ], Topic.order(:id).pluck(:id, :title) assert_equal [ [1, "The First Topic", "David"], [2, "The Second Topic of the day", "Mary"], - [3, "The Third Topic of the day", "Carl"], [4, "The Fourth Topic of the day", "Carl"] + [3, "The Third Topic of the day", "Carl"], [4, "The Fourth Topic of the day", "Carl"], + [5, "The Fifth Topic of the day", "Jason"] ], Topic.order(:id).pluck(:id, :title, :author_name) end @@ -587,7 +589,7 @@ class CalculationsTest < ActiveRecord::TestCase def test_pluck_replaces_select_clause taks_relation = Topic.select(:approved, :id).order(:id) - assert_equal [1,2,3,4], taks_relation.pluck(:id) - assert_equal [false, true, true, true], taks_relation.pluck(:approved) + assert_equal [1,2,3,4,5], taks_relation.pluck(:id) + assert_equal [false, true, true, true, true], taks_relation.pluck(:approved) end end diff --git a/activerecord/test/cases/connection_management_test.rb b/activerecord/test/cases/connection_management_test.rb index 00667cc52e..77d9ae9b8e 100644 --- a/activerecord/test/cases/connection_management_test.rb +++ b/activerecord/test/cases/connection_management_test.rb @@ -31,6 +31,8 @@ module ActiveRecord object_id = ActiveRecord::Base.connection.object_id rd, wr = IO.pipe + rd.binmode + wr.binmode pid = fork { rd.close diff --git a/activerecord/test/cases/enum_test.rb b/activerecord/test/cases/enum_test.rb index c09e58fbf1..1b95708cb3 100644 --- a/activerecord/test/cases/enum_test.rb +++ b/activerecord/test/cases/enum_test.rb @@ -51,6 +51,77 @@ class EnumTest < ActiveRecord::TestCase assert @book.written? end + test "enum changed attributes" do + old_status = @book.status + @book.status = :published + assert_equal old_status, @book.changed_attributes[:status] + end + + test "enum changes" do + old_status = @book.status + @book.status = :published + assert_equal [old_status, 'published'], @book.changes[:status] + end + + test "enum attribute was" do + old_status = @book.status + @book.status = :published + assert_equal old_status, @book.attribute_was(:status) + end + + test "enum attribute changed" do + @book.status = :published + assert @book.attribute_changed?(:status) + end + + test "enum attribute changed to" do + @book.status = :published + assert @book.attribute_changed?(:status, to: 'published') + end + + test "enum attribute changed from" do + old_status = @book.status + @book.status = :published + assert @book.attribute_changed?(:status, from: old_status) + end + + test "enum attribute changed from old status to new status" do + old_status = @book.status + @book.status = :published + assert @book.attribute_changed?(:status, from: old_status, to: 'published') + end + + test "enum didn't change" do + old_status = @book.status + @book.status = old_status + assert_not @book.attribute_changed?(:status) + end + + test "persist changes that are dirty" do + @book.status = :published + assert @book.attribute_changed?(:status) + @book.status = :written + assert @book.attribute_changed?(:status) + end + + test "reverted changes that are not dirty" do + old_status = @book.status + @book.status = :published + assert @book.attribute_changed?(:status) + @book.status = old_status + assert_not @book.attribute_changed?(:status) + end + + test "reverted changes are not dirty going from nil to value and back" do + book = Book.create!(nullable_status: nil) + + book.nullable_status = :married + assert book.attribute_changed?(:nullable_status) + + book.nullable_status = nil + assert_not book.attribute_changed?(:nullable_status) + end + test "assign non existing value raises an error" do e = assert_raises(ArgumentError) do @book.status = :unknown @@ -74,9 +145,9 @@ class EnumTest < ActiveRecord::TestCase end test "constant to access the mapping" do - assert_equal 0, Book::STATUS[:proposed] - assert_equal 1, Book::STATUS["written"] - assert_equal 2, Book::STATUS[:published] + assert_equal 0, Book.statuses[:proposed] + assert_equal 1, Book.statuses["written"] + assert_equal 2, Book.statuses[:published] end test "building new objects with enum scopes" do @@ -88,4 +159,67 @@ class EnumTest < ActiveRecord::TestCase assert Book.written.create.written? assert Book.read.create.read? end + + test "_before_type_cast returns the enum label (required for form fields)" do + assert_equal "proposed", @book.status_before_type_cast + end + + test "reserved enum names" do + klass = Class.new(ActiveRecord::Base) do + self.table_name = "books" + enum status: [:proposed, :written, :published] + end + + conflicts = [ + :column, # generates class method .columns, which conflicts with an AR method + :logger, # generates #logger, which conflicts with an AR method + :attributes, # generates #attributes=, which conflicts with an AR method + ] + + conflicts.each_with_index do |name, i| + assert_raises(ArgumentError, "enum name `#{name}` should not be allowed") do + klass.class_eval { enum name => ["value_#{i}"] } + end + end + end + + test "reserved enum values" do + klass = Class.new(ActiveRecord::Base) do + self.table_name = "books" + enum status: [:proposed, :written, :published] + end + + conflicts = [ + :new, # generates a scope that conflicts with an AR class method + :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 + ] + + conflicts.each_with_index do |value, i| + assert_raises(ArgumentError, "enum value `#{value}` should not be allowed") do + klass.class_eval { enum "status_#{i}" => [value] } + end + end + end + + test "overriding enum method should not raise" do + assert_nothing_raised do + Class.new(ActiveRecord::Base) do + self.table_name = "books" + + def published! + super + "do publish work..." + end + + enum status: [:proposed, :written, :published] + + def written! + super + "do written work..." + end + end + end + end end diff --git a/activerecord/test/cases/finder_respond_to_test.rb b/activerecord/test/cases/finder_respond_to_test.rb index 3ff22f222f..6ab2657c44 100644 --- a/activerecord/test/cases/finder_respond_to_test.rb +++ b/activerecord/test/cases/finder_respond_to_test.rb @@ -5,6 +5,11 @@ class FinderRespondToTest < ActiveRecord::TestCase fixtures :topics + def test_should_preserve_normal_respond_to_behaviour_on_base + assert_respond_to ActiveRecord::Base, :new + assert !ActiveRecord::Base.respond_to?(:find_by_something) + end + def test_should_preserve_normal_respond_to_behaviour_and_respond_to_newly_added_method class << Topic; self; end.send(:define_method, :method_added_for_finder_respond_to_test) { } assert_respond_to Topic, :method_added_for_finder_respond_to_test diff --git a/activerecord/test/cases/finder_test.rb b/activerecord/test/cases/finder_test.rb index 5125d5df2a..b1eded6494 100644 --- a/activerecord/test/cases/finder_test.rb +++ b/activerecord/test/cases/finder_test.rb @@ -254,6 +254,94 @@ class FinderTest < ActiveRecord::TestCase end end + def test_second + assert_equal topics(:second).title, Topic.second.title + end + + def test_second_with_offset + assert_equal topics(:fifth), Topic.offset(3).second + end + + def test_second_have_primary_key_order_by_default + expected = topics(:second) + expected.touch # PostgreSQL changes the default order if no order clause is used + assert_equal expected, Topic.second + end + + def test_model_class_responds_to_second_bang + assert Topic.second! + Topic.delete_all + assert_raises ActiveRecord::RecordNotFound do + Topic.second! + end + end + + def test_third + assert_equal topics(:third).title, Topic.third.title + end + + def test_third_with_offset + assert_equal topics(:fifth), Topic.offset(2).third + end + + def test_third_have_primary_key_order_by_default + expected = topics(:third) + expected.touch # PostgreSQL changes the default order if no order clause is used + assert_equal expected, Topic.third + end + + def test_model_class_responds_to_third_bang + assert Topic.third! + Topic.delete_all + assert_raises ActiveRecord::RecordNotFound do + Topic.third! + end + end + + def test_fourth + assert_equal topics(:fourth).title, Topic.fourth.title + end + + def test_fourth_with_offset + assert_equal topics(:fifth), Topic.offset(1).fourth + end + + def test_fourth_have_primary_key_order_by_default + expected = topics(:fourth) + expected.touch # PostgreSQL changes the default order if no order clause is used + assert_equal expected, Topic.fourth + end + + def test_model_class_responds_to_fourth_bang + assert Topic.fourth! + Topic.delete_all + assert_raises ActiveRecord::RecordNotFound do + Topic.fourth! + end + end + + def test_fifth + assert_equal topics(:fifth).title, Topic.fifth.title + end + + def test_fifth_with_offset + assert_equal topics(:fifth), Topic.offset(0).fifth + end + + def test_fifth_have_primary_key_order_by_default + expected = topics(:fifth) + expected.touch # PostgreSQL changes the default order if no order clause is used + assert_equal expected, Topic.fifth + end + + def test_model_class_responds_to_fifth_bang + assert Topic.fifth! + Topic.delete_all + assert_raises ActiveRecord::RecordNotFound do + Topic.fifth! + end + end + def test_last_bang_present assert_nothing_raised do assert_equal topics(:second), Topic.where("title = 'The Second Topic of the day'").last! @@ -267,7 +355,7 @@ class FinderTest < ActiveRecord::TestCase end def test_model_class_responds_to_last_bang - assert_equal topics(:fourth), Topic.last! + assert_equal topics(:fifth), Topic.last! assert_raises ActiveRecord::RecordNotFound do Topic.delete_all Topic.last! @@ -812,8 +900,8 @@ class FinderTest < ActiveRecord::TestCase end def test_select_values - assert_equal ["1","2","3","4","5","6","7","8","9", "10"], Company.connection.select_values("SELECT id FROM companies ORDER BY id").map! { |i| i.to_s } - assert_equal ["37signals","Summit","Microsoft", "Flamboyant Software", "Ex Nihilo", "RailsCore", "Leetsoft", "Jadedpixel", "Odegy", "Ex Nihilo Part Deux"], Company.connection.select_values("SELECT name FROM companies ORDER BY id") + assert_equal ["1","2","3","4","5","6","7","8","9", "10", "11"], Company.connection.select_values("SELECT id FROM companies ORDER BY id").map! { |i| i.to_s } + assert_equal ["37signals","Summit","Microsoft", "Flamboyant Software", "Ex Nihilo", "RailsCore", "Leetsoft", "Jadedpixel", "Odegy", "Ex Nihilo Part Deux", "Apex"], Company.connection.select_values("SELECT name FROM companies ORDER BY id") end def test_select_rows @@ -863,14 +951,23 @@ class FinderTest < ActiveRecord::TestCase end def test_find_one_message_with_custom_primary_key - Toy.primary_key = :name - begin - Toy.find 'Hello World!' - rescue ActiveRecord::RecordNotFound => e - assert_equal 'Couldn\'t find Toy with name=Hello World!', e.message + table_with_custom_primary_key do |model| + model.primary_key = :name + e = assert_raises(ActiveRecord::RecordNotFound) do + model.find 'Hello World!' + end + assert_equal %Q{Couldn't find MercedesCar with 'name'=Hello World!}, e.message + end + end + + def test_find_some_message_with_custom_primary_key + table_with_custom_primary_key do |model| + model.primary_key = :name + e = assert_raises(ActiveRecord::RecordNotFound) do + model.find 'Hello', 'World!' + end + assert_equal %Q{Couldn't find all MercedesCars with 'name': (Hello, World!) (found 0 results, but was looking for 2)}, e.message end - ensure - Toy.reset_primary_key end def test_find_without_primary_key @@ -892,10 +989,11 @@ class FinderTest < ActiveRecord::TestCase end end - def with_env_tz(new_tz = 'US/Eastern') - old_tz, ENV['TZ'] = ENV['TZ'], new_tz - yield - ensure - old_tz ? ENV['TZ'] = old_tz : ENV.delete('TZ') + def table_with_custom_primary_key + yield(Class.new(Toy) do + def self.name + 'MercedesCar' + end + end) end end diff --git a/activerecord/test/cases/hot_compatibility_test.rb b/activerecord/test/cases/hot_compatibility_test.rb index 96e581ab4c..367d04a154 100644 --- a/activerecord/test/cases/hot_compatibility_test.rb +++ b/activerecord/test/cases/hot_compatibility_test.rb @@ -5,7 +5,7 @@ class HotCompatibilityTest < ActiveRecord::TestCase setup do @klass = Class.new(ActiveRecord::Base) do - connection.create_table :hot_compatibilities do |t| + connection.create_table :hot_compatibilities, force: true do |t| t.string :foo t.string :bar end diff --git a/activerecord/test/cases/inheritance_test.rb b/activerecord/test/cases/inheritance_test.rb index cb0d374ef7..e2ff2aa451 100644 --- a/activerecord/test/cases/inheritance_test.rb +++ b/activerecord/test/cases/inheritance_test.rb @@ -1,10 +1,11 @@ -require "cases/helper" +require 'cases/helper' require 'models/company' require 'models/person' require 'models/post' require 'models/project' require 'models/subscriber' require 'models/vegetables' +require 'models/shop' class InheritanceTest < ActiveRecord::TestCase fixtures :companies, :projects, :subscribers, :accounts, :vegetables @@ -128,6 +129,17 @@ class InheritanceTest < ActiveRecord::TestCase assert_kind_of Cabbage, cabbage end + def test_alt_becomes_bang_resets_inheritance_type_column + vegetable = Vegetable.create!(name: "Red Pepper") + assert_nil vegetable.custom_type + + cabbage = vegetable.becomes!(Cabbage) + assert_equal "Cabbage", cabbage.custom_type + + vegetable = cabbage.becomes!(Vegetable) + assert_nil cabbage.custom_type + end + def test_inheritance_find_all companies = Company.all.merge!(:order => 'id').to_a assert_kind_of Firm, companies[0], "37signals should be a firm" @@ -210,9 +222,9 @@ class InheritanceTest < ActiveRecord::TestCase end def test_inheritance_condition - assert_equal 10, Company.count + assert_equal 11, Company.count assert_equal 2, Firm.count - assert_equal 4, Client.count + assert_equal 5, Client.count end def test_alt_inheritance_condition @@ -356,4 +368,10 @@ class InheritanceComputeTypeTest < ActiveRecord::TestCase ensure ActiveRecord::Base.store_full_sti_class = true end + + def test_sti_type_from_attributes_disabled_in_non_sti_class + phone = Shop::Product::Type.new(name: 'Phone') + product = Shop::Product.new(:type => phone) + assert product.save + end end diff --git a/activerecord/test/cases/integration_test.rb b/activerecord/test/cases/integration_test.rb index 2e71b1a40d..dfb8a608cb 100644 --- a/activerecord/test/cases/integration_test.rb +++ b/activerecord/test/cases/integration_test.rb @@ -3,12 +3,11 @@ require 'cases/helper' require 'models/company' require 'models/developer' -require 'models/car' -require 'models/bulb' require 'models/owner' +require 'models/pet' class IntegrationTest < ActiveRecord::TestCase - fixtures :companies, :developers, :owners + fixtures :companies, :developers, :owners, :pets def test_to_param_should_return_string assert_kind_of String, Client.first.to_param @@ -91,13 +90,14 @@ class IntegrationTest < ActiveRecord::TestCase end def test_cache_key_changes_when_child_touched - car = Car.create - Bulb.create(car: car) + owner = owners(:blackbeard) + pet = pets(:parrot) + + owner.update_column :updated_at, Time.current + key = owner.cache_key - key = car.cache_key - car.bulb.touch - car.reload - assert_not_equal key, car.cache_key + assert pet.touch + assert_not_equal key, owner.reload.cache_key end def test_cache_key_format_for_existing_record_with_nil_updated_timestamps diff --git a/activerecord/test/cases/locking_test.rb b/activerecord/test/cases/locking_test.rb index a16ed963fe..c373dc1511 100644 --- a/activerecord/test/cases/locking_test.rb +++ b/activerecord/test/cases/locking_test.rb @@ -431,6 +431,17 @@ unless current_adapter?(:SybaseAdapter, :OpenBaseAdapter) || in_memory_db? assert_equal old, person.reload.first_name end + if current_adapter?(:PostgreSQLAdapter) + def test_lock_sending_custom_lock_statement + Person.transaction do + person = Person.find(1) + assert_sql(/LIMIT 1 FOR SHARE NOWAIT/) do + person.lock!('FOR SHARE NOWAIT') + end + end + end + end + if current_adapter?(:PostgreSQLAdapter, :OracleAdapter) def test_no_locks_no_wait first, second = duel { Person.find 1 } diff --git a/activerecord/test/cases/migration_test.rb b/activerecord/test/cases/migration_test.rb index 0363bf1048..1bda472d23 100644 --- a/activerecord/test/cases/migration_test.rb +++ b/activerecord/test/cases/migration_test.rb @@ -328,6 +328,7 @@ class MigrationTest < ActiveRecord::TestCase end def test_proper_table_name_on_migrator + reminder_class = new_isolated_reminder_class assert_deprecated do assert_equal "table", ActiveRecord::Migrator.proper_table_name('table') end @@ -335,30 +336,30 @@ class MigrationTest < ActiveRecord::TestCase assert_equal "table", ActiveRecord::Migrator.proper_table_name(:table) end assert_deprecated do - assert_equal "reminders", ActiveRecord::Migrator.proper_table_name(Reminder) + assert_equal "reminders", ActiveRecord::Migrator.proper_table_name(reminder_class) end - Reminder.reset_table_name + reminder_class.reset_table_name assert_deprecated do - assert_equal Reminder.table_name, ActiveRecord::Migrator.proper_table_name(Reminder) + assert_equal reminder_class.table_name, ActiveRecord::Migrator.proper_table_name(reminder_class) end # Use the model's own prefix/suffix if a model is given ActiveRecord::Base.table_name_prefix = "ARprefix_" ActiveRecord::Base.table_name_suffix = "_ARsuffix" - Reminder.table_name_prefix = 'prefix_' - Reminder.table_name_suffix = '_suffix' - Reminder.reset_table_name + reminder_class.table_name_prefix = 'prefix_' + reminder_class.table_name_suffix = '_suffix' + reminder_class.reset_table_name assert_deprecated do - assert_equal "prefix_reminders_suffix", ActiveRecord::Migrator.proper_table_name(Reminder) + assert_equal "prefix_reminders_suffix", ActiveRecord::Migrator.proper_table_name(reminder_class) end - Reminder.table_name_prefix = '' - Reminder.table_name_suffix = '' - Reminder.reset_table_name + reminder_class.table_name_prefix = '' + reminder_class.table_name_suffix = '' + reminder_class.reset_table_name # Use AR::Base's prefix/suffix if string or symbol is given ActiveRecord::Base.table_name_prefix = "prefix_" ActiveRecord::Base.table_name_suffix = "_suffix" - Reminder.reset_table_name + reminder_class.reset_table_name assert_deprecated do assert_equal "prefix_table_suffix", ActiveRecord::Migrator.proper_table_name('table') end @@ -368,28 +369,29 @@ class MigrationTest < ActiveRecord::TestCase end def test_proper_table_name_on_migration + reminder_class = new_isolated_reminder_class migration = ActiveRecord::Migration.new assert_equal "table", migration.proper_table_name('table') assert_equal "table", migration.proper_table_name(:table) - assert_equal "reminders", migration.proper_table_name(Reminder) - Reminder.reset_table_name - assert_equal Reminder.table_name, migration.proper_table_name(Reminder) + assert_equal "reminders", migration.proper_table_name(reminder_class) + reminder_class.reset_table_name + assert_equal reminder_class.table_name, migration.proper_table_name(reminder_class) # Use the model's own prefix/suffix if a model is given ActiveRecord::Base.table_name_prefix = "ARprefix_" ActiveRecord::Base.table_name_suffix = "_ARsuffix" - Reminder.table_name_prefix = 'prefix_' - Reminder.table_name_suffix = '_suffix' - Reminder.reset_table_name - assert_equal "prefix_reminders_suffix", migration.proper_table_name(Reminder) - Reminder.table_name_prefix = '' - Reminder.table_name_suffix = '' - Reminder.reset_table_name + reminder_class.table_name_prefix = 'prefix_' + reminder_class.table_name_suffix = '_suffix' + reminder_class.reset_table_name + assert_equal "prefix_reminders_suffix", migration.proper_table_name(reminder_class) + reminder_class.table_name_prefix = '' + reminder_class.table_name_suffix = '' + reminder_class.reset_table_name # Use AR::Base's prefix/suffix if string or symbol is given ActiveRecord::Base.table_name_prefix = "prefix_" ActiveRecord::Base.table_name_suffix = "_suffix" - Reminder.reset_table_name + reminder_class.reset_table_name assert_equal "prefix_table_suffix", migration.proper_table_name('table', migration.table_name_options) assert_equal "prefix_table_suffix", migration.proper_table_name(:table, migration.table_name_options) end @@ -532,11 +534,13 @@ class MigrationTest < ActiveRecord::TestCase end protected - def with_env_tz(new_tz = 'US/Eastern') - old_tz, ENV['TZ'] = ENV['TZ'], new_tz - yield - ensure - old_tz ? ENV['TZ'] = old_tz : ENV.delete('TZ') + # This is needed to isolate class_attribute assignments like `table_name_prefix` + # for each test case. + def new_isolated_reminder_class + Class.new(Reminder) { + def self.name; "Reminder"; end + def self.base_class; self; end + } end end diff --git a/activerecord/test/cases/mixin_test.rb b/activerecord/test/cases/mixin_test.rb index ad0d5cce27..7ddb2bfee1 100644 --- a/activerecord/test/cases/mixin_test.rb +++ b/activerecord/test/cases/mixin_test.rb @@ -6,10 +6,14 @@ end class TouchTest < ActiveRecord::TestCase fixtures :mixins - def setup + setup do travel_to Time.now end + teardown do + travel_back + end + def test_update stamped = Mixin.new diff --git a/activerecord/test/cases/persistence_test.rb b/activerecord/test/cases/persistence_test.rb index 6f1e518f45..b9f0624f76 100644 --- a/activerecord/test/cases/persistence_test.rb +++ b/activerecord/test/cases/persistence_test.rb @@ -740,7 +740,7 @@ class PersistenceTest < ActiveRecord::TestCase assert_raise(ActiveRecord::RecordInvalid) { reply.update!(title: nil, content: "Have a nice evening") } ensure - Reply.reset_callbacks(:validate) + Reply.clear_validators! end def test_update_attributes! @@ -761,7 +761,7 @@ class PersistenceTest < ActiveRecord::TestCase assert_raise(ActiveRecord::RecordInvalid) { reply.update_attributes!(title: nil, content: "Have a nice evening") } ensure - Reply.reset_callbacks(:validate) + Reply.clear_validators! end def test_destroyed_returns_boolean diff --git a/activerecord/test/cases/query_cache_test.rb b/activerecord/test/cases/query_cache_test.rb index 5566563116..da8ae672fe 100644 --- a/activerecord/test/cases/query_cache_test.rb +++ b/activerecord/test/cases/query_cache_test.rb @@ -8,7 +8,7 @@ require 'rack' class QueryCacheTest < ActiveRecord::TestCase fixtures :tasks, :topics, :categories, :posts, :categories_posts - def setup + teardown do Task.connection.clear_query_cache ActiveRecord::Base.connection.disable_query_cache! end @@ -214,7 +214,7 @@ class QueryCacheExpiryTest < ActiveRecord::TestCase Post.find(1) # change the column definition - Post.connection.change_column :posts, :title, :string, :limit => 80 + Post.connection.change_column :posts, :title, :string, limit: 80 assert_nothing_raised { Post.find(1) } # restore the old definition @@ -241,7 +241,6 @@ class QueryCacheExpiryTest < ActiveRecord::TestCase def test_update Task.connection.expects(:clear_query_cache).times(2) - Task.cache do task = Task.find(1) task.starting = Time.now.utc @@ -251,7 +250,6 @@ class QueryCacheExpiryTest < ActiveRecord::TestCase def test_destroy Task.connection.expects(:clear_query_cache).times(2) - Task.cache do Task.find(1).destroy end @@ -259,7 +257,6 @@ class QueryCacheExpiryTest < ActiveRecord::TestCase def test_insert ActiveRecord::Base.connection.expects(:clear_query_cache).times(2) - Task.cache do Task.create! end diff --git a/activerecord/test/cases/relation/mutation_test.rb b/activerecord/test/cases/relation/mutation_test.rb index 7cb2a19bee..4fafa668fb 100644 --- a/activerecord/test/cases/relation/mutation_test.rb +++ b/activerecord/test/cases/relation/mutation_test.rb @@ -14,6 +14,10 @@ module ActiveRecord def relation_delegate_class(klass) self.class.relation_delegate_class(klass) end + + def attribute_alias?(name) + false + end end def relation diff --git a/activerecord/test/cases/relations_test.rb b/activerecord/test/cases/relations_test.rb index 031da8e6d6..e390d37871 100644 --- a/activerecord/test/cases/relations_test.rb +++ b/activerecord/test/cases/relations_test.rb @@ -65,7 +65,7 @@ class RelationTest < ActiveRecord::TestCase def test_scoped topics = Topic.all assert_kind_of ActiveRecord::Relation, topics - assert_equal 4, topics.size + assert_equal 5, topics.size end def test_to_json @@ -86,14 +86,14 @@ class RelationTest < ActiveRecord::TestCase def test_scoped_all topics = Topic.all.to_a assert_kind_of Array, topics - assert_no_queries { assert_equal 4, topics.size } + assert_no_queries { assert_equal 5, topics.size } end def test_loaded_all topics = Topic.all assert_queries(1) do - 2.times { assert_equal 4, topics.to_a.size } + 2.times { assert_equal 5, topics.to_a.size } end assert topics.loaded? @@ -164,27 +164,27 @@ class RelationTest < ActiveRecord::TestCase def test_finding_with_order topics = Topic.order('id') - assert_equal 4, topics.to_a.size + assert_equal 5, topics.to_a.size assert_equal topics(:first).title, topics.first.title end def test_finding_with_arel_order topics = Topic.order(Topic.arel_table[:id].asc) - assert_equal 4, topics.to_a.size + assert_equal 5, topics.to_a.size assert_equal topics(:first).title, topics.first.title end def test_finding_with_assoc_order topics = Topic.order(:id => :desc) - assert_equal 4, topics.to_a.size - assert_equal topics(:fourth).title, topics.first.title + assert_equal 5, topics.to_a.size + assert_equal topics(:fifth).title, topics.first.title end def test_finding_with_reverted_assoc_order topics = Topic.order(:id => :asc).reverse_order - assert_equal 4, topics.to_a.size - assert_equal topics(:fourth).title, topics.first.title + assert_equal 5, topics.to_a.size + assert_equal topics(:fifth).title, topics.first.title end def test_order_with_hash_and_symbol_generates_the_same_sql @@ -197,19 +197,43 @@ class RelationTest < ActiveRecord::TestCase def test_finding_last_with_arel_order topics = Topic.order(Topic.arel_table[:id].asc) - assert_equal topics(:fourth).title, topics.last.title + assert_equal topics(:fifth).title, topics.last.title end def test_finding_with_order_concatenated topics = Topic.order('author_name').order('title') - assert_equal 4, topics.to_a.size + assert_equal 5, topics.to_a.size assert_equal topics(:fourth).title, topics.first.title end + def test_finding_with_order_by_aliased_attributes + topics = Topic.order(:heading) + assert_equal 5, topics.to_a.size + assert_equal topics(:fifth).title, topics.first.title + end + + def test_finding_with_assoc_order_by_aliased_attributes + topics = Topic.order(heading: :desc) + assert_equal 5, topics.to_a.size + assert_equal topics(:third).title, topics.first.title + end + def test_finding_with_reorder topics = Topic.order('author_name').order('title').reorder('id').to_a topics_titles = topics.map{ |t| t.title } - assert_equal ['The First Topic', 'The Second Topic of the day', 'The Third Topic of the day', 'The Fourth Topic of the day'], topics_titles + 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 + + def test_finding_with_reorder_by_aliased_attributes + topics = Topic.order('author_name').reorder(:heading) + assert_equal 5, topics.to_a.size + assert_equal topics(:fifth).title, topics.first.title + end + + def test_finding_with_assoc_reorder_by_aliased_attributes + topics = Topic.order('author_name').reorder(heading: :desc) + assert_equal 5, topics.to_a.size + assert_equal topics(:third).title, topics.first.title end def test_finding_with_order_and_take @@ -775,6 +799,13 @@ class RelationTest < ActiveRecord::TestCase assert_equal david.salary, developer.salary end + def test_select_takes_an_aliased_attribute + first = topics(:first) + + topic = Topic.where(id: first.id).select(:heading).first + assert_equal first.heading, topic.heading + end + def test_select_argument_error assert_raises(ArgumentError) { Developer.select } end @@ -1505,23 +1536,25 @@ class RelationTest < ActiveRecord::TestCase end end + test "joins with select" do + posts = Post.joins(:author).select("id", "authors.author_address_id").order("posts.id").limit(3) + assert_equal [1, 2, 4], posts.map(&:id) + assert_equal [1, 1, 1], posts.map(&:author_address_id) + end + test "delegations do not leak to other classes" do Topic.all.by_lifo assert Topic.all.class.method_defined?(:by_lifo) assert !Post.all.respond_to?(:by_lifo) end - test "merge collapses wheres from the LHS only" do - left = Post.where(title: "omg").where(comments_count: 1) - right = Post.where(title: "wtf").where(title: "bbq") - - expected = [left.where_values[1]] + right.where_values - merged = left.merge(right) + def test_unscope_removes_binds + left = Post.where(id: Arel::Nodes::BindParam.new('?')) + column = Post.columns_hash['id'] + left.bind_values += [[column, 20]] - assert_equal expected, merged.where_values - assert !merged.to_sql.include?("omg") - assert merged.to_sql.include?("wtf") - assert merged.to_sql.include?("bbq") + relation = left.unscope(where: :id) + assert_equal [], relation.bind_values end def test_merging_removes_rhs_bind_parameters diff --git a/activerecord/test/cases/schema_dumper_test.rb b/activerecord/test/cases/schema_dumper_test.rb index 741827446d..c085663efb 100644 --- a/activerecord/test/cases/schema_dumper_test.rb +++ b/activerecord/test/cases/schema_dumper_test.rb @@ -190,6 +190,8 @@ class SchemaDumperTest < ActiveRecord::TestCase assert_equal 'add_index "companies", ["firm_id", "type"], name: "company_partial_index", where: "(rating > 10)", using: :btree', index_definition elsif current_adapter?(:MysqlAdapter) || current_adapter?(:Mysql2Adapter) assert_equal 'add_index "companies", ["firm_id", "type"], name: "company_partial_index", using: :btree', index_definition + elsif current_adapter?(:SQLite3Adapter) && ActiveRecord::Base.connection.supports_partial_index? + assert_equal 'add_index "companies", ["firm_id", "type"], name: "company_partial_index", where: "rating > 10"', index_definition else assert_equal 'add_index "companies", ["firm_id", "type"], name: "company_partial_index"', index_definition end diff --git a/activerecord/test/cases/scoping/default_scoping_test.rb b/activerecord/test/cases/scoping/default_scoping_test.rb index 71754cf0a2..170e9a49eb 100644 --- a/activerecord/test/cases/scoping/default_scoping_test.rb +++ b/activerecord/test/cases/scoping/default_scoping_test.rb @@ -149,6 +149,16 @@ class DefaultScopingTest < ActiveRecord::TestCase 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 } + + dev_ordered_relation = DeveloperOrderedBySalary.where(name: 'Jamis').where("created_at > ?", 1.year.ago) + received = dev_ordered_relation.unscope(where: [:name]).collect { |dev| dev.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 } diff --git a/activerecord/test/cases/scoping/named_scoping_test.rb b/activerecord/test/cases/scoping/named_scoping_test.rb index 72c9787b84..9dc26cfd4d 100644 --- a/activerecord/test/cases/scoping/named_scoping_test.rb +++ b/activerecord/test/cases/scoping/named_scoping_test.rb @@ -266,6 +266,63 @@ class NamedScopingTest < ActiveRecord::TestCase assert_equal 'lifo', topic.author_name end + def test_reserved_scope_names + klass = Class.new(ActiveRecord::Base) do + self.table_name = "topics" + + scope :approved, -> { where(approved: true) } + + class << self + public + def pub; end + + private + def pri; end + + protected + def pro; end + end + end + + subklass = Class.new(klass) + + conflicts = [ + :create, # public class method on AR::Base + :relation, # private class method on AR::Base + :new, # redefined class method on AR::Base + :all, # a default scope + ] + + non_conflicts = [ + :find_by_title, # dynamic finder method + :approved, # existing scope + :pub, # existing public class method + :pri, # existing private class method + :pro, # existing protected class method + :open, # a ::Kernel method + ] + + conflicts.each do |name| + assert_raises(ArgumentError, "scope `#{name}` should not be allowed") do + klass.class_eval { scope name, ->{ where(approved: true) } } + end + + assert_raises(ArgumentError, "scope `#{name}` should not be allowed") do + subklass.class_eval { scope name, ->{ where(approved: true) } } + end + end + + non_conflicts.each do |name| + assert_nothing_raised do + klass.class_eval { scope name, ->{ where(approved: true) } } + end + + assert_nothing_raised do + subklass.class_eval { scope name, ->{ where(approved: true) } } + end + end + end + # Method delegation for scope names which look like /\A[a-zA-Z_]\w*[!?]?\z/ # has been done by evaluating a string with a plain def statement. For scope # names which contain spaces this approach doesn't work. @@ -344,13 +401,13 @@ class NamedScopingTest < ActiveRecord::TestCase end def test_scopes_batch_finders - assert_equal 3, Topic.approved.count + assert_equal 4, Topic.approved.count - assert_queries(4) do + assert_queries(5) do Topic.approved.find_each(:batch_size => 1) {|t| assert t.approved? } end - assert_queries(2) do + assert_queries(3) do Topic.approved.find_in_batches(:batch_size => 2) do |group| group.each {|t| assert t.approved? } end @@ -366,7 +423,7 @@ class NamedScopingTest < ActiveRecord::TestCase def test_scopes_on_relations # Topic.replied approved_topics = Topic.all.approved.order('id DESC') - assert_equal topics(:fourth), approved_topics.first + assert_equal topics(:fifth), approved_topics.first replied_approved_topics = approved_topics.replied assert_equal topics(:third), replied_approved_topics.first diff --git a/activerecord/test/cases/store_test.rb b/activerecord/test/cases/store_test.rb index 6f632b4d8d..978cee9cfb 100644 --- a/activerecord/test/cases/store_test.rb +++ b/activerecord/test/cases/store_test.rb @@ -166,4 +166,28 @@ class StoreTest < ActiveRecord::TestCase test "YAML coder initializes the store when a Nil value is given" do assert_equal({}, @john.params) end + + test "attributes_for_coder should return stored fields already serialized" do + attributes = { + "id" => @john.id, + "name"=> @john.name, + "settings" => "--- !ruby/hash:ActiveSupport::HashWithIndifferentAccess\ncolor: black\n", + "preferences" => "--- !ruby/hash:ActiveSupport::HashWithIndifferentAccess\nremember_login: true\n", + "json_data" => "{\"height\":\"tall\"}", "json_data_empty"=>"{\"is_a_good_guy\":true}", + "params" => "--- !ruby/hash:ActiveSupport::HashWithIndifferentAccess {}\n", + "account_id"=> @john.account_id + } + + assert_equal attributes, @john.attributes_for_coder + end + + test "dump, load and dump again a model" do + dumped = YAML.dump(@john) + loaded = YAML.load(dumped) + assert_equal @john, loaded + + second_dump = YAML.dump(loaded) + assert_equal dumped, second_dump + assert_equal @john, YAML.load(second_dump) + end end diff --git a/activerecord/test/cases/transaction_callbacks_test.rb b/activerecord/test/cases/transaction_callbacks_test.rb index 5644a35385..7e7d95841b 100644 --- a/activerecord/test/cases/transaction_callbacks_test.rb +++ b/activerecord/test/cases/transaction_callbacks_test.rb @@ -1,9 +1,11 @@ require "cases/helper" +require 'models/owner' +require 'models/pet' require 'models/topic' class TransactionCallbacksTest < ActiveRecord::TestCase self.use_transactional_fixtures = false - fixtures :topics + fixtures :topics, :owners, :pets class ReplyWithCallbacks < ActiveRecord::Base self.table_name = :topics @@ -28,14 +30,14 @@ class TransactionCallbacksTest < ActiveRecord::TestCase has_many :replies, class_name: "ReplyWithCallbacks", foreign_key: "parent_id" - after_commit{|record| record.send(:do_after_commit, nil)} - after_commit(:on => :create){|record| record.send(:do_after_commit, :create)} - after_commit(:on => :update){|record| record.send(:do_after_commit, :update)} - after_commit(:on => :destroy){|record| record.send(:do_after_commit, :destroy)} - after_rollback{|record| record.send(:do_after_rollback, nil)} - after_rollback(:on => :create){|record| record.send(:do_after_rollback, :create)} - after_rollback(:on => :update){|record| record.send(:do_after_rollback, :update)} - after_rollback(:on => :destroy){|record| record.send(:do_after_rollback, :destroy)} + after_commit { |record| record.do_after_commit(nil) } + after_commit(on: :create) { |record| record.do_after_commit(:create) } + after_commit(on: :update) { |record| record.do_after_commit(:update) } + after_commit(on: :destroy) { |record| record.do_after_commit(:destroy) } + after_rollback { |record| record.do_after_rollback(nil) } + after_rollback(on: :create) { |record| record.do_after_rollback(:create) } + after_rollback(on: :update) { |record| record.do_after_rollback(:update) } + after_rollback(on: :destroy) { |record| record.do_after_rollback(:destroy) } def history @history ||= [] @@ -65,7 +67,7 @@ class TransactionCallbacksTest < ActiveRecord::TestCase end def setup - @first, @second = TopicWithCallbacks.find(1, 3).sort_by { |t| t.id } + @first = TopicWithCallbacks.find(1) end def test_call_after_commit_after_transaction_commits @@ -77,40 +79,25 @@ class TransactionCallbacksTest < ActiveRecord::TestCase end def test_only_call_after_commit_on_update_after_transaction_commits_for_existing_record - @first.after_commit_block(:create){|r| r.history << :commit_on_create} - @first.after_commit_block(:update){|r| r.history << :commit_on_update} - @first.after_commit_block(:destroy){|r| r.history << :commit_on_destroy} - @first.after_rollback_block(:create){|r| r.history << :rollback_on_create} - @first.after_rollback_block(:update){|r| r.history << :rollback_on_update} - @first.after_rollback_block(:destroy){|r| r.history << :rollback_on_destroy} + add_transaction_execution_blocks @first @first.save! assert_equal [:commit_on_update], @first.history end def test_only_call_after_commit_on_destroy_after_transaction_commits_for_destroyed_record - @first.after_commit_block(:create){|r| r.history << :commit_on_create} - @first.after_commit_block(:update){|r| r.history << :commit_on_update} - @first.after_commit_block(:destroy){|r| r.history << :commit_on_destroy} - @first.after_rollback_block(:create){|r| r.history << :rollback_on_create} - @first.after_rollback_block(:update){|r| r.history << :rollback_on_update} - @first.after_rollback_block(:destroy){|r| r.history << :rollback_on_destroy} + add_transaction_execution_blocks @first @first.destroy assert_equal [:commit_on_destroy], @first.history end def test_only_call_after_commit_on_create_after_transaction_commits_for_new_record - @new_record = TopicWithCallbacks.new(:title => "New topic", :written_on => Date.today) - @new_record.after_commit_block(:create){|r| r.history << :commit_on_create} - @new_record.after_commit_block(:update){|r| r.history << :commit_on_update} - @new_record.after_commit_block(:destroy){|r| r.history << :commit_on_destroy} - @new_record.after_rollback_block(:create){|r| r.history << :rollback_on_create} - @new_record.after_rollback_block(:update){|r| r.history << :rollback_on_update} - @new_record.after_rollback_block(:destroy){|r| r.history << :rollback_on_destroy} - - @new_record.save! - assert_equal [:commit_on_create], @new_record.history + new_record = TopicWithCallbacks.new(:title => "New topic", :written_on => Date.today) + add_transaction_execution_blocks new_record + + new_record.save! + assert_equal [:commit_on_create], new_record.history end def test_only_call_after_commit_on_create_after_transaction_commits_for_new_record_if_create_succeeds_creating_through_association @@ -120,6 +107,13 @@ class TransactionCallbacksTest < ActiveRecord::TestCase assert_equal [], reply.history end + def test_only_call_after_commit_on_update_after_transaction_commits_for_existing_record_on_touch + add_transaction_execution_blocks @first + + @first.touch + assert_equal [:commit_on_update], @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} @@ -133,12 +127,7 @@ class TransactionCallbacksTest < ActiveRecord::TestCase end def test_only_call_after_rollback_on_update_after_transaction_rollsback_for_existing_record - @first.after_commit_block(:create){|r| r.history << :commit_on_create} - @first.after_commit_block(:update){|r| r.history << :commit_on_update} - @first.after_commit_block(:destroy){|r| r.history << :commit_on_destroy} - @first.after_rollback_block(:create){|r| r.history << :rollback_on_create} - @first.after_rollback_block(:update){|r| r.history << :rollback_on_update} - @first.after_rollback_block(:destroy){|r| r.history << :rollback_on_destroy} + add_transaction_execution_blocks @first Topic.transaction do @first.save! @@ -148,13 +137,19 @@ class TransactionCallbacksTest < ActiveRecord::TestCase assert_equal [:rollback_on_update], @first.history end + def test_only_call_after_rollback_on_update_after_transaction_rollsback_for_existing_record_on_touch + add_transaction_execution_blocks @first + + Topic.transaction do + @first.touch + raise ActiveRecord::Rollback + end + + assert_equal [:rollback_on_update], @first.history + end + def test_only_call_after_rollback_on_destroy_after_transaction_rollsback_for_destroyed_record - @first.after_commit_block(:create){|r| r.history << :commit_on_create} - @first.after_commit_block(:update){|r| r.history << :commit_on_update} - @first.after_commit_block(:destroy){|r| r.history << :commit_on_update} - @first.after_rollback_block(:create){|r| r.history << :rollback_on_create} - @first.after_rollback_block(:update){|r| r.history << :rollback_on_update} - @first.after_rollback_block(:destroy){|r| r.history << :rollback_on_destroy} + add_transaction_execution_blocks @first Topic.transaction do @first.destroy @@ -165,20 +160,15 @@ class TransactionCallbacksTest < ActiveRecord::TestCase end def test_only_call_after_rollback_on_create_after_transaction_rollsback_for_new_record - @new_record = TopicWithCallbacks.new(:title => "New topic", :written_on => Date.today) - @new_record.after_commit_block(:create){|r| r.history << :commit_on_create} - @new_record.after_commit_block(:update){|r| r.history << :commit_on_update} - @new_record.after_commit_block(:destroy){|r| r.history << :commit_on_destroy} - @new_record.after_rollback_block(:create){|r| r.history << :rollback_on_create} - @new_record.after_rollback_block(:update){|r| r.history << :rollback_on_update} - @new_record.after_rollback_block(:destroy){|r| r.history << :rollback_on_destroy} + new_record = TopicWithCallbacks.new(:title => "New topic", :written_on => Date.today) + add_transaction_execution_blocks new_record Topic.transaction do - @new_record.save! + new_record.save! raise ActiveRecord::Rollback end - assert_equal [:rollback_on_create], @new_record.history + assert_equal [:rollback_on_create], new_record.history end def test_call_after_rollback_when_commit_fails @@ -205,23 +195,24 @@ class TransactionCallbacksTest < ActiveRecord::TestCase @first.after_rollback_block{|r| r.rollbacks(1)} @first.after_commit_block{|r| r.commits(1)} - def @second.rollbacks(i=0); @rollbacks ||= 0; @rollbacks += i if i; end - def @second.commits(i=0); @commits ||= 0; @commits += i if i; end - @second.after_rollback_block{|r| r.rollbacks(1)} - @second.after_commit_block{|r| r.commits(1)} + second = TopicWithCallbacks.find(3) + def second.rollbacks(i=0); @rollbacks ||= 0; @rollbacks += i if i; end + def second.commits(i=0); @commits ||= 0; @commits += i if i; end + second.after_rollback_block{|r| r.rollbacks(1)} + second.after_commit_block{|r| r.commits(1)} Topic.transaction do @first.save! Topic.transaction(:requires_new => true) do - @second.save! + second.save! raise ActiveRecord::Rollback end end assert_equal 1, @first.commits assert_equal 0, @first.rollbacks - assert_equal 0, @second.commits - assert_equal 1, @second.rollbacks + assert_equal 0, second.commits + assert_equal 1, second.rollbacks end def test_only_call_after_rollback_on_records_rolled_back_to_a_savepoint_when_release_savepoint_fails @@ -252,33 +243,61 @@ class TransactionCallbacksTest < ActiveRecord::TestCase def @first.last_after_transaction_error; @last_transaction_error; end @first.after_commit_block{|r| r.last_after_transaction_error = :commit; raise "fail!";} @first.after_rollback_block{|r| r.last_after_transaction_error = :rollback; raise "fail!";} - @second.after_commit_block{|r| r.history << :after_commit} - @second.after_rollback_block{|r| r.history << :after_rollback} + + second = TopicWithCallbacks.find(3) + second.after_commit_block{|r| r.history << :after_commit} + second.after_rollback_block{|r| r.history << :after_rollback} Topic.transaction do @first.save! - @second.save! + second.save! end assert_equal :commit, @first.last_after_transaction_error - assert_equal [:after_commit], @second.history + assert_equal [:after_commit], second.history - @second.history.clear + second.history.clear Topic.transaction do @first.save! - @second.save! + second.save! raise ActiveRecord::Rollback end assert_equal :rollback, @first.last_after_transaction_error - assert_equal [:after_rollback], @second.history + assert_equal [:after_rollback], second.history end def test_after_rollback_callbacks_should_validate_on_condition - assert_raise(ArgumentError) { Topic.send(:after_rollback, :on => :save) } + assert_raise(ArgumentError) { Topic.after_rollback(on: :save) } end def test_after_commit_callbacks_should_validate_on_condition - assert_raise(ArgumentError) { Topic.send(:after_commit, :on => :save) } + assert_raise(ArgumentError) { Topic.after_commit(on: :save) } end + + def test_saving_a_record_with_a_belongs_to_that_specifies_touching_the_parent_should_call_callbacks_on_the_parent_object + pet = Pet.first + owner = pet.owner + flag = false + + owner.on_after_commit do + flag = true + end + + pet.name = "Fluffy the Third" + pet.save + + assert flag + end + + private + + def add_transaction_execution_blocks(record) + record.after_commit_block(:create) { |r| r.history << :commit_on_create } + record.after_commit_block(:update) { |r| r.history << :commit_on_update } + record.after_commit_block(:destroy) { |r| r.history << :commit_on_destroy } + record.after_rollback_block(:create) { |r| r.history << :rollback_on_create } + record.after_rollback_block(:update) { |r| r.history << :rollback_on_update } + record.after_rollback_block(:destroy) { |r| r.history << :rollback_on_destroy } + end end class CallbacksOnMultipleActionsTest < ActiveRecord::TestCase diff --git a/activerecord/test/cases/transactions_test.rb b/activerecord/test/cases/transactions_test.rb index 89dab16975..1664f1a096 100644 --- a/activerecord/test/cases/transactions_test.rb +++ b/activerecord/test/cases/transactions_test.rb @@ -430,17 +430,26 @@ class TransactionTest < ActiveRecord::TestCase 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' + end + topic_1 = Topic.new(:title => 'test_1') topic_2 = Topic.new(:title => 'test_2') + topic_3 = topic_without_callbacks.new(:title => 'test_3') + Topic.transaction do assert topic_1.save assert topic_2.save + assert topic_3.save @first.save @second.destroy assert topic_1.persisted?, 'persisted' assert_not_nil topic_1.id assert topic_2.persisted?, 'persisted' assert_not_nil topic_2.id + assert topic_3.persisted?, 'persisted' + assert_not_nil topic_3.id assert @first.persisted?, 'persisted' assert_not_nil @first.id assert @second.destroyed?, 'destroyed' @@ -451,6 +460,8 @@ class TransactionTest < ActiveRecord::TestCase assert_nil topic_1.id assert !topic_2.persisted?, 'not persisted' assert_nil topic_2.id + assert !topic_3.persisted?, 'not persisted' + assert_nil topic_3.id assert @first.persisted?, 'persisted' assert_not_nil @first.id assert !@second.destroyed?, 'not destroyed' diff --git a/activerecord/test/cases/validations/i18n_generate_message_validation_test.rb b/activerecord/test/cases/validations/i18n_generate_message_validation_test.rb index a73c3bf1af..13d4d85afa 100644 --- a/activerecord/test/cases/validations/i18n_generate_message_validation_test.rb +++ b/activerecord/test/cases/validations/i18n_generate_message_validation_test.rb @@ -3,7 +3,7 @@ require 'models/topic' class I18nGenerateMessageValidationTest < ActiveRecord::TestCase def setup - Topic.reset_callbacks(:validate) + Topic.clear_validators! @topic = Topic.new I18n.backend = I18n::Backend::Simple.new end diff --git a/activerecord/test/fixtures/companies.yml b/activerecord/test/fixtures/companies.yml index 0766e92027..ab9d5378ad 100644 --- a/activerecord/test/fixtures/companies.yml +++ b/activerecord/test/fixtures/companies.yml @@ -57,3 +57,11 @@ odegy: id: 9 name: Odegy type: ExclusivelyDependentFirm + +another_first_firm_client: + id: 11 + type: Client + firm_id: 1 + client_of: 1 + name: Apex + firm_name: 37signals diff --git a/activerecord/test/fixtures/topics.yml b/activerecord/test/fixtures/topics.yml index 2b042bd135..bf049abbf1 100644 --- a/activerecord/test/fixtures/topics.yml +++ b/activerecord/test/fixtures/topics.yml @@ -40,3 +40,10 @@ fourth: type: Reply parent_id: 3 +fifth: + id: 5 + title: The Fifth Topic of the day + author_name: Jason + written_on: 2013-07-13t12:11:00.0099+01:00 + content: Omakase + approved: true diff --git a/activerecord/test/models/book.rb b/activerecord/test/models/book.rb index 4cb2c7606b..2170018068 100644 --- a/activerecord/test/models/book.rb +++ b/activerecord/test/models/book.rb @@ -9,6 +9,7 @@ class Book < ActiveRecord::Base enum status: [:proposed, :written, :published] enum read_status: {unread: 0, reading: 2, read: 3} + enum nullable_status: [:single, :married] def published! super diff --git a/activerecord/test/models/car.rb b/activerecord/test/models/car.rb index c4a15a79e2..db0f93f63b 100644 --- a/activerecord/test/models/car.rb +++ b/activerecord/test/models/car.rb @@ -15,7 +15,6 @@ class Car < ActiveRecord::Base scope :incl_engines, -> { includes(:engines) } scope :order_using_new_style, -> { order('name asc') } - end class CoolCar < Car diff --git a/activerecord/test/models/owner.rb b/activerecord/test/models/owner.rb index 1c7ed4aa3e..cf24502d3a 100644 --- a/activerecord/test/models/owner.rb +++ b/activerecord/test/models/owner.rb @@ -2,4 +2,21 @@ class Owner < ActiveRecord::Base self.primary_key = :owner_id has_many :pets, -> { order 'pets.name desc' } has_many :toys, :through => :pets + + after_commit :execute_blocks + + def blocks + @blocks ||= [] + end + + def on_after_commit(&block) + blocks << block + end + + def execute_blocks + blocks.each do |block| + block.call(self) + end + @blocks = [] + end end diff --git a/activerecord/test/models/shop.rb b/activerecord/test/models/shop.rb index 81414227ea..607a0a5b41 100644 --- a/activerecord/test/models/shop.rb +++ b/activerecord/test/models/shop.rb @@ -5,6 +5,11 @@ module Shop class Product < ActiveRecord::Base has_many :variants, :dependent => :delete_all + belongs_to :type + + class Type < ActiveRecord::Base + has_many :products + end end class Variant < ActiveRecord::Base diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb index ddfc1ac0d6..99a53434f6 100644 --- a/activerecord/test/schema/schema.rb +++ b/activerecord/test/schema/schema.rb @@ -97,6 +97,7 @@ ActiveRecord::Schema.define do t.column :name, :string t.column :status, :integer, default: 0 t.column :read_status, :integer, default: 0 + t.column :nullable_status, :integer end create_table :booleans, force: true do |t| @@ -557,9 +558,14 @@ ActiveRecord::Schema.define do create_table :products, force: true do |t| t.references :collection + t.references :type t.string :name end + create_table :product_types, force: true do |t| + t.string :name + end + create_table :projects, force: true do |t| t.string :name t.string :type |