diff options
Diffstat (limited to 'activerecord')
152 files changed, 1789 insertions, 1011 deletions
diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index 1f8163db12..786bef7359 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,3 +1,94 @@ +* Skip query caching when working with batches of records (`find_each`, `find_in_batches`, + `in_batches`). + + Previously, records would be fetched in batches, but all records would be retained in memory + until the end of the request or job. + + *Eugene Kenny* + +* Prevent errors raised by `sql.active_record` notification subscribers from being converted into + `ActiveRecord::StatementInvalid` exceptions. + + *Dennis Taylor* + +* Fix eager loading/preloading association with scope including joins. + + Fixes #28324. + + *Ryuta Kamizono* + +* Fix transactions to apply state to child transactions + + Previously if you had a nested transaction and the outer transaction was rolledback the record from the + inner transaction would still be marked as persisted. + + This change fixes that by applying the state of the parent transaction to the child transaction when the + parent transaction is rolledback. This will correctly mark records from the inner transaction as not persisted. + + *Eileen M. Uchitelle*, *Aaron Patterson* + +* Deprecate `set_state` method in `TransactionState` + + Deprecated the `set_state` method in favor of setting the state via specific methods. If you need to mark the + state of the transaction you can now use `rollback!`, `commit!` or `nullify!` instead of + `set_state(:rolledback)`, `set_state(:committed)`, or `set_state(nil)`. + + *Eileen M. Uchitelle*, *Aaron Patterson* + +* Deprecate delegating to `arel` in `Relation`. + + *Ryuta Kamizono* + +* Fix eager loading to respect `store_full_sti_class` setting. + + *Ryuta Kamizono* + +* Query cache was unavailable when entering the `ActiveRecord::Base.cache` block + without being connected. + + *Tsukasa Oishi* + +* Previously, when building records using a `has_many :through` association, + if the child records were deleted before the parent was saved, they would + still be persisted. Now, if child records are deleted before the parent is saved + on a `has_many :through` association, the child records will not be persisted. + + *Tobias Kraze* + +* Merging two relations representing nested joins no longer transforms the joins of + the merged relation into LEFT OUTER JOIN. Example to clarify: + + ``` + Author.joins(:posts).merge(Post.joins(:comments)) + # Before the change: + #=> SELECT ... FROM authors INNER JOIN posts ON ... LEFT OUTER JOIN comments ON... + + # After the change: + #=> SELECT ... FROM authors INNER JOIN posts ON ... INNER JOIN comments ON... + ``` + + TODO: Add to the Rails 5.2 upgrade guide + + *Maxime Handfield Lapointe* + +* `ActiveRecord::Persistence#touch` does not work well when optimistic locking enabled and + `locking_column`, without default value, is null in the database. + + *bogdanvlviv* + +* Fix destroying existing object does not work well when optimistic locking enabled and + `locking_column` is null in the database. + + *bogdanvlviv* + +* Use bulk INSERT to insert fixtures for better performance. + + *Kir Shatrov* + +* Prevent making bind param if casted value is nil. + + *Ryuta Kamizono* + * Deprecate passing arguments and block at the same time to `count` and `sum` in `ActiveRecord::Calculations`. *Ryuta Kamizono* diff --git a/activerecord/Rakefile b/activerecord/Rakefile index 2d0d5bd657..fe5f9d1071 100644 --- a/activerecord/Rakefile +++ b/activerecord/Rakefile @@ -1,7 +1,7 @@ require "rake/testtask" -require File.expand_path("test/config", __dir__) -require File.expand_path("test/support/config", __dir__) +require_relative "test/config" +require_relative "test/support/config" def run_without_aborting(*tasks) errors = [] diff --git a/activerecord/activerecord.gemspec b/activerecord/activerecord.gemspec index 450ec0bba9..a626a1f21b 100644 --- a/activerecord/activerecord.gemspec +++ b/activerecord/activerecord.gemspec @@ -21,6 +21,11 @@ Gem::Specification.new do |s| s.extra_rdoc_files = %w(README.rdoc) s.rdoc_options.concat ["--main", "README.rdoc"] + s.metadata = { + "source_code_uri" => "https://github.com/rails/rails/tree/v#{version}/activerecord", + "changelog_uri" => "https://github.com/rails/rails/blob/v#{version}/activerecord/CHANGELOG.md" + } + s.add_dependency "activesupport", version s.add_dependency "activemodel", version diff --git a/activerecord/bin/test b/activerecord/bin/test index 3a9547e5c1..ab69f4f603 100755 --- a/activerecord/bin/test +++ b/activerecord/bin/test @@ -1,7 +1,7 @@ #!/usr/bin/env ruby COMPONENT_ROOT = File.expand_path("..", __dir__) -require File.expand_path("../tools/test", COMPONENT_ROOT) +require_relative "../../tools/test" module Minitest def self.plugin_active_record_options(opts, options) diff --git a/activerecord/lib/active_record.rb b/activerecord/lib/active_record.rb index 29f49c6195..25d5e87317 100644 --- a/activerecord/lib/active_record.rb +++ b/activerecord/lib/active_record.rb @@ -26,8 +26,8 @@ require "active_support/rails" require "active_model" require "arel" -require "active_record/version" -require "active_record/attribute_set" +require_relative "active_record/version" +require_relative "active_record/attribute_set" module ActiveRecord extend ActiveSupport::Autoload diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index 7c37132d3a..e782196ce6 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -1,7 +1,7 @@ require "active_support/core_ext/enumerable" require "active_support/core_ext/string/conversions" require "active_support/core_ext/module/remove_method" -require "active_record/errors" +require_relative "errors" module ActiveRecord class AssociationNotFoundError < ConfigurationError #:nodoc: @@ -342,7 +342,7 @@ module ActiveRecord # | | belongs_to | # generated methods | belongs_to | :polymorphic | has_one # ----------------------------------+------------+--------------+--------- - # other(force_reload=false) | X | X | X + # other | X | X | X # other=(other) | X | X | X # build_other(attributes={}) | X | | X # create_other(attributes={}) | X | | X @@ -352,7 +352,7 @@ module ActiveRecord # | | | has_many # generated methods | habtm | has_many | :through # ----------------------------------+-------+----------+---------- - # others(force_reload=false) | X | X | X + # others | X | X | X # others=(other,other,...) | X | X | X # other_ids | X | X | X # other_ids=(id,id,...) | X | X | X @@ -1187,7 +1187,7 @@ module ActiveRecord # +collection+ is a placeholder for the symbol passed as the +name+ argument, so # <tt>has_many :clients</tt> would add among others <tt>clients.empty?</tt>. # - # [collection(force_reload = false)] + # [collection] # Returns an array of all the associated objects. # An empty array is returned if none are found. # [collection<<(object, ...)] @@ -1407,7 +1407,7 @@ module ActiveRecord # +association+ is a placeholder for the symbol passed as the +name+ argument, so # <tt>has_one :manager</tt> would add among others <tt>manager.nil?</tt>. # - # [association(force_reload = false)] + # [association] # 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, @@ -1539,7 +1539,7 @@ module ActiveRecord # +association+ is a placeholder for the symbol passed as the +name+ argument, so # <tt>belongs_to :author</tt> would add among others <tt>author.nil?</tt>. # - # [association(force_reload = false)] + # [association] # Returns the associated object. +nil+ is returned if none is found. # [association=(associate)] # Assigns the associate object, extracts the primary key, and sets it as the foreign key. @@ -1701,7 +1701,7 @@ module ActiveRecord # +collection+ is a placeholder for the symbol passed as the +name+ argument, so # <tt>has_and_belongs_to_many :categories</tt> would add among others <tt>categories.empty?</tt>. # - # [collection(force_reload = false)] + # [collection] # Returns an array of all the associated objects. # An empty array is returned if none are found. # [collection<<(object, ...)] diff --git a/activerecord/lib/active_record/associations/alias_tracker.rb b/activerecord/lib/active_record/associations/alias_tracker.rb index 4a5c821607..104de4f69d 100644 --- a/activerecord/lib/active_record/associations/alias_tracker.rb +++ b/activerecord/lib/active_record/associations/alias_tracker.rb @@ -4,21 +4,21 @@ module ActiveRecord module Associations # Keeps track of table aliases for ActiveRecord::Associations::JoinDependency class AliasTracker # :nodoc: - def self.create(connection, initial_table, type_caster) + def self.create(connection, initial_table) aliases = Hash.new(0) aliases[initial_table] = 1 - new connection, aliases, type_caster + new(connection, aliases) end - def self.create_with_joins(connection, initial_table, joins, type_caster) + def self.create_with_joins(connection, initial_table, joins) if joins.empty? - create(connection, initial_table, type_caster) + create(connection, initial_table) else aliases = Hash.new { |h, k| h[k] = initial_count_for(connection, k, joins) } aliases[initial_table] = 1 - new connection, aliases, type_caster + new(connection, aliases) end end @@ -51,17 +51,16 @@ module ActiveRecord end # table_joins is an array of arel joins which might conflict with the aliases we assign here - def initialize(connection, aliases, type_caster) + def initialize(connection, aliases) @aliases = aliases @connection = connection - @type_caster = type_caster end - def aliased_table_for(table_name, aliased_name) + def aliased_table_for(table_name, aliased_name, type_caster) if aliases[table_name].zero? # If it's zero, we can have our table_name aliases[table_name] = 1 - Arel::Table.new(table_name, type_caster: @type_caster) + Arel::Table.new(table_name, type_caster: type_caster) else # Otherwise, we need to use an alias aliased_name = @connection.table_alias_for(aliased_name) @@ -74,7 +73,7 @@ module ActiveRecord else aliased_name end - Arel::Table.new(table_name, type_caster: @type_caster).alias(table_alias) + Arel::Table.new(table_name, type_caster: type_caster).alias(table_alias) end end diff --git a/activerecord/lib/active_record/associations/association.rb b/activerecord/lib/active_record/associations/association.rb index ee2e79889f..1138ae3462 100644 --- a/activerecord/lib/active_record/associations/association.rb +++ b/activerecord/lib/active_record/associations/association.rb @@ -30,14 +30,6 @@ module ActiveRecord reset_scope end - # Returns the name of the table of the associated class: - # - # post.comments.aliased_table_name # => "comments" - # - def aliased_table_name - klass.table_name - end - # Resets the \loaded flag to +false+ and sets the \target to +nil+. def reset @loaded = false @@ -94,7 +86,7 @@ module ActiveRecord # actually gets built. def association_scope if klass - @association_scope ||= AssociationScope.scope(self, klass.connection) + @association_scope ||= AssociationScope.scope(self) end end @@ -276,7 +268,7 @@ module ActiveRecord end # Returns true if statement cache should be skipped on the association reader. - def skip_statement_cache? + def skip_statement_cache?(scope) reflection.has_scope? || scope.eager_loading? || klass.scope_attributes? || diff --git a/activerecord/lib/active_record/associations/association_scope.rb b/activerecord/lib/active_record/associations/association_scope.rb index 1593b94f0c..6ef225b725 100644 --- a/activerecord/lib/active_record/associations/association_scope.rb +++ b/activerecord/lib/active_record/associations/association_scope.rb @@ -1,8 +1,8 @@ module ActiveRecord module Associations class AssociationScope #:nodoc: - def self.scope(association, connection) - INSTANCE.scope(association, connection) + def self.scope(association) + INSTANCE.scope(association) end def self.create(&block) @@ -16,12 +16,12 @@ module ActiveRecord INSTANCE = create - def scope(association, connection) + def scope(association) klass = association.klass reflection = association.reflection scope = klass.unscoped owner = association.owner - alias_tracker = AliasTracker.create connection, association.klass.table_name, klass.type_caster + alias_tracker = AliasTracker.create(klass.connection, klass.table_name) chain_head, chain_tail = get_chain(reflection, association, alias_tracker) scope.extending! reflection.extensions @@ -112,7 +112,11 @@ module ActiveRecord runtime_reflection = Reflection::RuntimeReflection.new(reflection, association) previous_reflection = runtime_reflection reflection.chain.drop(1).each do |refl| - alias_name = tracker.aliased_table_for(refl.table_name, refl.alias_candidate(name)) + alias_name = tracker.aliased_table_for( + refl.table_name, + refl.alias_candidate(name), + refl.klass.type_caster + ) proxy = ReflectionProxy.new(refl, alias_name) previous_reflection.next = proxy previous_reflection = proxy @@ -138,7 +142,7 @@ module ActiveRecord # Exclude the scope of the association itself, because that # was already merged in the #scope method. reflection.constraints.each do |scope_chain_item| - item = eval_scope(reflection.klass, table, scope_chain_item, owner) + item = eval_scope(reflection, table, scope_chain_item, owner) if scope_chain_item == refl.scope scope.merge! item.except(:where, :includes) @@ -159,9 +163,8 @@ module ActiveRecord scope end - def eval_scope(klass, table, scope, owner) - predicate_builder = PredicateBuilder.new(TableMetadata.new(klass, table)) - ActiveRecord::Relation.create(klass, table, predicate_builder).instance_exec(owner, &scope) + def eval_scope(reflection, table, scope, owner) + reflection.build_scope(table).instance_exec(owner, &scope) end end end diff --git a/activerecord/lib/active_record/associations/builder/collection_association.rb b/activerecord/lib/active_record/associations/builder/collection_association.rb index edeb6491bd..c58b7d8160 100644 --- a/activerecord/lib/active_record/associations/builder/collection_association.rb +++ b/activerecord/lib/active_record/associations/builder/collection_association.rb @@ -1,6 +1,4 @@ -# This class is inherited by the has_many and has_many_and_belongs_to_many association classes - -require "active_record/associations" +require_relative "../../associations" module ActiveRecord::Associations::Builder # :nodoc: class CollectionAssociation < Association #:nodoc: diff --git a/activerecord/lib/active_record/associations/collection_association.rb b/activerecord/lib/active_record/associations/collection_association.rb index edc53e2517..a49fb155ee 100644 --- a/activerecord/lib/active_record/associations/collection_association.rb +++ b/activerecord/lib/active_record/associations/collection_association.rb @@ -44,10 +44,7 @@ module ActiveRecord if loaded? target.pluck(reflection.association_primary_key) else - @association_ids ||= ( - column = "#{reflection.quoted_table_name}.#{reflection.association_primary_key}" - scope.pluck(column) - ) + @association_ids ||= scope.pluck(reflection.association_primary_key) end end @@ -69,6 +66,7 @@ module ActiveRecord def reset super @target = [] + @association_ids = nil end def find(*args) @@ -300,13 +298,14 @@ module ActiveRecord private def find_target - return scope.to_a if skip_statement_cache? + scope = self.scope + return scope.to_a if skip_statement_cache?(scope) conn = klass.connection sc = reflection.association_scope_cache(conn, owner) do StatementCache.create(conn) { |params| as = AssociationScope.create { params.bind } - target_scope.merge as.scope(self, conn) + target_scope.merge!(as.scope(self)) } end @@ -357,7 +356,10 @@ module ActiveRecord transaction do add_to_target(build_record(attributes)) do |record| yield(record) if block_given? - insert_record(record, true, raise) { @_was_loaded = loaded? } + insert_record(record, true, raise) { + @_was_loaded = loaded? + @association_ids = nil + } end end end @@ -430,7 +432,12 @@ module ActiveRecord records.each do |record| raise_on_type_mismatch!(record) add_to_target(record) do - result &&= insert_record(record, true, raise) { @_was_loaded = loaded? } unless owner.new_record? + unless owner.new_record? + result &&= insert_record(record, true, raise) { + @_was_loaded = loaded? + @association_ids = nil + } + end end end diff --git a/activerecord/lib/active_record/associations/collection_proxy.rb b/activerecord/lib/active_record/associations/collection_proxy.rb index 8cdb508c43..d77fcaf668 100644 --- a/activerecord/lib/active_record/associations/collection_proxy.rb +++ b/activerecord/lib/active_record/associations/collection_proxy.rb @@ -1114,6 +1114,7 @@ module ActiveRecord end def reset_scope # :nodoc: + @offsets = {} @scope = nil self end diff --git a/activerecord/lib/active_record/associations/has_many_through_association.rb b/activerecord/lib/active_record/associations/has_many_through_association.rb index 53ffb3b68d..2fd20b4368 100644 --- a/activerecord/lib/active_record/associations/has_many_through_association.rb +++ b/activerecord/lib/active_record/associations/has_many_through_association.rb @@ -109,6 +109,11 @@ module ActiveRecord record end + def remove_records(existing_records, records, method) + super + delete_through_records(records) + end + def target_reflection_has_associated_record? !(through_reflection.belongs_to? && owner[through_reflection.foreign_key].blank?) end diff --git a/activerecord/lib/active_record/associations/join_dependency.rb b/activerecord/lib/active_record/associations/join_dependency.rb index 643226267c..04cdcb6a7f 100644 --- a/activerecord/lib/active_record/associations/join_dependency.rb +++ b/activerecord/lib/active_record/associations/join_dependency.rb @@ -93,7 +93,7 @@ module ActiveRecord # joins # => [] # def initialize(base, associations, joins, eager_loading: true) - @alias_tracker = AliasTracker.create_with_joins(base.connection, base.table_name, joins, base.type_caster) + @alias_tracker = AliasTracker.create_with_joins(base.connection, base.table_name, joins) @eager_loading = eager_loading tree = self.class.make_tree associations @join_root = JoinBase.new base, build(tree, base) @@ -104,17 +104,17 @@ module ActiveRecord join_root.drop(1).map!(&:reflection) end - def join_constraints(outer_joins, join_type) + def join_constraints(joins_to_add, join_type) joins = join_root.children.flat_map { |child| make_join_constraints(join_root, child, join_type) } - joins.concat outer_joins.flat_map { |oj| + joins.concat joins_to_add.flat_map { |oj| if join_root.match? oj.join_root walk join_root, oj.join_root else oj.join_root.children.flat_map { |child| - make_outer_joins oj.join_root, child + make_join_constraints(oj.join_root, child, join_type) } end } @@ -185,7 +185,8 @@ module ActiveRecord node.reflection.chain.map { |reflection| alias_tracker.aliased_table_for( reflection.table_name, - table_alias_for(reflection, parent, reflection != node.reflection) + table_alias_for(reflection, parent, reflection != node.reflection), + reflection.klass.type_caster ) } end @@ -197,8 +198,7 @@ module ActiveRecord def table_alias_for(reflection, parent, join) name = "#{reflection.plural_name}_#{parent.table_name}" - name << "_join" if join - name + join ? "#{name}_join" : name end def walk(left, right) 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 97cfec0302..b14ddfeeeb 100644 --- a/activerecord/lib/active_record/associations/join_dependency/join_association.rb +++ b/activerecord/lib/active_record/associations/join_dependency/join_association.rb @@ -1,4 +1,4 @@ -require "active_record/associations/join_dependency/join_part" +require_relative "join_part" module ActiveRecord module Associations @@ -34,37 +34,19 @@ module ActiveRecord table = tables.shift klass = reflection.klass - join_keys = reflection.join_keys - key = join_keys.key - foreign_key = join_keys.foreign_key + constraint = reflection.build_join_constraint(table, foreign_table) - constraint = build_constraint(klass, table, key, foreign_table, foreign_key) - - predicate_builder = PredicateBuilder.new(TableMetadata.new(klass, table)) - scope_chain_items = reflection.join_scopes(table, predicate_builder) - klass_scope = reflection.klass_join_scope(table, predicate_builder) - - scope_chain_items.concat [klass_scope].compact - - rel = scope_chain_items.inject(scope_chain_items.shift) do |left, right| - left.merge right - end - - if rel && !rel.arel.constraints.empty? - binds += rel.bound_attributes - constraint = constraint.and rel.arel.constraints - end + joins << table.create_join(table, table.create_on(constraint), join_type) - if reflection.type - value = foreign_klass.base_class.name - column = klass.columns_hash[reflection.type.to_s] + join_scope = reflection.join_scope(table, foreign_klass) - binds << Relation::QueryAttribute.new(column.name, value, klass.type_for_attribute(column.name)) - constraint = constraint.and klass.arel_attribute(reflection.type, table).eq(Arel::Nodes::BindParam.new) + if join_scope.arel.constraints.any? + binds.concat join_scope.bound_attributes + joins.concat join_scope.arel.join_sources + right = joins.last.right + right.expr = right.expr.and(join_scope.arel.constraints) end - joins << table.create_join(table, table.create_on(constraint), join_type) - # The current table in this iteration becomes the foreign table in the next foreign_table, foreign_klass = table, klass end @@ -72,34 +54,6 @@ module ActiveRecord JoinInformation.new joins, binds end - # Builds equality condition. - # - # Example: - # - # class Physician < ActiveRecord::Base - # has_many :appointments - # end - # - # If I execute `Physician.joins(:appointments).to_a` then - # klass # => Physician - # table # => #<Arel::Table @name="appointments" ...> - # key # => physician_id - # foreign_table # => #<Arel::Table @name="physicians" ...> - # foreign_key # => id - # - def build_constraint(klass, table, key, foreign_table, foreign_key) - constraint = table[key].eq(foreign_table[foreign_key]) - - if klass.finder_needs_type_condition? - constraint = table.create_and([ - constraint, - klass.send(:type_condition, table) - ]) - end - - constraint - end - def table tables.first end diff --git a/activerecord/lib/active_record/associations/join_dependency/join_base.rb b/activerecord/lib/active_record/associations/join_dependency/join_base.rb index fca20514d1..6e0963425d 100644 --- a/activerecord/lib/active_record/associations/join_dependency/join_base.rb +++ b/activerecord/lib/active_record/associations/join_dependency/join_base.rb @@ -1,4 +1,4 @@ -require "active_record/associations/join_dependency/join_part" +require_relative "join_part" module ActiveRecord module Associations diff --git a/activerecord/lib/active_record/associations/preloader.rb b/activerecord/lib/active_record/associations/preloader.rb index 9f77f38b35..a18994cec4 100644 --- a/activerecord/lib/active_record/associations/preloader.rb +++ b/activerecord/lib/active_record/associations/preloader.rb @@ -54,8 +54,6 @@ module ActiveRecord autoload :BelongsTo, "active_record/associations/preloader/belongs_to" end - NULL_RELATION = Struct.new(:values, :where_clause, :joins_values).new({}, Relation::WhereClause.empty, []) - # Eager loads the named associations for the given Active Record record(s). # # In this description, 'association name' shall refer to the name passed @@ -93,7 +91,6 @@ module ActiveRecord def preload(records, associations, preload_scope = nil) records = Array.wrap(records).compact.uniq associations = Array.wrap(associations) - preload_scope = preload_scope || NULL_RELATION if records.empty? [] @@ -147,7 +144,7 @@ module ActiveRecord def preloaders_for_one(association, records, scope) grouped_records(association, records).flat_map do |reflection, klasses| klasses.map do |rhs_klass, rs| - loader = preloader_for(reflection, rs, rhs_klass).new(rhs_klass, rs, reflection, scope) + loader = preloader_for(reflection, rs).new(rhs_klass, rs, reflection, scope) loader.run self loader end @@ -159,6 +156,7 @@ module ActiveRecord records.each do |record| next unless record assoc = record.association(association) + next unless assoc.klass klasses = h[assoc.reflection] ||= {} (klasses[assoc.klass] ||= []) << record end @@ -180,20 +178,11 @@ module ActiveRecord end end - class NullPreloader # :nodoc: - def self.new(klass, owners, reflection, preload_scope); self; end - def self.run(preloader); end - def self.preloaded_records; []; end - def self.owners; []; end - end - # Returns a class containing the logic needed to load preload the data # and attach it to a relation. For example +Preloader::Association+ or # +Preloader::HasManyThrough+. The class returned implements a `run` method # that accepts a preloader. - def preloader_for(reflection, owners, rhs_klass) - return NullPreloader unless rhs_klass - + def preloader_for(reflection, owners) if owners.first.association(reflection.name).loaded? return AlreadyLoaded end diff --git a/activerecord/lib/active_record/associations/preloader/association.rb b/activerecord/lib/active_record/associations/preloader/association.rb index 63ef3f2d8c..85343040db 100644 --- a/activerecord/lib/active_record/associations/preloader/association.rb +++ b/activerecord/lib/active_record/associations/preloader/association.rb @@ -11,7 +11,6 @@ module ActiveRecord @reflection = reflection @preload_scope = preload_scope @model = owners.first && owners.first.class - @scope = nil @preloaded_records = [] end @@ -23,29 +22,11 @@ module ActiveRecord raise NotImplementedError end - def scope - @scope ||= build_scope - end - - def records_for(ids) - scope.where(association_key_name => ids) - end - - def table - klass.arel_table - end - # The name of the key on the associated records def association_key_name raise NotImplementedError end - # This is overridden by HABTM as the condition should be on the foreign_key column in - # the join table - def association_key - klass.arel_attribute(association_key_name, table) - end - # The name of the key on the model which declares the association def owner_key_name raise NotImplementedError @@ -114,54 +95,35 @@ module ActiveRecord # Make several smaller queries if necessary or make one query if the adapter supports it slices = owner_keys.each_slice(klass.connection.in_clause_length || owner_keys.size) @preloaded_records = slices.flat_map do |slice| - records_for(slice).load(&block) + records_for(slice, &block) end @preloaded_records.group_by do |record| convert_key(record[association_key_name]) end end + def records_for(ids, &block) + scope.where(association_key_name => ids).load(&block) + end + + def scope + @scope ||= build_scope + end + def reflection_scope @reflection_scope ||= reflection.scope_for(klass) end def build_scope - scope = klass.unscoped - - values = reflection_scope.values - preload_values = preload_scope.values - - scope.where_clause = reflection_scope.where_clause + preload_scope.where_clause - scope.references_values = Array(values[:references]) + Array(preload_values[:references]) - - if preload_values[:select] || values[:select] - scope._select!(preload_values[:select] || values[:select]) - end - scope.includes! preload_values[:includes] || values[:includes] - if preload_scope.joins_values.any? - scope.joins!(preload_scope.joins_values) - else - scope.joins!(reflection_scope.joins_values) - end - - if order_values = preload_values[:order] || values[:order] - scope.order!(order_values) - end - - if preload_values[:reordering] || values[:reordering] - scope.reordering_value = true - end - - if preload_values[:readonly] || values[:readonly] - scope.readonly! - end + scope = klass.default_scoped - if options[:as] - scope.where!(klass.table_name => { reflection.type => model.base_class.sti_name }) + if reflection.type + scope.where!(reflection.type => model.base_class.sti_name) end - scope.unscope_values = Array(values[:unscope]) + Array(preload_values[:unscope]) - klass.default_scoped.merge(scope) + scope.merge!(reflection_scope) + scope.merge!(preload_scope) if preload_scope + scope end end end diff --git a/activerecord/lib/active_record/associations/preloader/through_association.rb b/activerecord/lib/active_record/associations/preloader/through_association.rb index 8b954138cd..0999746cd5 100644 --- a/activerecord/lib/active_record/associations/preloader/through_association.rb +++ b/activerecord/lib/active_record/associations/preloader/through_association.rb @@ -11,6 +11,8 @@ module ActiveRecord end def associated_records_by_owner(preloader) + through_scope = through_scope() + preloader.preload(owners, through_reflection.name, through_scope) @@ -20,7 +22,7 @@ module ActiveRecord [owner, Array(center)] end - reset_association owners, through_reflection.name + reset_association(owners, through_reflection.name, through_scope) middle_records = through_records.flat_map(&:last) @@ -63,7 +65,7 @@ module ActiveRecord id_map end - def reset_association(owners, association_name) + def reset_association(owners, association_name, through_scope) should_reset = (through_scope != through_reflection.klass.unscoped) || (options[:source_type] && through_reflection.collection?) @@ -77,17 +79,24 @@ module ActiveRecord def through_scope scope = through_reflection.klass.unscoped + values = reflection_scope.values if options[:source_type] scope.where! reflection.foreign_type => options[:source_type] else unless reflection_scope.where_clause.empty? - scope.includes_values = Array(reflection_scope.values[:includes] || options[:source]) + scope.includes_values = Array(values[:includes] || options[:source]) scope.where_clause = reflection_scope.where_clause + if joins = values[:joins] + scope.joins!(source_reflection.name => joins) + end + if left_outer_joins = values[:left_outer_joins] + scope.left_outer_joins!(source_reflection.name => left_outer_joins) + end end - scope.references! reflection_scope.values[:references] - if scope.eager_loading? && order_values = reflection_scope.values[:order] + scope.references! values[:references] + if scope.eager_loading? && order_values = values[:order] scope = scope.order(order_values) end end diff --git a/activerecord/lib/active_record/associations/singular_association.rb b/activerecord/lib/active_record/associations/singular_association.rb index 91580a28d0..f8bbe4c2ed 100644 --- a/activerecord/lib/active_record/associations/singular_association.rb +++ b/activerecord/lib/active_record/associations/singular_association.rb @@ -36,13 +36,14 @@ module ActiveRecord end def find_target - return scope.take if skip_statement_cache? + scope = self.scope + return scope.take if skip_statement_cache?(scope) conn = klass.connection sc = reflection.association_scope_cache(conn, owner) do StatementCache.create(conn) { |params| as = AssociationScope.create { params.bind } - target_scope.merge(as.scope(self, conn)).limit(1) + target_scope.merge!(as.scope(self)).limit(1) } end @@ -63,6 +64,10 @@ module ActiveRecord end def _create_record(attributes, raise_error = false) + unless owner.persisted? + raise ActiveRecord::RecordNotSaved, "You cannot call create unless the parent is saved" + end + record = build_record(attributes) yield(record) if block_given? saved = record.save diff --git a/activerecord/lib/active_record/attribute.rb b/activerecord/lib/active_record/attribute.rb index 38281158d8..78662433eb 100644 --- a/activerecord/lib/active_record/attribute.rb +++ b/activerecord/lib/active_record/attribute.rb @@ -122,7 +122,7 @@ module ActiveRecord def encode_with(coder) coder["name"] = name - coder["value_before_type_cast"] = value_before_type_cast if value_before_type_cast + coder["value_before_type_cast"] = value_before_type_cast unless value_before_type_cast.nil? coder["type"] = type if type coder["original_attribute"] = original_attribute if original_attribute coder["value"] = value if defined?(@value) diff --git a/activerecord/lib/active_record/attribute/user_provided_default.rb b/activerecord/lib/active_record/attribute/user_provided_default.rb index 57f8bbed76..c4e731fb28 100644 --- a/activerecord/lib/active_record/attribute/user_provided_default.rb +++ b/activerecord/lib/active_record/attribute/user_provided_default.rb @@ -1,4 +1,4 @@ -require "active_record/attribute" +require_relative "../attribute" module ActiveRecord class Attribute # :nodoc: diff --git a/activerecord/lib/active_record/attribute_methods/dirty.rb b/activerecord/lib/active_record/attribute_methods/dirty.rb index 76987fb8f4..8c58e63931 100644 --- a/activerecord/lib/active_record/attribute_methods/dirty.rb +++ b/activerecord/lib/active_record/attribute_methods/dirty.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true require "active_support/core_ext/module/attribute_accessors" -require "active_record/attribute_mutation_tracker" +require_relative "../attribute_mutation_tracker" module ActiveRecord module AttributeMethods @@ -80,7 +80,7 @@ module ActiveRecord clear_mutation_trackers end - def raw_write_attribute(attr_name, *) + def write_attribute_without_type_cast(attr_name, *) result = super clear_attribute_change(attr_name) result diff --git a/activerecord/lib/active_record/attribute_methods/primary_key.rb b/activerecord/lib/active_record/attribute_methods/primary_key.rb index 2f32caa257..081aad434d 100644 --- a/activerecord/lib/active_record/attribute_methods/primary_key.rb +++ b/activerecord/lib/active_record/attribute_methods/primary_key.rb @@ -8,23 +8,20 @@ module ActiveRecord # Returns this record's primary key value wrapped in an array if one is # available. def to_key - sync_with_transaction_state key = id [key] if key end # Returns the primary key value. def id - if pk = self.class.primary_key - sync_with_transaction_state - _read_attribute(pk) - end + sync_with_transaction_state + _read_attribute(self.class.primary_key) if self.class.primary_key end # Sets the primary key value. def id=(value) sync_with_transaction_state - write_attribute(self.class.primary_key, value) if self.class.primary_key + _write_attribute(self.class.primary_key, value) if self.class.primary_key end # Queries the primary key value. @@ -57,16 +54,12 @@ module ActiveRecord end module ClassMethods - def define_method_attribute(attr_name) - super + ID_ATTRIBUTE_METHODS = %w(id id= id? id_before_type_cast id_was id_in_database).to_set - if attr_name == primary_key && attr_name != "id" - generated_attribute_methods.send(:alias_method, :id, primary_key) - end + def instance_method_already_implemented?(method_name) + super || primary_key && ID_ATTRIBUTE_METHODS.include?(method_name) end - ID_ATTRIBUTE_METHODS = %w(id id= id? id_before_type_cast id_was id_in_database).to_set - def dangerous_attribute_method?(method_name) super && !ID_ATTRIBUTE_METHODS.include?(method_name) end diff --git a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb index 4a8d231503..1f1efe8812 100644 --- a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb +++ b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb @@ -54,8 +54,7 @@ module ActiveRecord extend ActiveSupport::Concern included do - mattr_accessor :time_zone_aware_attributes, instance_writer: false - self.time_zone_aware_attributes = false + mattr_accessor :time_zone_aware_attributes, instance_writer: false, default: false class_attribute :skip_time_zone_conversion_for_attributes, instance_writer: false, default: [] class_attribute :time_zone_aware_types, instance_writer: false, default: [ :datetime, :time ] diff --git a/activerecord/lib/active_record/attribute_methods/write.rb b/activerecord/lib/active_record/attribute_methods/write.rb index fe0e01db28..54b673c72e 100644 --- a/activerecord/lib/active_record/attribute_methods/write.rb +++ b/activerecord/lib/active_record/attribute_methods/write.rb @@ -17,7 +17,7 @@ module ActiveRecord generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ + 1 def __temp__#{safe_name}=(value) name = ::ActiveRecord::AttributeMethods::AttrNames::ATTR_#{safe_name} - write_attribute(name, value) + _write_attribute(name, value) end alias_method #{(name + '=').inspect}, :__temp__#{safe_name}= undef_method :__temp__#{safe_name}= @@ -35,30 +35,27 @@ module ActiveRecord attr_name.to_s end - write_attribute_with_type_cast(name, value, true) + name = self.class.primary_key if name == "id".freeze && self.class.primary_key + _write_attribute(name, value) end - def raw_write_attribute(attr_name, value) # :nodoc: - write_attribute_with_type_cast(attr_name, value, false) + # This method exists to avoid the expensive primary_key check internally, without + # breaking compatibility with the write_attribute API + def _write_attribute(attr_name, value) # :nodoc: + @attributes.write_from_user(attr_name.to_s, value) + value end private - # Handle *= for method_missing. - def attribute=(attribute_name, value) - write_attribute(attribute_name, value) + def write_attribute_without_type_cast(attr_name, value) + name = attr_name.to_s + @attributes.write_cast_value(name, value) + value end - def write_attribute_with_type_cast(attr_name, value, should_type_cast) - attr_name = attr_name.to_s - attr_name = self.class.primary_key if attr_name == "id" && self.class.primary_key - - if should_type_cast - @attributes.write_from_user(attr_name, value) - else - @attributes.write_cast_value(attr_name, value) - end - - value + # Handle *= for method_missing. + def attribute=(attribute_name, value) + _write_attribute(attribute_name, value) end end end diff --git a/activerecord/lib/active_record/attribute_mutation_tracker.rb b/activerecord/lib/active_record/attribute_mutation_tracker.rb index 4de993e169..a01a58f8a5 100644 --- a/activerecord/lib/active_record/attribute_mutation_tracker.rb +++ b/activerecord/lib/active_record/attribute_mutation_tracker.rb @@ -26,6 +26,7 @@ module ActiveRecord end def change_to_attribute(attr_name) + attr_name = attr_name.to_s if changed?(attr_name) [attributes[attr_name].original_value, attributes.fetch_value(attr_name)] end @@ -44,7 +45,7 @@ module ActiveRecord end def changed_in_place?(attr_name) - attributes[attr_name].changed_in_place? + attributes[attr_name.to_s].changed_in_place? end def forget_change(attr_name) @@ -54,7 +55,7 @@ module ActiveRecord end def original_value(attr_name) - attributes[attr_name].original_value + attributes[attr_name.to_s].original_value end def force_change(attr_name) diff --git a/activerecord/lib/active_record/attribute_set.rb b/activerecord/lib/active_record/attribute_set.rb index 01f9d815d5..6399e3de70 100644 --- a/activerecord/lib/active_record/attribute_set.rb +++ b/activerecord/lib/active_record/attribute_set.rb @@ -1,5 +1,5 @@ -require "active_record/attribute_set/builder" -require "active_record/attribute_set/yaml_encoder" +require_relative "attribute_set/builder" +require_relative "attribute_set/yaml_encoder" module ActiveRecord class AttributeSet # :nodoc: diff --git a/activerecord/lib/active_record/attribute_set/builder.rb b/activerecord/lib/active_record/attribute_set/builder.rb index 2f624d32af..abe22b9ae4 100644 --- a/activerecord/lib/active_record/attribute_set/builder.rb +++ b/activerecord/lib/active_record/attribute_set/builder.rb @@ -1,4 +1,4 @@ -require "active_record/attribute" +require_relative "../attribute" module ActiveRecord class AttributeSet # :nodoc: diff --git a/activerecord/lib/active_record/attributes.rb b/activerecord/lib/active_record/attributes.rb index 475b9beec4..dde22bcdaa 100644 --- a/activerecord/lib/active_record/attributes.rb +++ b/activerecord/lib/active_record/attributes.rb @@ -1,4 +1,4 @@ -require "active_record/attribute/user_provided_default" +require_relative "attribute/user_provided_default" module ActiveRecord # See ActiveRecord::Attributes::ClassMethods for documentation diff --git a/activerecord/lib/active_record/autosave_association.rb b/activerecord/lib/active_record/autosave_association.rb index 829a4f6e86..70f0e2af8e 100644 --- a/activerecord/lib/active_record/autosave_association.rb +++ b/activerecord/lib/active_record/autosave_association.rb @@ -140,8 +140,7 @@ module ActiveRecord included do Associations::Builder::Association.extensions << AssociationBuilderExtension - mattr_accessor :index_nested_attribute_errors, instance_writer: false - self.index_nested_attribute_errors = false + mattr_accessor :index_nested_attribute_errors, instance_writer: false, default: false end module ClassMethods # :nodoc: diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index ac1aa2df45..f0e455478a 100644 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -13,14 +13,14 @@ require "active_support/core_ext/kernel/singleton_class" require "active_support/core_ext/module/introspection" require "active_support/core_ext/object/duplicable" require "active_support/core_ext/class/subclasses" -require "active_record/attribute_decorators" -require "active_record/define_callbacks" -require "active_record/errors" -require "active_record/log_subscriber" -require "active_record/explain_subscriber" -require "active_record/relation/delegation" -require "active_record/attributes" -require "active_record/type_caster" +require_relative "attribute_decorators" +require_relative "define_callbacks" +require_relative "errors" +require_relative "log_subscriber" +require_relative "explain_subscriber" +require_relative "relation/delegation" +require_relative "attributes" +require_relative "type_caster" module ActiveRecord #:nodoc: # = Active Record diff --git a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb index 61bf5477aa..627b753f01 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb @@ -679,7 +679,7 @@ module ActiveRecord # this block can't be easily moved into attempt_to_checkout_all_existing_connections's # rescue block, because doing so would put it outside of synchronize section, without # being in a critical section thread_report might become inaccurate - msg = "could not obtain ownership of all database connections in #{checkout_timeout} seconds" + msg = "could not obtain ownership of all database connections in #{checkout_timeout} seconds".dup thread_report = [] @connections.each do |conn| 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 c6811a4802..879626b72a 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb @@ -51,9 +51,7 @@ module ActiveRecord # Returns a single value from a record def select_value(arel, name = nil, binds = []) - if result = select_rows(arel, name, binds).first - result.first - end + single_value_from_rows(select_rows(arel, name, binds)) end # Returns an array of the values of the first column in a select: @@ -68,6 +66,18 @@ module ActiveRecord select_all(arel, name, binds).rows end + def query_value(sql, name = nil) # :nodoc: + single_value_from_rows(query(sql, name)) + end + + def query_values(sql, name = nil) # :nodoc: + query(sql, name).map(&:first) + end + + def query(sql, name = nil) # :nodoc: + exec_query(sql, name).rows + end + # Executes the SQL statement in the context of this connection and returns # the raw result from the connection adapter. # Note: depending on your database connector, the result returned by this @@ -296,6 +306,9 @@ module ActiveRecord # Inserts the given fixture into the table. Overridden in adapters that require # something beyond a simple insert (eg. Oracle). + # Most of adapters should implement `insert_fixtures` that leverages bulk SQL insert. + # We keep this method to provide fallback + # for databases like sqlite that do not support bulk inserts. def insert_fixture(fixture, table_name) fixture = fixture.stringify_keys @@ -308,16 +321,52 @@ module ActiveRecord raise Fixture::FixtureError, %(table "#{table_name}" has no column named #{name.inspect}.) end end - key_list = fixture.keys.map { |name| quote_column_name(name) } - value_list = binds.map(&:value_for_database).map do |value| - begin - quote(value) - rescue TypeError - quote(YAML.dump(value)) + + table = Arel::Table.new(table_name) + + values = binds.map do |bind| + value = with_yaml_fallback(bind.value_for_database) + [table[bind.name], value] + end + + manager = Arel::InsertManager.new + manager.into(table) + manager.insert(values) + execute manager.to_sql, "Fixture Insert" + end + + # Inserts a set of fixtures into the table. Overridden in adapters that require + # something beyond a simple insert (eg. Oracle). + def insert_fixtures(fixtures, table_name) + return if fixtures.empty? + + columns = schema_cache.columns_hash(table_name) + + values = fixtures.map do |fixture| + fixture = fixture.stringify_keys + + unknown_columns = fixture.keys - columns.keys + if unknown_columns.any? + raise Fixture::FixtureError, %(table "#{table_name}" has no columns named #{unknown_columns.map(&:inspect).join(', ')}.) + end + + columns.map do |name, column| + if fixture.key?(name) + type = lookup_cast_type_from_column(column) + bind = Relation::QueryAttribute.new(name, fixture[name], type) + with_yaml_fallback(bind.value_for_database) + else + Arel.sql("DEFAULT") + end end end - execute "INSERT INTO #{quote_table_name(table_name)} (#{key_list.join(', ')}) VALUES (#{value_list.join(', ')})", "Fixture Insert" + table = Arel::Table.new(table_name) + manager = Arel::InsertManager.new + manager.into(table) + columns.each_key { |column| manager.columns << table[column] } + manager.values = manager.create_values_list(values) + execute manager.to_sql, "Fixtures Insert" end def empty_insert_statement_value @@ -371,7 +420,11 @@ module ActiveRecord end def last_inserted_id(result) - row = result.rows.first + single_value_from_rows(result.rows) + end + + def single_value_from_rows(rows) + row = rows.first row && row.first end @@ -381,6 +434,17 @@ module ActiveRecord end [relation, binds] end + + # Fixture value is quoted by Arel, however scalar values + # are not quotable. In this case we want to convert + # the column value to YAML. + def with_yaml_fallback(value) + if value.is_a?(Hash) || value.is_a?(Array) + YAML.dump(value) + else + value + end + end end end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb b/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb index e53ba4e666..c352ddfc11 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb @@ -108,6 +108,7 @@ module ActiveRecord "sql.active_record", sql: sql, binds: binds, + type_casted_binds: -> { type_casted_binds(binds) }, name: name, connection_id: object_id, cached: true, @@ -123,6 +124,7 @@ module ActiveRecord # If arel is locked this is a SELECT ... FOR UPDATE or somesuch. Such # queries should not be cached. def locked?(arel) + arel = arel.arel if arel.is_a?(Relation) arel.respond_to?(:locked) && arel.locked end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb index a4fecc4a8e..8865e7c703 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb @@ -22,7 +22,7 @@ module ActiveRecord private def visit_AlterTable(o) - sql = "ALTER TABLE #{quote_table_name(o.name)} " + sql = "ALTER TABLE #{quote_table_name(o.name)} ".dup sql << o.adds.map { |col| accept col }.join(" ") sql << o.foreign_key_adds.map { |fk| visit_AddForeignKey fk }.join(" ") sql << o.foreign_key_drops.map { |fk| visit_DropForeignKey fk }.join(" ") @@ -30,17 +30,17 @@ module ActiveRecord def visit_ColumnDefinition(o) o.sql_type = type_to_sql(o.type, o.options) - column_sql = "#{quote_column_name(o.name)} #{o.sql_type}" + column_sql = "#{quote_column_name(o.name)} #{o.sql_type}".dup add_column_options!(column_sql, column_options(o)) unless o.type == :primary_key column_sql end def visit_AddColumnDefinition(o) - "ADD #{accept(o.column)}" + "ADD #{accept(o.column)}".dup end def visit_TableDefinition(o) - create_sql = "CREATE#{' TEMPORARY' if o.temporary} TABLE #{quote_table_name(o.name)} " + create_sql = "CREATE#{' TEMPORARY' if o.temporary} TABLE #{quote_table_name(o.name)} ".dup statements = o.columns.map { |c| accept c } statements << accept(o.primary_keys) if o.primary_keys @@ -55,7 +55,7 @@ module ActiveRecord create_sql << "(#{statements.join(', ')})" if statements.present? add_table_options!(create_sql, table_options(o)) - create_sql << " AS #{@conn.to_sql(o.as)}" if o.as + create_sql << " AS #{to_sql(o.as)}" if o.as create_sql end @@ -114,6 +114,11 @@ module ActiveRecord sql end + def to_sql(sql) + sql = sql.to_sql if sql.respond_to?(:to_sql) + sql + end + def foreign_key_in_create(from_table, to_table, options) options = foreign_key_options(from_table, to_table, options) accept ForeignKeyDefinition.new(from_table, to_table, options) diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb index 16a398f631..475463c4fd 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb @@ -1,4 +1,4 @@ -require "active_record/migration/join_table" +require_relative "../../migration/join_table" require "active_support/core_ext/string/access" require "digest" @@ -31,7 +31,7 @@ module ActiveRecord # Returns the relation names useable to back Active Record models. # For most adapters this means all #tables and #views. def data_sources - select_values(data_source_sql, "SCHEMA") + query_values(data_source_sql, "SCHEMA") rescue NotImplementedError tables | views end @@ -41,14 +41,14 @@ module ActiveRecord # data_source_exists?(:ebooks) # def data_source_exists?(name) - select_values(data_source_sql(name), "SCHEMA").any? if name.present? + query_values(data_source_sql(name), "SCHEMA").any? if name.present? rescue NotImplementedError data_sources.include?(name.to_s) end # Returns an array of table names defined in the database. def tables - select_values(data_source_sql(type: "BASE TABLE"), "SCHEMA") + query_values(data_source_sql(type: "BASE TABLE"), "SCHEMA") end # Checks to see if the table +table_name+ exists on the database. @@ -56,14 +56,14 @@ module ActiveRecord # table_exists?(:developers) # def table_exists?(table_name) - select_values(data_source_sql(table_name, type: "BASE TABLE"), "SCHEMA").any? if table_name.present? + query_values(data_source_sql(table_name, type: "BASE TABLE"), "SCHEMA").any? if table_name.present? rescue NotImplementedError tables.include?(table_name.to_s) end # Returns an array of view names defined in the database. def views - select_values(data_source_sql(type: "VIEW"), "SCHEMA") + query_values(data_source_sql(type: "VIEW"), "SCHEMA") end # Checks to see if the view +view_name+ exists on the database. @@ -71,7 +71,7 @@ module ActiveRecord # view_exists?(:ebooks) # def view_exists?(view_name) - select_values(data_source_sql(view_name, type: "VIEW"), "SCHEMA").any? if view_name.present? + query_values(data_source_sql(view_name, type: "VIEW"), "SCHEMA").any? if view_name.present? rescue NotImplementedError views.include?(view_name.to_s) end @@ -188,6 +188,8 @@ module ActiveRecord # The name of the primary key, if one is to be added automatically. # Defaults to +id+. If <tt>:id</tt> is false, then this option is ignored. # + # If an array is passed, a composite primary key will be created. + # # Note that Active Record models will automatically detect their # primary key. This can be avoided by using # {self.primary_key=}[rdoc-ref:AttributeMethods::PrimaryKey::ClassMethods#primary_key=] on the model @@ -241,6 +243,23 @@ module ActiveRecord # label varchar # ) # + # ====== Create a composite primary key + # + # create_table(:orders, primary_key: [:product_id, :client_id]) do |t| + # t.belongs_to :product + # t.belongs_to :client + # end + # + # generates: + # + # CREATE TABLE order ( + # product_id integer NOT NULL, + # client_id integer NOT NULL + # ); + # + # ALTER TABLE ONLY "orders" + # ADD CONSTRAINT orders_pkey PRIMARY KEY (product_id, client_id); + # # ====== Do not add a primary key column # # create_table(:categories_suppliers, id: false) do |t| @@ -493,8 +512,7 @@ module ActiveRecord # * <tt>:default</tt> - # The column's default value. Use +nil+ for +NULL+. # * <tt>:null</tt> - - # Allows or disallows +NULL+ values in the column. This option could - # have been named <tt>:null_allowed</tt>. + # Allows or disallows +NULL+ values in the column. # * <tt>:precision</tt> - # Specifies the precision for the <tt>:decimal</tt> and <tt>:numeric</tt> columns. # * <tt>:scale</tt> - @@ -992,7 +1010,7 @@ module ActiveRecord def dump_schema_information #:nodoc: versions = ActiveRecord::SchemaMigration.all_versions - insert_versions_sql(versions) + insert_versions_sql(versions) if versions.any? end def initialize_schema_migrations_table # :nodoc: @@ -1330,7 +1348,7 @@ module ActiveRecord sm_table = quote_table_name(ActiveRecord::SchemaMigration.table_name) if versions.is_a?(Array) - sql = "INSERT INTO #{sm_table} (version) VALUES\n" + sql = "INSERT INTO #{sm_table} (version) VALUES\n".dup sql << versions.map { |v| "(#{quote(v)})" }.join(",\n") sql << ";\n\n" sql diff --git a/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb b/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb index 19b7821494..f63d09039f 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb @@ -1,10 +1,13 @@ module ActiveRecord module ConnectionAdapters class TransactionState - VALID_STATES = Set.new([:committed, :rolledback, nil]) - def initialize(state = nil) @state = state + @children = [] + end + + def add_child(state) + @children << state end def finalized? @@ -19,15 +22,43 @@ module ActiveRecord @state == :rolledback end + def fully_completed? + completed? + end + def completed? committed? || rolledback? end def set_state(state) - unless VALID_STATES.include?(state) + ActiveSupport::Deprecation.warn(<<-MSG.squish) + The set_state method is deprecated and will be removed in + Rails 6.0. Please use rollback! or commit! to set transaction + state directly. + MSG + case state + when :rolledback + rollback! + when :committed + commit! + when nil + nullify! + else raise ArgumentError, "Invalid transaction state: #{state}" end - @state = state + end + + def rollback! + @children.each { |c| c.rollback! } + @state = :rolledback + end + + def commit! + @state = :committed + end + + def nullify! + @state = nil end end @@ -57,7 +88,7 @@ module ActiveRecord end def rollback - @state.set_state(:rolledback) + @state.rollback! end def rollback_records @@ -72,7 +103,7 @@ module ActiveRecord end def commit - @state.set_state(:committed) + @state.commit! end def before_commit_records @@ -100,8 +131,11 @@ module ActiveRecord end class SavepointTransaction < Transaction - def initialize(connection, savepoint_name, options, *args) + def initialize(connection, savepoint_name, parent_transaction, options, *args) super(connection, options, *args) + + parent_transaction.state.add_child(@state) + if options[:isolation] raise ActiveRecord::TransactionIsolationError, "cannot set transaction isolation in a nested transaction" end @@ -155,7 +189,7 @@ module ActiveRecord if @stack.empty? RealTransaction.new(@connection, options, run_commit_callbacks: run_commit_callbacks) else - SavepointTransaction.new(@connection, "active_record_#{@stack.size}", options, + SavepointTransaction.new(@connection, "active_record_#{@stack.size}", @stack.last, options, run_commit_callbacks: run_commit_callbacks) end diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb index 85d6fbe8b3..30b29e7007 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb @@ -1,9 +1,9 @@ -require "active_record/type" -require "active_record/connection_adapters/determine_if_preparable_visitor" -require "active_record/connection_adapters/schema_cache" -require "active_record/connection_adapters/sql_type_metadata" -require "active_record/connection_adapters/abstract/schema_dumper" -require "active_record/connection_adapters/abstract/schema_creation" +require_relative "../type" +require_relative "determine_if_preparable_visitor" +require_relative "schema_cache" +require_relative "sql_type_metadata" +require_relative "abstract/schema_dumper" +require_relative "abstract/schema_creation" require "arel/collectors/bind" require "arel/collectors/sql_string" @@ -147,7 +147,7 @@ module ActiveRecord # this method must only be called while holding connection pool's mutex def lease if in_use? - msg = "Cannot lease connection, " + msg = "Cannot lease connection, ".dup if @owner == Thread.current msg << "it is already leased by the current thread." else @@ -576,12 +576,14 @@ module ActiveRecord type_casted_binds: type_casted_binds, statement_name: statement_name, connection_id: object_id) do + begin @lock.synchronize do yield end + rescue => e + raise translate_exception_class(e, sql) end - rescue => e - raise translate_exception_class(e, sql) + end end def translate_exception(exception, message) diff --git a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb index 648c869915..06976aa769 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb @@ -1,13 +1,13 @@ -require "active_record/connection_adapters/abstract_adapter" -require "active_record/connection_adapters/statement_pool" -require "active_record/connection_adapters/mysql/column" -require "active_record/connection_adapters/mysql/explain_pretty_printer" -require "active_record/connection_adapters/mysql/quoting" -require "active_record/connection_adapters/mysql/schema_creation" -require "active_record/connection_adapters/mysql/schema_definitions" -require "active_record/connection_adapters/mysql/schema_dumper" -require "active_record/connection_adapters/mysql/schema_statements" -require "active_record/connection_adapters/mysql/type_metadata" +require_relative "abstract_adapter" +require_relative "statement_pool" +require_relative "mysql/column" +require_relative "mysql/explain_pretty_printer" +require_relative "mysql/quoting" +require_relative "mysql/schema_creation" +require_relative "mysql/schema_definitions" +require_relative "mysql/schema_dumper" +require_relative "mysql/schema_statements" +require_relative "mysql/type_metadata" require "active_support/core_ext/string/strip" @@ -59,12 +59,12 @@ module ActiveRecord @statements = StatementPool.new(self.class.type_cast_config_to_integer(config[:statement_limit])) if version < "5.1.10" - raise "Your version of MySQL (#{full_version.match(/^\d+\.\d+\.\d+/)[0]}) is too old. Active Record supports MySQL >= 5.1.10." + raise "Your version of MySQL (#{version_string}) is too old. Active Record supports MySQL >= 5.1.10." end end def version #:nodoc: - @version ||= Version.new(full_version.match(/^\d+\.\d+\.\d+/)[0]) + @version ||= Version.new(version_string) end def mariadb? # :nodoc: @@ -120,11 +120,11 @@ module ActiveRecord end def get_advisory_lock(lock_name, timeout = 0) # :nodoc: - select_value("SELECT GET_LOCK(#{quote(lock_name)}, #{timeout})") == 1 + query_value("SELECT GET_LOCK(#{quote(lock_name)}, #{timeout})") == 1 end def release_advisory_lock(lock_name) # :nodoc: - select_value("SELECT RELEASE_LOCK(#{quote(lock_name)})") == 1 + query_value("SELECT RELEASE_LOCK(#{quote(lock_name)})") == 1 end def native_database_types @@ -132,7 +132,7 @@ module ActiveRecord end def index_algorithms - { default: "ALGORITHM = DEFAULT", copy: "ALGORITHM = COPY", inplace: "ALGORITHM = INPLACE" } + { default: "ALGORITHM = DEFAULT".dup, copy: "ALGORITHM = COPY".dup, inplace: "ALGORITHM = INPLACE".dup } end # HELPER METHODS =========================================== @@ -152,7 +152,7 @@ module ActiveRecord # REFERENTIAL INTEGRITY ==================================== def disable_referential_integrity #:nodoc: - old = select_value("SELECT @@FOREIGN_KEY_CHECKS") + old = query_value("SELECT @@FOREIGN_KEY_CHECKS") begin update("SET FOREIGN_KEY_CHECKS = 0") @@ -267,7 +267,7 @@ module ActiveRecord end def current_database - select_value "SELECT DATABASE() as db" + query_value("SELECT database()", "SCHEMA") end # Returns the database character set. @@ -287,7 +287,7 @@ module ActiveRecord def table_comment(table_name) # :nodoc: scope = quoted_scope(table_name) - select_value(<<-SQL.strip_heredoc, "SCHEMA") + query_value(<<-SQL.strip_heredoc, "SCHEMA") SELECT table_comment FROM information_schema.tables WHERE table_schema = #{scope[:schema]} @@ -379,7 +379,7 @@ module ActiveRecord def add_index(table_name, column_name, options = {}) #:nodoc: index_name, index_type, index_columns, _, index_algorithm, index_using, comment = add_index_options(table_name, column_name, options) - sql = "CREATE #{index_type} INDEX #{quote_column_name(index_name)} #{index_using} ON #{quote_table_name(table_name)} (#{index_columns}) #{index_algorithm}" + sql = "CREATE #{index_type} INDEX #{quote_column_name(index_name)} #{index_using} ON #{quote_table_name(table_name)} (#{index_columns}) #{index_algorithm}".dup execute add_sql_comment!(sql, comment) end @@ -393,7 +393,7 @@ module ActiveRecord scope = quoted_scope(table_name) - fk_info = select_all(<<-SQL.strip_heredoc, "SCHEMA") + fk_info = exec_query(<<-SQL.strip_heredoc, "SCHEMA") SELECT fk.referenced_table_name AS 'to_table', fk.referenced_column_name AS 'primary_key', fk.column_name AS 'column', @@ -464,13 +464,13 @@ module ActiveRecord super end - sql << " unsigned" if unsigned && type != :primary_key + sql = "#{sql} unsigned" if unsigned && type != :primary_key sql end # SHOW VARIABLES LIKE 'name' def show_variable(name) - select_value("SELECT @@#{name}", "SCHEMA") + query_value("SELECT @@#{name}", "SCHEMA") rescue ActiveRecord::StatementInvalid nil end @@ -480,7 +480,7 @@ module ActiveRecord scope = quoted_scope(table_name) - select_values(<<-SQL.strip_heredoc, "SCHEMA") + query_values(<<-SQL.strip_heredoc, "SCHEMA") SELECT column_name FROM information_schema.key_column_usage WHERE constraint_name = 'PRIMARY' @@ -526,8 +526,25 @@ module ActiveRecord index.using == :btree || super end + def insert_fixtures(*) + without_sql_mode("NO_AUTO_VALUE_ON_ZERO") { super } + end + private + def without_sql_mode(mode) + result = execute("SELECT @@SESSION.sql_mode") + current_mode = result.first[0] + return yield unless current_mode.include?(mode) + + sql_mode = "REPLACE(@@sql_mode, '#{mode}', '')" + execute("SET @@SESSION.sql_mode = #{sql_mode}") + yield + ensure + sql_mode = "CONCAT(@@sql_mode, ',#{mode}')" + execute("SET @@SESSION.sql_mode = #{sql_mode}") + end + def initialize_type_map(m) super @@ -677,7 +694,7 @@ module ActiveRecord auto_increment: column.auto_increment? } - current_type = select_one("SHOW COLUMNS FROM #{quote_table_name(table_name)} LIKE '#{column_name}'", "SCHEMA")["Type"] + current_type = exec_query("SHOW COLUMNS FROM #{quote_table_name(table_name)} LIKE #{quote(column_name)}", "SCHEMA").first["Type"] td = create_table_definition(table_name) cd = td.new_column_definition(new_column_name, current_type, options) schema_creation.accept(ChangeColumnDefinition.new(cd, column.name)) @@ -761,7 +778,7 @@ module ActiveRecord # http://dev.mysql.com/doc/refman/5.7/en/set-statement.html#id944430 # (trailing comma because variable_assignments will always have content) if @config[:encoding] - encoding = "NAMES #{@config[:encoding]}" + encoding = "NAMES #{@config[:encoding]}".dup encoding << " COLLATE #{@config[:collation]}" if @config[:collation] encoding << ", " end @@ -787,7 +804,7 @@ module ActiveRecord end def create_table_info(table_name) # :nodoc: - select_one("SHOW CREATE TABLE #{quote_table_name(table_name)}")["Create Table"] + exec_query("SHOW CREATE TABLE #{quote_table_name(table_name)}", "SCHEMA").first["Create Table"] end def arel_visitor @@ -837,7 +854,8 @@ module ActiveRecord end end - class MysqlJson < Type::Json # :nodoc: + def version_string + full_version.match(/^(?:5\.5\.5-)?(\d+\.\d+\.\d+)/)[1] end class MysqlString < Type::String # :nodoc: diff --git a/activerecord/lib/active_record/connection_adapters/mysql/database_statements.rb b/activerecord/lib/active_record/connection_adapters/mysql/database_statements.rb index 8c67a7a80b..bda482a00f 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql/database_statements.rb @@ -13,13 +13,8 @@ module ActiveRecord result end - # Returns an array of arrays containing the field values. - # Order is the same as that returned by +columns+. - def select_rows(arel, name = nil, binds = []) # :nodoc: - select_result(arel, name, binds) do |result| - @connection.next_result while @connection.more_results? - result.to_a - end + def query(sql, name = nil) # :nodoc: + execute(sql, name).to_a end # Executes the SQL statement in the context of this connection. @@ -58,16 +53,6 @@ module ActiveRecord @connection.last_id end - def select_result(arel, name, binds) - arel, binds = binds_from_relation(arel, binds) - sql = to_sql(arel, binds) - if without_prepared_statement?(binds) - execute_and_free(sql, name) { |result| yield result } - else - exec_stmt_and_free(sql, name, binds, cache_stmt: true) { |_, result| yield result } - end - end - def exec_stmt_and_free(sql, name, binds, cache_stmt: false) # make sure we carry over any changes to ActiveRecord::Base.default_timezone that have been # made since we established the connection diff --git a/activerecord/lib/active_record/connection_adapters/mysql/schema_creation.rb b/activerecord/lib/active_record/connection_adapters/mysql/schema_creation.rb index 083cd6340f..eea4984680 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql/schema_creation.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql/schema_creation.rb @@ -16,7 +16,7 @@ module ActiveRecord end def visit_ChangeColumnDefinition(o) - change_column_sql = "CHANGE #{quote_column_name(o.name)} #{accept(o.column)}" + change_column_sql = "CHANGE #{quote_column_name(o.name)} #{accept(o.column)}".dup add_column_position!(change_column_sql, column_options(o.column)) end @@ -63,7 +63,7 @@ module ActiveRecord def index_in_create(table_name, column_name, options) index_name, index_type, index_columns, _, _, index_using, comment = @conn.add_index_options(table_name, column_name, options) - add_sql_comment!("#{index_type} INDEX #{quote_column_name(index_name)} #{index_using} (#{index_columns})", comment) + add_sql_comment!("#{index_type} INDEX #{quote_column_name(index_name)} #{index_using} (#{index_columns})".dup, comment) end end end diff --git a/activerecord/lib/active_record/connection_adapters/mysql/schema_dumper.rb b/activerecord/lib/active_record/connection_adapters/mysql/schema_dumper.rb index e2ba0ba1a0..a46d9f8cbb 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql/schema_dumper.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql/schema_dumper.rb @@ -47,13 +47,13 @@ module ActiveRecord def schema_collation(column) if column.collation && table_name = column.table_name @table_collation_cache ||= {} - @table_collation_cache[table_name] ||= select_one("SHOW TABLE STATUS LIKE '#{table_name}'")["Collation"] + @table_collation_cache[table_name] ||= exec_query("SHOW TABLE STATUS LIKE #{quote(table_name)}", "SCHEMA").first["Collation"] column.collation.inspect if column.collation != @table_collation_cache[table_name] end end def extract_expression_for_virtual_column(column) - if mariadb? + if mariadb? && version < "10.2.5" create_table_info = create_table_info(column.table_name) if %r/#{quote_column_name(column.name)} #{Regexp.quote(column.sql_type)}(?: COLLATE \w+)? AS \((?<expression>.+?)\) #{column.extra}/ =~ create_table_info $~[:expression].inspect @@ -64,7 +64,7 @@ module ActiveRecord " WHERE table_schema = #{scope[:schema]}" \ " AND table_name = #{scope[:name]}" \ " AND column_name = #{quote(column.name)}" - select_value(sql, "SCHEMA").inspect + query_value(sql, "SCHEMA").inspect end end end diff --git a/activerecord/lib/active_record/connection_adapters/mysql/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/mysql/schema_statements.rb index f9e1e046ea..24f8ff6367 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql/schema_statements.rb @@ -45,6 +45,13 @@ module ActiveRecord indexes end + def remove_column(table_name, column_name, type = nil, options = {}) + if foreign_key_exists?(table_name, column: column_name) + remove_foreign_key(table_name, column: column_name) + end + super + end + def internal_string_options_for_primary_key super.tap do |options| if CHARSETS_OF_4BYTES_MAXLEN.include?(charset) && (mariadb? || version < "8.0.0") @@ -66,8 +73,8 @@ module ActiveRecord def new_column_from_field(table_name, field) type_metadata = fetch_type_metadata(field[:Type], field[:Extra]) - if type_metadata.type == :datetime && field[:Default] == "CURRENT_TIMESTAMP" - default, default_function = nil, field[:Default] + if type_metadata.type == :datetime && /\ACURRENT_TIMESTAMP(?:\(\))?\z/i.match?(field[:Default]) + default, default_function = nil, "CURRENT_TIMESTAMP" else default, default_function = field[:Default], nil end @@ -95,7 +102,7 @@ module ActiveRecord def data_source_sql(name = nil, type: nil) scope = quoted_scope(name, type: type) - sql = "SELECT table_name FROM information_schema.tables" + sql = "SELECT table_name FROM information_schema.tables".dup sql << " WHERE table_schema = #{scope[:schema]}" sql << " AND table_name = #{scope[:name]}" if scope[:name] sql << " AND table_type = #{scope[:type]}" if scope[:type] diff --git a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb index af55cfe2f6..c5c0a071e7 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb @@ -1,5 +1,5 @@ -require "active_record/connection_adapters/abstract_mysql_adapter" -require "active_record/connection_adapters/mysql/database_statements" +require_relative "abstract_mysql_adapter" +require_relative "mysql/database_statements" gem "mysql2", ">= 0.3.18", "< 0.5" require "mysql2" 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 705e6063dc..ebf1715ed0 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb @@ -7,30 +7,6 @@ module ActiveRecord PostgreSQL::ExplainPrettyPrinter.new.pp(exec_query(sql, "EXPLAIN", binds)) end - def select_value(arel, name = nil, binds = []) # :nodoc: - select_result(arel, name, binds) do |result| - result.getvalue(0, 0) if result.ntuples > 0 && result.nfields > 0 - end - end - - def select_values(arel, name = nil, binds = []) # :nodoc: - select_result(arel, name, binds) do |result| - if result.nfields > 0 - result.column_values(0) - else - [] - end - end - end - - # Executes a SELECT query and returns an array of rows. Each row is an - # array of field values. - def select_rows(arel, name = nil, binds = []) # :nodoc: - select_result(arel, name, binds) do |result| - result.values - end - end - # The internal PostgreSQL identifier of the money data type. MONEY_COLUMN_TYPE_OID = 790 #:nodoc: # The internal PostgreSQL identifier of the BYTEA data type. @@ -171,18 +147,14 @@ module ActiveRecord end private + # Returns the current ID of a table's sequence. + def last_insert_id_result(sequence_name) + exec_query("SELECT currval(#{quote(sequence_name)})", "SQL") + end def suppress_composite_primary_key(pk) pk unless pk.is_a?(Array) end - - def select_result(arel, name, binds) - arel, binds = binds_from_relation(arel, binds) - sql = to_sql(arel, binds) - execute_and_clear(sql, name, binds) do |result| - yield result - end - end end end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb index 4098250f3e..6666622c08 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb @@ -1,26 +1,25 @@ -require "active_record/connection_adapters/postgresql/oid/array" -require "active_record/connection_adapters/postgresql/oid/bit" -require "active_record/connection_adapters/postgresql/oid/bit_varying" -require "active_record/connection_adapters/postgresql/oid/bytea" -require "active_record/connection_adapters/postgresql/oid/cidr" -require "active_record/connection_adapters/postgresql/oid/date_time" -require "active_record/connection_adapters/postgresql/oid/decimal" -require "active_record/connection_adapters/postgresql/oid/enum" -require "active_record/connection_adapters/postgresql/oid/hstore" -require "active_record/connection_adapters/postgresql/oid/inet" -require "active_record/connection_adapters/postgresql/oid/json" -require "active_record/connection_adapters/postgresql/oid/jsonb" -require "active_record/connection_adapters/postgresql/oid/money" -require "active_record/connection_adapters/postgresql/oid/oid" -require "active_record/connection_adapters/postgresql/oid/point" -require "active_record/connection_adapters/postgresql/oid/legacy_point" -require "active_record/connection_adapters/postgresql/oid/range" -require "active_record/connection_adapters/postgresql/oid/specialized_string" -require "active_record/connection_adapters/postgresql/oid/uuid" -require "active_record/connection_adapters/postgresql/oid/vector" -require "active_record/connection_adapters/postgresql/oid/xml" +require_relative "oid/array" +require_relative "oid/bit" +require_relative "oid/bit_varying" +require_relative "oid/bytea" +require_relative "oid/cidr" +require_relative "oid/date_time" +require_relative "oid/decimal" +require_relative "oid/enum" +require_relative "oid/hstore" +require_relative "oid/inet" +require_relative "oid/jsonb" +require_relative "oid/money" +require_relative "oid/oid" +require_relative "oid/point" +require_relative "oid/legacy_point" +require_relative "oid/range" +require_relative "oid/specialized_string" +require_relative "oid/uuid" +require_relative "oid/vector" +require_relative "oid/xml" -require "active_record/connection_adapters/postgresql/oid/type_map_initializer" +require_relative "oid/type_map_initializer" module ActiveRecord module ConnectionAdapters diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/json.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/json.rb deleted file mode 100644 index 3c706c27c4..0000000000 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/json.rb +++ /dev/null @@ -1,10 +0,0 @@ -module ActiveRecord - module ConnectionAdapters - module PostgreSQL - module OID # :nodoc: - class Json < Type::Json # :nodoc: - end - end - end - end -end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/money.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/money.rb index 7a91272d1c..cd7442e751 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/money.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/money.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true module ActiveRecord module ConnectionAdapters module PostgreSQL @@ -22,7 +23,7 @@ module ActiveRecord # (3) -$2.55 # (4) ($2.55) - value.sub!(/^\((.+)\)$/, '-\1') # (4) + value = value.sub(/^\((.+)\)$/, '-\1') # (4) case value when /^-?\D+[\d,]+\.\d{2}$/ # (1) value.gsub!(/[^-\d.]/, "") diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/uuid.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/uuid.rb index 5e839228e9..db92333ef7 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/uuid.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/uuid.rb @@ -3,7 +3,7 @@ module ActiveRecord module PostgreSQL module OID # :nodoc: class Uuid < Type::Value # :nodoc: - ACCEPTABLE_UUID = %r{\A\{?([a-fA-F0-9]{4}-?){8}\}?\z}x + ACCEPTABLE_UUID = %r{\A(\{)?([a-fA-F0-9]{4}-?){8}(?(1)\}|)\z} alias_method :serialize, :deserialize diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb index 44eb666965..ee4230c6f2 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb @@ -78,7 +78,7 @@ module ActiveRecord private def lookup_cast_type(sql_type) - super(select_value("SELECT #{quote(sql_type)}::regtype::oid", "SCHEMA").to_i) + super(query_value("SELECT #{quote(sql_type)}::regtype::oid", "SCHEMA").to_i) end def _quote(value) diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb index cb45d7ba6b..a710ea6cc9 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb @@ -60,7 +60,7 @@ module ActiveRecord # Returns true if schema exists. def schema_exists?(name) - select_value("SELECT COUNT(*) FROM pg_namespace WHERE nspname = #{quote(name)}", "SCHEMA").to_i > 0 + query_value("SELECT COUNT(*) FROM pg_namespace WHERE nspname = #{quote(name)}", "SCHEMA").to_i > 0 end # Verifies existence of an index with a given name. @@ -73,7 +73,7 @@ module ActiveRecord table = quoted_scope(table_name) index = quoted_scope(index_name) - select_value(<<-SQL, "SCHEMA").to_i > 0 + query_value(<<-SQL, "SCHEMA").to_i > 0 SELECT COUNT(*) FROM pg_class t INNER JOIN pg_index d ON t.oid = d.indrelid @@ -163,7 +163,7 @@ module ActiveRecord def table_comment(table_name) # :nodoc: scope = quoted_scope(table_name, type: "BASE TABLE") if scope[:name] - select_value(<<-SQL.strip_heredoc, "SCHEMA") + query_value(<<-SQL.strip_heredoc, "SCHEMA") SELECT pg_catalog.obj_description(c.oid, 'pg_class') FROM pg_catalog.pg_class c LEFT JOIN pg_namespace n ON n.oid = c.relnamespace @@ -176,32 +176,32 @@ module ActiveRecord # Returns the current database name. def current_database - select_value("SELECT current_database()", "SCHEMA") + query_value("SELECT current_database()", "SCHEMA") end # Returns the current schema name. def current_schema - select_value("SELECT current_schema", "SCHEMA") + query_value("SELECT current_schema", "SCHEMA") end # Returns the current database encoding format. def encoding - select_value("SELECT pg_encoding_to_char(encoding) FROM pg_database WHERE datname = current_database()", "SCHEMA") + query_value("SELECT pg_encoding_to_char(encoding) FROM pg_database WHERE datname = current_database()", "SCHEMA") end # Returns the current database collation. def collation - select_value("SELECT datcollate FROM pg_database WHERE datname = current_database()", "SCHEMA") + query_value("SELECT datcollate FROM pg_database WHERE datname = current_database()", "SCHEMA") end # Returns the current database ctype. def ctype - select_value("SELECT datctype FROM pg_database WHERE datname = current_database()", "SCHEMA") + query_value("SELECT datctype FROM pg_database WHERE datname = current_database()", "SCHEMA") end # Returns an array of schema names. def schema_names - select_values(<<-SQL, "SCHEMA") + query_values(<<-SQL, "SCHEMA") SELECT nspname FROM pg_namespace WHERE nspname !~ '^pg_.*' @@ -234,12 +234,12 @@ module ActiveRecord # Returns the active schema search path. def schema_search_path - @schema_search_path ||= select_value("SHOW search_path", "SCHEMA") + @schema_search_path ||= query_value("SHOW search_path", "SCHEMA") end # Returns the current client message level. def client_min_messages - select_value("SHOW client_min_messages", "SCHEMA") + query_value("SHOW client_min_messages", "SCHEMA") end # Set the client message level. @@ -257,7 +257,7 @@ module ActiveRecord end def serial_sequence(table, column) - select_value("SELECT pg_get_serial_sequence('#{table}', '#{column}')", "SCHEMA") + query_value("SELECT pg_get_serial_sequence(#{quote(table)}, #{quote(column)})", "SCHEMA") end # Sets the sequence of a table's primary key to the specified value. @@ -268,7 +268,7 @@ module ActiveRecord if sequence quoted_sequence = quote_table_name(sequence) - select_value("SELECT setval('#{quoted_sequence}', #{value})", "SCHEMA") + query_value("SELECT setval(#{quote(quoted_sequence)}, #{value})", "SCHEMA") else @logger.warn "#{table} has primary key #{pk} with no default sequence." if @logger end @@ -290,18 +290,16 @@ module ActiveRecord if pk && sequence quoted_sequence = quote_table_name(sequence) - max_pk = select_value("select MAX(#{quote_column_name pk}) from #{quote_table_name(table)}") + max_pk = query_value("SELECT MAX(#{quote_column_name pk}) FROM #{quote_table_name(table)}", "SCHEMA") if max_pk.nil? if postgresql_version >= 100000 - minvalue = select_value("SELECT seqmin from pg_sequence where seqrelid = '#{quoted_sequence}'::regclass") + minvalue = query_value("SELECT seqmin FROM pg_sequence WHERE seqrelid = #{quote(quoted_sequence)}::regclass", "SCHEMA") else - minvalue = select_value("SELECT min_value FROM #{quoted_sequence}") + minvalue = query_value("SELECT min_value FROM #{quoted_sequence}", "SCHEMA") end end - select_value(<<-end_sql, "SCHEMA") - SELECT setval('#{quoted_sequence}', #{max_pk ? max_pk : minvalue}, #{max_pk ? true : false}) - end_sql + query_value("SELECT setval(#{quote(quoted_sequence)}, #{max_pk ? max_pk : minvalue}, #{max_pk ? true : false})", "SCHEMA") end end @@ -325,7 +323,7 @@ module ActiveRecord AND seq.relnamespace = nsp.oid AND cons.contype = 'p' AND dep.classid = 'pg_class'::regclass - AND dep.refobjid = '#{quote_table_name(table)}'::regclass + AND dep.refobjid = #{quote(quote_table_name(table))}::regclass end_sql if result.nil? || result.empty? @@ -343,7 +341,7 @@ module ActiveRecord JOIN pg_attrdef def ON (adrelid = attrelid AND adnum = attnum) JOIN pg_constraint cons ON (conrelid = adrelid AND adnum = conkey[1]) JOIN pg_namespace nsp ON (t.relnamespace = nsp.oid) - WHERE t.oid = '#{quote_table_name(table)}'::regclass + WHERE t.oid = #{quote(quote_table_name(table))}::regclass AND cons.contype = 'p' AND pg_get_expr(def.adbin, def.adrelid) ~* 'nextval|uuid_generate' end_sql @@ -360,7 +358,7 @@ module ActiveRecord end def primary_keys(table_name) # :nodoc: - select_values(<<-SQL.strip_heredoc, "SCHEMA") + query_values(<<-SQL.strip_heredoc, "SCHEMA") SELECT a.attname FROM ( SELECT indrelid, indkey, generate_subscripts(indkey, 1) idx @@ -408,7 +406,7 @@ module ActiveRecord quoted_table_name = quote_table_name(table_name) quoted_column_name = quote_column_name(column_name) sql_type = type_to_sql(type, options) - sql = "ALTER TABLE #{quoted_table_name} ALTER COLUMN #{quoted_column_name} TYPE #{sql_type}" + sql = "ALTER TABLE #{quoted_table_name} ALTER COLUMN #{quoted_column_name} TYPE #{sql_type}".dup if options[:collation] sql << " COLLATE \"#{options[:collation]}\"" end @@ -511,7 +509,7 @@ module ActiveRecord def foreign_keys(table_name) scope = quoted_scope(table_name) - fk_info = select_all(<<-SQL.strip_heredoc, "SCHEMA") + fk_info = exec_query(<<-SQL.strip_heredoc, "SCHEMA") SELECT t2.oid::regclass::text AS to_table, a1.attname AS column, a2.attname AS primary_key, c.conname AS name, c.confupdtype AS on_update, c.confdeltype AS on_delete FROM pg_constraint c JOIN pg_class t1 ON c.conrelid = t1.oid @@ -568,7 +566,7 @@ module ActiveRecord super end - sql << "[]" if array && type != :primary_key + sql = "#{sql}[]" if array && type != :primary_key sql end @@ -637,7 +635,7 @@ module ActiveRecord scope = quoted_scope(name, type: type) scope[:type] ||= "'r','v','m'" # (r)elation/table, (v)iew, (m)aterialized view - sql = "SELECT c.relname FROM pg_class c LEFT JOIN pg_namespace n ON n.oid = c.relnamespace" + sql = "SELECT c.relname FROM pg_class c LEFT JOIN pg_namespace n ON n.oid = c.relnamespace".dup sql << " WHERE n.nspname = #{scope[:schema]}" sql << " AND c.relname = #{scope[:name]}" if scope[:name] sql << " AND c.relkind IN (#{scope[:type]})" diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index 5287dd6a51..8baef19030 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -2,20 +2,20 @@ gem "pg", "~> 0.18" require "pg" -require "active_record/connection_adapters/abstract_adapter" -require "active_record/connection_adapters/statement_pool" -require "active_record/connection_adapters/postgresql/column" -require "active_record/connection_adapters/postgresql/database_statements" -require "active_record/connection_adapters/postgresql/explain_pretty_printer" -require "active_record/connection_adapters/postgresql/oid" -require "active_record/connection_adapters/postgresql/quoting" -require "active_record/connection_adapters/postgresql/referential_integrity" -require "active_record/connection_adapters/postgresql/schema_creation" -require "active_record/connection_adapters/postgresql/schema_definitions" -require "active_record/connection_adapters/postgresql/schema_dumper" -require "active_record/connection_adapters/postgresql/schema_statements" -require "active_record/connection_adapters/postgresql/type_metadata" -require "active_record/connection_adapters/postgresql/utils" +require_relative "abstract_adapter" +require_relative "statement_pool" +require_relative "postgresql/column" +require_relative "postgresql/database_statements" +require_relative "postgresql/explain_pretty_printer" +require_relative "postgresql/oid" +require_relative "postgresql/quoting" +require_relative "postgresql/referential_integrity" +require_relative "postgresql/schema_creation" +require_relative "postgresql/schema_definitions" +require_relative "postgresql/schema_dumper" +require_relative "postgresql/schema_statements" +require_relative "postgresql/type_metadata" +require_relative "postgresql/utils" module ActiveRecord module ConnectionHandling # :nodoc: @@ -312,14 +312,14 @@ module ActiveRecord unless lock_id.is_a?(Integer) && lock_id.bit_length <= 63 raise(ArgumentError, "Postgres requires advisory lock ids to be a signed 64 bit integer") end - select_value("SELECT pg_try_advisory_lock(#{lock_id});") + query_value("SELECT pg_try_advisory_lock(#{lock_id})") end def release_advisory_lock(lock_id) # :nodoc: unless lock_id.is_a?(Integer) && lock_id.bit_length <= 63 raise(ArgumentError, "Postgres requires advisory lock ids to be a signed 64 bit integer") end - select_value("SELECT pg_advisory_unlock(#{lock_id})") + query_value("SELECT pg_advisory_unlock(#{lock_id})") end def enable_extension(name) @@ -336,15 +336,14 @@ module ActiveRecord def extension_enabled?(name) if supports_extensions? - res = exec_query "SELECT EXISTS(SELECT * FROM pg_available_extensions WHERE name = '#{name}' AND installed_version IS NOT NULL) as enabled", - "SCHEMA" + res = exec_query("SELECT EXISTS(SELECT * FROM pg_available_extensions WHERE name = '#{name}' AND installed_version IS NOT NULL) as enabled", "SCHEMA") res.cast_values.first end end def extensions if supports_extensions? - exec_query("SELECT extname from pg_extension", "SCHEMA").cast_values + exec_query("SELECT extname FROM pg_extension", "SCHEMA").cast_values else super end @@ -352,14 +351,14 @@ module ActiveRecord # Returns the configured supported identifier length supported by PostgreSQL def table_alias_length - @max_identifier_length ||= select_value("SHOW max_identifier_length", "SCHEMA").to_i + @max_identifier_length ||= query_value("SHOW max_identifier_length", "SCHEMA").to_i end alias index_name_length table_alias_length # Set the authorized user for this session def session_auth=(user) clear_cache! - exec_query "SET SESSION AUTHORIZATION #{user}" + execute("SET SESSION AUTHORIZATION #{user}") end def use_insert_returning? @@ -723,11 +722,6 @@ module ActiveRecord end end - # Returns the current ID of a table's sequence. - def last_insert_id_result(sequence_name) - exec_query("SELECT currval('#{sequence_name}')", "SQL") - 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/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/sqlite3/schema_statements.rb index e02491edb6..31e83f9260 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3/schema_statements.rb @@ -11,7 +11,7 @@ module ActiveRecord end exec_query("PRAGMA index_list(#{quote_table_name(table_name)})", "SCHEMA").map do |row| - index_sql = select_value(<<-SQL, "SCHEMA") + index_sql = query_value(<<-SQL, "SCHEMA") SELECT sql FROM sqlite_master WHERE name = #{quote(row['name'])} AND type = 'index' @@ -67,7 +67,7 @@ module ActiveRecord scope = quoted_scope(name, type: type) scope[:type] ||= "'table','view'" - sql = "SELECT name FROM sqlite_master WHERE name <> 'sqlite_sequence'" + sql = "SELECT name FROM sqlite_master WHERE name <> 'sqlite_sequence'".dup sql << " AND name = #{scope[:name]}" if scope[:name] sql << " AND type IN (#{scope[:type]})" sql diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb index 7233325d5a..04129841e4 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb @@ -1,11 +1,11 @@ -require "active_record/connection_adapters/abstract_adapter" -require "active_record/connection_adapters/statement_pool" -require "active_record/connection_adapters/sqlite3/explain_pretty_printer" -require "active_record/connection_adapters/sqlite3/quoting" -require "active_record/connection_adapters/sqlite3/schema_creation" -require "active_record/connection_adapters/sqlite3/schema_definitions" -require "active_record/connection_adapters/sqlite3/schema_dumper" -require "active_record/connection_adapters/sqlite3/schema_statements" +require_relative "abstract_adapter" +require_relative "statement_pool" +require_relative "sqlite3/explain_pretty_printer" +require_relative "sqlite3/quoting" +require_relative "sqlite3/schema_creation" +require_relative "sqlite3/schema_definitions" +require_relative "sqlite3/schema_dumper" +require_relative "sqlite3/schema_statements" gem "sqlite3", "~> 1.3.6" require "sqlite3" @@ -169,7 +169,7 @@ module ActiveRecord # REFERENTIAL INTEGRITY ==================================== def disable_referential_integrity # :nodoc: - old = select_value("PRAGMA foreign_keys") + old = query_value("PRAGMA foreign_keys") begin execute("PRAGMA foreign_keys = OFF") @@ -337,7 +337,7 @@ module ActiveRecord alias :add_belongs_to :add_reference def foreign_keys(table_name) - fk_info = select_all("PRAGMA foreign_key_list(#{quote(table_name)})", "SCHEMA") + fk_info = exec_query("PRAGMA foreign_key_list(#{quote(table_name)})", "SCHEMA") fk_info.map do |row| options = { column: row["from"], @@ -349,6 +349,12 @@ module ActiveRecord end end + def insert_fixtures(rows, table_name) + rows.each do |row| + insert_fixture(row, table_name) + end + end + private def table_structure(table_name) @@ -437,7 +443,7 @@ module ActiveRecord end def sqlite_version - @sqlite_version ||= SQLite3Adapter::Version.new(select_value("SELECT sqlite_version(*)")) + @sqlite_version ||= SQLite3Adapter::Version.new(query_value("SELECT sqlite_version(*)")) end def translate_exception(exception, message) diff --git a/activerecord/lib/active_record/core.rb b/activerecord/lib/active_record/core.rb index 8f78330d4a..198c712abc 100644 --- a/activerecord/lib/active_record/core.rb +++ b/activerecord/lib/active_record/core.rb @@ -56,8 +56,7 @@ module ActiveRecord # :singleton-method: # Determines whether to use Time.utc (using :utc) or Time.local (using :local) when pulling # dates and times from the database. This is set to :utc by default. - mattr_accessor :default_timezone, instance_writer: false - self.default_timezone = :utc + mattr_accessor :default_timezone, instance_writer: false, default: :utc ## # :singleton-method: @@ -67,16 +66,14 @@ module ActiveRecord # ActiveRecord::Schema file which can be loaded into any database that # supports migrations. Use :ruby if you want to have different database # adapters for, e.g., your development and test environments. - mattr_accessor :schema_format, instance_writer: false - self.schema_format = :ruby + mattr_accessor :schema_format, instance_writer: false, default: :ruby ## # :singleton-method: # Specifies if an error should be raised if the query has an order being # ignored when doing batch queries. Useful in applications where the # scope being ignored is error-worthy, rather than a warning. - mattr_accessor :error_on_ignored_order, instance_writer: false - self.error_on_ignored_order = false + mattr_accessor :error_on_ignored_order, instance_writer: false, default: false def self.error_on_ignored_order_or_limit ActiveSupport::Deprecation.warn(<<-MSG.squish) @@ -101,8 +98,7 @@ module ActiveRecord ## # :singleton-method: # Specify whether or not to use timestamps for migration versions - mattr_accessor :timestamped_migrations, instance_writer: false - self.timestamped_migrations = true + mattr_accessor :timestamped_migrations, instance_writer: false, default: true ## # :singleton-method: @@ -110,8 +106,7 @@ module ActiveRecord # db:migrate rake task. This is true by default, which is useful for the # development environment. This should ideally be false in the production # environment where dumping schema is rarely needed. - mattr_accessor :dump_schema_after_migration, instance_writer: false - self.dump_schema_after_migration = true + mattr_accessor :dump_schema_after_migration, instance_writer: false, default: true ## # :singleton-method: @@ -120,8 +115,7 @@ module ActiveRecord # schema_search_path are dumped. Use :all to dump all schemas regardless # of schema_search_path, or a string of comma separated schemas for a # custom list. - mattr_accessor :dump_schemas, instance_writer: false - self.dump_schemas = :schema_search_path + mattr_accessor :dump_schemas, instance_writer: false, default: :schema_search_path ## # :singleton-method: @@ -130,7 +124,6 @@ module ActiveRecord # be used to identify queries which load thousands of records and # potentially cause memory bloat. mattr_accessor :warn_on_records_fetched_greater_than, instance_writer: false - self.warn_on_records_fetched_greater_than = nil mattr_accessor :maintain_test_schema, instance_accessor: false diff --git a/activerecord/lib/active_record/errors.rb b/activerecord/lib/active_record/errors.rb index 18fac5af1b..60d4fb70e0 100644 --- a/activerecord/lib/active_record/errors.rb +++ b/activerecord/lib/active_record/errors.rb @@ -105,7 +105,7 @@ module ActiveRecord class WrappedDatabaseException < StatementInvalid end - # Raised when a record cannot be inserted because it would violate a uniqueness constraint. + # Raised when a record cannot be inserted or updated because it would violate a uniqueness constraint. class RecordNotUnique < WrappedDatabaseException end diff --git a/activerecord/lib/active_record/explain.rb b/activerecord/lib/active_record/explain.rb index 8f7ae2c33c..eff5990f3a 100644 --- a/activerecord/lib/active_record/explain.rb +++ b/activerecord/lib/active_record/explain.rb @@ -1,4 +1,4 @@ -require "active_record/explain_registry" +require_relative "explain_registry" module ActiveRecord module Explain @@ -16,7 +16,7 @@ module ActiveRecord # Returns a formatted string ready to be logged. def exec_explain(queries) # :nodoc: str = queries.map do |sql, binds| - msg = "EXPLAIN for: #{sql}" + msg = "EXPLAIN for: #{sql}".dup unless binds.empty? msg << " " msg << binds.map { |attr| render_bind(attr) }.inspect diff --git a/activerecord/lib/active_record/explain_subscriber.rb b/activerecord/lib/active_record/explain_subscriber.rb index abd8cfc8f2..928720d011 100644 --- a/activerecord/lib/active_record/explain_subscriber.rb +++ b/activerecord/lib/active_record/explain_subscriber.rb @@ -1,5 +1,5 @@ require "active_support/notifications" -require "active_record/explain_registry" +require_relative "explain_registry" module ActiveRecord class ExplainSubscriber # :nodoc: diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb index bad6542be2..c9e97d9d2b 100644 --- a/activerecord/lib/active_record/fixtures.rb +++ b/activerecord/lib/active_record/fixtures.rb @@ -4,8 +4,8 @@ require "zlib" require "set" require "active_support/dependencies" require "active_support/core_ext/digest/uuid" -require "active_record/fixture_set/file" -require "active_record/errors" +require_relative "fixture_set/file" +require_relative "errors" module ActiveRecord class FixtureClassNotFound < ActiveRecord::ActiveRecordError #:nodoc: @@ -492,8 +492,7 @@ module ActiveRecord end end - cattr_accessor :all_loaded_fixtures - self.all_loaded_fixtures = {} + cattr_accessor :all_loaded_fixtures, default: {} class ClassCache def initialize(class_names, config) @@ -568,9 +567,7 @@ module ActiveRecord end table_rows.each do |fixture_set_name, rows| - rows.each do |row| - conn.insert_fixture(row, fixture_set_name) - end + conn.insert_fixtures(rows, fixture_set_name) end # Cap primary key sequences to max(pk). diff --git a/activerecord/lib/active_record/inheritance.rb b/activerecord/lib/active_record/inheritance.rb index 5776807507..1753322274 100644 --- a/activerecord/lib/active_record/inheritance.rb +++ b/activerecord/lib/active_record/inheritance.rb @@ -245,7 +245,7 @@ module ActiveRecord def ensure_proper_type klass = self.class if klass.finder_needs_type_condition? - write_attribute(klass.inheritance_column, klass.sti_name) + _write_attribute(klass.inheritance_column, klass.sti_name) end end end diff --git a/activerecord/lib/active_record/internal_metadata.rb b/activerecord/lib/active_record/internal_metadata.rb index 25ee9d6bfe..89e5d153b8 100644 --- a/activerecord/lib/active_record/internal_metadata.rb +++ b/activerecord/lib/active_record/internal_metadata.rb @@ -1,5 +1,5 @@ -require "active_record/scoping/default" -require "active_record/scoping/named" +require_relative "scoping/default" +require_relative "scoping/named" module ActiveRecord # This class is used to create a table that keeps track of values and keys such diff --git a/activerecord/lib/active_record/locking/optimistic.rb b/activerecord/lib/active_record/locking/optimistic.rb index 3c7110369b..522da6a571 100644 --- a/activerecord/lib/active_record/locking/optimistic.rb +++ b/activerecord/lib/active_record/locking/optimistic.rb @@ -62,8 +62,8 @@ module ActiveRecord def increment_lock lock_col = self.class.locking_column - previous_lock_value = send(lock_col).to_i - send(lock_col + "=", previous_lock_value + 1) + previous_lock_value = send(lock_col) + send("#{lock_col}=", previous_lock_value + 1) end def _create_record(attribute_names = self.attribute_names, *) @@ -107,7 +107,8 @@ module ActiveRecord # If something went wrong, revert the locking_column value. rescue Exception - send(lock_col + "=", previous_lock_value.to_i) + send("#{lock_col}=", previous_lock_value.to_i) + raise end end @@ -127,7 +128,7 @@ module ActiveRecord if locking_enabled? locking_column = self.class.locking_column - relation = relation.where(locking_column => _read_attribute(locking_column)) + relation = relation.where(locking_column => read_attribute_before_type_cast(locking_column)) end relation diff --git a/activerecord/lib/active_record/log_subscriber.rb b/activerecord/lib/active_record/log_subscriber.rb index 2297c77835..e39ca5f6dc 100644 --- a/activerecord/lib/active_record/log_subscriber.rb +++ b/activerecord/lib/active_record/log_subscriber.rb @@ -29,7 +29,7 @@ module ActiveRecord binds = nil unless (payload[:binds] || []).empty? - casted_params = type_casted_binds(payload[:binds], payload[:type_casted_binds]) + casted_params = type_casted_binds(payload[:type_casted_binds]) binds = " " + payload[:binds].zip(casted_params).map { |attr, value| render_bind(attr, value) }.inspect @@ -42,9 +42,8 @@ module ActiveRecord end private - - def type_casted_binds(binds, casted_binds) - casted_binds || ActiveRecord::Base.connection.type_casted_binds(binds) + def type_casted_binds(casted_binds) + casted_binds.respond_to?(:call) ? casted_binds.call : casted_binds end def render_bind(attr, value) diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb index 51c82f4ced..42220b9a5e 100644 --- a/activerecord/lib/active_record/migration.rb +++ b/activerecord/lib/active_record/migration.rb @@ -157,7 +157,7 @@ module ActiveRecord class ProtectedEnvironmentError < ActiveRecordError #:nodoc: def initialize(env = "production") - msg = "You are attempting to run a destructive action against your '#{env}' database.\n" + msg = "You are attempting to run a destructive action against your '#{env}' database.\n".dup msg << "If you are sure you want to continue, run the same command with the environment variable:\n" msg << "DISABLE_DATABASE_ENVIRONMENT_CHECK=1" super(msg) @@ -166,7 +166,7 @@ module ActiveRecord class EnvironmentMismatchError < ActiveRecordError def initialize(current: nil, stored: nil) - msg = "You are attempting to modify a database that was last run in `#{ stored }` environment.\n" + msg = "You are attempting to modify a database that was last run in `#{ stored }` environment.\n".dup msg << "You are running in `#{ current }` environment. " msg << "If you are sure you want to continue, first set the environment using:\n\n" msg << " bin/rails db:environment:set" @@ -863,15 +863,17 @@ module ActiveRecord source_migrations.each do |migration| source = File.binread(migration.filename) inserted_comment = "# This migration comes from #{scope} (originally #{migration.version})\n" - if /\A#.*\b(?:en)?coding:\s*\S+/ =~ source + magic_comments = "".dup + loop do # If we have a magic comment in the original migration, # insert our comment after the first newline(end of the magic comment line) # so the magic keep working. # Note that magic comments must be at the first line(except sh-bang). - source[/\n/] = "\n#{inserted_comment}" - else - source = "#{inserted_comment}#{source}" + source.sub!(/\A(?:#.*\b(?:en)?coding:\s*\S+|#\s*frozen_string_literal:\s*(?:true|false)).*\n/) do |magic_comment| + magic_comments << magic_comment; "" + end || break end + source = "#{magic_comments}#{inserted_comment}#{source}" if duplicate = destination_migrations.detect { |m| m.name == migration.name } if options[:on_skip] && duplicate.scope != scope.to_s @@ -1239,7 +1241,7 @@ module ActiveRecord record_version_state_after_migrating(migration.version) end rescue => e - msg = "An error has occurred, " + msg = "An error has occurred, ".dup msg << "this and " if use_transaction?(migration) msg << "all later migrations canceled:\n\n#{e}" raise StandardError, msg, e.backtrace diff --git a/activerecord/lib/active_record/model_schema.rb b/activerecord/lib/active_record/model_schema.rb index 1179a60e9b..14e0f5bff7 100644 --- a/activerecord/lib/active_record/model_schema.rb +++ b/activerecord/lib/active_record/model_schema.rb @@ -446,7 +446,11 @@ module ActiveRecord def load_schema return if schema_loaded? @load_schema_monitor.synchronize do - load_schema! unless defined?(@columns_hash) && @columns_hash + return if defined?(@columns_hash) && @columns_hash + + load_schema! + + @schema_loaded = true end end @@ -460,8 +464,6 @@ module ActiveRecord user_provided_default: false ) end - - @schema_loaded = true end def reload_schema_from_cache diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb index f652c7c3a1..a9509e562a 100644 --- a/activerecord/lib/active_record/persistence.rb +++ b/activerecord/lib/active_record/persistence.rb @@ -333,7 +333,7 @@ module ActiveRecord updated_count = self.class.unscoped.where(self.class.primary_key => id).update_all(attributes) attributes.each do |k, v| - raw_write_attribute(k, v) + write_attribute_without_type_cast(k, v) end updated_count == 1 @@ -526,7 +526,7 @@ module ActiveRecord if locking_enabled? locking_column = self.class.locking_column - scope = scope.where(locking_column => _read_attribute(locking_column)) + scope = scope.where(locking_column => read_attribute_before_type_cast(locking_column)) changes[locking_column] = increment_lock end diff --git a/activerecord/lib/active_record/query_cache.rb b/activerecord/lib/active_record/query_cache.rb index ec246e97bc..e4c2e1f86f 100644 --- a/activerecord/lib/active_record/query_cache.rb +++ b/activerecord/lib/active_record/query_cache.rb @@ -5,20 +5,20 @@ module ActiveRecord # Enable the query cache within the block if Active Record is configured. # If it's not, it will execute the given block. def cache(&block) - if connected? - connection.cache(&block) - else + if configurations.empty? yield + else + connection.cache(&block) end end # Disable the query cache within the block if Active Record is configured. # If it's not, it will execute the given block. def uncached(&block) - if connected? - connection.uncached(&block) - else + if configurations.empty? yield + else + connection.uncached(&block) end end end diff --git a/activerecord/lib/active_record/railtie.rb b/activerecord/lib/active_record/railtie.rb index 73518ca144..9cca103a18 100644 --- a/activerecord/lib/active_record/railtie.rb +++ b/activerecord/lib/active_record/railtie.rb @@ -48,8 +48,8 @@ module ActiveRecord # to avoid cross references when loading a constant for the # first time. Also, make it output to STDERR. console do |app| - require "active_record/railties/console_sandbox" if app.sandbox? - require "active_record/base" + require_relative "railties/console_sandbox" if app.sandbox? + require_relative "base" unless ActiveSupport::Logger.logger_outputs_to?(Rails.logger, STDERR, STDOUT) console = ActiveSupport::Logger.new(STDERR) Rails.logger.extend ActiveSupport::Logger.broadcast console @@ -57,7 +57,7 @@ module ActiveRecord end runner do - require "active_record/base" + require_relative "base" end initializer "active_record.initialize_timezone" do @@ -101,7 +101,7 @@ module ActiveRecord initializer "active_record.warn_on_records_fetched_greater_than" do if config.active_record.warn_on_records_fetched_greater_than ActiveSupport.on_load(:active_record) do - require "active_record/relation/record_fetch_warning" + require_relative "relation/record_fetch_warning" end end end @@ -139,7 +139,7 @@ end_warning # Expose database runtime to controller for logging. initializer "active_record.log_runtime" do - require "active_record/railties/controller_runtime" + require_relative "railties/controller_runtime" ActiveSupport.on_load(:action_controller) do include ActiveRecord::Railties::ControllerRuntime end diff --git a/activerecord/lib/active_record/railties/controller_runtime.rb b/activerecord/lib/active_record/railties/controller_runtime.rb index 8658188623..4030cdc158 100644 --- a/activerecord/lib/active_record/railties/controller_runtime.rb +++ b/activerecord/lib/active_record/railties/controller_runtime.rb @@ -1,5 +1,5 @@ require "active_support/core_ext/module/attr_internal" -require "active_record/log_subscriber" +require_relative "../log_subscriber" module ActiveRecord module Railties # :nodoc: diff --git a/activerecord/lib/active_record/railties/databases.rake b/activerecord/lib/active_record/railties/databases.rake index 711099e9e1..abc7323341 100644 --- a/activerecord/lib/active_record/railties/databases.rake +++ b/activerecord/lib/active_record/railties/databases.rake @@ -187,7 +187,7 @@ db_namespace = namespace :db do namespace :fixtures do desc "Loads fixtures into the current environment's database. Load specific fixtures using FIXTURES=x,y. Load from subdirectory in test/fixtures using FIXTURES_DIR=z. Specify an alternative path (eg. spec/fixtures) using FIXTURES_PATH=spec/fixtures." task load: [:environment, :load_config] do - require "active_record/fixtures" + require_relative "../fixtures" base_dir = ActiveRecord::Tasks::DatabaseTasks.fixtures_path @@ -209,7 +209,7 @@ db_namespace = namespace :db do # desc "Search for a fixture given a LABEL or ID. Specify an alternative path (eg. spec/fixtures) using FIXTURES_PATH=spec/fixtures." task identify: [:environment, :load_config] do - require "active_record/fixtures" + require_relative "../fixtures" label, id = ENV["LABEL"], ENV["ID"] raise "LABEL or ID required" if label.blank? && id.blank? @@ -235,7 +235,7 @@ db_namespace = namespace :db do namespace :schema do desc "Creates a db/schema.rb file that is portable against any DB supported by Active Record" task dump: [:environment, :load_config] do - require "active_record/schema_dumper" + require_relative "../schema_dumper" filename = ENV["SCHEMA"] || File.join(ActiveRecord::Tasks::DatabaseTasks.db_dir, "schema.rb") File.open(filename, "w:utf-8") do |file| ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, file) diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb index e8ee8279fd..a453ca55c7 100644 --- a/activerecord/lib/active_record/reflection.rb +++ b/activerecord/lib/active_record/reflection.rb @@ -171,7 +171,7 @@ module ActiveRecord JoinKeys = Struct.new(:key, :foreign_key) # :nodoc: def join_keys - get_join_keys klass + @join_keys ||= get_join_keys(klass) end # Returns a list of scopes that should be applied for this Reflection @@ -185,10 +185,34 @@ module ActiveRecord end deprecate :scope_chain + def build_join_constraint(table, foreign_table) + key = join_keys.key + foreign_key = join_keys.foreign_key + + constraint = table[key].eq(foreign_table[foreign_key]) + + if klass.finder_needs_type_condition? + table.create_and([constraint, klass.send(:type_condition, table)]) + else + constraint + end + end + + def join_scope(table, foreign_klass) + predicate_builder = predicate_builder(table) + scope_chain_items = join_scopes(table, predicate_builder) + klass_scope = klass_join_scope(table, predicate_builder) + + if type + klass_scope.where!(type => foreign_klass.base_class.sti_name) + end + + scope_chain_items.inject(klass_scope, &:merge!) + end + def join_scopes(table, predicate_builder) # :nodoc: if scope - [ActiveRecord::Relation.create(klass, table, predicate_builder) - .instance_exec(&scope)] + [build_scope(table, predicate_builder).instance_exec(&scope)] else [] end @@ -200,12 +224,7 @@ module ActiveRecord scope.joins_values = scope.left_outer_joins_values = [].freeze } else - relation = ActiveRecord::Relation.create( - klass, - table, - predicate_builder, - ) - klass.send(:build_default_scope, relation) + klass.default_scoped(build_scope(table, predicate_builder)) end end @@ -287,12 +306,19 @@ module ActiveRecord JoinKeys.new(join_pk(association_klass), join_fk) end + def build_scope(table, predicate_builder = predicate_builder(table)) + Relation.create(klass, table, predicate_builder) + end + protected def actual_source_reflection # FIXME: this is a horrible name self end private + def predicate_builder(table) + PredicateBuilder.new(TableMetadata.new(klass, table)) + end def join_pk(_) foreign_key @@ -570,7 +596,7 @@ module ActiveRecord end VALID_AUTOMATIC_INVERSE_MACROS = [:has_many, :has_one, :belongs_to] - INVALID_AUTOMATIC_INVERSE_OPTIONS = [:conditions, :through, :polymorphic, :foreign_key] + INVALID_AUTOMATIC_INVERSE_OPTIONS = [:conditions, :through, :foreign_key] def add_as_source(seed) seed @@ -641,9 +667,8 @@ module ActiveRecord # us from being able to guess the inverse automatically. First, the # <tt>inverse_of</tt> option cannot be set to false. Second, we must # have <tt>has_many</tt>, <tt>has_one</tt>, <tt>belongs_to</tt> associations. - # Third, we must not have options such as <tt>:polymorphic</tt> or - # <tt>:foreign_key</tt> which prevent us from correctly guessing the - # inverse association. + # Third, we must not have options such as <tt>:foreign_key</tt> + # which prevent us from correctly guessing the inverse association. # # Anything with a scope can additionally ruin our attempt at finding an # inverse, so we exclude reflections with scopes. @@ -746,10 +771,6 @@ module ActiveRecord end class HasAndBelongsToManyReflection < AssociationReflection # :nodoc: - def initialize(name, scope, options, active_record) - super - end - def macro; :has_and_belongs_to_many; end def collection? diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb index 7a8f9abb36..76cf47a3ed 100644 --- a/activerecord/lib/active_record/relation.rb +++ b/activerecord/lib/active_record/relation.rb @@ -6,7 +6,7 @@ module ActiveRecord :extending, :unscope] SINGLE_VALUE_METHODS = [:limit, :offset, :lock, :readonly, :reordering, - :reverse_order, :distinct, :create_with] + :reverse_order, :distinct, :create_with, :skip_query_cache] CLAUSE_METHODS = [:where, :having, :from] INVALID_METHODS_FOR_DELETE_ALL = [:limit, :distinct, :offset, :group, :having] @@ -18,7 +18,7 @@ module ActiveRecord attr_reader :table, :klass, :loaded, :predicate_builder alias :model :klass alias :loaded? :loaded - alias :locked? :locked + alias :locked? :lock_value def initialize(klass, table, predicate_builder, values = {}) @klass = klass @@ -333,7 +333,7 @@ module ActiveRecord # Please check unscoped if you want to remove all previous scopes (including # the default_scope) during the execution of a block. def scoping - previous, klass.current_scope = klass.current_scope, self + previous, klass.current_scope = klass.current_scope(true), self yield ensure klass.current_scope = previous @@ -657,20 +657,32 @@ module ActiveRecord end def exec_queries(&block) - @records = eager_loading? ? find_with_associations.freeze : @klass.find_by_sql(arel, bound_attributes, &block).freeze - - preload = preload_values - preload += includes_values unless eager_loading? - preloader = nil - preload.each do |associations| - preloader ||= build_preloader - preloader.preload @records, associations - end + skip_query_cache_if_necessary do + @records = eager_loading? ? find_with_associations.freeze : @klass.find_by_sql(arel, bound_attributes, &block).freeze + + preload = preload_values + preload += includes_values unless eager_loading? + preloader = nil + preload.each do |associations| + preloader ||= build_preloader + preloader.preload @records, associations + end - @records.each(&:readonly!) if readonly_value + @records.each(&:readonly!) if readonly_value - @loaded = true - @records + @loaded = true + @records + end + end + + def skip_query_cache_if_necessary + if skip_query_cache_value + uncached do + yield + end + else + yield + end end def build_preloader diff --git a/activerecord/lib/active_record/relation/batches.rb b/activerecord/lib/active_record/relation/batches.rb index 13a2c3f511..c7e4f8a88a 100644 --- a/activerecord/lib/active_record/relation/batches.rb +++ b/activerecord/lib/active_record/relation/batches.rb @@ -1,4 +1,4 @@ -require "active_record/relation/batches/batch_enumerator" +require_relative "batches/batch_enumerator" module ActiveRecord module Batches @@ -209,6 +209,7 @@ module ActiveRecord relation = relation.reorder(batch_order).limit(batch_limit) relation = apply_limits(relation, start, finish) + relation.skip_query_cache! # Retaining the results in the query cache would undermine the point of batching batch_relation = relation loop do diff --git a/activerecord/lib/active_record/relation/calculations.rb b/activerecord/lib/active_record/relation/calculations.rb index c562f214c9..aaba6c71f2 100644 --- a/activerecord/lib/active_record/relation/calculations.rb +++ b/activerecord/lib/active_record/relation/calculations.rb @@ -41,7 +41,7 @@ module ActiveRecord unless column_name.nil? ActiveSupport::Deprecation.warn \ "When `count' is called with a block, it ignores other arguments. " \ - "This behavior is now deprecated and will result in an ArgumentError in Rails 5.3." + "This behavior is now deprecated and will result in an ArgumentError in Rails 6.0." end return super() @@ -86,7 +86,7 @@ module ActiveRecord unless column_name.nil? ActiveSupport::Deprecation.warn \ "When `sum' is called with a block, it ignores other arguments. " \ - "This behavior is now deprecated and will result in an ArgumentError in Rails 5.3." + "This behavior is now deprecated and will result in an ArgumentError in Rails 6.0." end return super() @@ -184,7 +184,7 @@ module ActiveRecord relation.select_values = column_names.map { |cn| @klass.has_attribute?(cn) || @klass.attribute_alias?(cn) ? arel_attribute(cn) : cn } - result = klass.connection.select_all(relation.arel, nil, bound_attributes) + result = skip_query_cache_if_necessary { klass.connection.select_all(relation.arel, nil, bound_attributes) } result.cast_values(klass.attribute_types) end end @@ -260,7 +260,7 @@ module ActiveRecord query_builder = relation.arel end - result = @klass.connection.select_all(query_builder, nil, bound_attributes) + result = skip_query_cache_if_necessary { @klass.connection.select_all(query_builder, nil, bound_attributes) } row = result.first value = row && row.values.first type = result.column_types.fetch(column_alias) do @@ -311,7 +311,7 @@ module ActiveRecord relation.group_values = group_fields relation.select_values = select_values - calculated_data = @klass.connection.select_all(relation, nil, relation.bound_attributes) + calculated_data = skip_query_cache_if_necessary { @klass.connection.select_all(relation.arel, nil, relation.bound_attributes) } if association key_ids = calculated_data.collect { |row| row[group_aliases.first] } diff --git a/activerecord/lib/active_record/relation/delegation.rb b/activerecord/lib/active_record/relation/delegation.rb index dada0fddf8..48c4dcdef4 100644 --- a/activerecord/lib/active_record/relation/delegation.rb +++ b/activerecord/lib/active_record/relation/delegation.rb @@ -44,8 +44,6 @@ module ActiveRecord delegate :table_name, :quoted_table_name, :primary_key, :quoted_primary_key, :connection, :columns_hash, to: :klass - delegate :ast, :locked, to: :arel - module ClassSpecificRelation # :nodoc: extend ActiveSupport::Concern @@ -91,6 +89,8 @@ module ActiveRecord self.class.delegate_to_scoped_klass(method) scoping { @klass.public_send(method, *args, &block) } elsif arel.respond_to?(method) + ActiveSupport::Deprecation.warn \ + "Delegating #{method} to arel is deprecated and will be removed in Rails 6.0." self.class.delegate method, to: :arel arel.public_send(method, *args, &block) else @@ -115,15 +115,5 @@ module ActiveRecord def respond_to_missing?(method, _) super || @klass.respond_to?(method) || arel.respond_to?(method) end - - def method_missing(method, *args, &block) - if @klass.respond_to?(method) - scoping { @klass.public_send(method, *args, &block) } - elsif arel.respond_to?(method) - arel.public_send(method, *args, &block) - else - super - end - end end end diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb index 1d661fa8ed..ac0b4f597e 100644 --- a/activerecord/lib/active_record/relation/finder_methods.rb +++ b/activerecord/lib/active_record/relation/finder_methods.rb @@ -147,8 +147,7 @@ module ActiveRecord def last(limit = nil) return find_last(limit) if loaded? || limit_value - result = limit(limit) - result.order!(arel_attribute(primary_key)) if order_values.empty? && primary_key + result = ordered_relation.limit(limit) result = result.reverse_order! limit ? result.reverse : result.first @@ -316,7 +315,7 @@ module ActiveRecord relation = construct_relation_for_exists(relation, conditions) - connection.select_value(relation, "#{name} Exists", relation.bound_attributes) ? true : false + skip_query_cache_if_necessary { connection.select_value(relation.arel, "#{name} Exists", relation.bound_attributes) } ? true : false rescue ::RangeError false end @@ -335,14 +334,14 @@ module ActiveRecord name = @klass.name if ids.nil? - error = "Couldn't find #{name}" + error = "Couldn't find #{name}".dup error << " with#{conditions}" if conditions raise RecordNotFound.new(error, name) elsif Array(ids).size == 1 error = "Couldn't find #{name} with '#{key}'=#{ids}#{conditions}" raise RecordNotFound.new(error, name, key, ids) else - error = "Couldn't find all #{name.pluralize} with '#{key}': " + error = "Couldn't find all #{name.pluralize} with '#{key}': ".dup error << "(#{ids.join(", ")})#{conditions} (found #{result_size} results, but was looking for #{expected_size})" raise RecordNotFound.new(error, name, primary_key, ids) @@ -377,8 +376,7 @@ module ActiveRecord if ActiveRecord::NullRelation === relation [] else - arel = relation.arel - rows = connection.select_all(arel, "SQL", relation.bound_attributes) + rows = skip_query_cache_if_necessary { connection.select_all(relation.arel, "SQL", relation.bound_attributes) } join_dependency.instantiate(rows, aliases) end end @@ -425,9 +423,8 @@ module ActiveRecord "#{quoted_table_name}.#{quoted_primary_key}", relation.order_values) relation = relation.except(:select).select(values).distinct! - arel = relation.arel - id_rows = @klass.connection.select_all(arel, "SQL", relation.bound_attributes) + id_rows = skip_query_cache_if_necessary { @klass.connection.select_all(relation.arel, "SQL", relation.bound_attributes) } id_rows.map { |row| row[primary_key] } end @@ -535,11 +532,7 @@ module ActiveRecord if loaded? records[index, limit] || [] else - relation = if order_values.empty? && primary_key - order(arel_attribute(primary_key).asc) - else - self - end + relation = ordered_relation if limit_value.nil? || index < limit_value relation = relation.offset(offset_index + index) unless index.zero? @@ -554,11 +547,7 @@ module ActiveRecord if loaded? records[-index] else - relation = if order_values.empty? && primary_key - order(arel_attribute(primary_key).asc) - else - self - end + relation = ordered_relation relation.to_a[-index] # TODO: can be made more performant on large result sets by @@ -572,5 +561,13 @@ module ActiveRecord def find_last(limit) limit ? records.last(limit) : records.last end + + def ordered_relation + if order_values.empty? && primary_key + order(arel_attribute(primary_key).asc) + else + self + end + end end end diff --git a/activerecord/lib/active_record/relation/predicate_builder.rb b/activerecord/lib/active_record/relation/predicate_builder.rb index a6309e0b5c..eb80c9a00d 100644 --- a/activerecord/lib/active_record/relation/predicate_builder.rb +++ b/activerecord/lib/active_record/relation/predicate_builder.rb @@ -107,21 +107,26 @@ module ActiveRecord first = value.begin last = value.end unless first.respond_to?(:infinite?) && first.infinite? - binds << build_bind_param(column_name, first) + binds << build_bind_attribute(column_name, first) first = Arel::Nodes::BindParam.new end unless last.respond_to?(:infinite?) && last.infinite? - binds << build_bind_param(column_name, last) + binds << build_bind_attribute(column_name, last) last = Arel::Nodes::BindParam.new end result[column_name] = RangeHandler::RangeWithBinds.new(first, last, value.exclude_end?) + when value.is_a?(Relation) + binds.concat(value.bound_attributes) else if can_be_bound?(column_name, value) - result[column_name] = Arel::Nodes::BindParam.new - binds << build_bind_param(column_name, value) - elsif value.is_a?(Relation) - binds.concat(value.bound_attributes) + bind_attribute = build_bind_attribute(column_name, value) + if value.is_a?(StatementCache::Substitute) || !bind_attribute.value_for_database.nil? + result[column_name] = Arel::Nodes::BindParam.new + binds << bind_attribute + else + result[column_name] = nil + end end end end @@ -164,17 +169,17 @@ module ActiveRecord end end - def build_bind_param(column_name, value) + def build_bind_attribute(column_name, value) Relation::QueryAttribute.new(column_name.to_s, value, table.type(column_name)) end end end -require "active_record/relation/predicate_builder/array_handler" -require "active_record/relation/predicate_builder/base_handler" -require "active_record/relation/predicate_builder/basic_object_handler" -require "active_record/relation/predicate_builder/range_handler" -require "active_record/relation/predicate_builder/relation_handler" +require_relative "predicate_builder/array_handler" +require_relative "predicate_builder/base_handler" +require_relative "predicate_builder/basic_object_handler" +require_relative "predicate_builder/range_handler" +require_relative "predicate_builder/relation_handler" -require "active_record/relation/predicate_builder/association_query_value" -require "active_record/relation/predicate_builder/polymorphic_array_value" +require_relative "predicate_builder/association_query_value" +require_relative "predicate_builder/polymorphic_array_value" diff --git a/activerecord/lib/active_record/relation/query_attribute.rb b/activerecord/lib/active_record/relation/query_attribute.rb index a68e508fcc..0e1f64775d 100644 --- a/activerecord/lib/active_record/relation/query_attribute.rb +++ b/activerecord/lib/active_record/relation/query_attribute.rb @@ -1,4 +1,4 @@ -require "active_record/attribute" +require_relative "../attribute" module ActiveRecord class Relation diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb index 79e65baae5..79495ead91 100644 --- a/activerecord/lib/active_record/relation/query_methods.rb +++ b/activerecord/lib/active_record/relation/query_methods.rb @@ -1,7 +1,7 @@ -require "active_record/relation/from_clause" -require "active_record/relation/query_attribute" -require "active_record/relation/where_clause" -require "active_record/relation/where_clause_factory" +require_relative "from_clause" +require_relative "query_attribute" +require_relative "where_clause" +require_relative "where_clause_factory" require "active_model/forbidden_attributes_protection" module ActiveRecord @@ -913,6 +913,11 @@ module ActiveRecord self end + def skip_query_cache! # :nodoc: + self.skip_query_cache_value = true + self + end + # Returns the Arel object associated with the relation. def arel # :nodoc: @arel ||= build_arel @@ -1122,7 +1127,7 @@ module ActiveRecord validate_order_args(order_args) references = order_args.grep(String) - references.map! { |arg| arg =~ /^([a-zA-Z]\w*)\.(\w+)/ && $1 }.compact! + references.map! { |arg| arg =~ /^\W?(\w+)\W?\./ && $1 }.compact! references!(references) if references.any? # if a symbol is given we prepend the quoted table name @@ -1167,7 +1172,7 @@ module ActiveRecord end end - STRUCTURAL_OR_METHODS = Relation::VALUE_METHODS - [:extending, :where, :having] + STRUCTURAL_OR_METHODS = Relation::VALUE_METHODS - [:extending, :where, :having, :unscope] def structurally_incompatible_values_for_or(other) STRUCTURAL_OR_METHODS.reject do |method| get_value(method) == other.get_value(method) diff --git a/activerecord/lib/active_record/relation/spawn_methods.rb b/activerecord/lib/active_record/relation/spawn_methods.rb index ada89b5ec3..ddf7f825c1 100644 --- a/activerecord/lib/active_record/relation/spawn_methods.rb +++ b/activerecord/lib/active_record/relation/spawn_methods.rb @@ -1,6 +1,6 @@ require "active_support/core_ext/hash/except" require "active_support/core_ext/hash/slice" -require "active_record/relation/merger" +require_relative "merger" module ActiveRecord module SpawnMethods diff --git a/activerecord/lib/active_record/relation/where_clause_factory.rb b/activerecord/lib/active_record/relation/where_clause_factory.rb index 04bee73e8f..b862dd56a5 100644 --- a/activerecord/lib/active_record/relation/where_clause_factory.rb +++ b/activerecord/lib/active_record/relation/where_clause_factory.rb @@ -57,7 +57,7 @@ module ActiveRecord else column = klass.column_for_attribute(attribute) - binds << predicate_builder.send(:build_bind_param, attribute, value) + binds << predicate_builder.send(:build_bind_attribute, attribute, value) value = Arel::Nodes::BindParam.new predicate = if options[:case_sensitive] diff --git a/activerecord/lib/active_record/schema_dumper.rb b/activerecord/lib/active_record/schema_dumper.rb index 24b81aabc8..66a2846f3a 100644 --- a/activerecord/lib/active_record/schema_dumper.rb +++ b/activerecord/lib/active_record/schema_dumper.rb @@ -13,8 +13,7 @@ module ActiveRecord # A list of tables which should not be dumped to the schema. # Acceptable values are strings as well as regexp if ActiveRecord::Base.schema_format == :ruby. # Only strings are accepted if ActiveRecord::Base.schema_format == :sql. - cattr_accessor :ignore_tables - @@ignore_tables = [] + cattr_accessor :ignore_tables, default: [] class << self def dump(connection = ActiveRecord::Base.connection, stream = STDOUT, config = ActiveRecord::Base) diff --git a/activerecord/lib/active_record/schema_migration.rb b/activerecord/lib/active_record/schema_migration.rb index f59737afb0..6dbabd69a1 100644 --- a/activerecord/lib/active_record/schema_migration.rb +++ b/activerecord/lib/active_record/schema_migration.rb @@ -1,5 +1,5 @@ -require "active_record/scoping/default" -require "active_record/scoping/named" +require_relative "scoping/default" +require_relative "scoping/named" module ActiveRecord # This class is used to create a table that keeps track of which migrations diff --git a/activerecord/lib/active_record/scoping.rb b/activerecord/lib/active_record/scoping.rb index 7c00e7e4ed..94e0ef6724 100644 --- a/activerecord/lib/active_record/scoping.rb +++ b/activerecord/lib/active_record/scoping.rb @@ -10,8 +10,8 @@ module ActiveRecord end module ClassMethods - def current_scope #:nodoc: - ScopeRegistry.value_for(:current_scope, self) + def current_scope(skip_inherited_scope = false) # :nodoc: + ScopeRegistry.value_for(:current_scope, self, skip_inherited_scope) end def current_scope=(scope) #:nodoc: @@ -75,8 +75,9 @@ module ActiveRecord end # Obtains the value for a given +scope_type+ and +model+. - def value_for(scope_type, model) + def value_for(scope_type, model, skip_inherited_scope = false) raise_invalid_scope_type!(scope_type) + return @registry[scope_type][model.name] if skip_inherited_scope klass = model base = model.base_class while klass <= base diff --git a/activerecord/lib/active_record/scoping/named.rb b/activerecord/lib/active_record/scoping/named.rb index a61fdd6454..b4026fabb2 100644 --- a/activerecord/lib/active_record/scoping/named.rb +++ b/activerecord/lib/active_record/scoping/named.rb @@ -29,8 +29,7 @@ module ActiveRecord end end - def default_scoped # :nodoc: - scope = relation + def default_scoped(scope = relation) # :nodoc: build_default_scope(scope) || scope end @@ -160,7 +159,7 @@ module ActiveRecord if body.respond_to?(:to_proc) singleton_class.send(:define_method, name) do |*args| scope = all - scope = scope.scoping { instance_exec(*args, &body) || scope } + scope = scope.instance_exec(*args, &body) || scope scope = scope.extending(extension) if extension scope end diff --git a/activerecord/lib/active_record/tasks/mysql_database_tasks.rb b/activerecord/lib/active_record/tasks/mysql_database_tasks.rb index 541165b3d1..ff6745f7b5 100644 --- a/activerecord/lib/active_record/tasks/mysql_database_tasks.rb +++ b/activerecord/lib/active_record/tasks/mysql_database_tasks.rb @@ -59,7 +59,6 @@ module ActiveRecord args.concat(["--no-data"]) args.concat(["--routines"]) args.concat(["--skip-comments"]) - args.concat(Array(extra_flags)) if extra_flags ignore_tables = ActiveRecord::SchemaDumper.ignore_tables if ignore_tables.any? @@ -67,6 +66,7 @@ module ActiveRecord end args.concat(["#{configuration['database']}"]) + args.unshift(*extra_flags) if extra_flags run_cmd("mysqldump", args, "dumping") end @@ -75,7 +75,7 @@ module ActiveRecord args = prepare_command_options args.concat(["--execute", %{SET FOREIGN_KEY_CHECKS = 0; SOURCE #{filename}; SET FOREIGN_KEY_CHECKS = 1}]) args.concat(["--database", "#{configuration['database']}"]) - args.concat(Array(extra_flags)) if extra_flags + args.unshift(*extra_flags) if extra_flags run_cmd("mysql", args, "loading") end @@ -99,7 +99,7 @@ module ActiveRecord def error_class if configuration["adapter"].include?("jdbc") - require "active_record/railties/jdbcmysql_error" + require_relative "../railties/jdbcmysql_error" ArJdbcMySQL::Error elsif defined?(Mysql2) Mysql2::Error @@ -151,7 +151,7 @@ IDENTIFIED BY '#{configuration['password']}' WITH GRANT OPTION; end def run_cmd_error(cmd, args, action) - msg = "failed to execute: `#{cmd}`\n" + msg = "failed to execute: `#{cmd}`\n".dup msg << "Please check the output above for any errors and make sure that `#{cmd}` is installed in your PATH and has proper permissions.\n\n" msg end diff --git a/activerecord/lib/active_record/timestamp.rb b/activerecord/lib/active_record/timestamp.rb index 55f3a194a9..26eea0bc24 100644 --- a/activerecord/lib/active_record/timestamp.rb +++ b/activerecord/lib/active_record/timestamp.rb @@ -86,7 +86,7 @@ module ActiveRecord all_timestamp_attributes_in_model.each do |column| if !attribute_present?(column) - write_attribute(column, current_time) + _write_attribute(column, current_time) end end end @@ -100,7 +100,7 @@ module ActiveRecord timestamp_attributes_for_update_in_model.each do |column| next if will_save_change_to_attribute?(column) - write_attribute(column, current_time) + _write_attribute(column, current_time) end end super(*args) @@ -126,7 +126,7 @@ module ActiveRecord self.class.send(:current_time_from_proper_timezone) end - def max_updated_column_timestamp(timestamp_names = self.class.send(:timestamp_attributes_for_update)) + def max_updated_column_timestamp(timestamp_names = timestamp_attributes_for_update_in_model) timestamp_names .map { |attr| self[attr] } .compact diff --git a/activerecord/lib/active_record/transactions.rb b/activerecord/lib/active_record/transactions.rb index 45795fa287..463bb1f314 100644 --- a/activerecord/lib/active_record/transactions.rb +++ b/activerecord/lib/active_record/transactions.rb @@ -490,7 +490,7 @@ module ActiveRecord def update_attributes_from_transaction_state(transaction_state) if transaction_state && transaction_state.finalized? restore_transaction_record_state if transaction_state.rolledback? - clear_transaction_record_state + clear_transaction_record_state if transaction_state.fully_completed? end end end diff --git a/activerecord/lib/active_record/type.rb b/activerecord/lib/active_record/type.rb index 9ed6c95bf9..6f4e35b159 100644 --- a/activerecord/lib/active_record/type.rb +++ b/activerecord/lib/active_record/type.rb @@ -1,20 +1,20 @@ require "active_model/type" -require "active_record/type/internal/timezone" +require_relative "type/internal/timezone" -require "active_record/type/date" -require "active_record/type/date_time" -require "active_record/type/decimal_without_scale" -require "active_record/type/json" -require "active_record/type/time" -require "active_record/type/text" -require "active_record/type/unsigned_integer" +require_relative "type/date" +require_relative "type/date_time" +require_relative "type/decimal_without_scale" +require_relative "type/json" +require_relative "type/time" +require_relative "type/text" +require_relative "type/unsigned_integer" -require "active_record/type/serialized" -require "active_record/type/adapter_specific_registry" +require_relative "type/serialized" +require_relative "type/adapter_specific_registry" -require "active_record/type/type_map" -require "active_record/type/hash_lookup_type_map" +require_relative "type/type_map" +require_relative "type/hash_lookup_type_map" module ActiveRecord module Type diff --git a/activerecord/lib/active_record/type_caster.rb b/activerecord/lib/active_record/type_caster.rb index f1686e4913..8b12a30c6a 100644 --- a/activerecord/lib/active_record/type_caster.rb +++ b/activerecord/lib/active_record/type_caster.rb @@ -1,5 +1,5 @@ -require "active_record/type_caster/map" -require "active_record/type_caster/connection" +require_relative "type_caster/map" +require_relative "type_caster/connection" module ActiveRecord module TypeCaster # :nodoc: diff --git a/activerecord/lib/active_record/validations.rb b/activerecord/lib/active_record/validations.rb index 9633f226f0..046b69bee2 100644 --- a/activerecord/lib/active_record/validations.rb +++ b/activerecord/lib/active_record/validations.rb @@ -84,8 +84,8 @@ module ActiveRecord end end -require "active_record/validations/associated" -require "active_record/validations/uniqueness" -require "active_record/validations/presence" -require "active_record/validations/absence" -require "active_record/validations/length" +require_relative "validations/associated" +require_relative "validations/uniqueness" +require_relative "validations/presence" +require_relative "validations/absence" +require_relative "validations/length" diff --git a/activerecord/test/cases/adapter_test.rb b/activerecord/test/cases/adapter_test.rb index a1fb6427f9..827bcba121 100644 --- a/activerecord/test/cases/adapter_test.rb +++ b/activerecord/test/cases/adapter_test.rb @@ -211,6 +211,28 @@ module ActiveRecord end end + def test_exceptions_from_notifications_are_not_translated + original_error = StandardError.new("This StandardError shouldn't get translated") + subscriber = ActiveSupport::Notifications.subscribe("sql.active_record") { raise original_error } + actual_error = assert_raises(StandardError) do + @connection.execute("SELECT * FROM posts") + end + + assert_equal original_error, actual_error + + ensure + ActiveSupport::Notifications.unsubscribe(subscriber) if subscriber + end + + def test_database_related_exceptions_are_translated_to_statement_invalid + error = assert_raises(ActiveRecord::StatementInvalid) do + @connection.execute("This is a syntax error") + end + + assert_instance_of ActiveRecord::StatementInvalid, error + assert_kind_of Exception, error.cause + end + def test_select_all_always_return_activerecord_result result = @connection.select_all "SELECT * FROM posts" assert result.is_a?(ActiveRecord::Result) diff --git a/activerecord/test/cases/adapters/mysql2/reserved_word_test.rb b/activerecord/test/cases/adapters/mysql2/reserved_word_test.rb deleted file mode 100644 index 2c778b1150..0000000000 --- a/activerecord/test/cases/adapters/mysql2/reserved_word_test.rb +++ /dev/null @@ -1,151 +0,0 @@ -require "cases/helper" - -# a suite of tests to ensure the ConnectionAdapters#MysqlAdapter can handle tables with -# reserved word names (ie: group, order, values, etc...) -class Mysql2ReservedWordTest < ActiveRecord::Mysql2TestCase - class Group < ActiveRecord::Base - Group.table_name = "group" - belongs_to :select - has_one :values - end - - class Select < ActiveRecord::Base - Select.table_name = "select" - has_many :groups - end - - class Values < ActiveRecord::Base - Values.table_name = "values" - end - - class Distinct < ActiveRecord::Base - Distinct.table_name = "distinct" - has_and_belongs_to_many :selects - has_many :values, through: :groups - end - - def setup - @connection = ActiveRecord::Base.connection - - # we call execute directly here (and do similar below) because ActiveRecord::Base#create_table() - # will fail with these table names if these test cases fail - - create_tables_directly "group" => "id int auto_increment primary key, `order` varchar(255), select_id int", - "select" => "id int auto_increment primary key", - "values" => "id int auto_increment primary key, group_id int", - "distinct" => "id int auto_increment primary key", - "distinct_select" => "distinct_id int, select_id int" - end - - teardown do - drop_tables_directly ["group", "select", "values", "distinct", "distinct_select", "order"] - end - - # create tables with reserved-word names and columns - def test_create_tables - assert_nothing_raised { - @connection.create_table :order do |t| - t.column :group, :string - end - } - end - - # rename tables with reserved-word names - def test_rename_tables - assert_nothing_raised { @connection.rename_table(:group, :order) } - end - - # alter column with a reserved-word name in a table with a reserved-word name - def test_change_columns - assert_nothing_raised { @connection.change_column_default(:group, :order, "whatever") } - #the quoting here will reveal any double quoting issues in change_column's interaction with the column method in the adapter - assert_nothing_raised { @connection.change_column("group", "order", :Int, default: 0) } - assert_nothing_raised { @connection.rename_column(:group, :order, :values) } - end - - # introspect table with reserved word name - def test_introspect - assert_nothing_raised { @connection.columns(:group) } - assert_nothing_raised { @connection.indexes(:group) } - end - - #fixtures - self.use_instantiated_fixtures = true - self.use_transactional_tests = false - - #activerecord model class with reserved-word table name - def test_activerecord_model - create_test_fixtures :select, :distinct, :group, :values, :distinct_select - x = nil - assert_nothing_raised { x = Group.new } - x.order = "x" - assert_nothing_raised { x.save } - x.order = "y" - assert_nothing_raised { x.save } - assert_nothing_raised { Group.find_by_order("y") } - assert_nothing_raised { Group.find(1) } - end - - # has_one association with reserved-word table name - def test_has_one_associations - create_test_fixtures :select, :distinct, :group, :values, :distinct_select - v = nil - assert_nothing_raised { v = Group.find(1).values } - assert_equal 2, v.id - end - - # belongs_to association with reserved-word table name - def test_belongs_to_associations - create_test_fixtures :select, :distinct, :group, :values, :distinct_select - gs = nil - assert_nothing_raised { gs = Select.find(2).groups } - assert_equal gs.length, 2 - assert(gs.collect(&:id).sort == [2, 3]) - end - - # has_and_belongs_to_many with reserved-word table name - def test_has_and_belongs_to_many - create_test_fixtures :select, :distinct, :group, :values, :distinct_select - s = nil - assert_nothing_raised { s = Distinct.find(1).selects } - assert_equal s.length, 2 - assert(s.collect(&:id).sort == [1, 2]) - end - - # activerecord model introspection with reserved-word table and column names - def test_activerecord_introspection - assert_nothing_raised { Group.table_exists? } - assert_nothing_raised { Group.columns } - end - - # Calculations - def test_calculations_work_with_reserved_words - assert_nothing_raised { Group.count } - end - - def test_associations_work_with_reserved_words - assert_nothing_raised { Select.all.merge!(includes: [:groups]).to_a } - end - - #the following functions were added to DRY test cases - - private - # custom fixture loader, uses FixtureSet#create_fixtures and appends base_path to the current file's path - def create_test_fixtures(*fixture_names) - ActiveRecord::FixtureSet.create_fixtures(FIXTURES_ROOT + "/reserved_words", fixture_names) - end - - # custom drop table, uses execute on connection to drop a table if it exists. note: escapes table_name - def drop_tables_directly(table_names, connection = @connection) - table_names.each do |name| - connection.drop_table name, if_exists: true - end - end - - # custom create table, uses execute on connection to create a table, note: escapes table_name, does NOT escape columns - def create_tables_directly(tables, connection = @connection) - tables.each do |table_name, column_properties| - connection.execute("CREATE TABLE `#{table_name}` ( #{column_properties} )") - end - end -end diff --git a/activerecord/test/cases/adapters/mysql2/virtual_column_test.rb b/activerecord/test/cases/adapters/mysql2/virtual_column_test.rb index 442a4fb7b5..1c5ef2aa41 100644 --- a/activerecord/test/cases/adapters/mysql2/virtual_column_test.rb +++ b/activerecord/test/cases/adapters/mysql2/virtual_column_test.rb @@ -52,7 +52,7 @@ if ActiveRecord::Base.connection.supports_virtual_columns? def test_schema_dumping output = dump_table_schema("virtual_columns") - assert_match(/t\.virtual\s+"upper_name",\s+type: :string,\s+as: "UPPER\(`name`\)"$/i, output) + assert_match(/t\.virtual\s+"upper_name",\s+type: :string,\s+as: "(?:UPPER|UCASE)\(`name`\)"$/i, output) assert_match(/t\.virtual\s+"name_length",\s+type: :integer,\s+as: "LENGTH\(`name`\)",\s+stored: true$/i, output) end end diff --git a/activerecord/test/cases/adapters/postgresql/array_test.rb b/activerecord/test/cases/adapters/postgresql/array_test.rb index c78c6178ff..121c62dadf 100644 --- a/activerecord/test/cases/adapters/postgresql/array_test.rb +++ b/activerecord/test/cases/adapters/postgresql/array_test.rb @@ -191,6 +191,12 @@ class PostgresqlArrayTest < ActiveRecord::PostgreSQLTestCase assert_equal(PgArray.last.tags, tag_values) end + def test_insert_fixtures + tag_values = ["val1", "val2", "val3_with_'_multiple_quote_'_chars"] + @connection.insert_fixtures([{ "tags" => tag_values }], "pg_arrays") + assert_equal(PgArray.last.tags, tag_values) + end + def test_attribute_for_inspect_for_array_field record = PgArray.new { |a| a.ratings = (1..10).to_a } assert_equal("[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]", record.attribute_for_inspect(:ratings)) diff --git a/activerecord/test/cases/adapters/postgresql/bytea_test.rb b/activerecord/test/cases/adapters/postgresql/bytea_test.rb index 539c90f0bc..65baed34e9 100644 --- a/activerecord/test/cases/adapters/postgresql/bytea_test.rb +++ b/activerecord/test/cases/adapters/postgresql/bytea_test.rb @@ -47,7 +47,7 @@ class PostgresqlByteaTest < ActiveRecord::PostgreSQLTestCase end def test_type_cast_binary_value - data = "\u001F\x8B".force_encoding("BINARY") + data = "\u001F\x8B".dup.force_encoding("BINARY") assert_equal(data, @type.deserialize(data)) end diff --git a/activerecord/test/cases/adapters/postgresql/geometric_test.rb b/activerecord/test/cases/adapters/postgresql/geometric_test.rb index c1f3a4ae2c..3b6840a1c9 100644 --- a/activerecord/test/cases/adapters/postgresql/geometric_test.rb +++ b/activerecord/test/cases/adapters/postgresql/geometric_test.rb @@ -93,8 +93,6 @@ class PostgresqlPointTest < ActiveRecord::PostgreSQLTestCase end def test_empty_string_assignment - assert_nothing_raised { PostgresqlPoint.new(x: "") } - p = PostgresqlPoint.new(x: "") assert_nil p.x end diff --git a/activerecord/test/cases/adapters/postgresql/money_test.rb b/activerecord/test/cases/adapters/postgresql/money_test.rb index 1b5d8362af..ea060345a7 100644 --- a/activerecord/test/cases/adapters/postgresql/money_test.rb +++ b/activerecord/test/cases/adapters/postgresql/money_test.rb @@ -47,10 +47,10 @@ class PostgresqlMoneyTest < ActiveRecord::PostgreSQLTestCase def test_money_type_cast type = PostgresqlMoney.type_for_attribute("wealth") - assert_equal(12345678.12, type.cast("$12,345,678.12")) - assert_equal(12345678.12, type.cast("$12.345.678,12")) - assert_equal(-1.15, type.cast("-$1.15")) - assert_equal(-2.25, type.cast("($2.25)")) + assert_equal(12345678.12, type.cast("$12,345,678.12".dup)) + assert_equal(12345678.12, type.cast("$12.345.678,12".dup)) + assert_equal(-1.15, type.cast("-$1.15".dup)) + assert_equal(-2.25, type.cast("($2.25)".dup)) end def test_schema_dumping @@ -60,7 +60,7 @@ class PostgresqlMoneyTest < ActiveRecord::PostgreSQLTestCase end def test_create_and_update_money - money = PostgresqlMoney.create(wealth: "987.65") + money = PostgresqlMoney.create(wealth: "987.65".dup) assert_equal 987.65, money.wealth new_value = BigDecimal.new("123.45") diff --git a/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb b/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb index b55b766d20..26bf4d2090 100644 --- a/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb +++ b/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb @@ -324,16 +324,16 @@ module ActiveRecord reset_connection end - def test_only_reload_type_map_once_for_every_unknown_type + def test_only_reload_type_map_once_for_every_unrecognized_type reset_connection connection = ActiveRecord::Base.connection silence_warnings do assert_queries 2, ignore_none: true do - connection.select_all "SELECT NULL::anyelement" + connection.select_all "select 'pg_catalog.pg_class'::regclass" end assert_queries 1, ignore_none: true do - connection.select_all "SELECT NULL::anyelement" + connection.select_all "select 'pg_catalog.pg_class'::regclass" end assert_queries 2, ignore_none: true do connection.select_all "SELECT NULL::anyarray" @@ -343,16 +343,16 @@ module ActiveRecord reset_connection end - def test_only_warn_on_first_encounter_of_unknown_oid + def test_only_warn_on_first_encounter_of_unrecognized_oid reset_connection connection = ActiveRecord::Base.connection warning = capture(:stderr) { - connection.select_all "SELECT NULL::anyelement" - connection.select_all "SELECT NULL::anyelement" - connection.select_all "SELECT NULL::anyelement" + connection.select_all "select 'pg_catalog.pg_class'::regclass" + connection.select_all "select 'pg_catalog.pg_class'::regclass" + connection.select_all "select 'pg_catalog.pg_class'::regclass" } - assert_match(/\Aunknown OID \d+: failed to recognize type of 'anyelement'\. It will be treated as String\.\n\z/, warning) + assert_match(/\Aunknown OID \d+: failed to recognize type of 'regclass'\. It will be treated as String\.\n\z/, warning) ensure reset_connection end diff --git a/activerecord/test/cases/adapters/postgresql/uuid_test.rb b/activerecord/test/cases/adapters/postgresql/uuid_test.rb index 6ebe9d82a7..00de92cdfd 100644 --- a/activerecord/test/cases/adapters/postgresql/uuid_test.rb +++ b/activerecord/test/cases/adapters/postgresql/uuid_test.rb @@ -40,7 +40,8 @@ class PostgresqlUUIDTest < ActiveRecord::PostgreSQLTestCase drop_table "uuid_data_type" end - if ActiveRecord::Base.connection.supports_pgcrypto_uuid? + if ActiveRecord::Base.connection.respond_to?(:supports_pgcrypto_uuid?) && + ActiveRecord::Base.connection.supports_pgcrypto_uuid? def test_uuid_column_default connection.add_column :uuid_data_type, :thingy, :uuid, null: false, default: "gen_random_uuid()" UUIDType.reset_column_information @@ -64,11 +65,11 @@ class PostgresqlUUIDTest < ActiveRecord::PostgreSQLTestCase end def test_add_column_with_null_true_and_default_nil - assert_nothing_raised do - connection.add_column :uuid_data_type, :thingy, :uuid, null: true, default: nil - end + connection.add_column :uuid_data_type, :thingy, :uuid, null: true, default: nil + UUIDType.reset_column_information column = UUIDType.columns_hash["thingy"] + assert column.null assert_nil column.default end @@ -123,7 +124,9 @@ class PostgresqlUUIDTest < ActiveRecord::PostgreSQLTestCase "Z0000C99-9C0B-4EF8-BB6D-6BB9BD380A11", "a0eebc999r0b4ef8ab6d6bb9bd380a11", "a0ee-bc99------4ef8-bb6d-6bb9-bd38-0a11", - "{a0eebc99-bb6d6bb9-bd380a11}"].each do |invalid_uuid| + "{a0eebc99-bb6d6bb9-bd380a11}", + "{a0eebc99-9c0b4ef8-bb6d6bb9-bd380a11", + "a0eebc99-9c0b4ef8-bb6d6bb9-bd380a11}"].each do |invalid_uuid| uuid = UUIDType.new guid: invalid_uuid assert_nil uuid.guid end diff --git a/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb b/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb index 9a812e325e..cf0c37f70c 100644 --- a/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb +++ b/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb @@ -165,7 +165,7 @@ module ActiveRecord data binary ) eosql - str = "\x80".force_encoding("ASCII-8BIT") + str = "\x80".dup.force_encoding("ASCII-8BIT") binary = DualEncoding.new name: "いただきます!", data: str binary.save! assert_equal str, binary.data @@ -174,7 +174,7 @@ module ActiveRecord end def test_type_cast_should_not_mutate_encoding - name = "hello".force_encoding(Encoding::ASCII_8BIT) + name = "hello".dup.force_encoding(Encoding::ASCII_8BIT) Owner.create(name: name) assert_equal Encoding::ASCII_8BIT, name.encoding ensure diff --git a/activerecord/test/cases/associations/association_scope_test.rb b/activerecord/test/cases/associations/association_scope_test.rb index c322333f6d..c54542ff7b 100644 --- a/activerecord/test/cases/associations/association_scope_test.rb +++ b/activerecord/test/cases/associations/association_scope_test.rb @@ -6,8 +6,7 @@ module ActiveRecord module Associations class AssociationScopeTest < ActiveRecord::TestCase test "does not duplicate conditions" do - scope = AssociationScope.scope(Author.new.association(:welcome_posts), - Author.connection) + scope = AssociationScope.scope(Author.new.association(:welcome_posts)) binds = scope.where_clause.binds.map(&:value) assert_equal binds.uniq, binds end diff --git a/activerecord/test/cases/associations/belongs_to_associations_test.rb b/activerecord/test/cases/associations/belongs_to_associations_test.rb index c8b26232b6..a727cc6e60 100644 --- a/activerecord/test/cases/associations/belongs_to_associations_test.rb +++ b/activerecord/test/cases/associations/belongs_to_associations_test.rb @@ -380,7 +380,6 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase sponsor.sponsorable = Member.new name: "Bert" assert_equal Member, sponsor.association(:sponsorable).send(:klass) - assert_equal "members", sponsor.association(:sponsorable).aliased_table_name end def test_with_polymorphic_and_condition diff --git a/activerecord/test/cases/associations/cascaded_eager_loading_test.rb b/activerecord/test/cases/associations/cascaded_eager_loading_test.rb index 3638c87968..7b0445025c 100644 --- a/activerecord/test/cases/associations/cascaded_eager_loading_test.rb +++ b/activerecord/test/cases/associations/cascaded_eager_loading_test.rb @@ -34,18 +34,12 @@ class CascadedEagerLoadingTest < ActiveRecord::TestCase end def test_eager_association_loading_with_hmt_does_not_table_name_collide_when_joining_associations - assert_nothing_raised do - Author.joins(:posts).eager_load(:comments).where(posts: { tags_count: 1 }).to_a - end authors = Author.joins(:posts).eager_load(:comments).where(posts: { tags_count: 1 }).to_a assert_equal 1, assert_no_queries { authors.size } assert_equal 10, assert_no_queries { authors[0].comments.size } end def test_eager_association_loading_grafts_stashed_associations_to_correct_parent - assert_nothing_raised do - Person.eager_load(primary_contact: :primary_contact).where("primary_contacts_people_2.first_name = ?", "Susan").order("people.id").to_a - end assert_equal people(:michael), Person.eager_load(primary_contact: :primary_contact).where("primary_contacts_people_2.first_name = ?", "Susan").order("people.id").first end diff --git a/activerecord/test/cases/associations/eager_load_includes_full_sti_class_test.rb b/activerecord/test/cases/associations/eager_load_includes_full_sti_class_test.rb index 4f0fe3236e..61f39b4136 100644 --- a/activerecord/test/cases/associations/eager_load_includes_full_sti_class_test.rb +++ b/activerecord/test/cases/associations/eager_load_includes_full_sti_class_test.rb @@ -11,25 +11,32 @@ end class EagerLoadIncludeFullStiClassNamesTest < ActiveRecord::TestCase def setup - generate_test_objects - end - - def generate_test_objects post = Namespaced::Post.create(title: "Great stuff", body: "This is not", author_id: 1) - Tagging.create(taggable: post) + @tagging = Tagging.create(taggable: post) + @old = ActiveRecord::Base.store_full_sti_class end - def test_class_names - old = ActiveRecord::Base.store_full_sti_class + def teardown + ActiveRecord::Base.store_full_sti_class = @old + end + def test_class_names_with_includes ActiveRecord::Base.store_full_sti_class = false post = Namespaced::Post.includes(:tagging).find_by_title("Great stuff") assert_nil post.tagging ActiveRecord::Base.store_full_sti_class = true post = Namespaced::Post.includes(:tagging).find_by_title("Great stuff") - assert_instance_of Tagging, post.tagging - ensure - ActiveRecord::Base.store_full_sti_class = old + assert_equal @tagging, post.tagging + end + + def test_class_names_with_eager_load + ActiveRecord::Base.store_full_sti_class = false + post = Namespaced::Post.eager_load(:tagging).find_by_title("Great stuff") + assert_nil post.tagging + + ActiveRecord::Base.store_full_sti_class = true + post = Namespaced::Post.eager_load(:tagging).find_by_title("Great stuff") + assert_equal @tagging, post.tagging end end diff --git a/activerecord/test/cases/associations/eager_test.rb b/activerecord/test/cases/associations/eager_test.rb index 4271a09c9b..c0bab19e82 100644 --- a/activerecord/test/cases/associations/eager_test.rb +++ b/activerecord/test/cases/associations/eager_test.rb @@ -68,6 +68,11 @@ class EagerAssociationTest < ActiveRecord::TestCase "expected to find only david's posts" end + def test_loading_with_scope_including_joins + assert_equal clubs(:boring_club), Member.preload(:general_club).find(1).general_club + assert_equal clubs(:boring_club), Member.eager_load(:general_club).find(1).general_club + end + def test_with_ordering list = Post.all.merge!(includes: :comments, order: "posts.id DESC").to_a [:other_by_mary, :other_by_bob, :misc_by_mary, :misc_by_bob, :eager_other, @@ -271,9 +276,6 @@ class EagerAssociationTest < ActiveRecord::TestCase end def test_loading_from_an_association_that_has_a_hash_of_conditions - assert_nothing_raised do - Author.all.merge!(includes: :hello_posts_with_hash_conditions).to_a - end assert !Author.all.merge!(includes: :hello_posts_with_hash_conditions).find(authors(:david).id).hello_posts.empty? end diff --git a/activerecord/test/cases/associations/has_many_associations_test.rb b/activerecord/test/cases/associations/has_many_associations_test.rb index 6102f98682..eeba8fa74f 100644 --- a/activerecord/test/cases/associations/has_many_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_associations_test.rb @@ -241,6 +241,13 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_equal "defaulty", bulb.name end + def test_build_from_association_sets_inverse_instance + car = Car.new(name: "honda") + + bulb = car.bulbs.build + assert_equal car, bulb.car + end + def test_do_not_call_callbacks_for_delete_all car = Car.create(name: "honda") car.funky_bulbs.create! @@ -744,6 +751,41 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_equal client2, firm.clients.merge!(where: ["#{QUOTED_TYPE} = :type", { type: "Client" }], order: "id").first end + def test_find_first_after_reset_scope + firm = Firm.all.merge!(order: "id").first + collection = firm.clients + + original_object = collection.first + assert_same original_object, collection.first, "Expected second call to #first to cache the same object" + + # It should return a different object, since the association has been reloaded + assert_not_same original_object, firm.clients.first, "Expected #first to return a new object" + end + + def test_find_first_after_reset + firm = Firm.all.merge!(order: "id").first + collection = firm.clients + + original_object = collection.first + assert_same original_object, collection.first, "Expected second call to #first to cache the same object" + collection.reset + + # It should return a different object, since the association has been reloaded + assert_not_same original_object, collection.first, "Expected #first after #reset to return a new object" + end + + def test_find_first_after_reload + firm = Firm.all.merge!(order: "id").first + collection = firm.clients + + original_object = collection.first + assert_same original_object, collection.first, "Expected second call to #first to cache the same object" + collection.reload + + # It should return a different object, since the association has been reloaded + assert_not_same original_object, collection.first, "Expected #first after #reload to return a new object" + end + def test_find_all_with_include_and_conditions assert_nothing_raised do Developer.all.merge!(joins: :audit_logs, where: { "audit_logs.message" => nil, :name => "Smith" }).to_a @@ -2143,6 +2185,13 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_equal "Post", tagging.taggable_type end + def test_build_from_polymorphic_association_sets_inverse_instance + post = Post.new + tagging = post.taggings.build + + assert_equal post, tagging.taggable + end + def test_dont_call_save_callbacks_twice_on_has_many firm = companies(:first_firm) contract = firm.contracts.create! @@ -2322,8 +2371,9 @@ class HasManyAssociationsTest < ActiveRecord::TestCase car = Car.create! bulb = Bulb.create! name: "other", car: car - assert_equal bulb, Car.find(car.id).all_bulbs.first - assert_equal bulb, Car.includes(:all_bulbs).find(car.id).all_bulbs.first + assert_equal [bulb], Car.find(car.id).all_bulbs + assert_equal [bulb], Car.includes(:all_bulbs).find(car.id).all_bulbs + assert_equal [bulb], Car.eager_load(:all_bulbs).find(car.id).all_bulbs end test "raises RecordNotDestroyed when replaced child can't be destroyed" do @@ -2511,6 +2561,11 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_equal [bulb.id], car.bulb_ids assert_no_queries { car.bulb_ids } + + bulb2 = car.bulbs.create! + + assert_equal [bulb.id, bulb2.id], car.bulb_ids + assert_no_queries { car.bulb_ids } end def test_loading_association_in_validate_callback_doesnt_affect_persistence diff --git a/activerecord/test/cases/associations/has_many_through_associations_test.rb b/activerecord/test/cases/associations/has_many_through_associations_test.rb index 9156f6d57a..1c2138a3d0 100644 --- a/activerecord/test/cases/associations/has_many_through_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_through_associations_test.rb @@ -319,6 +319,17 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase assert_includes post.single_people, person end + def test_build_then_remove_then_save + post = posts(:thinking) + post.people.build(first_name: "Bob") + ted = post.people.build(first_name: "Ted") + post.people.delete(ted) + post.save! + post.reload + + assert_equal ["Bob"], post.people.collect(&:first_name) + end + def test_both_parent_ids_set_when_saving_new post = Post.new(title: "Hello", body: "world") person = Person.new(first_name: "Sean") diff --git a/activerecord/test/cases/associations/has_one_associations_test.rb b/activerecord/test/cases/associations/has_one_associations_test.rb index 7c11d2e7fc..bf3b8dcd63 100644 --- a/activerecord/test/cases/associations/has_one_associations_test.rb +++ b/activerecord/test/cases/associations/has_one_associations_test.rb @@ -307,6 +307,15 @@ class HasOneAssociationsTest < ActiveRecord::TestCase end end + def test_create_when_parent_is_new_raises + firm = Firm.new + error = assert_raise(ActiveRecord::RecordNotSaved) do + firm.create_account + end + + assert_equal "You cannot call create unless the parent is saved", error.message + end + def test_reload_association odegy = companies(:odegy) diff --git a/activerecord/test/cases/associations/inverse_associations_test.rb b/activerecord/test/cases/associations/inverse_associations_test.rb index 467cc73ecd..9fbaa43274 100644 --- a/activerecord/test/cases/associations/inverse_associations_test.rb +++ b/activerecord/test/cases/associations/inverse_associations_test.rb @@ -24,11 +24,9 @@ class AutomaticInverseFindingTests < ActiveRecord::TestCase monkey_reflection = MixedCaseMonkey.reflect_on_association(:man) man_reflection = Man.reflect_on_association(:mixed_case_monkey) - assert_respond_to monkey_reflection, :has_inverse? assert monkey_reflection.has_inverse?, "The monkey reflection should have an inverse" assert_equal man_reflection, monkey_reflection.inverse_of, "The monkey reflection's inverse should be the man reflection" - assert_respond_to man_reflection, :has_inverse? assert man_reflection.has_inverse?, "The man reflection should have an inverse" assert_equal monkey_reflection, man_reflection.inverse_of, "The man reflection's inverse should be the monkey reflection" end @@ -37,7 +35,6 @@ class AutomaticInverseFindingTests < ActiveRecord::TestCase account_reflection = Admin::Account.reflect_on_association(:users) user_reflection = Admin::User.reflect_on_association(:account) - assert_respond_to account_reflection, :has_inverse? assert account_reflection.has_inverse?, "The Admin::Account reflection should have an inverse" assert_equal user_reflection, account_reflection.inverse_of, "The Admin::Account reflection's inverse should be the Admin::User reflection" end @@ -46,11 +43,9 @@ class AutomaticInverseFindingTests < ActiveRecord::TestCase car_reflection = Car.reflect_on_association(:bulb) bulb_reflection = Bulb.reflect_on_association(:car) - assert_respond_to car_reflection, :has_inverse? assert car_reflection.has_inverse?, "The Car reflection should have an inverse" assert_equal bulb_reflection, car_reflection.inverse_of, "The Car reflection's inverse should be the Bulb reflection" - assert_respond_to bulb_reflection, :has_inverse? assert bulb_reflection.has_inverse?, "The Bulb reflection should have an inverse" assert_equal car_reflection, bulb_reflection.inverse_of, "The Bulb reflection's inverse should be the Car reflection" end @@ -59,7 +54,6 @@ class AutomaticInverseFindingTests < ActiveRecord::TestCase comment_reflection = Comment.reflect_on_association(:ratings) rating_reflection = Rating.reflect_on_association(:comment) - assert_respond_to comment_reflection, :has_inverse? assert comment_reflection.has_inverse?, "The Comment reflection should have an inverse" assert_equal rating_reflection, comment_reflection.inverse_of, "The Comment reflection's inverse should be the Rating reflection" end @@ -107,24 +101,17 @@ class AutomaticInverseFindingTests < ActiveRecord::TestCase def test_polymorphic_and_has_many_through_relationships_should_not_have_inverses sponsor_reflection = Sponsor.reflect_on_association(:sponsorable) - assert_respond_to sponsor_reflection, :has_inverse? assert !sponsor_reflection.has_inverse?, "A polymorphic association should not find an inverse automatically" club_reflection = Club.reflect_on_association(:members) - assert_respond_to club_reflection, :has_inverse? assert !club_reflection.has_inverse?, "A has_many_through association should not find an inverse automatically" end - def test_polymorphic_relationships_should_still_not_have_inverses_when_non_polymorphic_relationship_has_the_same_name + def test_polymorphic_has_one_should_find_inverse_automatically man_reflection = Man.reflect_on_association(:polymorphic_face_without_inverse) - face_reflection = Face.reflect_on_association(:man) - assert_respond_to face_reflection, :has_inverse? - assert face_reflection.has_inverse?, "For this test, the non-polymorphic association must have an inverse" - - assert_respond_to man_reflection, :has_inverse? - assert !man_reflection.has_inverse?, "The target of a polymorphic association should not find an inverse automatically" + assert man_reflection.has_inverse? end end @@ -145,41 +132,24 @@ class InverseAssociationTests < ActiveRecord::TestCase def test_should_be_able_to_ask_a_reflection_if_it_has_an_inverse has_one_with_inverse_ref = Man.reflect_on_association(:face) - assert_respond_to has_one_with_inverse_ref, :has_inverse? assert has_one_with_inverse_ref.has_inverse? has_many_with_inverse_ref = Man.reflect_on_association(:interests) - assert_respond_to has_many_with_inverse_ref, :has_inverse? assert has_many_with_inverse_ref.has_inverse? belongs_to_with_inverse_ref = Face.reflect_on_association(:man) - assert_respond_to belongs_to_with_inverse_ref, :has_inverse? assert belongs_to_with_inverse_ref.has_inverse? has_one_without_inverse_ref = Club.reflect_on_association(:sponsor) - assert_respond_to has_one_without_inverse_ref, :has_inverse? assert !has_one_without_inverse_ref.has_inverse? has_many_without_inverse_ref = Club.reflect_on_association(:memberships) - assert_respond_to has_many_without_inverse_ref, :has_inverse? assert !has_many_without_inverse_ref.has_inverse? belongs_to_without_inverse_ref = Sponsor.reflect_on_association(:sponsor_club) - assert_respond_to belongs_to_without_inverse_ref, :has_inverse? assert !belongs_to_without_inverse_ref.has_inverse? end - def test_should_be_able_to_ask_a_reflection_what_it_is_the_inverse_of - has_one_ref = Man.reflect_on_association(:face) - assert_respond_to has_one_ref, :inverse_of - - has_many_ref = Man.reflect_on_association(:interests) - assert_respond_to has_many_ref, :inverse_of - - belongs_to_ref = Face.reflect_on_association(:man) - assert_respond_to belongs_to_ref, :inverse_of - end - def test_inverse_of_method_should_supply_the_actual_reflection_instance_it_is_the_inverse_of has_one_ref = Man.reflect_on_association(:face) assert_equal Face.reflect_on_association(:man), has_one_ref.inverse_of diff --git a/activerecord/test/cases/attribute_test.rb b/activerecord/test/cases/attribute_test.rb index 7cf6b498c9..e856d551c0 100644 --- a/activerecord/test/cases/attribute_test.rb +++ b/activerecord/test/cases/attribute_test.rb @@ -76,7 +76,7 @@ module ActiveRecord end test "duping dups the value" do - @type.expect(:deserialize, "type cast", ["a value"]) + @type.expect(:deserialize, "type cast".dup, ["a value"]) attribute = Attribute.from_database(nil, "a value", @type) value_from_orig = attribute.value @@ -244,7 +244,7 @@ module ActiveRecord end test "with_type preserves mutations" do - attribute = Attribute.from_database(:foo, "", Type::Value.new) + attribute = Attribute.from_database(:foo, "".dup, Type::Value.new) attribute.value << "1" assert_equal 1, attribute.with_type(Type::Integer.new).value diff --git a/activerecord/test/cases/batches_test.rb b/activerecord/test/cases/batches_test.rb index fbc3fbb44f..dcd3af487b 100644 --- a/activerecord/test/cases/batches_test.rb +++ b/activerecord/test/cases/batches_test.rb @@ -1,4 +1,5 @@ require "cases/helper" +require "models/comment" require "models/post" require "models/subscriber" @@ -152,7 +153,7 @@ class EachTest < ActiveRecord::TestCase end def test_find_in_batches_should_not_use_records_after_yielding_them_in_case_original_array_is_modified - not_a_post = "not a post" + not_a_post = "not a post".dup def not_a_post.id; end not_a_post.stub(:id, -> { raise StandardError.new("not_a_post had #id called on it") }) do assert_nothing_raised do @@ -417,7 +418,7 @@ class EachTest < ActiveRecord::TestCase end def test_in_batches_should_not_use_records_after_yielding_them_in_case_original_array_is_modified - not_a_post = "not a post" + not_a_post = "not a post".dup def not_a_post.id raise StandardError.new("not_a_post had #id called on it") end @@ -610,4 +611,64 @@ class EachTest < ActiveRecord::TestCase end assert_equal expected, actual end + + test ".find_each bypasses the query cache for its own queries" do + Post.cache do + assert_queries(2) do + Post.find_each {} + Post.find_each {} + end + end + end + + test ".find_each does not disable the query cache inside the given block" do + Post.cache do + Post.find_each(start: 1, finish: 1) do |post| + assert_queries(1) do + post.comments.count + post.comments.count + end + end + end + end + + test ".find_in_batches bypasses the query cache for its own queries" do + Post.cache do + assert_queries(2) do + Post.find_in_batches {} + Post.find_in_batches {} + end + end + end + + test ".find_in_batches does not disable the query cache inside the given block" do + Post.cache do + Post.find_in_batches(start: 1, finish: 1) do |batch| + assert_queries(1) do + batch.first.comments.count + batch.first.comments.count + end + end + end + end + + test ".in_batches bypasses the query cache for its own queries" do + Post.cache do + assert_queries(2) do + Post.in_batches {} + Post.in_batches {} + end + end + end + + test ".in_batches does not disable the query cache inside the given block" do + Post.cache do + Post.in_batches(start: 1, finish: 1) do |relation| + assert_queries(1) do + relation.count + relation.count + end + end + end + end end diff --git a/activerecord/test/cases/binary_test.rb b/activerecord/test/cases/binary_test.rb index 1fc30e24d2..f6ac7990d1 100644 --- a/activerecord/test/cases/binary_test.rb +++ b/activerecord/test/cases/binary_test.rb @@ -10,7 +10,7 @@ unless current_adapter?(:DB2Adapter) FIXTURES = %w(flowers.jpg example.log test.txt) def test_mixed_encoding - str = "\x80" + str = "\x80".dup str.force_encoding("ASCII-8BIT") binary = Binary.new name: "いただきます!", data: str diff --git a/activerecord/test/cases/calculations_test.rb b/activerecord/test/cases/calculations_test.rb index adfca08289..9f44b583f6 100644 --- a/activerecord/test/cases/calculations_test.rb +++ b/activerecord/test/cases/calculations_test.rb @@ -580,8 +580,11 @@ class CalculationsTest < ActiveRecord::TestCase end def test_pluck_without_column_names - assert_equal [[1, "Firm", 1, nil, "37signals", nil, 1, nil, ""]], - Company.order(:id).limit(1).pluck + if current_adapter?(:OracleAdapter) + assert_equal [[1, "Firm", 1, nil, "37signals", nil, 1, nil, nil]], Company.order(:id).limit(1).pluck + else + assert_equal [[1, "Firm", 1, nil, "37signals", nil, 1, nil, ""]], Company.order(:id).limit(1).pluck + end end def test_pluck_type_cast @@ -820,4 +823,46 @@ class CalculationsTest < ActiveRecord::TestCase assert_equal 6, Account.sum(:firm_id) { 1 } end end + + test "#skip_query_cache! for #pluck" do + Account.cache do + assert_queries(1) do + Account.pluck(:credit_limit) + Account.pluck(:credit_limit) + end + + assert_queries(2) do + Account.all.skip_query_cache!.pluck(:credit_limit) + Account.all.skip_query_cache!.pluck(:credit_limit) + end + end + end + + test "#skip_query_cache! for a simple calculation" do + Account.cache do + assert_queries(1) do + Account.calculate(:sum, :credit_limit) + Account.calculate(:sum, :credit_limit) + end + + assert_queries(2) do + Account.all.skip_query_cache!.calculate(:sum, :credit_limit) + Account.all.skip_query_cache!.calculate(:sum, :credit_limit) + end + end + end + + test "#skip_query_cache! for a grouped calculation" do + Account.cache do + assert_queries(1) do + Account.group(:firm_id).calculate(:sum, :credit_limit) + Account.group(:firm_id).calculate(:sum, :credit_limit) + end + + assert_queries(2) do + Account.all.skip_query_cache!.group(:firm_id).calculate(:sum, :credit_limit) + Account.all.skip_query_cache!.group(:firm_id).calculate(:sum, :credit_limit) + end + end + end end diff --git a/activerecord/test/cases/connection_pool_test.rb b/activerecord/test/cases/connection_pool_test.rb index 7e88c9cf7a..00a0187b57 100644 --- a/activerecord/test/cases/connection_pool_test.rb +++ b/activerecord/test/cases/connection_pool_test.rb @@ -499,21 +499,8 @@ module ActiveRecord if failed second_thread_done.set - puts - puts ">>> test_disconnect_and_clear_reloadable_connections_are_able_to_preempt_other_waiting_threads / #{group_action_method}" - p [first_thread, second_thread] - p pool.stat - p pool.connections.map(&:owner) - first_thread.join(2) second_thread.join(2) - - puts "---" - p [first_thread, second_thread] - p pool.stat - p pool.connections.map(&:owner) - puts "<<<" - puts end first_thread.join(10) || raise("first_thread got stuck") diff --git a/activerecord/test/cases/core_test.rb b/activerecord/test/cases/core_test.rb index 3735572898..936f26ce04 100644 --- a/activerecord/test/cases/core_test.rb +++ b/activerecord/test/cases/core_test.rb @@ -35,7 +35,7 @@ class CoreTest < ActiveRecord::TestCase def test_pretty_print_new topic = Topic.new - actual = "" + actual = "".dup PP.pp(topic, StringIO.new(actual)) expected = <<-PRETTY.strip_heredoc #<Topic:0xXXXXXX @@ -64,7 +64,7 @@ class CoreTest < ActiveRecord::TestCase def test_pretty_print_persisted topic = topics(:first) - actual = "" + actual = "".dup PP.pp(topic, StringIO.new(actual)) expected = <<-PRETTY.strip_heredoc #<Topic:0x\\w+ @@ -92,7 +92,7 @@ class CoreTest < ActiveRecord::TestCase def test_pretty_print_uninitialized topic = Topic.allocate - actual = "" + actual = "".dup PP.pp(topic, StringIO.new(actual)) expected = "#<Topic:XXXXXX not initialized>\n" assert actual.start_with?(expected.split("XXXXXX").first) @@ -105,7 +105,7 @@ class CoreTest < ActiveRecord::TestCase "inspecting topic" end end - actual = "" + actual = "".dup PP.pp(subtopic.new, StringIO.new(actual)) assert_equal "inspecting topic\n", actual end diff --git a/activerecord/test/cases/date_time_test.rb b/activerecord/test/cases/date_time_test.rb index ad7da9de70..6cd98fe254 100644 --- a/activerecord/test/cases/date_time_test.rb +++ b/activerecord/test/cases/date_time_test.rb @@ -58,4 +58,17 @@ class DateTimeTest < ActiveRecord::TestCase assert_equal now, task.starting end end + + def test_date_time_with_string_value_with_subsecond_precision + skip unless subsecond_precision_supported? + string_value = "2017-07-04 14:19:00.5" + topic = Topic.create(written_on: string_value) + assert_equal topic, Topic.find_by(written_on: string_value) + end + + def test_date_time_with_string_value_with_non_iso_format + string_value = "04/07/2017 2:19pm" + topic = Topic.create(written_on: string_value) + assert_equal topic, Topic.find_by(written_on: string_value) + end end diff --git a/activerecord/test/cases/enum_test.rb b/activerecord/test/cases/enum_test.rb index db3da53487..4ef9a125e6 100644 --- a/activerecord/test/cases/enum_test.rb +++ b/activerecord/test/cases/enum_test.rb @@ -60,6 +60,7 @@ class EnumTest < ActiveRecord::TestCase assert_not_equal @book, Book.where(status: [:written]).first assert_not_equal @book, Book.where.not(status: :published).first assert_equal @book, Book.where.not(status: :written).first + assert_equal books(:ddd), Book.where(read_status: :forgotten).first end test "find via where with strings" do @@ -69,6 +70,7 @@ class EnumTest < ActiveRecord::TestCase assert_not_equal @book, Book.where(status: ["written"]).first assert_not_equal @book, Book.where.not(status: "published").first assert_equal @book, Book.where.not(status: "written").first + assert_equal books(:ddd), Book.where(read_status: "forgotten").first end test "build from scope" do diff --git a/activerecord/test/cases/errors_test.rb b/activerecord/test/cases/errors_test.rb index 73feb831d0..e90669e0c7 100644 --- a/activerecord/test/cases/errors_test.rb +++ b/activerecord/test/cases/errors_test.rb @@ -1,4 +1,4 @@ -require_relative "../cases/helper" +require "cases/helper" class ErrorsTest < ActiveRecord::TestCase def test_can_be_instantiated_with_no_args diff --git a/activerecord/test/cases/finder_test.rb b/activerecord/test/cases/finder_test.rb index 4837a169fa..af21cd529f 100644 --- a/activerecord/test/cases/finder_test.rb +++ b/activerecord/test/cases/finder_test.rb @@ -1023,16 +1023,6 @@ class FinderTest < ActiveRecord::TestCase assert_raise(ActiveRecord::StatementInvalid) { Topic.find_by_sql "select 1 from badtable" } end - def test_find_all_with_join - developers_on_project_one = Developer. - joins("LEFT JOIN developers_projects ON developers.id = developers_projects.developer_id"). - where("project_id=1").to_a - assert_equal 3, developers_on_project_one.length - developer_names = developers_on_project_one.map(&:name) - assert_includes developer_names, "David" - assert_includes developer_names, "Jamis" - end - def test_joins_dont_clobber_id first = Firm. joins("INNER JOIN companies clients ON clients.firm_id = companies.id"). @@ -1242,6 +1232,34 @@ class FinderTest < ActiveRecord::TestCase assert_equal tyre2, zyke.tyres.custom_find_by(id: tyre2.id) end + test "#skip_query_cache! for #exists?" do + Topic.cache do + assert_queries(1) do + Topic.exists? + Topic.exists? + end + + assert_queries(2) do + Topic.all.skip_query_cache!.exists? + Topic.all.skip_query_cache!.exists? + end + end + end + + test "#skip_query_cache! for #exists? with a limited eager load" do + Topic.cache do + assert_queries(2) do + Topic.eager_load(:replies).limit(1).exists? + Topic.eager_load(:replies).limit(1).exists? + end + + assert_queries(4) do + Topic.eager_load(:replies).limit(1).skip_query_cache!.exists? + Topic.eager_load(:replies).limit(1).skip_query_cache!.exists? + end + end + end + private def table_with_custom_primary_key yield(Class.new(Toy) do diff --git a/activerecord/test/cases/fixtures_test.rb b/activerecord/test/cases/fixtures_test.rb index e651f63c7e..f2fe724095 100644 --- a/activerecord/test/cases/fixtures_test.rb +++ b/activerecord/test/cases/fixtures_test.rb @@ -54,6 +54,31 @@ class FixturesTest < ActiveRecord::TestCase end end + class InsertQuerySubscriber + attr_reader :events + + def initialize + @events = [] + end + + def call(_, _, _, _, values) + @events << values[:sql] if values[:sql] =~ /INSERT/ + end + end + + if current_adapter?(:Mysql2Adapter, :PostgreSQLAdapter) + def test_bulk_insert + begin + subscriber = InsertQuerySubscriber.new + subscription = ActiveSupport::Notifications.subscribe("sql.active_record", subscriber) + create_fixtures("bulbs") + assert_equal 1, subscriber.events.size, "It takes one INSERT query to insert two fixtures" + ensure + ActiveSupport::Notifications.unsubscribe(subscription) + end + end + end + def test_broken_yaml_exception badyaml = Tempfile.new ["foo", ".yml"] badyaml.write "a: : " @@ -248,7 +273,12 @@ class FixturesTest < ActiveRecord::TestCase e = assert_raise(ActiveRecord::Fixture::FixtureError) do ActiveRecord::FixtureSet.create_fixtures(FIXTURES_ROOT + "/naked/yml", "parrots") end - assert_equal(%(table "parrots" has no column named "arrr".), e.message) + + if current_adapter?(:SQLite3Adapter) + assert_equal(%(table "parrots" has no column named "arrr".), e.message) + else + assert_equal(%(table "parrots" has no columns named "arrr", "foobar".), e.message) + end end def test_yaml_file_with_symbol_columns diff --git a/activerecord/test/cases/json_shared_test_cases.rb b/activerecord/test/cases/json_shared_test_cases.rb index 5aa41b9d6d..9a1c1c3f3f 100644 --- a/activerecord/test/cases/json_shared_test_cases.rb +++ b/activerecord/test/cases/json_shared_test_cases.rb @@ -169,6 +169,25 @@ module JSONSharedTestCases assert_not json.changed? end + def test_changes_in_place_ignores_key_order + json = klass.new + assert_not json.changed? + + json.payload = { "three" => "four", "one" => "two" } + json.save! + json.reload + + json.payload = { "three" => "four", "one" => "two" } + assert_not json.changed? + + json.payload = [{ "three" => "four", "one" => "two" }, { "seven" => "eight", "five" => "six" }] + json.save! + json.reload + + json.payload = [{ "three" => "four", "one" => "two" }, { "seven" => "eight", "five" => "six" }] + assert_not json.changed? + end + def test_changes_in_place_with_ruby_object time = Time.now.utc json = klass.create!(payload: time) diff --git a/activerecord/test/cases/locking_test.rb b/activerecord/test/cases/locking_test.rb index 3a3b8e51f9..2fc52393f2 100644 --- a/activerecord/test/cases/locking_test.rb +++ b/activerecord/test/cases/locking_test.rb @@ -167,6 +167,12 @@ class OptimisticLockingTest < ActiveRecord::TestCase assert_equal 0, p1.lock_version end + def test_lock_new_when_explicitly_passing_value + p1 = Person.new(first_name: "Douglas Adams", lock_version: 42) + p1.save! + assert_equal 42, p1.lock_version + end + def test_touch_existing_lock p1 = Person.find(1) assert_equal 0, p1.lock_version @@ -186,6 +192,19 @@ class OptimisticLockingTest < ActiveRecord::TestCase end end + def test_explicit_update_lock_column_raise_error + person = Person.find(1) + + assert_raises(ActiveRecord::StaleObjectError) do + person.first_name = "Douglas Adams" + person.lock_version = 42 + + assert person.lock_version_changed? + + person.save + end + end + def test_lock_column_name_existing t1 = LegacyThing.find(1) t2 = LegacyThing.find(1) @@ -225,10 +244,33 @@ class OptimisticLockingTest < ActiveRecord::TestCase assert_equal 0, t1.lock_version_before_type_cast end + def test_touch_existing_lock_without_default_should_work_with_null_in_the_database + ActiveRecord::Base.connection.execute("INSERT INTO lock_without_defaults(title) VALUES('title1')") + t1 = LockWithoutDefault.last + + assert_equal 0, t1.lock_version + assert_nil t1.lock_version_before_type_cast + + t1.touch + + assert_equal 1, t1.lock_version + end + + def test_touch_stale_object_with_lock_without_default + t1 = LockWithoutDefault.create!(title: "title1") + stale_object = LockWithoutDefault.find(t1.id) + + t1.update!(title: "title2") + + assert_raises(ActiveRecord::StaleObjectError) do + stale_object.touch + end + end + def test_lock_without_default_should_work_with_null_in_the_database ActiveRecord::Base.connection.execute("INSERT INTO lock_without_defaults(title) VALUES('title1')") t1 = LockWithoutDefault.last - t2 = LockWithoutDefault.last + t2 = LockWithoutDefault.find(t1.id) assert_equal 0, t1.lock_version assert_nil t1.lock_version_before_type_cast @@ -285,7 +327,7 @@ class OptimisticLockingTest < ActiveRecord::TestCase ActiveRecord::Base.connection.execute("INSERT INTO lock_without_defaults_cust(title) VALUES('title1')") t1 = LockWithCustomColumnWithoutDefault.last - t2 = LockWithCustomColumnWithoutDefault.last + t2 = LockWithCustomColumnWithoutDefault.find(t1.id) assert_equal 0, t1.custom_lock_version assert_nil t1.custom_lock_version_before_type_cast @@ -434,6 +476,31 @@ class OptimisticLockingWithSchemaChangeTest < ActiveRecord::TestCase PersonalLegacyThing.reset_column_information end + def test_destroy_existing_object_with_locking_column_value_null_in_the_database + ActiveRecord::Base.connection.execute("INSERT INTO lock_without_defaults(title) VALUES('title1')") + t1 = LockWithoutDefault.last + + assert_equal 0, t1.lock_version + assert_nil t1.lock_version_before_type_cast + + t1.destroy + + assert t1.destroyed? + end + + def test_destroy_stale_object + t1 = LockWithoutDefault.create!(title: "title1") + stale_object = LockWithoutDefault.find(t1.id) + + t1.update!(title: "title2") + + assert_raises(ActiveRecord::StaleObjectError) do + stale_object.destroy! + end + + refute stale_object.destroyed? + end + private def add_counter_column_to(model, col = "test_count") diff --git a/activerecord/test/cases/migration/compatibility_test.rb b/activerecord/test/cases/migration/compatibility_test.rb index 7a80bfb899..596a21dcbc 100644 --- a/activerecord/test/cases/migration/compatibility_test.rb +++ b/activerecord/test/cases/migration/compatibility_test.rb @@ -90,6 +90,21 @@ module ActiveRecord connection.drop_table :more_testings rescue nil end + def test_timestamps_have_null_constraints_if_not_present_in_migration_of_change_table + migration = Class.new(ActiveRecord::Migration[4.2]) { + def migrate(x) + change_table :testings do |t| + t.timestamps + end + end + }.new + + ActiveRecord::Migrator.new(:up, [migration]).migrate + + assert connection.columns(:testings).find { |c| c.name == "created_at" }.null + assert connection.columns(:testings).find { |c| c.name == "updated_at" }.null + end + def test_timestamps_have_null_constraints_if_not_present_in_migration_for_adding_timestamps_to_existing_table migration = Class.new(ActiveRecord::Migration[4.2]) { def migrate(x) diff --git a/activerecord/test/cases/migration/references_foreign_key_test.rb b/activerecord/test/cases/migration/references_foreign_key_test.rb index f1ddac1ee2..718b9a0613 100644 --- a/activerecord/test/cases/migration/references_foreign_key_test.rb +++ b/activerecord/test/cases/migration/references_foreign_key_test.rb @@ -139,6 +139,16 @@ if ActiveRecord::Base.connection.supports_foreign_keys? end end + test "removing column removes foreign key" do + @connection.create_table :testings do |t| + t.references :testing_parent, index: true, foreign_key: true + end + + assert_difference "@connection.foreign_keys('testings').size", -1 do + @connection.remove_column :testings, :testing_parent_id + end + end + test "foreign key methods respect pluralize_table_names" do begin original_pluralize_table_names = ActiveRecord::Base.pluralize_table_names diff --git a/activerecord/test/cases/migration_test.rb b/activerecord/test/cases/migration_test.rb index 57f94950f9..eff6e09eb7 100644 --- a/activerecord/test/cases/migration_test.rb +++ b/activerecord/test/cases/migration_test.rb @@ -502,11 +502,10 @@ class MigrationTest < ActiveRecord::TestCase unless mysql_enforcing_gtid_consistency? def test_create_table_with_query - Person.connection.create_table(:person, force: true) - - Person.connection.create_table :table_from_query_testings, as: "SELECT id FROM person" + Person.connection.create_table :table_from_query_testings, as: "SELECT id FROM people WHERE id = 1" columns = Person.connection.columns(:table_from_query_testings) + assert_equal [1], Person.connection.select_values("SELECT * FROM table_from_query_testings") assert_equal 1, columns.length assert_equal "id", columns.first.name ensure @@ -514,11 +513,10 @@ class MigrationTest < ActiveRecord::TestCase end def test_create_table_with_query_from_relation - Person.connection.create_table(:person, force: true) - - Person.connection.create_table :table_from_query_testings, as: Person.select(:id) + Person.connection.create_table :table_from_query_testings, as: Person.select(:id).where(id: 1) columns = Person.connection.columns(:table_from_query_testings) + assert_equal [1], Person.connection.select_values("SELECT * FROM table_from_query_testings") assert_equal 1, columns.length assert_equal "id", columns.first.name ensure @@ -1017,8 +1015,8 @@ class CopyMigrationsTest < ActiveRecord::TestCase assert File.exist?(@migrations_path + "/4_currencies_have_symbols.bukkits.rb") assert_equal [@migrations_path + "/4_currencies_have_symbols.bukkits.rb"], copied.map(&:filename) - expected = "# coding: ISO-8859-15\n# This migration comes from bukkits (originally 1)" - assert_equal expected, IO.readlines(@migrations_path + "/4_currencies_have_symbols.bukkits.rb")[0..1].join.chomp + expected = "# frozen_string_literal: true\n# coding: ISO-8859-15\n# This migration comes from bukkits (originally 1)" + assert_equal expected, IO.readlines(@migrations_path + "/4_currencies_have_symbols.bukkits.rb")[0..2].join.chomp files_count = Dir[@migrations_path + "/*.rb"].length copied = ActiveRecord::Migration.copy(@migrations_path, bukkits: MIGRATIONS_ROOT + "/magic") diff --git a/activerecord/test/cases/nested_attributes_test.rb b/activerecord/test/cases/nested_attributes_test.rb index 5a62cbd3a6..154faa56aa 100644 --- a/activerecord/test/cases/nested_attributes_test.rb +++ b/activerecord/test/cases/nested_attributes_test.rb @@ -117,7 +117,7 @@ class TestNestedAttributesInGeneral < ActiveRecord::TestCase def test_reject_if_with_a_proc_which_returns_true_always_for_has_one Pirate.accepts_nested_attributes_for :ship, reject_if: proc { |attributes| true } - pirate = Pirate.new(catchphrase: "Stop wastin' me time") + pirate = Pirate.create(catchphrase: "Stop wastin' me time") ship = pirate.create_ship(name: "s1") pirate.update(ship_attributes: { name: "s2", id: ship.id }) assert_equal "s1", ship.reload.name diff --git a/activerecord/test/cases/primary_keys_test.rb b/activerecord/test/cases/primary_keys_test.rb index 80cbfdba06..31892cfff0 100644 --- a/activerecord/test/cases/primary_keys_test.rb +++ b/activerecord/test/cases/primary_keys_test.rb @@ -46,7 +46,7 @@ class PrimaryKeysTest < ActiveRecord::TestCase topic = Topic.new topic.title = "New Topic" assert_nil topic.id - assert_nothing_raised { topic.save! } + topic.save! id = topic.id topicReloaded = Topic.find(id) @@ -56,23 +56,36 @@ class PrimaryKeysTest < ActiveRecord::TestCase def test_customized_primary_key_auto_assigns_on_save Keyboard.delete_all keyboard = Keyboard.new(name: "HHKB") - assert_nothing_raised { keyboard.save! } + keyboard.save! assert_equal keyboard.id, Keyboard.find_by_name("HHKB").id end def test_customized_primary_key_can_be_get_before_saving keyboard = Keyboard.new assert_nil keyboard.id - assert_nothing_raised { assert_nil keyboard.key_number } + assert_nil keyboard.key_number end def test_customized_string_primary_key_settable_before_save subscriber = Subscriber.new - assert_nothing_raised { subscriber.id = "webster123" } + subscriber.id = "webster123" assert_equal "webster123", subscriber.id assert_equal "webster123", subscriber.nick end + def test_update_with_non_primary_key_id_column + subscriber = Subscriber.first + subscriber.update(update_count: 1) + subscriber.reload + assert_equal 1, subscriber.update_count + end + + def test_update_columns_with_non_primary_key_id_column + subscriber = Subscriber.first + subscriber.update_columns(id: 1) + assert_not_equal 1, subscriber.nick + end + def test_string_key subscriber = Subscriber.find(subscribers(:first).nick) assert_equal(subscribers(:first).name, subscriber.name) @@ -83,7 +96,7 @@ class PrimaryKeysTest < ActiveRecord::TestCase subscriber.id = "jdoe" assert_equal("jdoe", subscriber.id) subscriber.name = "John Doe" - assert_nothing_raised { subscriber.save! } + subscriber.save! assert_equal("jdoe", subscriber.id) subscriberReloaded = Subscriber.find("jdoe") diff --git a/activerecord/test/cases/query_cache_test.rb b/activerecord/test/cases/query_cache_test.rb index 17b5304cc4..54b65d1a86 100644 --- a/activerecord/test/cases/query_cache_test.rb +++ b/activerecord/test/cases/query_cache_test.rb @@ -42,7 +42,8 @@ class QueryCacheTest < ActiveRecord::TestCase mw = middleware { |env| Task.find 1 Task.find 1 - assert_equal 1, ActiveRecord::Base.connection.query_cache.length + query_cache = ActiveRecord::Base.connection.query_cache + assert_equal 1, query_cache.length, query_cache.keys raise "lol borked" } assert_raises(RuntimeError) { mw.call({}) } @@ -149,7 +150,8 @@ class QueryCacheTest < ActiveRecord::TestCase mw = middleware { |env| Task.find 1 Task.find 1 - assert_equal 1, ActiveRecord::Base.connection.query_cache.length + query_cache = ActiveRecord::Base.connection.query_cache + assert_equal 1, query_cache.length, query_cache.keys [200, {}, nil] } mw.call({}) @@ -204,6 +206,52 @@ class QueryCacheTest < ActiveRecord::TestCase end end + def test_exists_queries_with_cache + Post.cache do + assert_queries(1) { Post.exists?; Post.exists? } + end + end + + def test_select_all_with_cache + Post.cache do + assert_queries(1) do + 2.times { Post.connection.select_all(Post.all) } + end + end + end + + def test_select_one_with_cache + Post.cache do + assert_queries(1) do + 2.times { Post.connection.select_one(Post.all) } + end + end + end + + def test_select_value_with_cache + Post.cache do + assert_queries(1) do + 2.times { Post.connection.select_value(Post.all) } + end + end + end + + def test_select_values_with_cache + Post.cache do + assert_queries(1) do + 2.times { Post.connection.select_values(Post.all) } + end + end + end + + def test_select_rows_with_cache + Post.cache do + assert_queries(1) do + 2.times { Post.connection.select_rows(Post.all) } + end + end + end + def test_query_cache_dups_results_correctly Task.cache do now = Time.now.utc @@ -274,18 +322,8 @@ class QueryCacheTest < ActiveRecord::TestCase end end - def test_cache_is_available_when_connection_is_connected - conf = ActiveRecord::Base.configurations - - ActiveRecord::Base.configurations = {} - Task.cache do - assert_queries(1) { Task.find(1); Task.find(1) } - end - ensure - ActiveRecord::Base.configurations = conf - end - - def test_cache_is_not_available_when_using_a_not_connected_connection + def test_cache_is_available_when_using_a_not_connected_connection + skip "In-Memory DB can't test for using a not connected connection" if in_memory_db? with_temporary_connection_pool do spec_name = Task.connection_specification_name conf = ActiveRecord::Base.configurations["arunit"].merge("name" => "test2") @@ -295,15 +333,7 @@ class QueryCacheTest < ActiveRecord::TestCase Task.cache do begin - if in_memory_db? - Task.connection.create_table :tasks do |t| - t.datetime :starting - t.datetime :ending - end - ActiveRecord::FixtureSet.create_fixtures(self.class.fixture_path, ["tasks"], {}, ActiveRecord::Base) - end - Task.connection # warmup postgresql connection setup queries - assert_queries(2) { Task.find(1); Task.find(1) } + assert_queries(1) { Task.find(1); Task.find(1) } ensure ActiveRecord::Base.connection_handler.remove_connection(Task.connection_specification_name) Task.connection_specification_name = spec_name diff --git a/activerecord/test/cases/relation/delegation_test.rb b/activerecord/test/cases/relation/delegation_test.rb index cb6e4d76d3..3b15f051b2 100644 --- a/activerecord/test/cases/relation/delegation_test.rb +++ b/activerecord/test/cases/relation/delegation_test.rb @@ -21,8 +21,22 @@ module ActiveRecord end end + module DeprecatedArelDelegationTests + AREL_METHODS = [ + :with, :orders, :froms, :project, :projections, :taken, :constraints, :exists, :locked, :where_sql, + :ast, :source, :join_sources, :to_dot, :bind_values, :create_insert, :create_true, :create_false + ] + + def test_deprecate_arel_delegation + AREL_METHODS.each do |method| + assert_deprecated { target.public_send(method) } + end + end + end + class DelegationAssociationTest < ActiveRecord::TestCase include DelegationWhitelistTests + include DeprecatedArelDelegationTests fixtures :posts @@ -33,6 +47,7 @@ module ActiveRecord class DelegationRelationTest < ActiveRecord::TestCase include DelegationWhitelistTests + include DeprecatedArelDelegationTests fixtures :comments diff --git a/activerecord/test/cases/relation/merging_test.rb b/activerecord/test/cases/relation/merging_test.rb index c3b39a9295..3901824aac 100644 --- a/activerecord/test/cases/relation/merging_test.rb +++ b/activerecord/test/cases/relation/merging_test.rb @@ -143,7 +143,7 @@ class MergingDifferentRelationsTest < ActiveRecord::TestCase assert_equal ["Mary", "Mary", "Mary", "David"], posts_by_author_name end - test "relation merging (using a proc argument)" do + test "relation merging (using a proc argument)" do dev = Developer.where(name: "Jamis").first comment_1 = dev.comments.create!(body: "I'm Jamis", post: Post.first) diff --git a/activerecord/test/cases/relation/mutation_test.rb b/activerecord/test/cases/relation/mutation_test.rb index dea787c07f..8e73baa70a 100644 --- a/activerecord/test/cases/relation/mutation_test.rb +++ b/activerecord/test/cases/relation/mutation_test.rb @@ -90,7 +90,7 @@ module ActiveRecord assert_equal [], relation.extending_values end - (Relation::SINGLE_VALUE_METHODS - [:lock, :reordering, :reverse_order, :create_with]).each do |method| + (Relation::SINGLE_VALUE_METHODS - [:lock, :reordering, :reverse_order, :create_with, :skip_query_cache]).each do |method| test "##{method}!" do assert relation.public_send("#{method}!", :foo).equal?(relation) assert_equal :foo, relation.public_send("#{method}_value") @@ -162,5 +162,10 @@ module ActiveRecord relation.distinct! :foo assert_equal :foo, relation.distinct_value end + + test "skip_query_cache!" do + relation.skip_query_cache! + assert relation.skip_query_cache_value + end end end diff --git a/activerecord/test/cases/relation/or_test.rb b/activerecord/test/cases/relation/or_test.rb index abb7ca72dd..61b6601580 100644 --- a/activerecord/test/cases/relation/or_test.rb +++ b/activerecord/test/cases/relation/or_test.rb @@ -59,6 +59,31 @@ module ActiveRecord assert_equal "Relation passed to #or must be structurally compatible. Incompatible values: [:order]", error.message end + def test_or_with_unscope_where + expected = Post.where("id = 1 or id = 2") + partial = Post.where("id = 1 and id != 2") + assert_equal expected, partial.or(partial.unscope(:where).where("id = 2")).to_a + end + + def test_or_with_unscope_where_column + expected = Post.where("id = 1 or id = 2") + partial = Post.where(id: 1).where.not(id: 2) + assert_equal expected, partial.or(partial.unscope(where: :id).where("id = 2")).to_a + end + + def test_or_with_unscope_order + expected = Post.where("id = 1 or id = 2") + assert_equal expected, Post.order("body asc").where("id = 1").unscope(:order).or(Post.where("id = 2")).to_a + end + + def test_or_with_incompatible_unscope + error = assert_raises ArgumentError do + Post.order("body asc").where("id = 1").or(Post.order("body asc").where("id = 2").unscope(:order)).to_a + end + + assert_equal "Relation passed to #or must be structurally compatible. Incompatible values: [:order]", error.message + end + def test_or_when_grouping groups = Post.where("id < 10").group("body").select("body, COUNT(*) AS c") expected = groups.having("COUNT(*) > 1 OR body like 'Such%'").to_a.map { |o| [o.body, o.c] } diff --git a/activerecord/test/cases/relation/where_test.rb b/activerecord/test/cases/relation/where_test.rb index cbc466d6b8..42dae4d569 100644 --- a/activerecord/test/cases/relation/where_test.rb +++ b/activerecord/test/cases/relation/where_test.rb @@ -15,7 +15,7 @@ require "models/vertex" module ActiveRecord class WhereTest < ActiveRecord::TestCase - fixtures :posts, :edges, :authors, :author_addresses, :binaries, :essays, :cars, :treasures, :price_estimates + fixtures :posts, :edges, :authors, :author_addresses, :binaries, :essays, :cars, :treasures, :price_estimates, :topics def test_where_copies_bind_params author = authors(:david) @@ -48,6 +48,10 @@ module ActiveRecord assert_equal [chef], chefs.to_a end + def test_where_with_casted_value_is_nil + assert_equal 4, Topic.where(last_read: "").count + end + def test_rewhere_on_root assert_equal posts(:welcome), Post.rewhere(title: "Welcome to the weblog").first end diff --git a/activerecord/test/cases/relation_test.rb b/activerecord/test/cases/relation_test.rb index e31e21e765..e4f68eef1b 100644 --- a/activerecord/test/cases/relation_test.rb +++ b/activerecord/test/cases/relation_test.rb @@ -7,7 +7,7 @@ require "models/rating" module ActiveRecord class RelationTest < ActiveRecord::TestCase - fixtures :posts, :comments, :authors, :author_addresses + fixtures :posts, :comments, :authors, :author_addresses, :ratings FakeKlass = Struct.new(:table_name, :name) do extend ActiveRecord::Delegation::DelegateCache @@ -225,7 +225,26 @@ module ActiveRecord def test_relation_merging_with_merged_joins_as_symbols special_comments_with_ratings = SpecialComment.joins(:ratings) posts_with_special_comments_with_ratings = Post.group("posts.id").joins(:special_comments).merge(special_comments_with_ratings) - assert_equal({ 2 => 1, 4 => 3, 5 => 1 }, authors(:david).posts.merge(posts_with_special_comments_with_ratings).count) + assert_equal({ 4 => 2 }, authors(:david).posts.merge(posts_with_special_comments_with_ratings).count) + end + + def test_relation_merging_with_merged_symbol_joins_keeps_inner_joins + queries = capture_sql { Author.joins(:posts).merge(Post.joins(:comments)).to_a } + + nb_inner_join = queries.sum { |sql| sql.scan(/INNER\s+JOIN/i).size } + assert_equal 2, nb_inner_join, "Wrong amount of INNER JOIN in query" + assert queries.none? { |sql| /LEFT\s+(OUTER)?\s+JOIN/i.match?(sql) }, "Shouldn't have any LEFT JOIN in query" + end + + def test_relation_merging_with_merged_symbol_joins_has_correct_size_and_count + # Has one entry per comment + merged_authors_with_commented_posts_relation = Author.joins(:posts).merge(Post.joins(:comments)) + + post_ids_with_author = Post.joins(:author).pluck(:id) + manual_comments_on_post_that_have_author = Comment.where(post_id: post_ids_with_author).pluck(:id) + + assert_equal manual_comments_on_post_that_have_author.size, merged_authors_with_commented_posts_relation.count + assert_equal manual_comments_on_post_that_have_author.size, merged_authors_with_commented_posts_relation.to_a.size end def test_relation_merging_with_joins_as_join_dependency_pick_proper_parent diff --git a/activerecord/test/cases/relations_test.rb b/activerecord/test/cases/relations_test.rb index 81173945a3..eb3449b331 100644 --- a/activerecord/test/cases/relations_test.rb +++ b/activerecord/test/cases/relations_test.rb @@ -695,16 +695,6 @@ class RelationTest < ActiveRecord::TestCase end end - def test_default_scope_with_conditions_string - assert_equal Developer.where(name: "David").map(&:id).sort, DeveloperCalledDavid.all.map(&:id).sort - assert_nil DeveloperCalledDavid.create!.name - end - - def test_default_scope_with_conditions_hash - assert_equal Developer.where(name: "Jamis").map(&:id).sort, DeveloperCalledJamis.all.map(&:id).sort - assert_equal "Jamis", DeveloperCalledJamis.create!.name - end - def test_default_scoping_finder_methods developers = DeveloperCalledDavid.order("id").map(&:id).sort assert_equal Developer.where(name: "David").map(&:id).sort, developers @@ -1727,6 +1717,9 @@ class RelationTest < ActiveRecord::TestCase scope = Post.order("comments.body") assert_equal ["comments"], scope.references_values + scope = Post.order("#{Comment.quoted_table_name}.#{Comment.quoted_primary_key}") + assert_equal ["comments"], scope.references_values + scope = Post.order("comments.body", "yaks.body") assert_equal ["comments", "yaks"], scope.references_values @@ -1745,6 +1738,9 @@ class RelationTest < ActiveRecord::TestCase scope = Post.reorder("comments.body") assert_equal %w(comments), scope.references_values + scope = Post.reorder("#{Comment.quoted_table_name}.#{Comment.quoted_primary_key}") + assert_equal ["comments"], scope.references_values + scope = Post.reorder("comments.body", "yaks.body") assert_equal %w(comments yaks), scope.references_values @@ -2011,6 +2007,12 @@ class RelationTest < ActiveRecord::TestCase assert_equal binds, merged.bound_attributes end + def test_locked_should_not_build_arel + posts = Post.locked + assert posts.locked? + assert_nothing_raised { posts.lock!(false) } + end + def test_relation_join_method assert_equal "Thank you for the welcome,Thank you again for the welcome", Post.first.comments.join(",") end @@ -2032,4 +2034,46 @@ class RelationTest < ActiveRecord::TestCase assert_equal 2, posts.to_a.length end + + test "#skip_query_cache!" do + Post.cache do + assert_queries(1) do + Post.all.load + Post.all.load + end + + assert_queries(2) do + Post.all.skip_query_cache!.load + Post.all.skip_query_cache!.load + end + end + end + + test "#skip_query_cache! with an eager load" do + Post.cache do + assert_queries(1) do + Post.eager_load(:comments).load + Post.eager_load(:comments).load + end + + assert_queries(2) do + Post.eager_load(:comments).skip_query_cache!.load + Post.eager_load(:comments).skip_query_cache!.load + end + end + end + + test "#skip_query_cache! with a preload" do + Post.cache do + assert_queries(2) do + Post.preload(:comments).load + Post.preload(:comments).load + end + + assert_queries(4) do + Post.preload(:comments).skip_query_cache!.load + Post.preload(:comments).skip_query_cache!.load + end + end + end end diff --git a/activerecord/test/cases/reserved_word_test.rb b/activerecord/test/cases/reserved_word_test.rb new file mode 100644 index 0000000000..f3019a5326 --- /dev/null +++ b/activerecord/test/cases/reserved_word_test.rb @@ -0,0 +1,132 @@ +require "cases/helper" + +class ReservedWordTest < ActiveRecord::TestCase + self.use_instantiated_fixtures = true + self.use_transactional_tests = false + + class Group < ActiveRecord::Base + Group.table_name = "group" + belongs_to :select + has_one :values + end + + class Select < ActiveRecord::Base + Select.table_name = "select" + has_many :groups + end + + class Values < ActiveRecord::Base + Values.table_name = "values" + end + + class Distinct < ActiveRecord::Base + Distinct.table_name = "distinct" + has_and_belongs_to_many :selects + has_many :values, through: :groups + end + + def setup + @connection = ActiveRecord::Base.connection + @connection.create_table :select, force: true + @connection.create_table :distinct, force: true + @connection.create_table :distinct_select, id: false, force: true do |t| + t.belongs_to :distinct + t.belongs_to :select + end + @connection.create_table :group, force: true do |t| + t.string :order + t.belongs_to :select + end + @connection.create_table :values, force: true do |t| + t.belongs_to :group + end + end + + def teardown + @connection.drop_table :select, if_exists: true + @connection.drop_table :distinct, if_exists: true + @connection.drop_table :distinct_select, if_exists: true + @connection.drop_table :group, if_exists: true + @connection.drop_table :values, if_exists: true + @connection.drop_table :order, if_exists: true + end + + def test_create_tables + assert_not @connection.table_exists?(:order) + + @connection.create_table :order do |t| + t.string :group + end + + assert @connection.table_exists?(:order) + end + + def test_rename_tables + assert_nothing_raised { @connection.rename_table(:group, :order) } + end + + def test_change_columns + assert_nothing_raised { @connection.change_column_default(:group, :order, "whatever") } + assert_nothing_raised { @connection.change_column("group", "order", :text, default: nil) } + assert_nothing_raised { @connection.rename_column(:group, :order, :values) } + end + + def test_introspect + assert_equal ["id", "order", "select_id"], @connection.columns(:group).map(&:name).sort + assert_equal ["index_group_on_select_id"], @connection.indexes(:group).map(&:name).sort + end + + def test_activerecord_model + x = Group.new + x.order = "x" + x.save! + x.order = "y" + x.save! + assert_equal x, Group.find_by_order("y") + assert_equal x, Group.find(x.id) + end + + def test_has_one_associations + create_test_fixtures :group, :values + v = Group.find(1).values + assert_equal 2, v.id + end + + def test_belongs_to_associations + create_test_fixtures :select, :group + gs = Select.find(2).groups + assert_equal 2, gs.length + assert_equal [2, 3], gs.collect(&:id).sort + end + + def test_has_and_belongs_to_many + create_test_fixtures :select, :distinct, :distinct_select + s = Distinct.find(1).selects + assert_equal 2, s.length + assert_equal [1, 2], s.collect(&:id).sort + end + + def test_activerecord_introspection + assert Group.table_exists? + assert_equal ["id", "order", "select_id"], Group.columns.map(&:name).sort + end + + def test_calculations_work_with_reserved_words + create_test_fixtures :group + assert_equal 3, Group.count + end + + def test_associations_work_with_reserved_words + create_test_fixtures :select, :group + selects = Select.all.merge!(includes: [:groups]).to_a + assert_no_queries do + selects.each { |select| select.groups } + end + end + + private + # custom fixture loader, uses FixtureSet#create_fixtures and appends base_path to the current file's path + def create_test_fixtures(*fixture_names) + ActiveRecord::FixtureSet.create_fixtures(FIXTURES_ROOT + "/reserved_words", fixture_names) + end +end diff --git a/activerecord/test/cases/schema_dumper_test.rb b/activerecord/test/cases/schema_dumper_test.rb index 417c6f4832..4c81e825fa 100644 --- a/activerecord/test/cases/schema_dumper_test.rb +++ b/activerecord/test/cases/schema_dumper_test.rb @@ -17,6 +17,12 @@ class SchemaDumperTest < ActiveRecord::TestCase dump_all_table_schema [] end + def test_dump_schema_information_with_empty_versions + ActiveRecord::SchemaMigration.delete_all + schema_info = ActiveRecord::Base.connection.dump_schema_information + assert_no_match(/INSERT INTO/, schema_info) + end + def test_dump_schema_information_outputs_lexically_ordered_versions versions = %w{ 20100101010101 20100201010101 20100301010101 } versions.reverse_each do |v| @@ -320,7 +326,7 @@ class SchemaDumperTest < ActiveRecord::TestCase def test_schema_dump_keeps_id_false_when_id_is_false_and_unique_not_null_column_added output = standard_dump - assert_match %r{create_table "subscribers", id: false}, output + assert_match %r{create_table "string_key_objects", id: false}, output end if ActiveRecord::Base.connection.supports_foreign_keys? diff --git a/activerecord/test/cases/scoping/relation_scoping_test.rb b/activerecord/test/cases/scoping/relation_scoping_test.rb index 8535be8402..10553bf057 100644 --- a/activerecord/test/cases/scoping/relation_scoping_test.rb +++ b/activerecord/test/cases/scoping/relation_scoping_test.rb @@ -229,6 +229,15 @@ class RelationScopingTest < ActiveRecord::TestCase end end + def test_scoping_is_correctly_restored + Comment.unscoped do + SpecialComment.unscoped.created + end + + assert_nil Comment.current_scope + assert_nil SpecialComment.current_scope + end + def test_circular_joins_with_scoping_does_not_crash posts = Post.joins(comments: :post).scoping do Post.first(10) diff --git a/activerecord/test/cases/tasks/mysql_rake_test.rb b/activerecord/test/cases/tasks/mysql_rake_test.rb index c22d974536..9c6fb14376 100644 --- a/activerecord/test/cases/tasks/mysql_rake_test.rb +++ b/activerecord/test/cases/tasks/mysql_rake_test.rb @@ -296,7 +296,7 @@ if current_adapter?(:Mysql2Adapter) def test_structure_dump_with_extra_flags filename = "awesome-file.sql" - expected_command = ["mysqldump", "--result-file", filename, "--no-data", "--routines", "--skip-comments", "--noop", "test-db"] + expected_command = ["mysqldump", "--noop", "--result-file", filename, "--no-data", "--routines", "--skip-comments", "test-db"] assert_called_with(Kernel, :system, expected_command, returns: true) do with_structure_dump_flags(["--noop"]) do @@ -364,7 +364,7 @@ if current_adapter?(:Mysql2Adapter) def test_structure_load filename = "awesome-file.sql" - expected_command = ["mysql", "--execute", %{SET FOREIGN_KEY_CHECKS = 0; SOURCE #{filename}; SET FOREIGN_KEY_CHECKS = 1}, "--database", "test-db", "--noop"] + expected_command = ["mysql", "--noop", "--execute", %{SET FOREIGN_KEY_CHECKS = 0; SOURCE #{filename}; SET FOREIGN_KEY_CHECKS = 1}, "--database", "test-db"] assert_called_with(Kernel, :system, expected_command, returns: true) do with_structure_load_flags(["--noop"]) do diff --git a/activerecord/test/cases/transactions_test.rb b/activerecord/test/cases/transactions_test.rb index 5c6d78b574..76a997ba8b 100644 --- a/activerecord/test/cases/transactions_test.rb +++ b/activerecord/test/cases/transactions_test.rb @@ -304,6 +304,76 @@ class TransactionTest < ActiveRecord::TestCase assert !Topic.find(2).approved?, "Second should have been unapproved" end + def test_nested_transaction_with_new_transaction_applies_parent_state_on_rollback + topic_one = Topic.new(title: "A new topic") + topic_two = Topic.new(title: "Another new topic") + + Topic.transaction do + topic_one.save + + Topic.transaction(requires_new: true) do + topic_two.save + + assert_predicate topic_one, :persisted? + assert_predicate topic_two, :persisted? + end + + raise ActiveRecord::Rollback + end + + refute_predicate topic_one, :persisted? + refute_predicate topic_two, :persisted? + end + + def test_nested_transaction_without_new_transaction_applies_parent_state_on_rollback + topic_one = Topic.new(title: "A new topic") + topic_two = Topic.new(title: "Another new topic") + + Topic.transaction do + topic_one.save + + Topic.transaction do + topic_two.save + + assert_predicate topic_one, :persisted? + assert_predicate topic_two, :persisted? + end + + raise ActiveRecord::Rollback + end + + refute_predicate topic_one, :persisted? + refute_predicate topic_two, :persisted? + end + + def test_double_nested_transaction_applies_parent_state_on_rollback + topic_one = Topic.new(title: "A new topic") + topic_two = Topic.new(title: "Another new topic") + topic_three = Topic.new(title: "Another new topic of course") + + Topic.transaction do + topic_one.save + + Topic.transaction do + topic_two.save + + Topic.transaction do + topic_three.save + end + end + + assert_predicate topic_one, :persisted? + assert_predicate topic_two, :persisted? + assert_predicate topic_three, :persisted? + + raise ActiveRecord::Rollback + end + + refute_predicate topic_one, :persisted? + refute_predicate topic_two, :persisted? + refute_predicate topic_three, :persisted? + end + def test_manually_rolling_back_a_transaction Topic.transaction do @first.approved = true @@ -595,6 +665,52 @@ class TransactionTest < ActiveRecord::TestCase assert_not topic.frozen? end + def test_restore_id_after_rollback + topic = Topic.new + + Topic.transaction do + topic.save! + raise ActiveRecord::Rollback + end + + assert_nil topic.id + end + + def test_restore_custom_primary_key_after_rollback + movie = Movie.new(name: "foo") + + Movie.transaction do + movie.save! + raise ActiveRecord::Rollback + end + + assert_nil movie.id + end + + def test_assign_id_after_rollback + topic = Topic.create! + + Topic.transaction do + topic.save! + raise ActiveRecord::Rollback + end + + topic.id = nil + assert_nil topic.id + end + + def test_assign_custom_primary_key_after_rollback + movie = Movie.create!(name: "foo") + + Movie.transaction do + movie.save! + raise ActiveRecord::Rollback + end + + movie.id = nil + assert_nil movie.id + end + def test_rollback_of_frozen_records topic = Topic.create.freeze Topic.transaction do @@ -679,6 +795,44 @@ class TransactionTest < ActiveRecord::TestCase assert transaction.state.committed? end + def test_set_state_method_is_deprecated + connection = Topic.connection + transaction = ActiveRecord::ConnectionAdapters::TransactionManager.new(connection).begin_transaction + + transaction.commit + + assert_deprecated do + transaction.state.set_state(:rolledback) + end + end + + def test_mark_transaction_state_as_committed + connection = Topic.connection + transaction = ActiveRecord::ConnectionAdapters::TransactionManager.new(connection).begin_transaction + + transaction.rollback + + assert_equal :committed, transaction.state.commit! + end + + def test_mark_transaction_state_as_rolledback + connection = Topic.connection + transaction = ActiveRecord::ConnectionAdapters::TransactionManager.new(connection).begin_transaction + + transaction.commit + + assert_equal :rolledback, transaction.state.rollback! + end + + def test_mark_transaction_state_as_nil + connection = Topic.connection + transaction = ActiveRecord::ConnectionAdapters::TransactionManager.new(connection).begin_transaction + + transaction.commit + + assert_nil transaction.state.nullify! + end + def test_transaction_rollback_with_primarykeyless_tables connection = ActiveRecord::Base.connection connection.create_table(:transaction_without_primary_keys, force: true, id: false) do |t| diff --git a/activerecord/test/cases/yaml_serialization_test.rb b/activerecord/test/cases/yaml_serialization_test.rb index bfc13d683d..dd8d2c1178 100644 --- a/activerecord/test/cases/yaml_serialization_test.rb +++ b/activerecord/test/cases/yaml_serialization_test.rb @@ -119,6 +119,14 @@ class YamlSerializationTest < ActiveRecord::TestCase assert_equal author.changes, dumped.changes end + def test_yaml_encoding_keeps_false_values + topic = Topic.first + topic.approved = false + dumped = YAML.load(YAML.dump(topic)) + + assert_equal false, dumped.approved + end + private def yaml_fixture(file_name) diff --git a/activerecord/test/fixtures/naked/yml/parrots.yml b/activerecord/test/fixtures/naked/yml/parrots.yml index 3e10331105..76f66e01ae 100644 --- a/activerecord/test/fixtures/naked/yml/parrots.yml +++ b/activerecord/test/fixtures/naked/yml/parrots.yml @@ -1,2 +1,3 @@ george: arrr: "Curious George" + foobar: Foobar diff --git a/activerecord/test/migrations/magic/1_currencies_have_symbols.rb b/activerecord/test/migrations/magic/1_currencies_have_symbols.rb index d4b0e6cd95..2ba2875751 100644 --- a/activerecord/test/migrations/magic/1_currencies_have_symbols.rb +++ b/activerecord/test/migrations/magic/1_currencies_have_symbols.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true # coding: ISO-8859-15 class CurrenciesHaveSymbols < ActiveRecord::Migration::Current diff --git a/activerecord/test/models/club.rb b/activerecord/test/models/club.rb index 49d7b24a3b..3d441b1d48 100644 --- a/activerecord/test/models/club.rb +++ b/activerecord/test/models/club.rb @@ -8,6 +8,8 @@ class Club < ActiveRecord::Base has_many :favourites, -> { where(memberships: { favourite: true }) }, through: :memberships, source: :member + scope :general, -> { left_joins(:category).where(categories: { name: "General" }) } + private def private_method diff --git a/activerecord/test/models/comment.rb b/activerecord/test/models/comment.rb index d227f6fe86..eecf923046 100644 --- a/activerecord/test/models/comment.rb +++ b/activerecord/test/models/comment.rb @@ -9,7 +9,6 @@ class Comment < ActiveRecord::Base belongs_to :post, counter_cache: true belongs_to :author, polymorphic: true belongs_to :resource, polymorphic: true - belongs_to :developer has_many :ratings diff --git a/activerecord/test/models/member.rb b/activerecord/test/models/member.rb index 36f2461b84..b9597c6b9a 100644 --- a/activerecord/test/models/member.rb +++ b/activerecord/test/models/member.rb @@ -22,6 +22,7 @@ class Member < ActiveRecord::Base has_many :organization_member_details_2, through: :organization, source: :member_details has_one :club_category, through: :club, source: :category + has_one :general_club, -> { general }, through: :current_membership, source: :club has_many :current_memberships, -> { where favourite: true } has_many :clubs, through: :current_memberships diff --git a/activerecord/test/models/membership.rb b/activerecord/test/models/membership.rb index 2c3ad230a7..0f8be0ad85 100644 --- a/activerecord/test/models/membership.rb +++ b/activerecord/test/models/membership.rb @@ -1,4 +1,5 @@ class Membership < ActiveRecord::Base + enum type: %i(Membership CurrentMembership SuperMembership SelectedMembership TenantMembership) belongs_to :member belongs_to :club end diff --git a/activerecord/test/models/post.rb b/activerecord/test/models/post.rb index 4c913b3b72..ed64e0ee52 100644 --- a/activerecord/test/models/post.rb +++ b/activerecord/test/models/post.rb @@ -22,6 +22,7 @@ class Post < ActiveRecord::Base scope :ranked_by_comments, -> { order("comments_count DESC") } scope :limit_by, lambda { |l| limit(l) } + scope :locked, -> { lock } belongs_to :author belongs_to :readonly_author, -> { readonly }, class_name: "Author", foreign_key: :author_id diff --git a/activerecord/test/models/subject.rb b/activerecord/test/models/subject.rb deleted file mode 100644 index 504f68a296..0000000000 --- a/activerecord/test/models/subject.rb +++ /dev/null @@ -1,14 +0,0 @@ -# used for OracleSynonymTest, see test/synonym_test_oracle.rb -# -class Subject < ActiveRecord::Base - # added initialization of author_email_address in the same way as in Topic class - # as otherwise synonym test was failing - after_initialize :set_email_address - - private - def set_email_address - unless persisted? - self.author_email_address = "test@test.com" - end - end -end diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb index 50f1d9bfe7..f534e9c00e 100644 --- a/activerecord/test/schema/schema.rb +++ b/activerecord/test/schema/schema.rb @@ -107,7 +107,7 @@ ActiveRecord::Schema.define do t.boolean :has_fun, null: false, default: false end - create_table :bulbs, force: true do |t| + create_table :bulbs, primary_key: "ID", force: true do |t| t.integer :car_id t.string :name t.boolean :frickinawesome, default: false @@ -453,11 +453,13 @@ ActiveRecord::Schema.define do create_table :lock_without_defaults, force: true do |t| t.column :title, :string t.column :lock_version, :integer + t.timestamps null: true end create_table :lock_without_defaults_cust, force: true do |t| t.column :title, :string t.column :custom_lock_version, :integer + t.timestamps null: true end create_table :magazines, force: true do |t| @@ -489,7 +491,7 @@ ActiveRecord::Schema.define do t.datetime :joined_on t.integer :club_id, :member_id t.boolean :favourite, default: false - t.string :type + t.integer :type end create_table :member_types, force: true do |t| @@ -807,16 +809,19 @@ ActiveRecord::Schema.define do t.string :sponsorable_type end - create_table :string_key_objects, id: false, primary_key: :id, force: true do |t| - t.string :id - t.string :name - t.integer :lock_version, null: false, default: 0 + create_table :string_key_objects, id: false, force: true do |t| + t.string :id, null: false + t.string :name + t.integer :lock_version, null: false, default: 0 + t.index :id, unique: true end - create_table :subscribers, force: true, id: false do |t| + create_table :subscribers, id: false, force: true do |t| t.string :nick, null: false t.string :name - t.column :books_count, :integer, null: false, default: 0 + t.integer :id + t.integer :books_count, null: false, default: 0 + t.integer :update_count, null: false, default: 0 t.index :nick, unique: true end |