diff options
Diffstat (limited to 'activerecord')
156 files changed, 1916 insertions, 1499 deletions
diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index 4e264f5f2a..7e8ca51f25 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,4 +1,73 @@ -* Respect 'SchemaDumper.ignore_tables' in rake tasks for databases structure dump +* 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* + +* Loading model schema from database is now thread-safe. + + Fixes #28589. + + *Vikrant Chaudhary*, *David Abdemoulaie* + +* Add `ActiveRecord::Base#cache_version` to support recyclable cache keys via the new versioned entries + in `ActiveSupport::Cache`. This also means that `ActiveRecord::Base#cache_key` will now return a stable key + that does not include a timestamp any more. + + NOTE: This feature is turned off by default, and `#cache_key` will still return cache keys with timestamps + until you set `ActiveRecord::Base.cache_versioning = true`. That's the setting for all new apps on Rails 5.2+ + + *DHH* + +* Respect `SchemaDumper.ignore_tables` in rake tasks for databases structure dump *Rusty Geldmacher*, *Guillermo Iguaran* diff --git a/activerecord/Rakefile b/activerecord/Rakefile index 7be3d851f1..fe5f9d1071 100644 --- a/activerecord/Rakefile +++ b/activerecord/Rakefile @@ -1,7 +1,7 @@ require "rake/testtask" -require File.expand_path(File.dirname(__FILE__)) + "/test/config" -require File.expand_path(File.dirname(__FILE__)) + "/test/support/config" +require_relative "test/config" +require_relative "test/support/config" def run_without_aborting(*tasks) errors = [] @@ -134,7 +134,7 @@ task drop_postgresql_databases: "db:postgresql:drop" task rebuild_postgresql_databases: "db:postgresql:rebuild" task :lines do - load File.expand_path("..", File.dirname(__FILE__)) + "/tools/line_statistics" + load File.expand_path("../tools/line_statistics", __dir__) files = FileList["lib/active_record/**/*.rb"] CodeTools::LineStatistics.new(files).print_loc end diff --git a/activerecord/activerecord.gemspec b/activerecord/activerecord.gemspec index 0b37e9076c..a626a1f21b 100644 --- a/activerecord/activerecord.gemspec +++ b/activerecord/activerecord.gemspec @@ -1,4 +1,4 @@ -version = File.read(File.expand_path("../../RAILS_VERSION", __FILE__)).strip +version = File.read(File.expand_path("../RAILS_VERSION", __dir__)).strip Gem::Specification.new do |s| s.platform = Gem::Platform::RUBY @@ -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 96b8545dfc..29f49c6195 100644 --- a/activerecord/lib/active_record.rb +++ b/activerecord/lib/active_record.rb @@ -177,5 +177,5 @@ ActiveSupport.on_load(:active_record) do end ActiveSupport.on_load(:i18n) do - I18n.load_path << File.dirname(__FILE__) + "/active_record/locale/en.yml" + I18n.load_path << File.expand_path("active_record/locale/en.yml", __dir__) end diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index 7c37132d3a..f05a122544 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -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 3963008a76..104de4f69d 100644 --- a/activerecord/lib/active_record/associations/alias_tracker.rb +++ b/activerecord/lib/active_record/associations/alias_tracker.rb @@ -4,23 +4,21 @@ module ActiveRecord module Associations # Keeps track of table aliases for ActiveRecord::Associations::JoinDependency class AliasTracker # :nodoc: - attr_reader :aliases - - 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 @@ -53,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) @@ -76,10 +73,15 @@ 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 + # TODO Change this to private once we've dropped Ruby 2.2 support. + # Workaround for Ruby 2.2 "private attribute?" warning. + protected + attr_reader :aliases + private def truncate(name) diff --git a/activerecord/lib/active_record/associations/association.rb b/activerecord/lib/active_record/associations/association.rb index 1cb2b2d7c6..44cf1f8915 100644 --- a/activerecord/lib/active_record/associations/association.rb +++ b/activerecord/lib/active_record/associations/association.rb @@ -94,7 +94,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 @@ -133,6 +133,16 @@ module ActiveRecord AssociationRelation.create(klass, klass.arel_table, klass.predicate_builder, self).merge!(klass.all) end + def extensions + extensions = klass.default_extensions | reflection.extensions + + if scope = reflection.scope + extensions |= klass.unscoped.instance_exec(owner, &scope).extensions + end + + extensions + end + # Loads the \target if needed and returns it. # # This method is abstract in the sense that it relies on +find_target+, @@ -152,14 +162,6 @@ module ActiveRecord reset end - def interpolate(sql, record = nil) - if sql.respond_to?(:to_proc) - owner.instance_exec(record, &sql) - else - sql - end - end - # We can't dump @reflection since it contains the scope proc def marshal_dump ivars = (instance_variables - [:@reflection]).map { |name| [name, instance_variable_get(name)] } @@ -274,7 +276,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 120d75416c..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,15 +16,15 @@ 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! Array(reflection.options[:extend]) + scope.extending! reflection.extensions add_constraints(scope, owner, reflection, chain_head, chain_tail) end @@ -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/collection_association.rb b/activerecord/lib/active_record/associations/collection_association.rb index 62c944fce3..bbf3dbb75e 100644 --- a/activerecord/lib/active_record/associations/collection_association.rb +++ b/activerecord/lib/active_record/associations/collection_association.rb @@ -30,7 +30,8 @@ module ActiveRecord reload end - CollectionProxy.create(klass, self) + @proxy ||= CollectionProxy.create(klass, self) + @proxy.reset_scope end # Implements the writer method, e.g. foo.items= for Foo.has_many :items @@ -43,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 @@ -299,13 +297,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 diff --git a/activerecord/lib/active_record/associations/collection_proxy.rb b/activerecord/lib/active_record/associations/collection_proxy.rb index 74a4d515c2..d77fcaf668 100644 --- a/activerecord/lib/active_record/associations/collection_proxy.rb +++ b/activerecord/lib/active_record/associations/collection_proxy.rb @@ -31,6 +31,9 @@ module ActiveRecord def initialize(klass, association) #:nodoc: @association = association super klass, klass.arel_table, klass.predicate_builder + + extensions = association.extensions + extend(*extensions) if extensions.any? end def target @@ -1084,9 +1087,8 @@ module ActiveRecord # person.pets(true) # fetches pets from the database # # => [#<Pet id: 1, name: "Snoop", group: "dogs", person_id: 1>] def reload - @scope = nil proxy_association.reload - self + reset_scope end # Unloads the association. Returns +self+. @@ -1106,9 +1108,14 @@ module ActiveRecord # person.pets # fetches pets from the database # # => [#<Pet id: 1, name: "Snoop", group: "dogs", person_id: 1>] def reset - @scope = nil proxy_association.reset proxy_association.reset_scope + reset_scope + end + + def reset_scope # :nodoc: + @offsets = {} + @scope = nil self end @@ -1121,19 +1128,6 @@ module ActiveRecord delegate(*delegate_methods, to: :scope) - module DelegateExtending # :nodoc: - private - def method_missing(method, *args, &block) - extending_values = association_scope.extending_values - if extending_values.any? && (extending_values - self.class.included_modules).any? - self.class.include(*extending_values) - public_send(method, *args, &block) - else - super - end - end - end - private def find_nth_with_limit(index, limit) @@ -1154,17 +1148,9 @@ module ActiveRecord @association.find_from_target? end - def association_scope - @association.association_scope - end - def exec_queries load_target end - - def respond_to_missing?(method, _) - association_scope.respond_to?(method) || super - end end end end diff --git a/activerecord/lib/active_record/associations/has_many_through_association.rb b/activerecord/lib/active_record/associations/has_many_through_association.rb index 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 8995b1e352..bc66194aef 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,22 +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| - - if join_type == Arel::Nodes::OuterJoin - make_left_outer_joins join_root, child - else - make_inner_joins join_root, child - end + make_join_constraints(join_root, child, join_type) } - joins.concat outer_joins.flat_map { |oj| + 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 } @@ -175,34 +170,23 @@ module ActiveRecord end def make_outer_joins(parent, child) - tables = table_aliases_for(parent, child) - join_type = Arel::Nodes::OuterJoin - info = make_constraints parent, child, tables, join_type - - [info] + child.children.flat_map { |c| make_outer_joins(child, c) } - end - - def make_left_outer_joins(parent, child) - tables = child.tables join_type = Arel::Nodes::OuterJoin - info = make_constraints parent, child, tables, join_type - - [info] + child.children.flat_map { |c| make_left_outer_joins(child, c) } + make_join_constraints(parent, child, join_type, true) end - def make_inner_joins(parent, child) - tables = child.tables - join_type = Arel::Nodes::InnerJoin - info = make_constraints parent, child, tables, join_type + def make_join_constraints(parent, child, join_type, aliasing = false) + tables = aliasing ? table_aliases_for(parent, child) : child.tables + info = make_constraints(parent, child, tables, join_type) - [info] + child.children.flat_map { |c| make_inner_joins(child, c) } + [info] + child.children.flat_map { |c| make_join_constraints(child, c, join_type, aliasing) } end def table_aliases_for(parent, node) 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 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..005410d598 100644 --- a/activerecord/lib/active_record/associations/join_dependency/join_association.rb +++ b/activerecord/lib/active_record/associations/join_dependency/join_association.rb @@ -34,34 +34,10 @@ module ActiveRecord table = tables.shift klass = reflection.klass - join_keys = reflection.join_keys - key = join_keys.key - foreign_key = join_keys.foreign_key + join_scope = reflection.join_scope(table, foreign_table, foreign_klass) - 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 - - if reflection.type - value = foreign_klass.base_class.name - column = klass.columns_hash[reflection.type.to_s] - - 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) - end + binds.concat join_scope.bound_attributes + constraint = join_scope.arel.constraints joins << table.create_join(table, table.create_on(constraint), join_type) @@ -72,34 +48,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_part.rb b/activerecord/lib/active_record/associations/join_dependency/join_part.rb index 61cec5403a..80c9fde5d1 100644 --- a/activerecord/lib/active_record/associations/join_dependency/join_part.rb +++ b/activerecord/lib/active_record/associations/join_dependency/join_part.rb @@ -22,10 +22,6 @@ module ActiveRecord @children = children end - def name - reflection.name - end - def match?(other) self.class == other.class end diff --git a/activerecord/lib/active_record/associations/preloader.rb b/activerecord/lib/active_record/associations/preloader.rb index 9f77f38b35..208d1b2670 100644 --- a/activerecord/lib/active_record/associations/preloader.rb +++ b/activerecord/lib/active_record/associations/preloader.rb @@ -147,7 +147,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 +159,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 +181,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 4072d19380..63ef3f2d8c 100644 --- a/activerecord/lib/active_record/associations/preloader/association.rb +++ b/activerecord/lib/active_record/associations/preloader/association.rb @@ -51,11 +51,10 @@ module ActiveRecord raise NotImplementedError end - def options - reflection.options - end - private + def options + reflection.options + end def associated_records_by_owner(preloader) records = load_records do |record| diff --git a/activerecord/lib/active_record/associations/preloader/belongs_to.rb b/activerecord/lib/active_record/associations/preloader/belongs_to.rb index 38e231826c..c20145770f 100644 --- a/activerecord/lib/active_record/associations/preloader/belongs_to.rb +++ b/activerecord/lib/active_record/associations/preloader/belongs_to.rb @@ -3,7 +3,7 @@ module ActiveRecord class Preloader class BelongsTo < SingularAssociation #:nodoc: def association_key_name - reflection.options[:primary_key] || klass && klass.primary_key + options[:primary_key] || klass && klass.primary_key end def owner_key_name diff --git a/activerecord/lib/active_record/associations/preloader/through_association.rb b/activerecord/lib/active_record/associations/preloader/through_association.rb index 0749b038b6..34587fd3f5 100644 --- a/activerecord/lib/active_record/associations/preloader/through_association.rb +++ b/activerecord/lib/active_record/associations/preloader/through_association.rb @@ -67,7 +67,7 @@ module ActiveRecord def reset_association(owners, association_name, through_scope) should_reset = (through_scope != through_reflection.klass.unscoped) || - (reflection.options[:source_type] && through_reflection.collection?) + (options[:source_type] && through_reflection.collection?) # Don't cache the association - we would only be caching a subset if should_reset 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_decorators.rb b/activerecord/lib/active_record/attribute_decorators.rb index c39e9ce4c5..5bc8527745 100644 --- a/activerecord/lib/active_record/attribute_decorators.rb +++ b/activerecord/lib/active_record/attribute_decorators.rb @@ -3,8 +3,7 @@ module ActiveRecord extend ActiveSupport::Concern included do - class_attribute :attribute_type_decorations, instance_accessor: false # :internal: - self.attribute_type_decorations = TypeDecorator.new + class_attribute :attribute_type_decorations, instance_accessor: false, default: TypeDecorator.new # :internal: end module ClassMethods # :nodoc: diff --git a/activerecord/lib/active_record/attribute_methods.rb b/activerecord/lib/active_record/attribute_methods.rb index ebe06566cc..83c61fad19 100644 --- a/activerecord/lib/active_record/attribute_methods.rb +++ b/activerecord/lib/active_record/attribute_methods.rb @@ -62,7 +62,6 @@ module ActiveRecord super(attribute_names) @attribute_methods_generated = true end - true end def undefine_attribute_methods # :nodoc: diff --git a/activerecord/lib/active_record/attribute_methods/dirty.rb b/activerecord/lib/active_record/attribute_methods/dirty.rb index bd5003d63a..76987fb8f4 100644 --- a/activerecord/lib/active_record/attribute_methods/dirty.rb +++ b/activerecord/lib/active_record/attribute_methods/dirty.rb @@ -14,8 +14,7 @@ module ActiveRecord raise "You cannot include Dirty after Timestamp" end - class_attribute :partial_writes, instance_writer: false - self.partial_writes = true + class_attribute :partial_writes, instance_writer: false, default: true after_create { changes_internally_applied } after_update { changes_internally_applied } diff --git a/activerecord/lib/active_record/attribute_methods/primary_key.rb b/activerecord/lib/active_record/attribute_methods/primary_key.rb index 2f32caa257..b9b2acff37 100644 --- a/activerecord/lib/active_record/attribute_methods/primary_key.rb +++ b/activerecord/lib/active_record/attribute_methods/primary_key.rb @@ -8,17 +8,14 @@ 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. @@ -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 321d039ed4..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,14 +54,10 @@ 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 - self.skip_time_zone_conversion_for_attributes = [] - - class_attribute :time_zone_aware_types, instance_writer: false - self.time_zone_aware_types = [:datetime, :time] + class_attribute :skip_time_zone_conversion_for_attributes, instance_writer: false, default: [] + class_attribute :time_zone_aware_types, instance_writer: false, default: [ :datetime, :time ] end module ClassMethods diff --git a/activerecord/lib/active_record/attribute_methods/write.rb b/activerecord/lib/active_record/attribute_methods/write.rb index fe0e01db28..75c5a1a600 100644 --- a/activerecord/lib/active_record/attribute_methods/write.rb +++ b/activerecord/lib/active_record/attribute_methods/write.rb @@ -35,11 +35,15 @@ 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 + @attributes.write_from_user(name, value) + value end def raw_write_attribute(attr_name, value) # :nodoc: - write_attribute_with_type_cast(attr_name, value, false) + name = attr_name.to_s + @attributes.write_cast_value(name, value) + value end private @@ -47,19 +51,6 @@ module ActiveRecord def attribute=(attribute_name, value) write_attribute(attribute_name, 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 - end end end end diff --git a/activerecord/lib/active_record/attribute_set.rb b/activerecord/lib/active_record/attribute_set.rb index 66b278219a..01f9d815d5 100644 --- a/activerecord/lib/active_record/attribute_set.rb +++ b/activerecord/lib/active_record/attribute_set.rb @@ -64,7 +64,7 @@ module ActiveRecord end def deep_dup - dup.tap do |copy| + self.class.allocate.tap do |copy| copy.instance_variable_set(:@attributes, attributes.deep_dup) end end diff --git a/activerecord/lib/active_record/attributes.rb b/activerecord/lib/active_record/attributes.rb index 75f5ba3a96..475b9beec4 100644 --- a/activerecord/lib/active_record/attributes.rb +++ b/activerecord/lib/active_record/attributes.rb @@ -6,8 +6,7 @@ module ActiveRecord extend ActiveSupport::Concern included do - class_attribute :attributes_to_define_after_schema_loads, instance_accessor: false # :internal: - self.attributes_to_define_after_schema_loads = {} + class_attribute :attributes_to_define_after_schema_loads, instance_accessor: false, default: {} # :internal: end module ClassMethods diff --git a/activerecord/lib/active_record/autosave_association.rb b/activerecord/lib/active_record/autosave_association.rb index ebe92e7878..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: @@ -216,13 +215,7 @@ module ActiveRecord method = :validate_single_association end - define_non_cyclic_method(validation_method) do - send(method, reflection) - # TODO: remove the following line as soon as the return value of - # callbacks is ignored, that is, returning `false` does not - # display a deprecation warning or halts the callback chain. - true - end + define_non_cyclic_method(validation_method) { send(method, reflection) } validate validation_method after_validation :_ensure_no_duplicate_errors end @@ -369,7 +362,6 @@ module ActiveRecord # association whether or not the parent was a new record before saving. def before_save_collection_association @new_record_before_save = new_record? - true end def after_save_collection_association 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..af5314c1d6 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb @@ -296,6 +296,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 +311,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 @@ -381,6 +420,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..33695c0537 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb @@ -123,6 +123,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..93f4529202 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb @@ -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_definitions.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb index 46d7f84efd..a30fbe0e05 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb @@ -146,7 +146,7 @@ module ActiveRecord end def polymorphic_options - as_options(polymorphic) + as_options(polymorphic).merge(null: options[:null]) end def index_options diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb index 13629dee7f..22d7791dec 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb @@ -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: @@ -1280,9 +1298,10 @@ module ActiveRecord end def foreign_key_name(table_name, options) - identifier = "#{table_name}_#{options.fetch(:column)}_fk" - hashed_identifier = Digest::SHA256.hexdigest(identifier).first(10) options.fetch(:name) do + identifier = "#{table_name}_#{options.fetch(:column)}_fk" + hashed_identifier = Digest::SHA256.hexdigest(identifier).first(10) + "fk_rails_#{hashed_identifier}" end end 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 abc15f595f..c42e80ea2c 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb @@ -29,8 +29,7 @@ module ActiveRecord # to your application.rb file: # # ActiveRecord::ConnectionAdapters::Mysql2Adapter.emulate_booleans = false - class_attribute :emulate_booleans - self.emulate_booleans = true + class_attribute :emulate_booleans, default: true NATIVE_DATABASE_TYPES = { primary_key: "bigint auto_increment PRIMARY KEY", @@ -527,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 @@ -544,7 +560,7 @@ module ActiveRecord m.register_type %r(longblob)i, Type::Binary.new(limit: 2**32 - 1) m.register_type %r(^float)i, Type::Float.new(limit: 24) m.register_type %r(^double)i, Type::Float.new(limit: 53) - m.register_type %r(^json)i, MysqlJson.new + m.register_type %r(^json)i, Type::Json.new register_integer_type m, %r(^bigint)i, limit: 8 register_integer_type m, %r(^int)i, limit: 4 @@ -838,12 +854,7 @@ module ActiveRecord end end - class MysqlJson < Type::Internal::AbstractJson # :nodoc: - def changed_in_place?(raw_old_value, new_value) - # Normalization is required because MySQL JSON data format includes - # the space between the elements. - super(serialize(deserialize(raw_old_value)), new_value) - end + class MysqlJson < Type::Json # :nodoc: end class MysqlString < Type::String # :nodoc: @@ -866,7 +877,6 @@ module ActiveRecord end end - ActiveRecord::Type.register(:json, MysqlJson, adapter: :mysql2) ActiveRecord::Type.register(:string, MysqlString, adapter: :mysql2) ActiveRecord::Type.register(:unsigned_integer, Type::UnsignedInteger, adapter: :mysql2) end 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..9f1021456b 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql/database_statements.rb @@ -13,15 +13,6 @@ 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 - end - # Executes the SQL statement in the context of this connection. def execute(sql, name = nil) # make sure we carry over any changes to ActiveRecord::Base.default_timezone that have been @@ -58,16 +49,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_statements.rb b/activerecord/lib/active_record/connection_adapters/mysql/schema_statements.rb index f9e1e046ea..fc57e41fc9 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") 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..ac5efbebeb 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. @@ -175,14 +151,6 @@ module ActiveRecord 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/json.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/json.rb index dbc879ffd4..3c706c27c4 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/json.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/json.rb @@ -2,7 +2,7 @@ module ActiveRecord module ConnectionAdapters module PostgreSQL module OID # :nodoc: - class Json < Type::Internal::AbstractJson + class Json < Type::Json # :nodoc: end end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/jsonb.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/jsonb.rb index 87391b5dc7..a1fec289d4 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/jsonb.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/jsonb.rb @@ -2,20 +2,10 @@ module ActiveRecord module ConnectionAdapters module PostgreSQL module OID # :nodoc: - class Jsonb < Json # :nodoc: + class Jsonb < Type::Json # :nodoc: def type :jsonb end - - def changed_in_place?(raw_old_value, new_value) - # Postgres does not preserve insignificant whitespaces when - # round-tripping jsonb columns. This causes some false positives for - # the comparison here. Therefore, we need to parse and re-dump the - # raw value here to ensure the insignificant whitespaces are - # consistent with our encoder's output. - raw_old_value = serialize(deserialize(raw_old_value)) - super(raw_old_value, new_value) - end end end end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb index da8d0c6992..44eb666965 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb @@ -62,7 +62,7 @@ module ActiveRecord def quote_default_expression(value, column) # :nodoc: if value.is_a?(Proc) value.call - elsif column.type == :uuid && value.include?("()") + elsif column.type == :uuid && /\(\)/.match?(value) value # Does not quote function default values for UUID columns elsif column.respond_to?(:array?) value = type_cast_from_column(column, 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 5b483ad4ab..cb45d7ba6b 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb @@ -186,17 +186,17 @@ module ActiveRecord # Returns the current database encoding format. def encoding - select_value("SELECT pg_encoding_to_char(encoding) FROM pg_database WHERE datname LIKE '#{current_database}'", "SCHEMA") + select_value("SELECT pg_encoding_to_char(encoding) FROM pg_database WHERE datname = current_database()", "SCHEMA") end # Returns the current database collation. def collation - select_value("SELECT datcollate FROM pg_database WHERE datname LIKE '#{current_database}'", "SCHEMA") + select_value("SELECT datcollate FROM pg_database WHERE datname = current_database()", "SCHEMA") end # Returns the current database ctype. def ctype - select_value("SELECT datctype FROM pg_database WHERE datname LIKE '#{current_database}'", "SCHEMA") + select_value("SELECT datctype FROM pg_database WHERE datname = current_database()", "SCHEMA") end # Returns an array of schema names. @@ -290,9 +290,17 @@ 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)}") + if max_pk.nil? + if postgresql_version >= 100000 + minvalue = select_value("SELECT seqmin from pg_sequence where seqrelid = '#{quoted_sequence}'::regclass") + else + minvalue = select_value("SELECT min_value FROM #{quoted_sequence}") + end + end select_value(<<-end_sql, "SCHEMA") - SELECT setval('#{quoted_sequence}', (SELECT COALESCE(MAX(#{quote_column_name pk})+(SELECT increment_by FROM #{quoted_sequence}), (SELECT min_value FROM #{quoted_sequence})) FROM #{quote_table_name(table)}), false) + SELECT setval('#{quoted_sequence}', #{max_pk ? max_pk : minvalue}, #{max_pk ? true : false}) end_sql end end @@ -377,14 +385,15 @@ module ActiveRecord clear_cache! execute "ALTER TABLE #{quote_table_name(table_name)} RENAME TO #{quote_table_name(new_name)}" pk, seq = pk_and_sequence_for(new_name) - if seq && seq.identifier == "#{table_name}_#{pk}_seq" - new_seq = "#{new_name}_#{pk}_seq" + if pk idx = "#{table_name}_pkey" new_idx = "#{new_name}_pkey" - execute "ALTER TABLE #{seq.quoted} RENAME TO #{quote_table_name(new_seq)}" execute "ALTER INDEX #{quote_table_name(idx)} RENAME TO #{quote_table_name(new_idx)}" + if seq && seq.identifier == "#{table_name}_#{pk}_seq" + new_seq = "#{new_name}_#{pk}_seq" + execute "ALTER TABLE #{seq.quoted} RENAME TO #{quote_table_name(new_seq)}" + end end - rename_table_indexes(table_name, new_name) end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index f74f966fd9..5287dd6a51 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -458,7 +458,7 @@ module ActiveRecord m.register_type "bytea", OID::Bytea.new m.register_type "point", OID::Point.new m.register_type "hstore", OID::Hstore.new - m.register_type "json", OID::Json.new + m.register_type "json", Type::Json.new m.register_type "jsonb", OID::Jsonb.new m.register_type "cidr", OID::Cidr.new m.register_type "inet", OID::Inet.new @@ -843,7 +843,6 @@ module ActiveRecord ActiveRecord::Type.register(:enum, OID::Enum, adapter: :postgresql) ActiveRecord::Type.register(:hstore, OID::Hstore, adapter: :postgresql) ActiveRecord::Type.register(:inet, OID::Inet, adapter: :postgresql) - ActiveRecord::Type.register(:json, OID::Json, adapter: :postgresql) ActiveRecord::Type.register(:jsonb, OID::Jsonb, adapter: :postgresql) ActiveRecord::Type.register(:money, OID::Money, adapter: :postgresql) ActiveRecord::Type.register(:point, OID::Point, adapter: :postgresql) diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb index 7233325d5a..ee2faf43b5 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb @@ -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) diff --git a/activerecord/lib/active_record/connection_handling.rb b/activerecord/lib/active_record/connection_handling.rb index 2ede92feff..b8fbb489b6 100644 --- a/activerecord/lib/active_record/connection_handling.rb +++ b/activerecord/lib/active_record/connection_handling.rb @@ -1,6 +1,6 @@ module ActiveRecord module ConnectionHandling - RAILS_ENV = -> { (Rails.env if defined?(Rails.env)) || ENV["RAILS_ENV"] || ENV["RACK_ENV"] } + RAILS_ENV = -> { (Rails.env if defined?(Rails.env)) || ENV["RAILS_ENV"].presence || ENV["RACK_ENV"].presence } DEFAULT_ENV = -> { RAILS_ENV.call || "default_env" } # Establishes the connection to the database. Accepts a hash as input where diff --git a/activerecord/lib/active_record/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/enum.rb b/activerecord/lib/active_record/enum.rb index 0ab03b2ab3..12ef58a941 100644 --- a/activerecord/lib/active_record/enum.rb +++ b/activerecord/lib/active_record/enum.rb @@ -95,8 +95,7 @@ module ActiveRecord module Enum def self.extended(base) # :nodoc: - base.class_attribute(:defined_enums, instance_writer: false) - base.defined_enums = {} + base.class_attribute(:defined_enums, instance_writer: false, default: {}) end def inherited(base) # :nodoc: @@ -154,11 +153,12 @@ module ActiveRecord definitions.each do |name, values| # statuses = { } enum_values = ActiveSupport::HashWithIndifferentAccess.new - name = name.to_sym + name = name.to_s # def self.statuses() statuses end - detect_enum_conflict!(name, name.to_s.pluralize, true) - klass.singleton_class.send(:define_method, name.to_s.pluralize) { enum_values } + detect_enum_conflict!(name, name.pluralize, true) + singleton_class.send(:define_method, name.pluralize) { enum_values } + defined_enums[name] = enum_values detect_enum_conflict!(name, name) detect_enum_conflict!(name, "#{name}=") @@ -170,7 +170,7 @@ module ActiveRecord _enum_methods_module.module_eval do pairs = values.respond_to?(:each_pair) ? values.each_pair : values.each_with_index - pairs.each do |value, i| + pairs.each do |label, value| if enum_prefix == true prefix = "#{name}_" elsif enum_prefix @@ -182,23 +182,23 @@ module ActiveRecord suffix = "_#{enum_suffix}" end - value_method_name = "#{prefix}#{value}#{suffix}" - enum_values[value] = i + value_method_name = "#{prefix}#{label}#{suffix}" + enum_values[label] = value + label = label.to_s - # def active?() status == 0 end + # def active?() status == "active" end klass.send(:detect_enum_conflict!, name, "#{value_method_name}?") - define_method("#{value_method_name}?") { self[attr] == value.to_s } + define_method("#{value_method_name}?") { self[attr] == label } - # def active!() update! status: :active end + # def active!() update!(status: 0) end klass.send(:detect_enum_conflict!, name, "#{value_method_name}!") define_method("#{value_method_name}!") { update!(attr => value) } - # scope :active, -> { where status: 0 } + # scope :active, -> { where(status: 0) } klass.send(:detect_enum_conflict!, name, value_method_name, true) klass.scope value_method_name, -> { where(attr => value) } end end - defined_enums[name.to_s] = enum_values end end diff --git a/activerecord/lib/active_record/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/fixtures.rb b/activerecord/lib/active_record/fixtures.rb index c19216702c..e9acb8acae 100644 --- a/activerecord/lib/active_record/fixtures.rb +++ b/activerecord/lib/active_record/fixtures.rb @@ -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). @@ -878,20 +875,12 @@ module ActiveRecord included do class_attribute :fixture_path, instance_writer: false - class_attribute :fixture_table_names - class_attribute :fixture_class_names - class_attribute :use_transactional_tests - class_attribute :use_instantiated_fixtures # true, false, or :no_instances - class_attribute :pre_loaded_fixtures - class_attribute :config - - self.fixture_table_names = [] - self.use_instantiated_fixtures = false - self.pre_loaded_fixtures = false - self.config = ActiveRecord::Base - - self.fixture_class_names = {} - self.use_transactional_tests = true + class_attribute :fixture_table_names, default: [] + class_attribute :fixture_class_names, default: {} + class_attribute :use_transactional_tests, default: true + class_attribute :use_instantiated_fixtures, default: false # true, false, or :no_instances + class_attribute :pre_loaded_fixtures, default: false + class_attribute :config, default: ActiveRecord::Base end module ClassMethods diff --git a/activerecord/lib/active_record/inheritance.rb b/activerecord/lib/active_record/inheritance.rb index fbdaeaae51..5776807507 100644 --- a/activerecord/lib/active_record/inheritance.rb +++ b/activerecord/lib/active_record/inheritance.rb @@ -38,8 +38,7 @@ module ActiveRecord included do # Determines whether to store the full constant name including namespace when using STI. # This is true, by default. - class_attribute :store_full_sti_class, instance_writer: false - self.store_full_sti_class = true + class_attribute :store_full_sti_class, instance_writer: false, default: true end module ClassMethods @@ -217,7 +216,7 @@ module ActiveRecord def subclass_from_attributes(attrs) attrs = attrs.to_h if attrs.respond_to?(:permitted?) if attrs.is_a?(Hash) - subclass_name = attrs.with_indifferent_access[inheritance_column] + subclass_name = attrs[inheritance_column] || attrs[inheritance_column.to_sym] if subclass_name.present? find_sti_class(subclass_name) diff --git a/activerecord/lib/active_record/integration.rb b/activerecord/lib/active_record/integration.rb index 8e71b60b29..cf954852bc 100644 --- a/activerecord/lib/active_record/integration.rb +++ b/activerecord/lib/active_record/integration.rb @@ -7,12 +7,19 @@ module ActiveRecord included do ## # :singleton-method: - # Indicates the format used to generate the timestamp in the cache key. - # Accepts any of the symbols in <tt>Time::DATE_FORMATS</tt>. + # Indicates the format used to generate the timestamp in the cache key, if + # versioning is off. Accepts any of the symbols in <tt>Time::DATE_FORMATS</tt>. # # This is +:usec+, by default. - class_attribute :cache_timestamp_format, instance_writer: false - self.cache_timestamp_format = :usec + class_attribute :cache_timestamp_format, instance_writer: false, default: :usec + + ## + # :singleton-method: + # Indicates whether to use a stable #cache_key method that is accompanied + # by a changing version in the #cache_version method. + # + # This is +false+, by default until Rails 6.0. + class_attribute :cache_versioning, instance_writer: false, default: false end # Returns a +String+, which Action Pack uses for constructing a URL to this @@ -42,35 +49,65 @@ module ActiveRecord id && id.to_s # Be sure to stringify the id for routes end - # Returns a cache key that can be used to identify this record. + # Returns a stable cache key that can be used to identify this record. # # Product.new.cache_key # => "products/new" - # Product.find(5).cache_key # => "products/5" (updated_at not available) - # Person.find(5).cache_key # => "people/5-20071224150000" (updated_at available) + # Product.find(5).cache_key # => "products/5" # - # You can also pass a list of named timestamps, and the newest in the list will be - # used to generate the key: + # If ActiveRecord::Base.cache_versioning is turned off, as it was in Rails 5.1 and earlier, + # the cache key will also include a version. # - # Person.find(5).cache_key(:updated_at, :last_reviewed_at) + # Product.cache_versioning = false + # Person.find(5).cache_key # => "people/5-20071224150000" (updated_at available) def cache_key(*timestamp_names) if new_record? "#{model_name.cache_key}/new" else - timestamp = if timestamp_names.any? - max_updated_column_timestamp(timestamp_names) + if cache_version && timestamp_names.none? + "#{model_name.cache_key}/#{id}" else - max_updated_column_timestamp - end + timestamp = if timestamp_names.any? + ActiveSupport::Deprecation.warn(<<-MSG.squish) + Specifying a timestamp name for #cache_key has been deprecated in favor of + the explicit #cache_version method that can be overwritten. + MSG - if timestamp - timestamp = timestamp.utc.to_s(cache_timestamp_format) - "#{model_name.cache_key}/#{id}-#{timestamp}" - else - "#{model_name.cache_key}/#{id}" + max_updated_column_timestamp(timestamp_names) + else + max_updated_column_timestamp + end + + if timestamp + timestamp = timestamp.utc.to_s(cache_timestamp_format) + "#{model_name.cache_key}/#{id}-#{timestamp}" + else + "#{model_name.cache_key}/#{id}" + end end end end + # Returns a cache version that can be used together with the cache key to form + # a recyclable caching scheme. By default, the #updated_at column is used for the + # cache_version, but this method can be overwritten to return something else. + # + # Note, this method will return nil if ActiveRecord::Base.cache_versioning is set to + # +false+ (which it is by default until Rails 6.0). + def cache_version + if cache_versioning && timestamp = try(:updated_at) + timestamp.utc.to_s(:usec) + end + end + + # Returns a cache key along with the version. + def cache_key_with_version + if version = cache_version + "#{cache_key}-#{version}" + else + cache_key + end + end + module ClassMethods # Defines your model's +to_param+ method to generate "pretty" URLs # using +method_name+, which can be any attribute or method that diff --git a/activerecord/lib/active_record/locking/optimistic.rb b/activerecord/lib/active_record/locking/optimistic.rb index 78ce9f8291..522da6a571 100644 --- a/activerecord/lib/active_record/locking/optimistic.rb +++ b/activerecord/lib/active_record/locking/optimistic.rb @@ -51,8 +51,7 @@ module ActiveRecord extend ActiveSupport::Concern included do - class_attribute :lock_optimistically, instance_writer: false - self.lock_optimistically = true + class_attribute :lock_optimistically, instance_writer: false, default: true end def locking_enabled? #:nodoc: @@ -63,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, *) @@ -108,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 @@ -128,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/model_schema.rb b/activerecord/lib/active_record/model_schema.rb index 54216caaaf..14e0f5bff7 100644 --- a/activerecord/lib/active_record/model_schema.rb +++ b/activerecord/lib/active_record/model_schema.rb @@ -1,3 +1,5 @@ +require "monitor" + module ActiveRecord module ModelSchema extend ActiveSupport::Concern @@ -128,30 +130,19 @@ module ActiveRecord included do mattr_accessor :primary_key_prefix_type, instance_writer: false - class_attribute :table_name_prefix, instance_writer: false - self.table_name_prefix = "" - - class_attribute :table_name_suffix, instance_writer: false - self.table_name_suffix = "" - - class_attribute :schema_migrations_table_name, instance_accessor: false - self.schema_migrations_table_name = "schema_migrations" - - class_attribute :internal_metadata_table_name, instance_accessor: false - self.internal_metadata_table_name = "ar_internal_metadata" - - class_attribute :protected_environments, instance_accessor: false - self.protected_environments = ["production"] - - class_attribute :pluralize_table_names, instance_writer: false - self.pluralize_table_names = true - - class_attribute :ignored_columns, instance_accessor: false - self.ignored_columns = [].freeze + class_attribute :table_name_prefix, instance_writer: false, default: "" + class_attribute :table_name_suffix, instance_writer: false, default: "" + class_attribute :schema_migrations_table_name, instance_accessor: false, default: "schema_migrations" + class_attribute :internal_metadata_table_name, instance_accessor: false, default: "ar_internal_metadata" + class_attribute :protected_environments, instance_accessor: false, default: [ "production" ] + class_attribute :pluralize_table_names, instance_writer: false, default: true + class_attribute :ignored_columns, instance_accessor: false, default: [].freeze self.inheritance_column = "type" delegate :type_for_attribute, to: :class + + initialize_load_schema_monitor end # Derives the join table name for +first_table+ and +second_table+. The @@ -377,7 +368,7 @@ module ActiveRecord # default values when instantiating the Active Record object for this table. def column_defaults load_schema - _default_attributes.to_hash + @column_defaults ||= _default_attributes.to_hash end def _default_attributes # :nodoc: @@ -435,15 +426,31 @@ module ActiveRecord initialize_find_by_cache end + protected + + def initialize_load_schema_monitor + @load_schema_monitor = Monitor.new + end + private + def inherited(child_class) + super + child_class.initialize_load_schema_monitor + end + def schema_loaded? - defined?(@columns_hash) && @columns_hash + defined?(@schema_loaded) && @schema_loaded end def load_schema - unless schema_loaded? + return if schema_loaded? + @load_schema_monitor.synchronize do + return if defined?(@columns_hash) && @columns_hash + load_schema! + + @schema_loaded = true end end @@ -466,10 +473,12 @@ module ActiveRecord @attribute_types = nil @content_columns = nil @default_attributes = nil + @column_defaults = nil @inheritance_column = nil unless defined?(@explicit_inheritance_column) && @explicit_inheritance_column @attributes_builder = nil @columns = nil @columns_hash = nil + @schema_loaded = false @attribute_names = nil @yaml_encoder = nil direct_descendants.each do |descendant| diff --git a/activerecord/lib/active_record/nested_attributes.rb b/activerecord/lib/active_record/nested_attributes.rb index 01ecd79b8f..917bc76993 100644 --- a/activerecord/lib/active_record/nested_attributes.rb +++ b/activerecord/lib/active_record/nested_attributes.rb @@ -10,8 +10,7 @@ module ActiveRecord extend ActiveSupport::Concern included do - class_attribute :nested_attributes_options, instance_writer: false - self.nested_attributes_options = {} + class_attribute :nested_attributes_options, instance_writer: false, default: {} end # = Active Record Nested Attributes @@ -458,7 +457,7 @@ module ActiveRecord end unless attributes_collection.is_a?(Hash) || attributes_collection.is_a?(Array) - raise ArgumentError, "Hash or Array expected, got #{attributes_collection.class.name} (#{attributes_collection.inspect})" + raise ArgumentError, "Hash or Array expected for attribute `#{association_name}`, got #{attributes_collection.class.name} (#{attributes_collection.inspect})" end check_record_limit!(options[:limit], attributes_collection) diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb index f652c7c3a1..b2dba5516e 100644 --- a/activerecord/lib/active_record/persistence.rb +++ b/activerecord/lib/active_record/persistence.rb @@ -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/querying.rb b/activerecord/lib/active_record/querying.rb index c4a22398f0..b16e178358 100644 --- a/activerecord/lib/active_record/querying.rb +++ b/activerecord/lib/active_record/querying.rb @@ -8,7 +8,7 @@ module ActiveRecord delegate :destroy, :destroy_all, :delete, :delete_all, :update, :update_all, to: :all delegate :find_each, :find_in_batches, :in_batches, to: :all delegate :select, :group, :order, :except, :reorder, :limit, :offset, :joins, :left_joins, :left_outer_joins, :or, - :where, :rewhere, :preload, :eager_load, :includes, :from, :lock, :readonly, + :where, :rewhere, :preload, :eager_load, :includes, :from, :lock, :readonly, :extending, :having, :create_with, :distinct, :references, :none, :unscope, :merge, to: :all delegate :count, :average, :minimum, :maximum, :sum, :calculate, to: :all delegate :pluck, :ids, to: :all diff --git a/activerecord/lib/active_record/readonly_attributes.rb b/activerecord/lib/active_record/readonly_attributes.rb index 6274996ab8..af6473d250 100644 --- a/activerecord/lib/active_record/readonly_attributes.rb +++ b/activerecord/lib/active_record/readonly_attributes.rb @@ -3,8 +3,7 @@ module ActiveRecord extend ActiveSupport::Concern included do - class_attribute :_attr_readonly, instance_accessor: false - self._attr_readonly = [] + class_attribute :_attr_readonly, instance_accessor: false, default: [] end module ClassMethods diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb index 1a9e0a4a40..73761ed7ed 100644 --- a/activerecord/lib/active_record/reflection.rb +++ b/activerecord/lib/active_record/reflection.rb @@ -8,10 +8,8 @@ module ActiveRecord extend ActiveSupport::Concern included do - class_attribute :_reflections, instance_writer: false - class_attribute :aggregate_reflections, instance_writer: false - self._reflections = {} - self.aggregate_reflections = {} + class_attribute :_reflections, instance_writer: false, default: {} + class_attribute :aggregate_reflections, instance_writer: false, default: {} end def self.create(macro, name, scope, options, ar) @@ -173,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 @@ -187,10 +185,30 @@ module ActiveRecord end deprecate :scope_chain + def join_scope(table, foreign_table, foreign_klass) + predicate_builder = predicate_builder(table) + scope_chain_items = join_scopes(table, predicate_builder) + klass_scope = klass_join_scope(table, predicate_builder) + + key = join_keys.key + foreign_key = join_keys.foreign_key + + klass_scope.where!(table[key].eq(foreign_table[foreign_key])) + + if klass.finder_needs_type_condition? + klass_scope.where!(klass.send(:type_condition, table)) + end + + 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 @@ -199,15 +217,10 @@ module ActiveRecord def klass_join_scope(table, predicate_builder) # :nodoc: if klass.current_scope klass.current_scope.clone.tap { |scope| - scope.joins_values = [] + scope.joins_values = scope.left_outer_joins_values = [].freeze } else - relation = ActiveRecord::Relation.create( - klass, - table, - predicate_builder, - ) - klass.send(:build_default_scope, relation) + klass.default_scoped(build_scope(table, predicate_builder)) end end @@ -289,7 +302,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 @@ -581,11 +606,9 @@ module ActiveRecord seed + [self] end - protected - - def actual_source_reflection # FIXME: this is a horrible name - self - end + def extensions + Array(options[:extend]) + end private @@ -745,10 +768,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? @@ -759,7 +778,6 @@ module ActiveRecord # Holds all the metadata about a :through association as it was specified # in the Active Record class. class ThroughReflection < AbstractReflection #:nodoc: - attr_reader :delegate_reflection delegate :foreign_key, :foreign_type, :association_foreign_key, :active_record_primary_key, :type, :get_join_keys, to: :source_reflection @@ -985,19 +1003,23 @@ module ActiveRecord collect_join_reflections(seed + [self]) end - def collect_join_reflections(seed) - a = source_reflection.add_as_source seed - if options[:source_type] - through_reflection.add_as_polymorphic_through self, a - else - through_reflection.add_as_through a + # TODO Change this to private once we've dropped Ruby 2.2 support. + # Workaround for Ruby 2.2 "private attribute?" warning. + protected + attr_reader :delegate_reflection + + def actual_source_reflection # FIXME: this is a horrible name + source_reflection.actual_source_reflection end - end private - - def actual_source_reflection # FIXME: this is a horrible name - source_reflection.send(:actual_source_reflection) + def collect_join_reflections(seed) + a = source_reflection.add_as_source seed + if options[:source_type] + through_reflection.add_as_polymorphic_through self, a + else + through_reflection.add_as_through a + end end def primary_key(klass) diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb index 333ad16e11..133c1a6318 100644 --- a/activerecord/lib/active_record/relation.rb +++ b/activerecord/lib/active_record/relation.rb @@ -2,7 +2,7 @@ module ActiveRecord # = Active Record \Relation class Relation MULTI_VALUE_METHODS = [:includes, :eager_load, :preload, :select, :group, - :order, :joins, :left_joins, :left_outer_joins, :references, + :order, :joins, :left_outer_joins, :references, :extending, :unscope] SINGLE_VALUE_METHODS = [:limit, :offset, :lock, :readonly, :reordering, @@ -18,6 +18,7 @@ module ActiveRecord attr_reader :table, :klass, :loaded, :predicate_builder alias :model :klass alias :loaded? :loaded + alias :locked? :lock_value def initialize(klass, table, predicate_builder, values = {}) @klass = klass @@ -403,9 +404,9 @@ module ActiveRecord # # Note: Updating a large number of records will run an # UPDATE query for each record, which may cause a performance - # issue. So if it is not needed to run callbacks for each update, it is - # preferred to use #update_all for updating all records using - # a single query. + # issue. When running callbacks is not needed for each record update, + # it is preferred to use #update_all for updating all records + # in a single query. def update(id = :all, attributes) if id.is_a?(Array) id.map.with_index { |one_id, idx| update(one_id, attributes[idx]) } diff --git a/activerecord/lib/active_record/relation/batches.rb b/activerecord/lib/active_record/relation/batches.rb index 76031515fd..13a2c3f511 100644 --- a/activerecord/lib/active_record/relation/batches.rb +++ b/activerecord/lib/active_record/relation/batches.rb @@ -30,14 +30,14 @@ module ActiveRecord # end # # ==== Options - # * <tt>:batch_size</tt> - Specifies the size of the batch. Default to 1000. + # * <tt>:batch_size</tt> - Specifies the size of the batch. Defaults to 1000. # * <tt>:start</tt> - Specifies the primary key value to start from, inclusive of the value. # * <tt>:finish</tt> - Specifies the primary key value to end at, inclusive of the value. # * <tt>:error_on_ignore</tt> - Overrides the application config to specify if an error should be raised when - # an order is present in the relation. + # an order is present in the relation. # # Limits are honored, and if present there is no requirement for the batch - # size, it can be less than, equal, or greater than the limit. + # size: it can be less than, equal to, or greater than the limit. # # The options +start+ and +finish+ are especially useful if you want # multiple workers dealing with the same processing queue. You can make @@ -89,14 +89,14 @@ module ActiveRecord # To be yielded each record one by one, use #find_each instead. # # ==== Options - # * <tt>:batch_size</tt> - Specifies the size of the batch. Default to 1000. + # * <tt>:batch_size</tt> - Specifies the size of the batch. Defaults to 1000. # * <tt>:start</tt> - Specifies the primary key value to start from, inclusive of the value. # * <tt>:finish</tt> - Specifies the primary key value to end at, inclusive of the value. # * <tt>:error_on_ignore</tt> - Overrides the application config to specify if an error should be raised when - # an order is present in the relation. + # an order is present in the relation. # # Limits are honored, and if present there is no requirement for the batch - # size, it can be less than, equal, or greater than the limit. + # size: it can be less than, equal to, or greater than the limit. # # The options +start+ and +finish+ are especially useful if you want # multiple workers dealing with the same processing queue. You can make @@ -140,9 +140,9 @@ module ActiveRecord # If you do not provide a block to #in_batches, it will return a # BatchEnumerator which is enumerable. # - # Person.in_batches.with_index do |relation, batch_index| + # Person.in_batches.each_with_index do |relation, batch_index| # puts "Processing relation ##{batch_index}" - # relation.each { |relation| relation.delete_all } + # relation.delete_all # end # # Examples of calling methods on the returned BatchEnumerator object: @@ -152,12 +152,12 @@ module ActiveRecord # Person.in_batches.each_record(&:party_all_night!) # # ==== Options - # * <tt>:of</tt> - Specifies the size of the batch. Default to 1000. - # * <tt>:load</tt> - Specifies if the relation should be loaded. Default to false. + # * <tt>:of</tt> - Specifies the size of the batch. Defaults to 1000. + # * <tt>:load</tt> - Specifies if the relation should be loaded. Defaults to false. # * <tt>:start</tt> - Specifies the primary key value to start from, inclusive of the value. # * <tt>:finish</tt> - Specifies the primary key value to end at, inclusive of the value. # * <tt>:error_on_ignore</tt> - Overrides the application config to specify if an error should be raised when - # an order is present in the relation. + # an order is present in the relation. # # Limits are honored, and if present there is no requirement for the batch # size, it can be less than, equal, or greater than the limit. @@ -186,7 +186,7 @@ module ActiveRecord # # NOTE: It's not possible to set the order. That is automatically set to # ascending on the primary key ("id ASC") to make the batch ordering - # consistent. Therefore the primary key must be orderable, e.g an integer + # consistent. Therefore the primary key must be orderable, e.g. an integer # or a string. # # NOTE: By its nature, batch processing is subject to race conditions if diff --git a/activerecord/lib/active_record/relation/calculations.rb b/activerecord/lib/active_record/relation/calculations.rb index 861b2125d4..24b8be0242 100644 --- a/activerecord/lib/active_record/relation/calculations.rb +++ b/activerecord/lib/active_record/relation/calculations.rb @@ -37,7 +37,16 @@ module ActiveRecord # Note: not all valid {Relation#select}[rdoc-ref:QueryMethods#select] expressions are valid #count expressions. The specifics differ # between databases. In invalid cases, an error from the database is thrown. def count(column_name = nil) - return super() if block_given? + if block_given? + unless column_name.nil? + ActiveSupport::Deprecation.warn \ + "When `count' is called with a block, it ignores other arguments. " \ + "This behavior is now deprecated and will result in an ArgumentError in Rails 5.3." + end + + return super() + end + calculate(:count, column_name) end @@ -73,7 +82,16 @@ module ActiveRecord # # Person.sum(:age) # => 4562 def sum(column_name = nil) - return super() if block_given? + if block_given? + unless column_name.nil? + ActiveSupport::Deprecation.warn \ + "When `sum' is called with a block, it ignores other arguments. " \ + "This behavior is now deprecated and will result in an ArgumentError in Rails 5.3." + end + + return super() + end + calculate(:sum, column_name) end @@ -293,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 = @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 257ae04ff4..8b4dd25689 100644 --- a/activerecord/lib/active_record/relation/delegation.rb +++ b/activerecord/lib/active_record/relation/delegation.rb @@ -25,8 +25,6 @@ module ActiveRecord def inherited(child_class) child_class.initialize_relation_delegate_cache - delegate = child_class.relation_delegate_class(ActiveRecord::Associations::CollectionProxy) - delegate.include ActiveRecord::Associations::CollectionProxy::DelegateExtending super end end diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb index 1d661fa8ed..df8909379f 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 + connection.select_value(relation.arel, "#{name} Exists", relation.bound_attributes) ? true : false rescue ::RangeError false end @@ -377,8 +376,7 @@ module ActiveRecord if ActiveRecord::NullRelation === relation [] else - arel = relation.arel - rows = connection.select_all(arel, "SQL", relation.bound_attributes) + rows = 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 = @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 183fe91c05..7dea5deec5 100644 --- a/activerecord/lib/active_record/relation/predicate_builder.rb +++ b/activerecord/lib/active_record/relation/predicate_builder.rb @@ -1,12 +1,3 @@ -require "active_record/relation/predicate_builder/array_handler" -require "active_record/relation/predicate_builder/base_handler" -require "active_record/relation/predicate_builder/basic_object_handler" -require "active_record/relation/predicate_builder/range_handler" -require "active_record/relation/predicate_builder/relation_handler" - -require "active_record/relation/predicate_builder/association_query_value" -require "active_record/relation/predicate_builder/polymorphic_array_value" - module ActiveRecord class PredicateBuilder # :nodoc: delegate :resolve_column_aliases, to: :table @@ -116,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 @@ -173,8 +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 "active_record/relation/predicate_builder/association_query_value" +require "active_record/relation/predicate_builder/polymorphic_array_value" diff --git a/activerecord/lib/active_record/relation/predicate_builder/association_query_value.rb b/activerecord/lib/active_record/relation/predicate_builder/association_query_value.rb index 2fe0f81cab..3e19646ae5 100644 --- a/activerecord/lib/active_record/relation/predicate_builder/association_query_value.rb +++ b/activerecord/lib/active_record/relation/predicate_builder/association_query_value.rb @@ -1,8 +1,6 @@ module ActiveRecord class PredicateBuilder class AssociationQueryValue # :nodoc: - attr_reader :associated_table, :value - def initialize(associated_table, value) @associated_table = associated_table @value = value @@ -12,6 +10,11 @@ module ActiveRecord [associated_table.association_foreign_key.to_s => ids] end + # TODO Change this to private once we've dropped Ruby 2.2 support. + # Workaround for Ruby 2.2 "private attribute?" warning. + protected + attr_reader :associated_table, :value + private def ids case value diff --git a/activerecord/lib/active_record/relation/predicate_builder/polymorphic_array_value.rb b/activerecord/lib/active_record/relation/predicate_builder/polymorphic_array_value.rb index 9bb2f8c8dc..7029ae5f47 100644 --- a/activerecord/lib/active_record/relation/predicate_builder/polymorphic_array_value.rb +++ b/activerecord/lib/active_record/relation/predicate_builder/polymorphic_array_value.rb @@ -1,8 +1,6 @@ module ActiveRecord class PredicateBuilder class PolymorphicArrayValue # :nodoc: - attr_reader :associated_table, :values - def initialize(associated_table, values) @associated_table = associated_table @values = values @@ -17,6 +15,11 @@ module ActiveRecord end end + # TODO Change this to private once we've dropped Ruby 2.2 support. + # Workaround for Ruby 2.2 "private attribute?" warning. + protected + attr_reader :associated_table, :values + private def type_to_ids_mapping default_hash = Hash.new { |hsh, key| hsh[key] = [] } diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb index 76e529f2de..d44f6fd572 100644 --- a/activerecord/lib/active_record/relation/query_methods.rb +++ b/activerecord/lib/active_record/relation/query_methods.rb @@ -1100,14 +1100,16 @@ module ActiveRecord end VALID_DIRECTIONS = [:asc, :desc, :ASC, :DESC, - "asc", "desc", "ASC", "DESC"] # :nodoc: + "asc", "desc", "ASC", "DESC"].to_set # :nodoc: def validate_order_args(args) args.each do |arg| next unless arg.is_a?(Hash) arg.each do |_key, value| - raise ArgumentError, "Direction \"#{value}\" is invalid. Valid " \ - "directions are: #{VALID_DIRECTIONS.inspect}" unless VALID_DIRECTIONS.include?(value) + unless VALID_DIRECTIONS.include?(value) + raise ArgumentError, + "Direction \"#{value}\" is invalid. Valid directions are: #{VALID_DIRECTIONS.to_a.inspect}" + end end end end @@ -1120,7 +1122,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 @@ -1165,7 +1167,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/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/scoping/default.rb b/activerecord/lib/active_record/scoping/default.rb index 2daa48859a..70b2693b28 100644 --- a/activerecord/lib/active_record/scoping/default.rb +++ b/activerecord/lib/active_record/scoping/default.rb @@ -5,11 +5,8 @@ module ActiveRecord included do # Stores the default scope for the class. - class_attribute :default_scopes, instance_writer: false, instance_predicate: false - class_attribute :default_scope_override, instance_writer: false, instance_predicate: false - - self.default_scopes = [] - self.default_scope_override = nil + class_attribute :default_scopes, instance_writer: false, instance_predicate: false, default: [] + class_attribute :default_scope_override, instance_writer: false, instance_predicate: false, default: nil end module ClassMethods @@ -110,7 +107,11 @@ module ActiveRecord if default_scope_override # The user has defined their own default scope method, so call that - evaluate_default_scope { default_scope } + evaluate_default_scope do + if scope = default_scope + (base_rel ||= relation).merge(scope) + end + end elsif default_scopes.any? base_rel ||= relation evaluate_default_scope do diff --git a/activerecord/lib/active_record/scoping/named.rb b/activerecord/lib/active_record/scoping/named.rb index 27cdf8cb7e..b4026fabb2 100644 --- a/activerecord/lib/active_record/scoping/named.rb +++ b/activerecord/lib/active_record/scoping/named.rb @@ -29,13 +29,15 @@ module ActiveRecord end end - def default_scoped # :nodoc: - scope = build_default_scope + def default_scoped(scope = relation) # :nodoc: + build_default_scope(scope) || scope + end - if scope - relation.spawn.merge!(scope) + def default_extensions # :nodoc: + if scope = current_scope || build_default_scope + scope.extensions else - relation + [] end end @@ -156,17 +158,17 @@ module ActiveRecord if body.respond_to?(:to_proc) singleton_class.send(:define_method, name) do |*args| - scope = all.scoping { instance_exec(*args, &body) } + scope = all + scope = scope.instance_exec(*args, &body) || scope scope = scope.extending(extension) if extension - - scope || all + scope end else singleton_class.send(:define_method, name) do |*args| - scope = all.scoping { body.call(*args) } + scope = all + scope = scope.scoping { body.call(*args) || scope } scope = scope.extending(extension) if extension - - scope || all + scope end end end diff --git a/activerecord/lib/active_record/tasks/database_tasks.rb b/activerecord/lib/active_record/tasks/database_tasks.rb index c53dee43d7..ba686fc562 100644 --- a/activerecord/lib/active_record/tasks/database_tasks.rb +++ b/activerecord/lib/active_record/tasks/database_tasks.rb @@ -71,9 +71,9 @@ module ActiveRecord @tasks[pattern] = task end - register_task(/mysql/, 'ActiveRecord::Tasks::MySQLDatabaseTasks') - register_task(/postgresql/, 'ActiveRecord::Tasks::PostgreSQLDatabaseTasks') - register_task(/sqlite/, 'ActiveRecord::Tasks::SQLiteDatabaseTasks') + register_task(/mysql/, "ActiveRecord::Tasks::MySQLDatabaseTasks") + register_task(/postgresql/, "ActiveRecord::Tasks::PostgreSQLDatabaseTasks") + register_task(/sqlite/, "ActiveRecord::Tasks::SQLiteDatabaseTasks") def db_dir @db_dir ||= Rails.application.config.paths["db"].first diff --git a/activerecord/lib/active_record/tasks/mysql_database_tasks.rb b/activerecord/lib/active_record/tasks/mysql_database_tasks.rb index 541165b3d1..c25d87dd3e 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 diff --git a/activerecord/lib/active_record/tasks/sqlite_database_tasks.rb b/activerecord/lib/active_record/tasks/sqlite_database_tasks.rb index 7043d2f0c8..01562b21e9 100644 --- a/activerecord/lib/active_record/tasks/sqlite_database_tasks.rb +++ b/activerecord/lib/active_record/tasks/sqlite_database_tasks.rb @@ -42,7 +42,7 @@ module ActiveRecord ignore_tables = ActiveRecord::SchemaDumper.ignore_tables if ignore_tables.any? - condition = ignore_tables.map { |table| connection.quote_table_name(table) }.join(", ") + condition = ignore_tables.map { |table| connection.quote(table) }.join(", ") args << "SELECT sql FROM sqlite_master WHERE tbl_name NOT IN (#{condition}) ORDER BY tbl_name, type DESC, name" else args << ".schema" diff --git a/activerecord/lib/active_record/timestamp.rb b/activerecord/lib/active_record/timestamp.rb index 09d8d1cdd4..dc4540eea6 100644 --- a/activerecord/lib/active_record/timestamp.rb +++ b/activerecord/lib/active_record/timestamp.rb @@ -43,8 +43,7 @@ module ActiveRecord extend ActiveSupport::Concern included do - class_attribute :record_timestamps - self.record_timestamps = true + class_attribute :record_timestamps, default: true end def initialize_dup(other) # :nodoc: @@ -127,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/type.rb b/activerecord/lib/active_record/type.rb index 4f632660a8..9ed6c95bf9 100644 --- a/activerecord/lib/active_record/type.rb +++ b/activerecord/lib/active_record/type.rb @@ -1,11 +1,11 @@ require "active_model/type" -require "active_record/type/internal/abstract_json" require "active_record/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" @@ -69,6 +69,7 @@ module ActiveRecord register(:decimal, Type::Decimal, override: false) register(:float, Type::Float, override: false) register(:integer, Type::Integer, override: false) + register(:json, Type::Json, override: false) register(:string, Type::String, override: false) register(:text, Type::Text, override: false) register(:time, Type::Time, override: false) diff --git a/activerecord/lib/active_record/type/internal/abstract_json.rb b/activerecord/lib/active_record/type/internal/abstract_json.rb deleted file mode 100644 index e19c5a14da..0000000000 --- a/activerecord/lib/active_record/type/internal/abstract_json.rb +++ /dev/null @@ -1,33 +0,0 @@ -module ActiveRecord - module Type - module Internal # :nodoc: - class AbstractJson < ActiveModel::Type::Value # :nodoc: - include ActiveModel::Type::Helpers::Mutable - - def type - :json - end - - def deserialize(value) - if value.is_a?(::String) - ::ActiveSupport::JSON.decode(value) rescue nil - else - value - end - end - - def serialize(value) - if value.nil? - nil - else - ::ActiveSupport::JSON.encode(value) - end - end - - def accessor - ActiveRecord::Store::StringKeyedHashAccessor - end - end - end - end -end diff --git a/activerecord/lib/active_record/type/json.rb b/activerecord/lib/active_record/type/json.rb new file mode 100644 index 0000000000..c4732fe388 --- /dev/null +++ b/activerecord/lib/active_record/type/json.rb @@ -0,0 +1,28 @@ +module ActiveRecord + module Type + class Json < ActiveModel::Type::Value + include ActiveModel::Type::Helpers::Mutable + + def type + :json + end + + def deserialize(value) + return value unless value.is_a?(::String) + ActiveSupport::JSON.decode(value) rescue nil + end + + def serialize(value) + ActiveSupport::JSON.encode(value) unless value.nil? + end + + def changed_in_place?(raw_old_value, new_value) + deserialize(raw_old_value) != new_value + end + + def accessor + ActiveRecord::Store::StringKeyedHashAccessor + end + end + end +end diff --git a/activerecord/lib/rails/generators/active_record.rb b/activerecord/lib/rails/generators/active_record.rb index 68fca44e3b..a79b8eafea 100644 --- a/activerecord/lib/rails/generators/active_record.rb +++ b/activerecord/lib/rails/generators/active_record.rb @@ -10,7 +10,7 @@ module ActiveRecord # Set the current directory as base for the inherited generators. def self.base_root - File.dirname(__FILE__) + __dir__ end end end diff --git a/activerecord/test/cases/adapters/mysql2/json_test.rb b/activerecord/test/cases/adapters/mysql2/json_test.rb index 6954006003..26c69edc7b 100644 --- a/activerecord/test/cases/adapters/mysql2/json_test.rb +++ b/activerecord/test/cases/adapters/mysql2/json_test.rb @@ -1,195 +1,22 @@ require "cases/helper" -require "support/schema_dumping_helper" +require "cases/json_shared_test_cases" if ActiveRecord::Base.connection.supports_json? class Mysql2JSONTest < ActiveRecord::Mysql2TestCase - include SchemaDumpingHelper + include JSONSharedTestCases self.use_transactional_tests = false - class JsonDataType < ActiveRecord::Base - self.table_name = "json_data_type" - - store_accessor :settings, :resolution - end - def setup - @connection = ActiveRecord::Base.connection - begin - @connection.create_table("json_data_type") do |t| - t.json "payload" - t.json "settings" - end + super + @connection.create_table("json_data_type") do |t| + t.json "payload" + t.json "settings" end end - def teardown - @connection.drop_table :json_data_type, if_exists: true - JsonDataType.reset_column_information - end - - def test_column - column = JsonDataType.columns_hash["payload"] - assert_equal :json, column.type - assert_equal "json", column.sql_type - - type = JsonDataType.type_for_attribute("payload") - assert_not type.binary? - end - - def test_change_table_supports_json - @connection.change_table("json_data_type") do |t| - t.json "users" + private + def column_type + :json end - JsonDataType.reset_column_information - column = JsonDataType.columns_hash["users"] - assert_equal :json, column.type - end - - def test_schema_dumping - output = dump_table_schema("json_data_type") - assert_match(/t\.json\s+"settings"/, output) - end - - def test_cast_value_on_write - x = JsonDataType.new payload: { "string" => "foo", :symbol => :bar } - assert_equal({ "string" => "foo", :symbol => :bar }, x.payload_before_type_cast) - assert_equal({ "string" => "foo", "symbol" => "bar" }, x.payload) - x.save - assert_equal({ "string" => "foo", "symbol" => "bar" }, x.reload.payload) - end - - def test_type_cast_json - type = JsonDataType.type_for_attribute("payload") - - data = "{\"a_key\":\"a_value\"}" - hash = type.deserialize(data) - assert_equal({ "a_key" => "a_value" }, hash) - assert_equal({ "a_key" => "a_value" }, type.deserialize(data)) - - assert_equal({}, type.deserialize("{}")) - assert_equal({ "key" => nil }, type.deserialize('{"key": null}')) - assert_equal({ "c" => "}", '"a"' => 'b "a b' }, type.deserialize(%q({"c":"}", "\"a\"":"b \"a b"}))) - end - - def test_rewrite - @connection.execute "insert into json_data_type (payload) VALUES ('{\"k\":\"v\"}')" - x = JsonDataType.first - x.payload = { '"a\'' => "b" } - assert x.save! - end - - def test_select - @connection.execute "insert into json_data_type (payload) VALUES ('{\"k\":\"v\"}')" - x = JsonDataType.first - assert_equal({ "k" => "v" }, x.payload) - end - - def test_select_multikey - @connection.execute %q|insert into json_data_type (payload) VALUES ('{"k1":"v1", "k2":"v2", "k3":[1,2,3]}')| - x = JsonDataType.first - assert_equal({ "k1" => "v1", "k2" => "v2", "k3" => [1, 2, 3] }, x.payload) - end - - def test_null_json - @connection.execute "insert into json_data_type (payload) VALUES(null)" - x = JsonDataType.first - assert_nil(x.payload) - end - - def test_select_array_json_value - @connection.execute %q|insert into json_data_type (payload) VALUES ('["v0",{"k1":"v1"}]')| - x = JsonDataType.first - assert_equal(["v0", { "k1" => "v1" }], x.payload) - end - - def test_select_nil_json_after_create - json = JsonDataType.create(payload: nil) - x = JsonDataType.where(payload: nil).first - assert_equal(json, x) - end - - def test_select_nil_json_after_update - json = JsonDataType.create(payload: "foo") - x = JsonDataType.where(payload: nil).first - assert_nil(x) - - json.update_attributes payload: nil - x = JsonDataType.where(payload: nil).first - assert_equal(json.reload, x) - end - - def test_rewrite_array_json_value - @connection.execute %q|insert into json_data_type (payload) VALUES ('["v0",{"k1":"v1"}]')| - x = JsonDataType.first - x.payload = ["v1", { "k2" => "v2" }, "v3"] - assert x.save! - end - - def test_with_store_accessors - x = JsonDataType.new(resolution: "320×480") - assert_equal "320×480", x.resolution - - x.save! - x = JsonDataType.first - assert_equal "320×480", x.resolution - - x.resolution = "640×1136" - x.save! - - x = JsonDataType.first - assert_equal "640×1136", x.resolution - end - - def test_duplication_with_store_accessors - x = JsonDataType.new(resolution: "320×480") - assert_equal "320×480", x.resolution - - y = x.dup - assert_equal "320×480", y.resolution - end - - def test_yaml_round_trip_with_store_accessors - x = JsonDataType.new(resolution: "320×480") - assert_equal "320×480", x.resolution - - y = YAML.load(YAML.dump(x)) - assert_equal "320×480", y.resolution - end - - def test_changes_in_place - json = JsonDataType.new - assert_not json.changed? - - json.payload = { "one" => "two" } - assert json.changed? - assert json.payload_changed? - - json.save! - assert_not json.changed? - - json.payload["three"] = "four" - assert json.payload_changed? - - json.save! - json.reload - - assert_equal({ "one" => "two", "three" => "four" }, json.payload) - assert_not json.changed? - end - - def test_assigning_string_literal - json = JsonDataType.create(payload: "foo") - assert_equal "foo", json.payload - end - - def test_assigning_number - json = JsonDataType.create(payload: 1.234) - assert_equal 1.234, json.payload - end - - def test_assigning_boolean - json = JsonDataType.create(payload: true) - assert_equal true, json.payload - end end end diff --git a/activerecord/test/cases/adapters/mysql2/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/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 99175e8091..539c90f0bc 100644 --- a/activerecord/test/cases/adapters/postgresql/bytea_test.rb +++ b/activerecord/test/cases/adapters/postgresql/bytea_test.rb @@ -96,7 +96,7 @@ class PostgresqlByteaTest < ActiveRecord::PostgreSQLTestCase end def test_write_binary - data = File.read(File.join(File.dirname(__FILE__), "..", "..", "..", "assets", "example.log")) + data = File.read(File.join(__dir__, "..", "..", "..", "assets", "example.log")) assert(data.size > 1) record = ByteaDataType.create(payload: data) assert_not record.new_record? diff --git a/activerecord/test/cases/adapters/postgresql/connection_test.rb b/activerecord/test/cases/adapters/postgresql/connection_test.rb index 3deb007513..32afe331fa 100644 --- a/activerecord/test/cases/adapters/postgresql/connection_test.rb +++ b/activerecord/test/cases/adapters/postgresql/connection_test.rb @@ -31,15 +31,21 @@ module ActiveRecord end def test_encoding - assert_not_nil @connection.encoding + assert_queries(1) do + assert_not_nil @connection.encoding + end end def test_collation - assert_not_nil @connection.collation + assert_queries(1) do + assert_not_nil @connection.collation + end end def test_ctype - assert_not_nil @connection.ctype + assert_queries(1) do + assert_not_nil @connection.ctype + end end def test_default_client_min_messages 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/json_test.rb b/activerecord/test/cases/adapters/postgresql/json_test.rb index d4e627001c..6aa60630c2 100644 --- a/activerecord/test/cases/adapters/postgresql/json_test.rb +++ b/activerecord/test/cases/adapters/postgresql/json_test.rb @@ -1,223 +1,36 @@ require "cases/helper" -require "support/schema_dumping_helper" +require "cases/json_shared_test_cases" module PostgresqlJSONSharedTestCases - include SchemaDumpingHelper - - class JsonDataType < ActiveRecord::Base - self.table_name = "json_data_type" - - store_accessor :settings, :resolution - end + include JSONSharedTestCases def setup - @connection = ActiveRecord::Base.connection - begin - @connection.create_table("json_data_type") do |t| - t.public_send column_type, "payload", default: {} # t.json 'payload', default: {} - t.public_send column_type, "settings" # t.json 'settings' - t.public_send column_type, "objects", array: true # t.json 'objects', array: true - end - rescue ActiveRecord::StatementInvalid - skip "do not test on PostgreSQL without #{column_type} type." + super + @connection.create_table("json_data_type") do |t| + t.public_send column_type, "payload", default: {} # t.json 'payload', default: {} + t.public_send column_type, "settings" # t.json 'settings' + t.public_send column_type, "objects", array: true # t.json 'objects', array: true end - end - - def teardown - @connection.drop_table :json_data_type, if_exists: true - JsonDataType.reset_column_information - end - - def test_column - column = JsonDataType.columns_hash["payload"] - assert_equal column_type, column.type - assert_equal column_type.to_s, column.sql_type - assert_not column.array? - - type = JsonDataType.type_for_attribute("payload") - assert_not type.binary? + rescue ActiveRecord::StatementInvalid + skip "do not test on PostgreSQL without #{column_type} type." end def test_default @connection.add_column "json_data_type", "permissions", column_type, default: { "users": "read", "posts": ["read", "write"] } - JsonDataType.reset_column_information + klass.reset_column_information assert_equal({ "users" => "read", "posts" => ["read", "write"] }, JsonDataType.column_defaults["permissions"]) assert_equal({ "users" => "read", "posts" => ["read", "write"] }, JsonDataType.new.permissions) - ensure - JsonDataType.reset_column_information - end - - def test_change_table_supports_json - @connection.transaction do - @connection.change_table("json_data_type") do |t| - t.public_send column_type, "users", default: "{}" # t.json 'users', default: '{}' - end - JsonDataType.reset_column_information - column = JsonDataType.columns_hash["users"] - assert_equal column_type, column.type - - raise ActiveRecord::Rollback # reset the schema change - end - ensure - JsonDataType.reset_column_information - end - - def test_schema_dumping - output = dump_table_schema("json_data_type") - assert_match(/t\.#{column_type.to_s}\s+"payload",\s+default: {}/, output) - end - - def test_cast_value_on_write - x = JsonDataType.new payload: { "string" => "foo", :symbol => :bar } - assert_equal({ "string" => "foo", :symbol => :bar }, x.payload_before_type_cast) - assert_equal({ "string" => "foo", "symbol" => "bar" }, x.payload) - x.save - assert_equal({ "string" => "foo", "symbol" => "bar" }, x.reload.payload) end def test_deserialize_with_array - x = JsonDataType.new(objects: ["foo" => "bar"]) + x = klass.new(objects: ["foo" => "bar"]) assert_equal ["foo" => "bar"], x.objects x.save! assert_equal ["foo" => "bar"], x.objects x.reload assert_equal ["foo" => "bar"], x.objects end - - def test_type_cast_json - type = JsonDataType.type_for_attribute("payload") - - data = "{\"a_key\":\"a_value\"}" - hash = type.deserialize(data) - assert_equal({ "a_key" => "a_value" }, hash) - assert_equal({ "a_key" => "a_value" }, type.deserialize(data)) - - assert_equal({}, type.deserialize("{}")) - assert_equal({ "key" => nil }, type.deserialize('{"key": null}')) - assert_equal({ "c" => "}", '"a"' => 'b "a b' }, type.deserialize(%q({"c":"}", "\"a\"":"b \"a b"}))) - end - - def test_rewrite - @connection.execute "insert into json_data_type (payload) VALUES ('{\"k\":\"v\"}')" - x = JsonDataType.first - x.payload = { '"a\'' => "b" } - assert x.save! - end - - def test_select - @connection.execute "insert into json_data_type (payload) VALUES ('{\"k\":\"v\"}')" - x = JsonDataType.first - assert_equal({ "k" => "v" }, x.payload) - end - - def test_select_multikey - @connection.execute %q|insert into json_data_type (payload) VALUES ('{"k1":"v1", "k2":"v2", "k3":[1,2,3]}')| - x = JsonDataType.first - assert_equal({ "k1" => "v1", "k2" => "v2", "k3" => [1, 2, 3] }, x.payload) - end - - def test_null_json - @connection.execute "insert into json_data_type (payload) VALUES(null)" - x = JsonDataType.first - assert_nil(x.payload) - end - - def test_select_nil_json_after_create - json = JsonDataType.create(payload: nil) - x = JsonDataType.where(payload: nil).first - assert_equal(json, x) - end - - def test_select_nil_json_after_update - json = JsonDataType.create(payload: "foo") - x = JsonDataType.where(payload: nil).first - assert_nil(x) - - json.update_attributes payload: nil - x = JsonDataType.where(payload: nil).first - assert_equal(json.reload, x) - end - - def test_select_array_json_value - @connection.execute %q|insert into json_data_type (payload) VALUES ('["v0",{"k1":"v1"}]')| - x = JsonDataType.first - assert_equal(["v0", { "k1" => "v1" }], x.payload) - end - - def test_rewrite_array_json_value - @connection.execute %q|insert into json_data_type (payload) VALUES ('["v0",{"k1":"v1"}]')| - x = JsonDataType.first - x.payload = ["v1", { "k2" => "v2" }, "v3"] - assert x.save! - end - - def test_with_store_accessors - x = JsonDataType.new(resolution: "320×480") - assert_equal "320×480", x.resolution - - x.save! - x = JsonDataType.first - assert_equal "320×480", x.resolution - - x.resolution = "640×1136" - x.save! - - x = JsonDataType.first - assert_equal "640×1136", x.resolution - end - - def test_duplication_with_store_accessors - x = JsonDataType.new(resolution: "320×480") - assert_equal "320×480", x.resolution - - y = x.dup - assert_equal "320×480", y.resolution - end - - def test_yaml_round_trip_with_store_accessors - x = JsonDataType.new(resolution: "320×480") - assert_equal "320×480", x.resolution - - y = YAML.load(YAML.dump(x)) - assert_equal "320×480", y.resolution - end - - def test_changes_in_place - json = JsonDataType.new - assert_not json.changed? - - json.payload = { "one" => "two" } - assert json.changed? - assert json.payload_changed? - - json.save! - assert_not json.changed? - - json.payload["three"] = "four" - assert json.payload_changed? - - json.save! - json.reload - - assert_equal({ "one" => "two", "three" => "four" }, json.payload) - assert_not json.changed? - end - - def test_assigning_string_literal - json = JsonDataType.create(payload: "foo") - assert_equal "foo", json.payload - end - - def test_assigning_number - json = JsonDataType.create(payload: 1.234) - assert_equal 1.234, json.payload - end - - def test_assigning_boolean - json = JsonDataType.create(payload: true) - assert_equal true, json.payload - end end class PostgresqlJSONTest < ActiveRecord::PostgreSQLTestCase diff --git a/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb b/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb index bfc763e1ef..76e0ad60fe 100644 --- a/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb +++ b/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb @@ -324,13 +324,13 @@ 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 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" @@ -340,13 +340,13 @@ 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 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/schema_authorization_test.rb b/activerecord/test/cases/adapters/postgresql/schema_authorization_test.rb index bf570176f4..f86a76e08a 100644 --- a/activerecord/test/cases/adapters/postgresql/schema_authorization_test.rb +++ b/activerecord/test/cases/adapters/postgresql/schema_authorization_test.rb @@ -75,17 +75,6 @@ class SchemaAuthorizationTest < ActiveRecord::PostgreSQLTestCase end end - def test_schema_uniqueness - assert_nothing_raised do - set_session_auth - USERS.each do |u| - set_session_auth u - assert_equal u, @connection.select_value("SELECT name FROM #{TABLE_NAME} WHERE id = 1") - set_session_auth - end - end - end - def test_sequence_schema_caching assert_nothing_raised do USERS.each do |u| diff --git a/activerecord/test/cases/adapters/postgresql/uuid_test.rb b/activerecord/test/cases/adapters/postgresql/uuid_test.rb index 52e4a38cae..8eddd81c38 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 @@ -63,6 +64,16 @@ class PostgresqlUUIDTest < ActiveRecord::PostgreSQLTestCase UUIDType.reset_column_information end + def test_add_column_with_null_true_and_default_nil + 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 + def test_data_type_of_uuid_types column = UUIDType.columns_hash["guid"] assert_equal :uuid, column.type 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/callbacks_test.rb b/activerecord/test/cases/associations/callbacks_test.rb index 7721bd5cd9..f9d1e44595 100644 --- a/activerecord/test/cases/associations/callbacks_test.rb +++ b/activerecord/test/cases/associations/callbacks_test.rb @@ -128,7 +128,7 @@ class AssociationCallbacksTest < ActiveRecord::TestCase assert ar.developers_log.empty? alice = Developer.new(name: "alice") ar.developers_with_callbacks << alice - assert_equal"after_adding#{alice.id}", ar.developers_log.last + assert_equal "after_adding#{alice.id}", ar.developers_log.last bob = ar.developers_with_callbacks.create(name: "bob") assert_equal "after_adding#{bob.id}", ar.developers_log.last diff --git a/activerecord/test/cases/associations/cascaded_eager_loading_test.rb b/activerecord/test/cases/associations/cascaded_eager_loading_test.rb index 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 36238dd088..55b294cfaa 100644 --- a/activerecord/test/cases/associations/eager_test.rb +++ b/activerecord/test/cases/associations/eager_test.rb @@ -271,9 +271,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 @@ -1094,12 +1091,6 @@ class EagerAssociationTest < ActiveRecord::TestCase assert_equal authors(:david), assert_no_queries { posts[0].author } posts = assert_queries(2) do - Post.all.merge!(select: "distinct posts.*", includes: :author, joins: [:comments], where: "comments.body like 'Thank you%'", order: "posts.id").to_a - end - assert_equal [posts(:welcome)], posts - assert_equal authors(:david), assert_no_queries { posts[0].author } - - posts = assert_queries(2) do Post.all.merge!(includes: :author, joins: { taggings: :tag }, where: "tags.name = 'General'", order: "posts.id").to_a end assert_equal posts(:welcome, :thinking), posts diff --git a/activerecord/test/cases/associations/extension_test.rb b/activerecord/test/cases/associations/extension_test.rb index 87d842f21d..f707a170f5 100644 --- a/activerecord/test/cases/associations/extension_test.rb +++ b/activerecord/test/cases/associations/extension_test.rb @@ -78,6 +78,12 @@ class AssociationsExtensionsTest < ActiveRecord::TestCase assert_equal post.association(:comments), post.comments.where("1=1").the_association end + def test_association_with_default_scope + assert_raises OopsError do + posts(:welcome).comments.destroy_all + end + end + private def extend!(model) diff --git a/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb b/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb index 8060790594..f73005b3cb 100644 --- a/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb +++ b/activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb @@ -367,19 +367,6 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase assert_equal Developer.find(1).projects.sort_by(&:id).last, proj # prove join table is updated end - def test_create_by_new_record - devel = Developer.new(name: "Marcel", salary: 75000) - devel.projects.build(name: "Make bed") - proj2 = devel.projects.build(name: "Lie in it") - assert_equal devel.projects.last, proj2 - assert !proj2.persisted? - devel.save - assert devel.persisted? - assert proj2.persisted? - assert_equal devel.projects.last, proj2 - assert_equal Developer.find_by_name("Marcel").projects.last, proj2 # prove join table is updated - end - def test_creation_respects_hash_condition # in Oracle '' is saved as null therefore need to save ' ' in not null column post = categories(:general).post_with_conditions.build(body: " ") @@ -954,7 +941,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase assert_not_nil Developer._reflections["shared_computers"] # Checking the fixture for named association is important here, because it's the only way # we've been able to reproduce this bug - assert_not_nil File.read(File.expand_path("../../../fixtures/developers.yml", __FILE__)).index("shared_computers") + assert_not_nil File.read(File.expand_path("../../fixtures/developers.yml", __dir__)).index("shared_computers") assert_equal developers(:david).shared_computers.first, computers(:laptop) end diff --git a/activerecord/test/cases/associations/has_many_associations_test.rb b/activerecord/test/cases/associations/has_many_associations_test.rb index 6a479a344c..a936017ae3 100644 --- a/activerecord/test/cases/associations/has_many_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_associations_test.rb @@ -741,6 +741,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 @@ -1355,7 +1390,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase Client.create(client_of: firm.id, name: "SmallTime Inc.") # only one of two clients is included in the association due to the :conditions key assert_equal 2, Client.where(client_of: firm.id).size - assert_equal 1, firm.dependent_sanitized_conditional_clients_of_firm.size + assert_equal 1, firm.dependent_hash_conditional_clients_of_firm.size firm.destroy # only the correctly associated client should have been deleted assert_equal 1, Client.where(client_of: firm.id).size @@ -2251,7 +2286,15 @@ class HasManyAssociationsTest < ActiveRecord::TestCase test "association with extend option with multiple extensions" do post = posts(:welcome) assert_equal "lifo", post.comments_with_extend_2.author - assert_equal "hello", post.comments_with_extend_2.greeting + assert_equal "hullo", post.comments_with_extend_2.greeting + end + + test "extend option affects per association" do + post = posts(:welcome) + assert_equal "lifo", post.comments_with_extend.author + assert_equal "lifo", post.comments_with_extend_2.author + assert_equal "hello", post.comments_with_extend.greeting + assert_equal "hullo", post.comments_with_extend_2.greeting end test "delete record with complex joins" do @@ -2311,8 +2354,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 diff --git a/activerecord/test/cases/associations/has_many_through_associations_test.rb b/activerecord/test/cases/associations/has_many_through_associations_test.rb index ea52fb5a67..1c2138a3d0 100644 --- a/activerecord/test/cases/associations/has_many_through_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_through_associations_test.rb @@ -64,10 +64,6 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase club1.members.sort_by(&:id) end - def make_model(name) - Class.new(ActiveRecord::Base) { define_singleton_method(:name) { name } } - end - def test_ordered_has_many_through person_prime = Class.new(ActiveRecord::Base) do def self.name; "Person"; end @@ -152,20 +148,6 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase assert after_destroy_called, "after destroy should be called" end - def make_no_pk_hm_t - lesson = make_model "Lesson" - student = make_model "Student" - - lesson_student = make_model "LessonStudent" - lesson_student.table_name = "lessons_students" - - lesson_student.belongs_to :lesson, anonymous_class: lesson - lesson_student.belongs_to :student, anonymous_class: student - lesson.has_many :lesson_students, anonymous_class: lesson_student - lesson.has_many :students, through: :lesson_students, anonymous_class: student - [lesson, lesson_student, student] - end - def test_pk_is_not_required_for_join post = Post.includes(:scategories).first post2 = Post.includes(:categories).first @@ -337,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") @@ -1252,4 +1245,23 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase ) end end + + private + def make_model(name) + Class.new(ActiveRecord::Base) { define_singleton_method(:name) { name } } + end + + def make_no_pk_hm_t + lesson = make_model "Lesson" + student = make_model "Student" + + lesson_student = make_model "LessonStudent" + lesson_student.table_name = "lessons_students" + + lesson_student.belongs_to :lesson, anonymous_class: lesson + lesson_student.belongs_to :student, anonymous_class: student + lesson.has_many :lesson_students, anonymous_class: lesson_student + lesson.has_many :students, through: :lesson_students, anonymous_class: student + [lesson, lesson_student, student] + end end diff --git a/activerecord/test/cases/associations/has_one_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 287b3e9ebc..467cc73ecd 100644 --- a/activerecord/test/cases/associations/inverse_associations_test.rb +++ b/activerecord/test/cases/associations/inverse_associations_test.rb @@ -651,20 +651,6 @@ class InversePolymorphicBelongsToTests < ActiveRecord::TestCase assert_equal face.description, new_man.polymorphic_face.description, "Description of face should be the same after changes to replaced-parent-owned instance" end - def test_child_instance_should_be_shared_with_replaced_via_method_parent - face = faces(:confused) - new_man = Man.new - - assert_not_nil face.polymorphic_man - face.polymorphic_man = new_man - - assert_equal face.description, new_man.polymorphic_face.description, "Description of face should be the same before changes to parent instance" - face.description = "Bongo" - assert_equal face.description, new_man.polymorphic_face.description, "Description of face should be the same after changes to parent instance" - new_man.polymorphic_face.description = "Mungo" - assert_equal face.description, new_man.polymorphic_face.description, "Description of face should be the same after changes to replaced-parent-owned instance" - end - def test_inversed_instance_should_not_be_reloaded_after_stale_state_changed new_man = Man.new face = Face.new diff --git a/activerecord/test/cases/associations_test.rb b/activerecord/test/cases/associations_test.rb index 4ab690bfc6..2eb31326a5 100644 --- a/activerecord/test/cases/associations_test.rb +++ b/activerecord/test/cases/associations_test.rb @@ -220,6 +220,18 @@ class AssociationProxyTest < ActiveRecord::TestCase assert_equal david.projects, david.projects.scope end + test "proxy object is cached" do + david = developers(:david) + assert_same david.projects, david.projects + end + + test "proxy object can be stubbed" do + david = developers(:david) + david.projects.define_singleton_method(:extra_method) { 42 } + + assert_equal 42, david.projects.extra_method + end + test "inverses get set of subsets of the association" do man = Man.create man.interests.create diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb index 15c253890b..dc32e995a4 100644 --- a/activerecord/test/cases/base_test.rb +++ b/activerecord/test/cases/base_test.rb @@ -907,80 +907,6 @@ class BasicsTest < ActiveRecord::TestCase end end - class NumericData < ActiveRecord::Base - self.table_name = "numeric_data" - - attribute :my_house_population, :integer - attribute :atoms_in_universe, :integer - end - - def test_big_decimal_conditions - m = NumericData.new( - bank_balance: 1586.43, - big_bank_balance: BigDecimal("1000234000567.95"), - world_population: 6000000000, - my_house_population: 3 - ) - assert m.save - assert_equal 0, NumericData.where("bank_balance > ?", 2000.0).count - end - - def test_numeric_fields - m = NumericData.new( - bank_balance: 1586.43, - big_bank_balance: BigDecimal("1000234000567.95"), - world_population: 6000000000, - my_house_population: 3 - ) - assert m.save - - m1 = NumericData.find(m.id) - assert_not_nil m1 - - # As with migration_test.rb, we should make world_population >= 2**62 - # to cover 64-bit platforms and test it is a Bignum, but the main thing - # is that it's an Integer. - assert_kind_of Integer, m1.world_population - assert_equal 6000000000, m1.world_population - - assert_kind_of Integer, m1.my_house_population - assert_equal 3, m1.my_house_population - - assert_kind_of BigDecimal, m1.bank_balance - assert_equal BigDecimal("1586.43"), m1.bank_balance - - assert_kind_of BigDecimal, m1.big_bank_balance - assert_equal BigDecimal("1000234000567.95"), m1.big_bank_balance - end - - def test_numeric_fields_with_scale - m = NumericData.new( - bank_balance: 1586.43122334, - big_bank_balance: BigDecimal("234000567.952344"), - world_population: 6000000000, - my_house_population: 3 - ) - assert m.save - - m1 = NumericData.find(m.id) - assert_not_nil m1 - - # As with migration_test.rb, we should make world_population >= 2**62 - # to cover 64-bit platforms and test it is a Bignum, but the main thing - # is that it's an Integer. - assert_kind_of Integer, m1.world_population - assert_equal 6000000000, m1.world_population - - assert_kind_of Integer, m1.my_house_population - assert_equal 3, m1.my_house_population - - assert_kind_of BigDecimal, m1.bank_balance - assert_equal BigDecimal("1586.43"), m1.bank_balance - - assert_kind_of BigDecimal, m1.big_bank_balance - assert_equal BigDecimal("234000567.95"), m1.big_bank_balance - end - def test_auto_id auto = AutoId.new auto.save diff --git a/activerecord/test/cases/cache_key_test.rb b/activerecord/test/cases/cache_key_test.rb index 2c6a38ec35..7b8264e6e8 100644 --- a/activerecord/test/cases/cache_key_test.rb +++ b/activerecord/test/cases/cache_key_test.rb @@ -4,15 +4,23 @@ module ActiveRecord class CacheKeyTest < ActiveRecord::TestCase self.use_transactional_tests = false - class CacheMe < ActiveRecord::Base; end + class CacheMe < ActiveRecord::Base + self.cache_versioning = false + end + + class CacheMeWithVersion < ActiveRecord::Base + self.cache_versioning = true + end setup do @connection = ActiveRecord::Base.connection - @connection.create_table(:cache_mes) { |t| t.timestamps } + @connection.create_table(:cache_mes, force: true) { |t| t.timestamps } + @connection.create_table(:cache_me_with_versions, force: true) { |t| t.timestamps } end teardown do @connection.drop_table :cache_mes, if_exists: true + @connection.drop_table :cache_me_with_versions, if_exists: true end test "cache_key format is not too precise" do @@ -21,5 +29,23 @@ module ActiveRecord assert_equal key, record.reload.cache_key end + + test "cache_key has no version when versioning is on" do + record = CacheMeWithVersion.create + assert_equal "active_record/cache_key_test/cache_me_with_versions/#{record.id}", record.cache_key + end + + test "cache_version is only there when versioning is on" do + assert CacheMeWithVersion.create.cache_version.present? + assert_not CacheMe.create.cache_version.present? + end + + test "cache_key_with_version always has both key and version" do + r1 = CacheMeWithVersion.create + assert_equal "active_record/cache_key_test/cache_me_with_versions/#{r1.id}-#{r1.updated_at.to_s(:usec)}", r1.cache_key_with_version + + r2 = CacheMe.create + assert_equal "active_record/cache_key_test/cache_mes/#{r2.id}-#{r2.updated_at.to_s(:usec)}", r2.cache_key_with_version + end end end diff --git a/activerecord/test/cases/calculations_test.rb b/activerecord/test/cases/calculations_test.rb index 3214d778d4..80baaac30a 100644 --- a/activerecord/test/cases/calculations_test.rb +++ b/activerecord/test/cases/calculations_test.rb @@ -8,6 +8,7 @@ require "models/organization" require "models/possession" require "models/topic" require "models/reply" +require "models/numeric_data" require "models/minivan" require "models/speedometer" require "models/ship_part" @@ -17,14 +18,6 @@ require "models/post" require "models/comment" require "models/rating" -class NumericData < ActiveRecord::Base - self.table_name = "numeric_data" - - attribute :world_population, :integer - attribute :my_house_population, :integer - attribute :atoms_in_universe, :integer -end - class CalculationsTest < ActiveRecord::TestCase fixtures :companies, :accounts, :topics, :speedometers, :minivans, :books @@ -587,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 @@ -809,4 +805,16 @@ class CalculationsTest < ActiveRecord::TestCase def test_group_by_attribute_with_custom_type assert_equal({ "proposed" => 2, "published" => 2 }, Book.group(:status).count) end + + def test_deprecate_count_with_block_and_column_name + assert_deprecated do + assert_equal 6, Account.count(:firm_id) { true } + end + end + + def test_deprecate_sum_with_block_and_column_name + assert_deprecated do + assert_equal 6, Account.sum(:firm_id) { 1 } + end + end end diff --git a/activerecord/test/cases/connection_adapters/connection_handler_test.rb b/activerecord/test/cases/connection_adapters/connection_handler_test.rb index 681399c8bb..2a71f08d90 100644 --- a/activerecord/test/cases/connection_adapters/connection_handler_test.rb +++ b/activerecord/test/cases/connection_adapters/connection_handler_test.rb @@ -9,6 +9,17 @@ module ActiveRecord @pool = @handler.establish_connection(ActiveRecord::Base.configurations["arunit"]) end + def test_default_env_fall_back_to_default_env_when_rails_env_or_rack_env_is_empty_string + original_rails_env = ENV["RAILS_ENV"] + original_rack_env = ENV["RACK_ENV"] + ENV["RAILS_ENV"] = ENV["RACK_ENV"] = "" + + assert_equal "default_env", ActiveRecord::ConnectionHandling::DEFAULT_ENV.call + ensure + ENV["RAILS_ENV"] = original_rails_env + ENV["RACK_ENV"] = original_rack_env + end + def test_establish_connection_uses_spec_name config = { "readonly" => { "adapter" => "sqlite3" } } resolver = ConnectionAdapters::ConnectionSpecification::Resolver.new(config) diff --git a/activerecord/test/cases/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/dirty_test.rb b/activerecord/test/cases/dirty_test.rb index 721861975a..f72e0d2ead 100644 --- a/activerecord/test/cases/dirty_test.rb +++ b/activerecord/test/cases/dirty_test.rb @@ -4,10 +4,7 @@ require "models/pirate" # For timestamps require "models/parrot" require "models/person" # For optimistic locking require "models/aircraft" - -class NumericData < ActiveRecord::Base - self.table_name = "numeric_data" -end +require "models/numeric_data" class DirtyTest < ActiveRecord::TestCase include InTimeZone diff --git a/activerecord/test/cases/enum_test.rb b/activerecord/test/cases/enum_test.rb index 7f63bb2473..4ef9a125e6 100644 --- a/activerecord/test/cases/enum_test.rb +++ b/activerecord/test/cases/enum_test.rb @@ -6,7 +6,6 @@ class EnumTest < ActiveRecord::TestCase fixtures :books, :authors setup do - @author = authors(:david) @book = books(:awdr) end @@ -39,6 +38,8 @@ class EnumTest < ActiveRecord::TestCase assert_equal @book, Book.author_visibility_visible.first assert_equal @book, Book.illustrator_visibility_visible.first assert_equal @book, Book.medium_to_read.first + assert_equal books(:ddd), Book.forgotten.first + assert_equal books(:rfr), authors(:david).unpublished_books.first end test "find via where with values" do @@ -57,9 +58,9 @@ class EnumTest < ActiveRecord::TestCase assert_not_equal @book, Book.where(status: :written).first assert_equal @book, Book.where(status: [:published]).first assert_not_equal @book, Book.where(status: [:written]).first - assert_not @author.unpublished_books.include?(@book) 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 a7b6333010..420f552ef6 100644 --- a/activerecord/test/cases/finder_test.rb +++ b/activerecord/test/cases/finder_test.rb @@ -743,7 +743,6 @@ class FinderTest < ActiveRecord::TestCase assert Topic.where(author_name: "David", title: "The First Topic", replies_count: 1, approved: false).find(1) assert_raise(ActiveRecord::RecordNotFound) { Topic.where(author_name: "David", title: "The First Topic", replies_count: 1, approved: true).find(1) } assert_raise(ActiveRecord::RecordNotFound) { Topic.where(author_name: "David", title: "HHC", replies_count: 1, approved: false).find(1) } - assert_raise(ActiveRecord::RecordNotFound) { Topic.where(author_name: "David", title: "The First Topic", replies_count: 1, approved: true).find(1) } end def test_condition_interpolation @@ -1024,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"). diff --git a/activerecord/test/cases/fixtures_test.rb b/activerecord/test/cases/fixtures_test.rb index a0a6d3c7ef..b499e60922 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/inheritance_test.rb b/activerecord/test/cases/inheritance_test.rb index d70572d6eb..fb5a7bcc31 100644 --- a/activerecord/test/cases/inheritance_test.rb +++ b/activerecord/test/cases/inheritance_test.rb @@ -1,6 +1,7 @@ require "cases/helper" require "models/author" require "models/company" +require "models/membership" require "models/person" require "models/post" require "models/project" @@ -29,7 +30,7 @@ end class InheritanceTest < ActiveRecord::TestCase include InheritanceTestHelper - fixtures :companies, :projects, :subscribers, :accounts, :vegetables + fixtures :companies, :projects, :subscribers, :accounts, :vegetables, :memberships def test_class_with_store_full_sti_class_returns_full_name with_store_full_sti_class do @@ -316,7 +317,7 @@ class InheritanceTest < ActiveRecord::TestCase end def test_new_with_autoload_paths - path = File.expand_path("../../models/autoloadable", __FILE__) + path = File.expand_path("../models/autoloadable", __dir__) ActiveSupport::Dependencies.autoload_paths << path firm = Company.new(type: "ExtraFirm") @@ -435,6 +436,10 @@ class InheritanceTest < ActiveRecord::TestCase assert_nothing_raised { Company.of_first_firm } assert_nothing_raised { Client.of_first_firm } end + + def test_inheritance_with_default_scope + assert_equal 1, SelectedMembership.count(:all) + end end class InheritanceComputeTypeTest < ActiveRecord::TestCase diff --git a/activerecord/test/cases/integration_test.rb b/activerecord/test/cases/integration_test.rb index 0678bb714f..9104976126 100644 --- a/activerecord/test/cases/integration_test.rb +++ b/activerecord/test/cases/integration_test.rb @@ -168,13 +168,65 @@ class IntegrationTest < ActiveRecord::TestCase end def test_named_timestamps_for_cache_key - owner = owners(:blackbeard) - assert_equal "owners/#{owner.id}-#{owner.happy_at.utc.to_s(:usec)}", owner.cache_key(:updated_at, :happy_at) + assert_deprecated do + owner = owners(:blackbeard) + assert_equal "owners/#{owner.id}-#{owner.happy_at.utc.to_s(:usec)}", owner.cache_key(:updated_at, :happy_at) + end end def test_cache_key_when_named_timestamp_is_nil - owner = owners(:blackbeard) - owner.happy_at = nil - assert_equal "owners/#{owner.id}", owner.cache_key(:happy_at) + assert_deprecated do + owner = owners(:blackbeard) + owner.happy_at = nil + assert_equal "owners/#{owner.id}", owner.cache_key(:happy_at) + end + end + + def test_cache_key_is_stable_with_versioning_on + Developer.cache_versioning = true + + developer = Developer.first + first_key = developer.cache_key + + developer.touch + second_key = developer.cache_key + + assert_equal first_key, second_key + ensure + Developer.cache_versioning = false + end + + def test_cache_version_changes_with_versioning_on + Developer.cache_versioning = true + + developer = Developer.first + first_version = developer.cache_version + + travel 10.seconds do + developer.touch + end + + second_version = developer.cache_version + + assert_not_equal first_version, second_version + ensure + Developer.cache_versioning = false + end + + def test_cache_key_retains_version_when_custom_timestamp_is_used + Developer.cache_versioning = true + + developer = Developer.first + first_key = developer.cache_key_with_version + + travel 10.seconds do + developer.touch + end + + second_key = developer.cache_key_with_version + + assert_not_equal first_key, second_key + ensure + Developer.cache_versioning = false end end diff --git a/activerecord/test/cases/json_attribute_test.rb b/activerecord/test/cases/json_attribute_test.rb new file mode 100644 index 0000000000..e5848b45f8 --- /dev/null +++ b/activerecord/test/cases/json_attribute_test.rb @@ -0,0 +1,33 @@ +require "cases/helper" +require "cases/json_shared_test_cases" + +class JsonAttributeTest < ActiveRecord::TestCase + include JSONSharedTestCases + self.use_transactional_tests = false + + class JsonDataTypeOnText < ActiveRecord::Base + self.table_name = "json_data_type" + + attribute :payload, :json + attribute :settings, :json + + store_accessor :settings, :resolution + end + + def setup + super + @connection.create_table("json_data_type") do |t| + t.text "payload" + t.text "settings" + end + end + + private + def column_type + :text + end + + def klass + JsonDataTypeOnText + end +end diff --git a/activerecord/test/cases/json_shared_test_cases.rb b/activerecord/test/cases/json_shared_test_cases.rb new file mode 100644 index 0000000000..9a1c1c3f3f --- /dev/null +++ b/activerecord/test/cases/json_shared_test_cases.rb @@ -0,0 +1,221 @@ +require "support/schema_dumping_helper" + +module JSONSharedTestCases + include SchemaDumpingHelper + + class JsonDataType < ActiveRecord::Base + self.table_name = "json_data_type" + + store_accessor :settings, :resolution + end + + def setup + @connection = ActiveRecord::Base.connection + end + + def teardown + @connection.drop_table :json_data_type, if_exists: true + klass.reset_column_information + end + + def test_column + column = klass.columns_hash["payload"] + assert_equal column_type, column.type + assert_equal column_type.to_s, column.sql_type + + type = klass.type_for_attribute("payload") + assert_not type.binary? + end + + def test_change_table_supports_json + @connection.change_table("json_data_type") do |t| + t.public_send column_type, "users" + end + klass.reset_column_information + column = klass.columns_hash["users"] + assert_equal column_type, column.type + assert_equal column_type.to_s, column.sql_type + end + + def test_schema_dumping + output = dump_table_schema("json_data_type") + assert_match(/t\.#{column_type}\s+"settings"/, output) + end + + def test_cast_value_on_write + x = klass.new(payload: { "string" => "foo", :symbol => :bar }) + assert_equal({ "string" => "foo", :symbol => :bar }, x.payload_before_type_cast) + assert_equal({ "string" => "foo", "symbol" => "bar" }, x.payload) + x.save! + assert_equal({ "string" => "foo", "symbol" => "bar" }, x.reload.payload) + end + + def test_type_cast_json + type = klass.type_for_attribute("payload") + + data = '{"a_key":"a_value"}' + hash = type.deserialize(data) + assert_equal({ "a_key" => "a_value" }, hash) + assert_equal({ "a_key" => "a_value" }, type.deserialize(data)) + + assert_equal({}, type.deserialize("{}")) + assert_equal({ "key" => nil }, type.deserialize('{"key": null}')) + assert_equal({ "c" => "}", '"a"' => 'b "a b' }, type.deserialize(%q({"c":"}", "\"a\"":"b \"a b"}))) + end + + def test_rewrite + @connection.execute(%q|insert into json_data_type (payload) VALUES ('{"k":"v"}')|) + x = klass.first + x.payload = { '"a\'' => "b" } + assert x.save! + end + + def test_select + @connection.execute(%q|insert into json_data_type (payload) VALUES ('{"k":"v"}')|) + x = klass.first + assert_equal({ "k" => "v" }, x.payload) + end + + def test_select_multikey + @connection.execute(%q|insert into json_data_type (payload) VALUES ('{"k1":"v1", "k2":"v2", "k3":[1,2,3]}')|) + x = klass.first + assert_equal({ "k1" => "v1", "k2" => "v2", "k3" => [1, 2, 3] }, x.payload) + end + + def test_null_json + @connection.execute("insert into json_data_type (payload) VALUES(null)") + x = klass.first + assert_nil(x.payload) + end + + def test_select_nil_json_after_create + json = klass.create!(payload: nil) + x = klass.where(payload: nil).first + assert_equal(json, x) + end + + def test_select_nil_json_after_update + json = klass.create!(payload: "foo") + x = klass.where(payload: nil).first + assert_nil(x) + + json.update_attributes(payload: nil) + x = klass.where(payload: nil).first + assert_equal(json.reload, x) + end + + def test_select_array_json_value + @connection.execute(%q|insert into json_data_type (payload) VALUES ('["v0",{"k1":"v1"}]')|) + x = klass.first + assert_equal(["v0", { "k1" => "v1" }], x.payload) + end + + def test_rewrite_array_json_value + @connection.execute(%q|insert into json_data_type (payload) VALUES ('["v0",{"k1":"v1"}]')|) + x = klass.first + x.payload = ["v1", { "k2" => "v2" }, "v3"] + assert x.save! + end + + def test_with_store_accessors + x = klass.new(resolution: "320×480") + assert_equal "320×480", x.resolution + + x.save! + x = klass.first + assert_equal "320×480", x.resolution + + x.resolution = "640×1136" + x.save! + + x = klass.first + assert_equal "640×1136", x.resolution + end + + def test_duplication_with_store_accessors + x = klass.new(resolution: "320×480") + assert_equal "320×480", x.resolution + + y = x.dup + assert_equal "320×480", y.resolution + end + + def test_yaml_round_trip_with_store_accessors + x = klass.new(resolution: "320×480") + assert_equal "320×480", x.resolution + + y = YAML.load(YAML.dump(x)) + assert_equal "320×480", y.resolution + end + + def test_changes_in_place + json = klass.new + assert_not json.changed? + + json.payload = { "one" => "two" } + assert json.changed? + assert json.payload_changed? + + json.save! + assert_not json.changed? + + json.payload["three"] = "four" + assert json.payload_changed? + + json.save! + json.reload + + assert_equal({ "one" => "two", "three" => "four" }, json.payload) + assert_not json.changed? + end + + def test_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) + + json.reload + assert_not json.changed? + + json.payload = time + assert_not json.changed? + end + + def test_assigning_string_literal + json = klass.create!(payload: "foo") + assert_equal "foo", json.payload + end + + def test_assigning_number + json = klass.create!(payload: 1.234) + assert_equal 1.234, json.payload + end + + def test_assigning_boolean + json = klass.create!(payload: true) + assert_equal true, json.payload + end + + private + def klass + JsonDataType + end +end 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/command_recorder_test.rb b/activerecord/test/cases/migration/command_recorder_test.rb index 802a969cb7..007926f1b9 100644 --- a/activerecord/test/cases/migration/command_recorder_test.rb +++ b/activerecord/test/cases/migration/command_recorder_test.rb @@ -211,11 +211,6 @@ module ActiveRecord assert_equal [:remove_index, [:table, { name: "new_index" }]], remove end - def test_invert_add_index_with_no_options - remove = @recorder.inverse_of :add_index, [:table, [:one, :two]] - assert_equal [:remove_index, [:table, { column: [:one, :two] }]], remove - end - def test_invert_remove_index add = @recorder.inverse_of :remove_index, [:table, :one] assert_equal [:add_index, [:table, :one]], add diff --git a/activerecord/test/cases/migration/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/references_statements_test.rb b/activerecord/test/cases/migration/references_statements_test.rb index 06c44c8c52..e9eb9968cb 100644 --- a/activerecord/test/cases/migration/references_statements_test.rb +++ b/activerecord/test/cases/migration/references_statements_test.rb @@ -50,6 +50,14 @@ module ActiveRecord assert column_exists?(table_name, :taggable_type, :string, default: "Photo") end + def test_creates_reference_type_column_with_not_null + connection.create_table table_name, force: true do |t| + t.references :taggable, null: false, polymorphic: true + end + assert column_exists?(table_name, :taggable_id, :integer, null: false) + assert column_exists?(table_name, :taggable_type, :string, null: false) + end + def test_does_not_share_options_with_reference_type_column add_reference table_name, :taggable, type: :integer, limit: 2, polymorphic: true assert column_exists?(table_name, :taggable_id, :integer, limit: 2) diff --git a/activerecord/test/cases/migration/rename_table_test.rb b/activerecord/test/cases/migration/rename_table_test.rb index 7bcabd0cc6..5da3ad33a3 100644 --- a/activerecord/test/cases/migration/rename_table_test.rb +++ b/activerecord/test/cases/migration/rename_table_test.rb @@ -79,10 +79,33 @@ module ActiveRecord assert_equal ConnectionAdapters::PostgreSQL::Name.new("public", "octopi_#{pk}_seq"), seq end + def test_renaming_table_renames_primary_key + connection.create_table :cats, id: :uuid, default: "uuid_generate_v4()" + rename_table :cats, :felines + + assert connection.table_exists? :felines + refute connection.table_exists? :cats + + primary_key_name = connection.select_values(<<-SQL.strip_heredoc, "SCHEMA")[0] + SELECT c.relname + FROM pg_class c + JOIN pg_index i + ON c.oid = i.indexrelid + WHERE i.indisprimary + AND i.indrelid = 'felines'::regclass + SQL + + assert_equal "felines_pkey", primary_key_name + ensure + connection.drop_table :cats, if_exists: true + connection.drop_table :felines, if_exists: true + end + def test_renaming_table_doesnt_attempt_to_rename_non_existent_sequences connection.create_table :cats, id: :uuid, default: "uuid_generate_v4()" assert_nothing_raised { rename_table :cats, :felines } assert connection.table_exists? :felines + refute connection.table_exists? :cats ensure connection.drop_table :cats, if_exists: true connection.drop_table :felines, if_exists: true diff --git a/activerecord/test/cases/migration_test.rb b/activerecord/test/cases/migration_test.rb index da7875187a..3a49a41580 100644 --- a/activerecord/test/cases/migration_test.rb +++ b/activerecord/test/cases/migration_test.rb @@ -402,33 +402,6 @@ class MigrationTest < ActiveRecord::TestCase ActiveRecord::Migrator.up(migrations_path) end - def test_migration_sets_internal_metadata_even_when_fully_migrated - current_env = ActiveRecord::ConnectionHandling::DEFAULT_ENV.call - migrations_path = MIGRATIONS_ROOT + "/valid" - old_path = ActiveRecord::Migrator.migrations_paths - ActiveRecord::Migrator.migrations_paths = migrations_path - - ActiveRecord::Migrator.up(migrations_path) - assert_equal current_env, ActiveRecord::InternalMetadata[:environment] - - original_rails_env = ENV["RAILS_ENV"] - original_rack_env = ENV["RACK_ENV"] - ENV["RAILS_ENV"] = ENV["RACK_ENV"] = "foofoo" - new_env = ActiveRecord::ConnectionHandling::DEFAULT_ENV.call - - refute_equal current_env, new_env - - sleep 1 # mysql by default does not store fractional seconds in the database - - ActiveRecord::Migrator.up(migrations_path) - assert_equal new_env, ActiveRecord::InternalMetadata[:environment] - ensure - ActiveRecord::Migrator.migrations_paths = old_path - ENV["RAILS_ENV"] = original_rails_env - ENV["RACK_ENV"] = original_rack_env - ActiveRecord::Migrator.up(migrations_path) - end - def test_internal_metadata_stores_environment_when_other_data_exists ActiveRecord::InternalMetadata.delete_all ActiveRecord::InternalMetadata[:foo] = "bar" @@ -529,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 @@ -541,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 diff --git a/activerecord/test/cases/nested_attributes_test.rb b/activerecord/test/cases/nested_attributes_test.rb index b87419d203..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 @@ -752,7 +752,7 @@ module NestedAttributesOnACollectionAssociationTests exception = assert_raise ArgumentError do @pirate.send(association_setter, "foo") end - assert_equal 'Hash or Array expected, got String ("foo")', exception.message + assert_equal %{Hash or Array expected for attribute `#{@association_name}`, got String ("foo")}, exception.message end def test_should_work_with_update_as_well diff --git a/activerecord/test/cases/numeric_data_test.rb b/activerecord/test/cases/numeric_data_test.rb new file mode 100644 index 0000000000..76b97033af --- /dev/null +++ b/activerecord/test/cases/numeric_data_test.rb @@ -0,0 +1,71 @@ +require "cases/helper" +require "models/numeric_data" + +class NumericDataTest < ActiveRecord::TestCase + def test_big_decimal_conditions + m = NumericData.new( + bank_balance: 1586.43, + big_bank_balance: BigDecimal("1000234000567.95"), + world_population: 6000000000, + my_house_population: 3 + ) + assert m.save + assert_equal 0, NumericData.where("bank_balance > ?", 2000.0).count + end + + def test_numeric_fields + m = NumericData.new( + bank_balance: 1586.43, + big_bank_balance: BigDecimal("1000234000567.95"), + world_population: 6000000000, + my_house_population: 3 + ) + assert m.save + + m1 = NumericData.find(m.id) + assert_not_nil m1 + + # As with migration_test.rb, we should make world_population >= 2**62 + # to cover 64-bit platforms and test it is a Bignum, but the main thing + # is that it's an Integer. + assert_kind_of Integer, m1.world_population + assert_equal 6000000000, m1.world_population + + assert_kind_of Integer, m1.my_house_population + assert_equal 3, m1.my_house_population + + assert_kind_of BigDecimal, m1.bank_balance + assert_equal BigDecimal("1586.43"), m1.bank_balance + + assert_kind_of BigDecimal, m1.big_bank_balance + assert_equal BigDecimal("1000234000567.95"), m1.big_bank_balance + end + + def test_numeric_fields_with_scale + m = NumericData.new( + bank_balance: 1586.43122334, + big_bank_balance: BigDecimal("234000567.952344"), + world_population: 6000000000, + my_house_population: 3 + ) + assert m.save + + m1 = NumericData.find(m.id) + assert_not_nil m1 + + # As with migration_test.rb, we should make world_population >= 2**62 + # to cover 64-bit platforms and test it is a Bignum, but the main thing + # is that it's an Integer. + assert_kind_of Integer, m1.world_population + assert_equal 6000000000, m1.world_population + + assert_kind_of Integer, m1.my_house_population + assert_equal 3, m1.my_house_population + + assert_kind_of BigDecimal, m1.bank_balance + assert_equal BigDecimal("1586.43"), m1.bank_balance + + assert_kind_of BigDecimal, m1.big_bank_balance + assert_equal BigDecimal("234000567.95"), m1.big_bank_balance + end +end diff --git a/activerecord/test/cases/primary_keys_test.rb b/activerecord/test/cases/primary_keys_test.rb index 5ded619716..56229b70bc 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 494663eb04..68d18e6471 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,7 @@ 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 with_temporary_connection_pool do spec_name = Task.connection_specification_name conf = ActiveRecord::Base.configurations["arunit"].merge("name" => "test2") @@ -302,8 +339,7 @@ class QueryCacheTest < ActiveRecord::TestCase 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/merging_test.rb b/activerecord/test/cases/relation/merging_test.rb index 64866eaf2d..3901824aac 100644 --- a/activerecord/test/cases/relation/merging_test.rb +++ b/activerecord/test/cases/relation/merging_test.rb @@ -56,7 +56,7 @@ class RelationMergingTest < ActiveRecord::TestCase def test_relation_merging_with_locks devs = Developer.lock.where("salary >= 80000").order("id DESC").merge(Developer.limit(2)) - assert devs.locked.present? + assert devs.locked? end def test_relation_merging_with_preload @@ -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 11ef0d8743..dea787c07f 100644 --- a/activerecord/test/cases/relation/mutation_test.rb +++ b/activerecord/test/cases/relation/mutation_test.rb @@ -36,7 +36,7 @@ module ActiveRecord @relation ||= Relation.new FakeKlass.new("posts"), Post.arel_table, Post.predicate_builder end - (Relation::MULTI_VALUE_METHODS - [:references, :extending, :order, :unscope, :select, :left_joins]).each do |method| + (Relation::MULTI_VALUE_METHODS - [:references, :extending, :order, :unscope, :select]).each do |method| test "##{method}!" do assert relation.public_send("#{method}!", :foo).equal?(relation) assert_equal [:foo], relation.public_send("#{method}_values") 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 5fb32270b7..a403824f1a 100644 --- a/activerecord/test/cases/relation_test.rb +++ b/activerecord/test/cases/relation_test.rb @@ -6,7 +6,7 @@ require "models/rating" module ActiveRecord class RelationTest < ActiveRecord::TestCase - fixtures :posts, :comments, :authors, :author_addresses + fixtures :posts, :comments, :authors, :author_addresses, :ratings FakeKlass = Struct.new(:table_name, :name) do extend ActiveRecord::Delegation::DelegateCache @@ -224,7 +224,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..5767dec315 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 diff --git a/activerecord/test/cases/reload_models_test.rb b/activerecord/test/cases/reload_models_test.rb index 5dc9d6d8b7..3f4c0c03e3 100644 --- a/activerecord/test/cases/reload_models_test.rb +++ b/activerecord/test/cases/reload_models_test.rb @@ -13,7 +13,7 @@ class ReloadModelsTest < ActiveRecord::TestCase # development environment. Note that meanwhile the class Pet is not # reloaded, simulating a class that is present in a plugin. Object.class_eval { remove_const :Owner } - Kernel.load(File.expand_path(File.join(File.dirname(__FILE__), "../models/owner.rb"))) + Kernel.load(File.expand_path("../models/owner.rb", __dir__)) pet = Pet.find_by_name("parrot") pet.owner = Owner.find_by_name("ashley") diff --git a/activerecord/test/cases/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 cb8d449ba9..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| @@ -116,32 +122,22 @@ class SchemaDumperTest < ActiveRecord::TestCase def test_schema_dump_includes_limit_constraint_for_integer_columns output = dump_all_table_schema([/^(?!integer_limits)/]) - assert_match %r{c_int_without_limit}, output + assert_match %r{"c_int_without_limit"(?!.*limit)}, output if current_adapter?(:PostgreSQLAdapter) - assert_no_match %r{c_int_without_limit.*limit:}, output - assert_match %r{c_int_1.*limit: 2}, output assert_match %r{c_int_2.*limit: 2}, output # int 3 is 4 bytes in postgresql - assert_match %r{c_int_3.*}, output - assert_no_match %r{c_int_3.*limit:}, output - - assert_match %r{c_int_4.*}, output - assert_no_match %r{c_int_4.*limit:}, output + assert_match %r{"c_int_3"(?!.*limit)}, output + assert_match %r{"c_int_4"(?!.*limit)}, output elsif current_adapter?(:Mysql2Adapter) - assert_match %r{c_int_without_limit"$}, output - assert_match %r{c_int_1.*limit: 1}, output assert_match %r{c_int_2.*limit: 2}, output assert_match %r{c_int_3.*limit: 3}, output - assert_match %r{c_int_4.*}, output - assert_no_match %r{c_int_4.*:limit}, output + assert_match %r{"c_int_4"(?!.*limit)}, output elsif current_adapter?(:SQLite3Adapter) - assert_no_match %r{c_int_without_limit.*limit:}, output - assert_match %r{c_int_1.*limit: 1}, output assert_match %r{c_int_2.*limit: 2}, output assert_match %r{c_int_3.*limit: 3}, output @@ -330,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/named_scoping_test.rb b/activerecord/test/cases/scoping/named_scoping_test.rb index 0c2cffe0d3..483ea7128d 100644 --- a/activerecord/test/cases/scoping/named_scoping_test.rb +++ b/activerecord/test/cases/scoping/named_scoping_test.rb @@ -551,6 +551,12 @@ class NamedScopingTest < ActiveRecord::TestCase assert_equal 1, SpecialComment.where(body: "go crazy").created.count end + def test_model_class_should_respond_to_extending + assert_raises OopsError do + Comment.unscoped.oops_comments.destroy_all + end + end + def test_model_class_should_respond_to_none assert !Topic.none? Topic.delete_all diff --git a/activerecord/test/cases/scoping/relation_scoping_test.rb b/activerecord/test/cases/scoping/relation_scoping_test.rb index 3fbff7664b..8535be8402 100644 --- a/activerecord/test/cases/scoping/relation_scoping_test.rb +++ b/activerecord/test/cases/scoping/relation_scoping_test.rb @@ -229,12 +229,19 @@ class RelationScopingTest < ActiveRecord::TestCase end end - def test_circular_joins_with_current_scope_does_not_crash + def test_circular_joins_with_scoping_does_not_crash posts = Post.joins(comments: :post).scoping do - Post.current_scope.first(10) + Post.first(10) end assert_equal posts, Post.joins(comments: :post).first(10) end + + def test_circular_left_joins_with_scoping_does_not_crash + posts = Post.left_joins(comments: :post).scoping do + Post.first(10) + end + assert_equal posts, Post.left_joins(comments: :post).first(10) + end end class NestedRelationScopingTest < ActiveRecord::TestCase diff --git a/activerecord/test/cases/serialized_attribute_test.rb b/activerecord/test/cases/serialized_attribute_test.rb index 673392b4c4..e1bdaab5cf 100644 --- a/activerecord/test/cases/serialized_attribute_test.rb +++ b/activerecord/test/cases/serialized_attribute_test.rb @@ -349,4 +349,32 @@ class SerializedAttributeTest < ActiveRecord::TestCase topic.foo refute topic.changed? end + + def test_serialized_attribute_works_under_concurrent_initial_access + model = Topic.dup + + topic = model.last + topic.update group: "1" + + model.serialize :group, JSON + model.reset_column_information + + # This isn't strictly necessary for the test, but a little bit of + # knowledge of internals allows us to make failures far more likely. + model.define_singleton_method(:define_attribute) do |*args| + Thread.pass + super(*args) + end + + threads = 4.times.map do + Thread.new do + topic.reload.group + end + end + + # All the threads should retrieve the value knowing it is JSON, and + # thus decode it. If this fails, some threads will instead see the + # raw string ("1"), or raise an exception. + assert_equal [1] * threads.size, threads.map(&:value) + end end diff --git a/activerecord/test/cases/tasks/mysql_rake_test.rb b/activerecord/test/cases/tasks/mysql_rake_test.rb index 33da3d11fc..9c6fb14376 100644 --- a/activerecord/test/cases/tasks/mysql_rake_test.rb +++ b/activerecord/test/cases/tasks/mysql_rake_test.rb @@ -294,16 +294,9 @@ if current_adapter?(:Mysql2Adapter) ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, filename) end - def test_structure_dump - filename = "awesome-file.sql" - Kernel.expects(:system).with("mysqldump", "--result-file", filename, "--no-data", "--routines", "--skip-comments", "test-db").returns(true) - - ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, filename) - end - 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 @@ -371,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..79ba306ef5 100644 --- a/activerecord/test/cases/transactions_test.rb +++ b/activerecord/test/cases/transactions_test.rb @@ -595,6 +595,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 diff --git a/activerecord/test/cases/validations_test.rb b/activerecord/test/cases/validations_test.rb index 5d9aa99497..a305aa295a 100644 --- a/activerecord/test/cases/validations_test.rb +++ b/activerecord/test/cases/validations_test.rb @@ -167,6 +167,20 @@ class ValidationsTest < ActiveRecord::TestCase assert topic.valid? end + def test_numericality_validation_checks_against_raw_value + klass = Class.new(Topic) do + def self.model_name + ActiveModel::Name.new(self, nil, "Topic") + end + attribute :wibble, :decimal, scale: 2, precision: 9 + validates_numericality_of :wibble, greater_than_or_equal_to: BigDecimal.new("97.18") + end + + assert_not klass.new(wibble: "97.179").valid? + assert_not klass.new(wibble: 97.179).valid? + assert_not klass.new(wibble: BigDecimal.new("97.179")).valid? + end + def test_acceptance_validator_doesnt_require_db_connection klass = Class.new(ActiveRecord::Base) do self.table_name = "posts" diff --git a/activerecord/test/cases/yaml_serialization_test.rb b/activerecord/test/cases/yaml_serialization_test.rb index ab0e67cd9d..bfc13d683d 100644 --- a/activerecord/test/cases/yaml_serialization_test.rb +++ b/activerecord/test/cases/yaml_serialization_test.rb @@ -123,8 +123,8 @@ class YamlSerializationTest < ActiveRecord::TestCase def yaml_fixture(file_name) path = File.expand_path( - "../../support/yaml_compatibility_fixtures/#{file_name}.yml", - __FILE__ + "../support/yaml_compatibility_fixtures/#{file_name}.yml", + __dir__ ) File.read(path) end diff --git a/activerecord/test/config.rb b/activerecord/test/config.rb index 6e2e8b2145..a65e6ff776 100644 --- a/activerecord/test/config.rb +++ b/activerecord/test/config.rb @@ -1,4 +1,4 @@ -TEST_ROOT = File.expand_path(File.dirname(__FILE__)) +TEST_ROOT = __dir__ ASSETS_ROOT = TEST_ROOT + "/assets" FIXTURES_ROOT = TEST_ROOT + "/fixtures" MIGRATIONS_ROOT = TEST_ROOT + "/migrations" diff --git a/activerecord/test/fixtures/books.yml b/activerecord/test/fixtures/books.yml index b3625ee72e..699623a6f9 100644 --- a/activerecord/test/fixtures/books.yml +++ b/activerecord/test/fixtures/books.yml @@ -25,6 +25,7 @@ ddd: name: "Domain-Driven Design" format: "hardcover" status: 2 + read_status: "forgotten" tlg: author_id: 1 diff --git a/activerecord/test/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/models/book.rb b/activerecord/test/models/book.rb index 5f8a8a96dd..6466e1b341 100644 --- a/activerecord/test/models/book.rb +++ b/activerecord/test/models/book.rb @@ -8,7 +8,7 @@ class Book < ActiveRecord::Base has_many :subscribers, through: :subscriptions enum status: [:proposed, :written, :published] - enum read_status: { unread: 0, reading: 2, read: 3 } + enum read_status: { unread: 0, reading: 2, read: 3, forgotten: nil } enum nullable_status: [:single, :married] enum language: [:english, :spanish, :french], _prefix: :in enum author_visibility: [:visible, :invisible], _prefix: true diff --git a/activerecord/test/models/category.rb b/activerecord/test/models/category.rb index e8654dca01..4b2840c653 100644 --- a/activerecord/test/models/category.rb +++ b/activerecord/test/models/category.rb @@ -29,6 +29,15 @@ class Category < ActiveRecord::Base has_many :authors_with_select, -> { select "authors.*, categorizations.post_id" }, through: :categorizations, source: :author scope :general, -> { where(name: "General") } + + # Should be delegated `ast` and `locked` to `arel`. + def self.ast + raise + end + + def self.locked + raise + end end class SpecialCategory < Category diff --git a/activerecord/test/models/comment.rb b/activerecord/test/models/comment.rb index 76b484e616..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 @@ -19,6 +18,18 @@ class Comment < ActiveRecord::Base has_many :children, class_name: "Comment", foreign_key: :parent_id belongs_to :parent, class_name: "Comment", counter_cache: :children_count + class ::OopsError < RuntimeError; end + + module OopsExtension + def destroy_all(*) + raise OopsError + end + end + + default_scope { extending OopsExtension } + + scope :oops_comments, -> { extending OopsExtension } + # Should not be called if extending modules that having the method exists on an association. def self.greeting raise diff --git a/activerecord/test/models/company.rb b/activerecord/test/models/company.rb index d269a95e8c..c6a5bf1c92 100644 --- a/activerecord/test/models/company.rb +++ b/activerecord/test/models/company.rb @@ -175,6 +175,7 @@ end class ExclusivelyDependentFirm < Company has_one :account, foreign_key: "firm_id", dependent: :delete has_many :dependent_sanitized_conditional_clients_of_firm, -> { order("id").where("name = 'BigShot Inc.'") }, foreign_key: "client_of", class_name: "Client", dependent: :delete_all + has_many :dependent_hash_conditional_clients_of_firm, -> { order("id").where(name: "BigShot Inc.") }, foreign_key: "client_of", class_name: "Client", dependent: :delete_all has_many :dependent_conditional_clients_of_firm, -> { order("id").where("name = ?", "BigShot Inc.") }, foreign_key: "client_of", class_name: "Client", dependent: :delete_all end 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/numeric_data.rb b/activerecord/test/models/numeric_data.rb new file mode 100644 index 0000000000..c6e025a9ce --- /dev/null +++ b/activerecord/test/models/numeric_data.rb @@ -0,0 +1,8 @@ +class NumericData < ActiveRecord::Base + self.table_name = "numeric_data" + # Decimal columns with 0 scale being automatically treated as integers + # is deprecated, and will be removed in a future version of Rails. + attribute :world_population, :big_integer + attribute :my_house_population, :big_integer + attribute :atoms_in_universe, :big_integer +end diff --git a/activerecord/test/models/post.rb b/activerecord/test/models/post.rb index a2028b3eb9..ed64e0ee52 100644 --- a/activerecord/test/models/post.rb +++ b/activerecord/test/models/post.rb @@ -13,7 +13,7 @@ class Post < ActiveRecord::Base module NamedExtension2 def greeting - "hello" + "hullo" end end @@ -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/models/topic.rb b/activerecord/test/models/topic.rb index 0420e2d15c..d9381ac9cf 100644 --- a/activerecord/test/models/topic.rb +++ b/activerecord/test/models/topic.rb @@ -14,7 +14,7 @@ class Topic < ActiveRecord::Base scope :replied, -> { where "replies_count > 0" } scope "approved_as_string", -> { where(approved: true) } - scope :anonymous_extension, -> { all } do + scope :anonymous_extension, -> {} do def one 1 end diff --git a/activerecord/test/schema/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 |