diff options
Diffstat (limited to 'activerecord')
40 files changed, 371 insertions, 133 deletions
diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index 7404c1bd8f..786bef7359 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,3 +1,22 @@ +* Skip query caching when working with batches of records (`find_each`, `find_in_batches`, + `in_batches`). + + Previously, records would be fetched in batches, but all records would be retained in memory + until the end of the request or job. + + *Eugene Kenny* + +* Prevent errors raised by `sql.active_record` notification subscribers from being converted into + `ActiveRecord::StatementInvalid` exceptions. + + *Dennis Taylor* + +* Fix eager loading/preloading association with scope including joins. + + Fixes #28324. + + *Ryuta Kamizono* + * Fix transactions to apply state to child transactions Previously if you had a nested transaction and the outer transaction was rolledback the record from the 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 d0c1848079..b14ddfeeeb 100644 --- a/activerecord/lib/active_record/associations/join_dependency/join_association.rb +++ b/activerecord/lib/active_record/associations/join_dependency/join_association.rb @@ -34,13 +34,19 @@ module ActiveRecord table = tables.shift klass = reflection.klass - join_scope = reflection.join_scope(table, foreign_table, foreign_klass) - - binds.concat join_scope.bound_attributes - constraint = join_scope.arel.constraints + constraint = reflection.build_join_constraint(table, foreign_table) joins << table.create_join(table, table.create_on(constraint), join_type) + join_scope = reflection.join_scope(table, foreign_klass) + + if join_scope.arel.constraints.any? + binds.concat join_scope.bound_attributes + joins.concat join_scope.arel.join_sources + right = joins.last.right + right.expr = right.expr.and(join_scope.arel.constraints) + end + # The current table in this iteration becomes the foreign table in the next foreign_table, foreign_klass = table, klass end diff --git a/activerecord/lib/active_record/associations/preloader.rb b/activerecord/lib/active_record/associations/preloader.rb index 208d1b2670..a18994cec4 100644 --- a/activerecord/lib/active_record/associations/preloader.rb +++ b/activerecord/lib/active_record/associations/preloader.rb @@ -54,8 +54,6 @@ module ActiveRecord autoload :BelongsTo, "active_record/associations/preloader/belongs_to" end - NULL_RELATION = Struct.new(:values, :where_clause, :joins_values).new({}, Relation::WhereClause.empty, []) - # Eager loads the named associations for the given Active Record record(s). # # In this description, 'association name' shall refer to the name passed @@ -93,7 +91,6 @@ module ActiveRecord def preload(records, associations, preload_scope = nil) records = Array.wrap(records).compact.uniq associations = Array.wrap(associations) - preload_scope = preload_scope || NULL_RELATION if records.empty? [] diff --git a/activerecord/lib/active_record/associations/preloader/association.rb b/activerecord/lib/active_record/associations/preloader/association.rb index 63ef3f2d8c..85343040db 100644 --- a/activerecord/lib/active_record/associations/preloader/association.rb +++ b/activerecord/lib/active_record/associations/preloader/association.rb @@ -11,7 +11,6 @@ module ActiveRecord @reflection = reflection @preload_scope = preload_scope @model = owners.first && owners.first.class - @scope = nil @preloaded_records = [] end @@ -23,29 +22,11 @@ module ActiveRecord raise NotImplementedError end - def scope - @scope ||= build_scope - end - - def records_for(ids) - scope.where(association_key_name => ids) - end - - def table - klass.arel_table - end - # The name of the key on the associated records def association_key_name raise NotImplementedError end - # This is overridden by HABTM as the condition should be on the foreign_key column in - # the join table - def association_key - klass.arel_attribute(association_key_name, table) - end - # The name of the key on the model which declares the association def owner_key_name raise NotImplementedError @@ -114,54 +95,35 @@ module ActiveRecord # Make several smaller queries if necessary or make one query if the adapter supports it slices = owner_keys.each_slice(klass.connection.in_clause_length || owner_keys.size) @preloaded_records = slices.flat_map do |slice| - records_for(slice).load(&block) + records_for(slice, &block) end @preloaded_records.group_by do |record| convert_key(record[association_key_name]) end end + def records_for(ids, &block) + scope.where(association_key_name => ids).load(&block) + end + + def scope + @scope ||= build_scope + end + def reflection_scope @reflection_scope ||= reflection.scope_for(klass) end def build_scope - scope = klass.unscoped - - values = reflection_scope.values - preload_values = preload_scope.values - - scope.where_clause = reflection_scope.where_clause + preload_scope.where_clause - scope.references_values = Array(values[:references]) + Array(preload_values[:references]) - - if preload_values[:select] || values[:select] - scope._select!(preload_values[:select] || values[:select]) - end - scope.includes! preload_values[:includes] || values[:includes] - if preload_scope.joins_values.any? - scope.joins!(preload_scope.joins_values) - else - scope.joins!(reflection_scope.joins_values) - end - - if order_values = preload_values[:order] || values[:order] - scope.order!(order_values) - end - - if preload_values[:reordering] || values[:reordering] - scope.reordering_value = true - end - - if preload_values[:readonly] || values[:readonly] - scope.readonly! - end + scope = klass.default_scoped - if options[:as] - scope.where!(klass.table_name => { reflection.type => model.base_class.sti_name }) + if reflection.type + scope.where!(reflection.type => model.base_class.sti_name) end - scope.unscope_values = Array(values[:unscope]) + Array(preload_values[:unscope]) - klass.default_scoped.merge(scope) + scope.merge!(reflection_scope) + scope.merge!(preload_scope) if preload_scope + scope end end end diff --git a/activerecord/lib/active_record/associations/preloader/through_association.rb b/activerecord/lib/active_record/associations/preloader/through_association.rb index 34587fd3f5..0999746cd5 100644 --- a/activerecord/lib/active_record/associations/preloader/through_association.rb +++ b/activerecord/lib/active_record/associations/preloader/through_association.rb @@ -79,17 +79,24 @@ module ActiveRecord def through_scope scope = through_reflection.klass.unscoped + values = reflection_scope.values if options[:source_type] scope.where! reflection.foreign_type => options[:source_type] else unless reflection_scope.where_clause.empty? - scope.includes_values = Array(reflection_scope.values[:includes] || options[:source]) + scope.includes_values = Array(values[:includes] || options[:source]) scope.where_clause = reflection_scope.where_clause + if joins = values[:joins] + scope.joins!(source_reflection.name => joins) + end + if left_outer_joins = values[:left_outer_joins] + scope.left_outer_joins!(source_reflection.name => left_outer_joins) + end end - scope.references! reflection_scope.values[:references] - if scope.eager_loading? && order_values = reflection_scope.values[:order] + scope.references! values[:references] + if scope.eager_loading? && order_values = values[:order] scope = scope.order(order_values) end end diff --git a/activerecord/lib/active_record/attribute_methods/dirty.rb b/activerecord/lib/active_record/attribute_methods/dirty.rb index 948249a6fd..8c58e63931 100644 --- a/activerecord/lib/active_record/attribute_methods/dirty.rb +++ b/activerecord/lib/active_record/attribute_methods/dirty.rb @@ -80,7 +80,7 @@ module ActiveRecord clear_mutation_trackers end - def raw_write_attribute(attr_name, *) + def write_attribute_without_type_cast(attr_name, *) result = super clear_attribute_change(attr_name) result diff --git a/activerecord/lib/active_record/attribute_methods/primary_key.rb b/activerecord/lib/active_record/attribute_methods/primary_key.rb index b9b2acff37..081aad434d 100644 --- a/activerecord/lib/active_record/attribute_methods/primary_key.rb +++ b/activerecord/lib/active_record/attribute_methods/primary_key.rb @@ -21,7 +21,7 @@ module ActiveRecord # Sets the primary key value. def id=(value) sync_with_transaction_state - write_attribute(self.class.primary_key, value) if self.class.primary_key + _write_attribute(self.class.primary_key, value) if self.class.primary_key end # Queries the primary key value. diff --git a/activerecord/lib/active_record/attribute_methods/write.rb b/activerecord/lib/active_record/attribute_methods/write.rb index 75c5a1a600..54b673c72e 100644 --- a/activerecord/lib/active_record/attribute_methods/write.rb +++ b/activerecord/lib/active_record/attribute_methods/write.rb @@ -17,7 +17,7 @@ module ActiveRecord generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ + 1 def __temp__#{safe_name}=(value) name = ::ActiveRecord::AttributeMethods::AttrNames::ATTR_#{safe_name} - write_attribute(name, value) + _write_attribute(name, value) end alias_method #{(name + '=').inspect}, :__temp__#{safe_name}= undef_method :__temp__#{safe_name}= @@ -36,20 +36,26 @@ module ActiveRecord end name = self.class.primary_key if name == "id".freeze && self.class.primary_key - @attributes.write_from_user(name, value) - value + _write_attribute(name, value) end - def raw_write_attribute(attr_name, value) # :nodoc: - name = attr_name.to_s - @attributes.write_cast_value(name, value) + # This method exists to avoid the expensive primary_key check internally, without + # breaking compatibility with the write_attribute API + def _write_attribute(attr_name, value) # :nodoc: + @attributes.write_from_user(attr_name.to_s, value) value end private + def write_attribute_without_type_cast(attr_name, value) + name = attr_name.to_s + @attributes.write_cast_value(name, value) + value + end + # Handle *= for method_missing. def attribute=(attribute_name, value) - write_attribute(attribute_name, value) + _write_attribute(attribute_name, value) end end end diff --git a/activerecord/lib/active_record/attribute_mutation_tracker.rb b/activerecord/lib/active_record/attribute_mutation_tracker.rb index 4de993e169..a01a58f8a5 100644 --- a/activerecord/lib/active_record/attribute_mutation_tracker.rb +++ b/activerecord/lib/active_record/attribute_mutation_tracker.rb @@ -26,6 +26,7 @@ module ActiveRecord end def change_to_attribute(attr_name) + attr_name = attr_name.to_s if changed?(attr_name) [attributes[attr_name].original_value, attributes.fetch_value(attr_name)] end @@ -44,7 +45,7 @@ module ActiveRecord end def changed_in_place?(attr_name) - attributes[attr_name].changed_in_place? + attributes[attr_name.to_s].changed_in_place? end def forget_change(attr_name) @@ -54,7 +55,7 @@ module ActiveRecord end def original_value(attr_name) - attributes[attr_name].original_value + attributes[attr_name.to_s].original_value end def force_change(attr_name) diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb index cfe1892d78..30b29e7007 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb @@ -576,12 +576,14 @@ module ActiveRecord type_casted_binds: type_casted_binds, statement_name: statement_name, connection_id: object_id) do + begin @lock.synchronize do yield end + rescue => e + raise translate_exception_class(e, sql) end - rescue => e - raise translate_exception_class(e, sql) + end end def translate_exception(exception, message) diff --git a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb index caa125576c..06976aa769 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb @@ -59,12 +59,12 @@ module ActiveRecord @statements = StatementPool.new(self.class.type_cast_config_to_integer(config[:statement_limit])) if version < "5.1.10" - raise "Your version of MySQL (#{full_version.match(/^\d+\.\d+\.\d+/)[0]}) is too old. Active Record supports MySQL >= 5.1.10." + raise "Your version of MySQL (#{version_string}) is too old. Active Record supports MySQL >= 5.1.10." end end def version #:nodoc: - @version ||= Version.new(full_version.match(/^\d+\.\d+\.\d+/)[0]) + @version ||= Version.new(version_string) end def mariadb? # :nodoc: @@ -854,7 +854,8 @@ module ActiveRecord end end - class MysqlJson < Type::Json # :nodoc: + def version_string + full_version.match(/^(?:5\.5\.5-)?(\d+\.\d+\.\d+)/)[1] end class MysqlString < Type::String # :nodoc: diff --git a/activerecord/lib/active_record/connection_adapters/mysql/schema_dumper.rb b/activerecord/lib/active_record/connection_adapters/mysql/schema_dumper.rb index eff96e329f..a46d9f8cbb 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql/schema_dumper.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql/schema_dumper.rb @@ -53,7 +53,7 @@ module ActiveRecord end def extract_expression_for_virtual_column(column) - if mariadb? + if mariadb? && version < "10.2.5" create_table_info = create_table_info(column.table_name) if %r/#{quote_column_name(column.name)} #{Regexp.quote(column.sql_type)}(?: COLLATE \w+)? AS \((?<expression>.+?)\) #{column.extra}/ =~ create_table_info $~[:expression].inspect 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 a01fbba201..24f8ff6367 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql/schema_statements.rb @@ -73,8 +73,8 @@ module ActiveRecord def new_column_from_field(table_name, field) type_metadata = fetch_type_metadata(field[:Type], field[:Extra]) - if type_metadata.type == :datetime && field[:Default] == "CURRENT_TIMESTAMP" - default, default_function = nil, field[:Default] + if type_metadata.type == :datetime && /\ACURRENT_TIMESTAMP(?:\(\))?\z/i.match?(field[:Default]) + default, default_function = nil, "CURRENT_TIMESTAMP" else default, default_function = field[:Default], nil end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb index 0e8888a2b7..6666622c08 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb @@ -8,7 +8,6 @@ require_relative "oid/decimal" require_relative "oid/enum" require_relative "oid/hstore" require_relative "oid/inet" -require_relative "oid/json" require_relative "oid/jsonb" require_relative "oid/money" require_relative "oid/oid" diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/json.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/json.rb deleted file mode 100644 index 3c706c27c4..0000000000 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/json.rb +++ /dev/null @@ -1,10 +0,0 @@ -module ActiveRecord - module ConnectionAdapters - module PostgreSQL - module OID # :nodoc: - class Json < Type::Json # :nodoc: - end - end - end - end -end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/uuid.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/uuid.rb index 5e839228e9..db92333ef7 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/uuid.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/uuid.rb @@ -3,7 +3,7 @@ module ActiveRecord module PostgreSQL module OID # :nodoc: class Uuid < Type::Value # :nodoc: - ACCEPTABLE_UUID = %r{\A\{?([a-fA-F0-9]{4}-?){8}\}?\z}x + ACCEPTABLE_UUID = %r{\A(\{)?([a-fA-F0-9]{4}-?){8}(?(1)\}|)\z} alias_method :serialize, :deserialize diff --git a/activerecord/lib/active_record/inheritance.rb b/activerecord/lib/active_record/inheritance.rb index 5776807507..1753322274 100644 --- a/activerecord/lib/active_record/inheritance.rb +++ b/activerecord/lib/active_record/inheritance.rb @@ -245,7 +245,7 @@ module ActiveRecord def ensure_proper_type klass = self.class if klass.finder_needs_type_condition? - write_attribute(klass.inheritance_column, klass.sti_name) + _write_attribute(klass.inheritance_column, klass.sti_name) end end end diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb index 1ff1dcad43..42220b9a5e 100644 --- a/activerecord/lib/active_record/migration.rb +++ b/activerecord/lib/active_record/migration.rb @@ -863,15 +863,17 @@ module ActiveRecord source_migrations.each do |migration| source = File.binread(migration.filename) inserted_comment = "# This migration comes from #{scope} (originally #{migration.version})\n" - if /\A#.*\b(?:en)?coding:\s*\S+/ =~ source + magic_comments = "".dup + loop do # If we have a magic comment in the original migration, # insert our comment after the first newline(end of the magic comment line) # so the magic keep working. # Note that magic comments must be at the first line(except sh-bang). - source[/\n/] = "\n#{inserted_comment}" - else - source = "#{inserted_comment}#{source}" + source.sub!(/\A(?:#.*\b(?:en)?coding:\s*\S+|#\s*frozen_string_literal:\s*(?:true|false)).*\n/) do |magic_comment| + magic_comments << magic_comment; "" + end || break end + source = "#{magic_comments}#{inserted_comment}#{source}" if duplicate = destination_migrations.detect { |m| m.name == migration.name } if options[:on_skip] && duplicate.scope != scope.to_s diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb index b2dba5516e..a9509e562a 100644 --- a/activerecord/lib/active_record/persistence.rb +++ b/activerecord/lib/active_record/persistence.rb @@ -333,7 +333,7 @@ module ActiveRecord updated_count = self.class.unscoped.where(self.class.primary_key => id).update_all(attributes) attributes.each do |k, v| - raw_write_attribute(k, v) + write_attribute_without_type_cast(k, v) end updated_count == 1 diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb index 8ff2f50fdb..a453ca55c7 100644 --- a/activerecord/lib/active_record/reflection.rb +++ b/activerecord/lib/active_record/reflection.rb @@ -185,19 +185,23 @@ 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) - + def build_join_constraint(table, foreign_table) key = join_keys.key foreign_key = join_keys.foreign_key - klass_scope.where!(table[key].eq(foreign_table[foreign_key])) + constraint = table[key].eq(foreign_table[foreign_key]) if klass.finder_needs_type_condition? - klass_scope.where!(klass.send(:type_condition, table)) + table.create_and([constraint, klass.send(:type_condition, table)]) + else + constraint end + end + + def join_scope(table, foreign_klass) + predicate_builder = predicate_builder(table) + scope_chain_items = join_scopes(table, predicate_builder) + klass_scope = klass_join_scope(table, predicate_builder) if type klass_scope.where!(type => foreign_klass.base_class.sti_name) diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb index 52f5d5f3e3..76cf47a3ed 100644 --- a/activerecord/lib/active_record/relation.rb +++ b/activerecord/lib/active_record/relation.rb @@ -6,7 +6,7 @@ module ActiveRecord :extending, :unscope] SINGLE_VALUE_METHODS = [:limit, :offset, :lock, :readonly, :reordering, - :reverse_order, :distinct, :create_with] + :reverse_order, :distinct, :create_with, :skip_query_cache] CLAUSE_METHODS = [:where, :having, :from] INVALID_METHODS_FOR_DELETE_ALL = [:limit, :distinct, :offset, :group, :having] @@ -657,20 +657,32 @@ module ActiveRecord end def exec_queries(&block) - @records = eager_loading? ? find_with_associations.freeze : @klass.find_by_sql(arel, bound_attributes, &block).freeze - - preload = preload_values - preload += includes_values unless eager_loading? - preloader = nil - preload.each do |associations| - preloader ||= build_preloader - preloader.preload @records, associations - end + skip_query_cache_if_necessary do + @records = eager_loading? ? find_with_associations.freeze : @klass.find_by_sql(arel, bound_attributes, &block).freeze + + preload = preload_values + preload += includes_values unless eager_loading? + preloader = nil + preload.each do |associations| + preloader ||= build_preloader + preloader.preload @records, associations + end - @records.each(&:readonly!) if readonly_value + @records.each(&:readonly!) if readonly_value - @loaded = true - @records + @loaded = true + @records + end + end + + def skip_query_cache_if_necessary + if skip_query_cache_value + uncached do + yield + end + else + yield + end end def build_preloader diff --git a/activerecord/lib/active_record/relation/batches.rb b/activerecord/lib/active_record/relation/batches.rb index ee1f25ec84..c7e4f8a88a 100644 --- a/activerecord/lib/active_record/relation/batches.rb +++ b/activerecord/lib/active_record/relation/batches.rb @@ -209,6 +209,7 @@ module ActiveRecord relation = relation.reorder(batch_order).limit(batch_limit) relation = apply_limits(relation, start, finish) + relation.skip_query_cache! # Retaining the results in the query cache would undermine the point of batching batch_relation = relation loop do diff --git a/activerecord/lib/active_record/relation/calculations.rb b/activerecord/lib/active_record/relation/calculations.rb index 8a54f8f2c3..aaba6c71f2 100644 --- a/activerecord/lib/active_record/relation/calculations.rb +++ b/activerecord/lib/active_record/relation/calculations.rb @@ -184,7 +184,7 @@ module ActiveRecord relation.select_values = column_names.map { |cn| @klass.has_attribute?(cn) || @klass.attribute_alias?(cn) ? arel_attribute(cn) : cn } - result = klass.connection.select_all(relation.arel, nil, bound_attributes) + result = skip_query_cache_if_necessary { klass.connection.select_all(relation.arel, nil, bound_attributes) } result.cast_values(klass.attribute_types) end end @@ -260,7 +260,7 @@ module ActiveRecord query_builder = relation.arel end - result = @klass.connection.select_all(query_builder, nil, bound_attributes) + result = skip_query_cache_if_necessary { @klass.connection.select_all(query_builder, nil, bound_attributes) } row = result.first value = row && row.values.first type = result.column_types.fetch(column_alias) do @@ -311,7 +311,7 @@ module ActiveRecord relation.group_values = group_fields relation.select_values = select_values - calculated_data = @klass.connection.select_all(relation.arel, nil, relation.bound_attributes) + calculated_data = skip_query_cache_if_necessary { @klass.connection.select_all(relation.arel, nil, relation.bound_attributes) } if association key_ids = calculated_data.collect { |row| row[group_aliases.first] } diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb index eee0f36f63..ac0b4f597e 100644 --- a/activerecord/lib/active_record/relation/finder_methods.rb +++ b/activerecord/lib/active_record/relation/finder_methods.rb @@ -315,7 +315,7 @@ module ActiveRecord relation = construct_relation_for_exists(relation, conditions) - connection.select_value(relation.arel, "#{name} Exists", relation.bound_attributes) ? true : false + skip_query_cache_if_necessary { connection.select_value(relation.arel, "#{name} Exists", relation.bound_attributes) } ? true : false rescue ::RangeError false end @@ -376,7 +376,7 @@ module ActiveRecord if ActiveRecord::NullRelation === relation [] else - rows = connection.select_all(relation.arel, "SQL", relation.bound_attributes) + rows = skip_query_cache_if_necessary { connection.select_all(relation.arel, "SQL", relation.bound_attributes) } join_dependency.instantiate(rows, aliases) end end @@ -424,7 +424,7 @@ module ActiveRecord relation = relation.except(:select).select(values).distinct! - id_rows = @klass.connection.select_all(relation.arel, "SQL", relation.bound_attributes) + id_rows = skip_query_cache_if_necessary { @klass.connection.select_all(relation.arel, "SQL", relation.bound_attributes) } id_rows.map { |row| row[primary_key] } end diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb index 9da8f96337..79495ead91 100644 --- a/activerecord/lib/active_record/relation/query_methods.rb +++ b/activerecord/lib/active_record/relation/query_methods.rb @@ -913,6 +913,11 @@ module ActiveRecord self end + def skip_query_cache! # :nodoc: + self.skip_query_cache_value = true + self + end + # Returns the Arel object associated with the relation. def arel # :nodoc: @arel ||= build_arel diff --git a/activerecord/lib/active_record/timestamp.rb b/activerecord/lib/active_record/timestamp.rb index dc4540eea6..26eea0bc24 100644 --- a/activerecord/lib/active_record/timestamp.rb +++ b/activerecord/lib/active_record/timestamp.rb @@ -86,7 +86,7 @@ module ActiveRecord all_timestamp_attributes_in_model.each do |column| if !attribute_present?(column) - write_attribute(column, current_time) + _write_attribute(column, current_time) end end end @@ -100,7 +100,7 @@ module ActiveRecord timestamp_attributes_for_update_in_model.each do |column| next if will_save_change_to_attribute?(column) - write_attribute(column, current_time) + _write_attribute(column, current_time) end end super(*args) diff --git a/activerecord/test/cases/adapter_test.rb b/activerecord/test/cases/adapter_test.rb index a1fb6427f9..827bcba121 100644 --- a/activerecord/test/cases/adapter_test.rb +++ b/activerecord/test/cases/adapter_test.rb @@ -211,6 +211,28 @@ module ActiveRecord end end + def test_exceptions_from_notifications_are_not_translated + original_error = StandardError.new("This StandardError shouldn't get translated") + subscriber = ActiveSupport::Notifications.subscribe("sql.active_record") { raise original_error } + actual_error = assert_raises(StandardError) do + @connection.execute("SELECT * FROM posts") + end + + assert_equal original_error, actual_error + + ensure + ActiveSupport::Notifications.unsubscribe(subscriber) if subscriber + end + + def test_database_related_exceptions_are_translated_to_statement_invalid + error = assert_raises(ActiveRecord::StatementInvalid) do + @connection.execute("This is a syntax error") + end + + assert_instance_of ActiveRecord::StatementInvalid, error + assert_kind_of Exception, error.cause + end + def test_select_all_always_return_activerecord_result result = @connection.select_all "SELECT * FROM posts" assert result.is_a?(ActiveRecord::Result) diff --git a/activerecord/test/cases/adapters/mysql2/virtual_column_test.rb b/activerecord/test/cases/adapters/mysql2/virtual_column_test.rb index 442a4fb7b5..1c5ef2aa41 100644 --- a/activerecord/test/cases/adapters/mysql2/virtual_column_test.rb +++ b/activerecord/test/cases/adapters/mysql2/virtual_column_test.rb @@ -52,7 +52,7 @@ if ActiveRecord::Base.connection.supports_virtual_columns? def test_schema_dumping output = dump_table_schema("virtual_columns") - assert_match(/t\.virtual\s+"upper_name",\s+type: :string,\s+as: "UPPER\(`name`\)"$/i, output) + assert_match(/t\.virtual\s+"upper_name",\s+type: :string,\s+as: "(?:UPPER|UCASE)\(`name`\)"$/i, output) assert_match(/t\.virtual\s+"name_length",\s+type: :integer,\s+as: "LENGTH\(`name`\)",\s+stored: true$/i, output) end end diff --git a/activerecord/test/cases/adapters/postgresql/uuid_test.rb b/activerecord/test/cases/adapters/postgresql/uuid_test.rb index 8eddd81c38..00de92cdfd 100644 --- a/activerecord/test/cases/adapters/postgresql/uuid_test.rb +++ b/activerecord/test/cases/adapters/postgresql/uuid_test.rb @@ -124,7 +124,9 @@ class PostgresqlUUIDTest < ActiveRecord::PostgreSQLTestCase "Z0000C99-9C0B-4EF8-BB6D-6BB9BD380A11", "a0eebc999r0b4ef8ab6d6bb9bd380a11", "a0ee-bc99------4ef8-bb6d-6bb9-bd38-0a11", - "{a0eebc99-bb6d6bb9-bd380a11}"].each do |invalid_uuid| + "{a0eebc99-bb6d6bb9-bd380a11}", + "{a0eebc99-9c0b4ef8-bb6d6bb9-bd380a11", + "a0eebc99-9c0b4ef8-bb6d6bb9-bd380a11}"].each do |invalid_uuid| uuid = UUIDType.new guid: invalid_uuid assert_nil uuid.guid end diff --git a/activerecord/test/cases/associations/eager_test.rb b/activerecord/test/cases/associations/eager_test.rb index 55b294cfaa..c0bab19e82 100644 --- a/activerecord/test/cases/associations/eager_test.rb +++ b/activerecord/test/cases/associations/eager_test.rb @@ -68,6 +68,11 @@ class EagerAssociationTest < ActiveRecord::TestCase "expected to find only david's posts" end + def test_loading_with_scope_including_joins + assert_equal clubs(:boring_club), Member.preload(:general_club).find(1).general_club + assert_equal clubs(:boring_club), Member.eager_load(:general_club).find(1).general_club + end + def test_with_ordering list = Post.all.merge!(includes: :comments, order: "posts.id DESC").to_a [:other_by_mary, :other_by_bob, :misc_by_mary, :misc_by_bob, :eager_other, diff --git a/activerecord/test/cases/batches_test.rb b/activerecord/test/cases/batches_test.rb index 1a66b82b2e..dcd3af487b 100644 --- a/activerecord/test/cases/batches_test.rb +++ b/activerecord/test/cases/batches_test.rb @@ -1,4 +1,5 @@ require "cases/helper" +require "models/comment" require "models/post" require "models/subscriber" @@ -610,4 +611,64 @@ class EachTest < ActiveRecord::TestCase end assert_equal expected, actual end + + test ".find_each bypasses the query cache for its own queries" do + Post.cache do + assert_queries(2) do + Post.find_each {} + Post.find_each {} + end + end + end + + test ".find_each does not disable the query cache inside the given block" do + Post.cache do + Post.find_each(start: 1, finish: 1) do |post| + assert_queries(1) do + post.comments.count + post.comments.count + end + end + end + end + + test ".find_in_batches bypasses the query cache for its own queries" do + Post.cache do + assert_queries(2) do + Post.find_in_batches {} + Post.find_in_batches {} + end + end + end + + test ".find_in_batches does not disable the query cache inside the given block" do + Post.cache do + Post.find_in_batches(start: 1, finish: 1) do |batch| + assert_queries(1) do + batch.first.comments.count + batch.first.comments.count + end + end + end + end + + test ".in_batches bypasses the query cache for its own queries" do + Post.cache do + assert_queries(2) do + Post.in_batches {} + Post.in_batches {} + end + end + end + + test ".in_batches does not disable the query cache inside the given block" do + Post.cache do + Post.in_batches(start: 1, finish: 1) do |relation| + assert_queries(1) do + relation.count + relation.count + end + end + end + end end diff --git a/activerecord/test/cases/calculations_test.rb b/activerecord/test/cases/calculations_test.rb index 80baaac30a..7d6dc21e34 100644 --- a/activerecord/test/cases/calculations_test.rb +++ b/activerecord/test/cases/calculations_test.rb @@ -817,4 +817,46 @@ class CalculationsTest < ActiveRecord::TestCase assert_equal 6, Account.sum(:firm_id) { 1 } end end + + test "#skip_query_cache! for #pluck" do + Account.cache do + assert_queries(1) do + Account.pluck(:credit_limit) + Account.pluck(:credit_limit) + end + + assert_queries(2) do + Account.all.skip_query_cache!.pluck(:credit_limit) + Account.all.skip_query_cache!.pluck(:credit_limit) + end + end + end + + test "#skip_query_cache! for a simple calculation" do + Account.cache do + assert_queries(1) do + Account.calculate(:sum, :credit_limit) + Account.calculate(:sum, :credit_limit) + end + + assert_queries(2) do + Account.all.skip_query_cache!.calculate(:sum, :credit_limit) + Account.all.skip_query_cache!.calculate(:sum, :credit_limit) + end + end + end + + test "#skip_query_cache! for a grouped calculation" do + Account.cache do + assert_queries(1) do + Account.group(:firm_id).calculate(:sum, :credit_limit) + Account.group(:firm_id).calculate(:sum, :credit_limit) + end + + assert_queries(2) do + Account.all.skip_query_cache!.group(:firm_id).calculate(:sum, :credit_limit) + Account.all.skip_query_cache!.group(:firm_id).calculate(:sum, :credit_limit) + end + end + end end diff --git a/activerecord/test/cases/date_time_test.rb b/activerecord/test/cases/date_time_test.rb index ad7da9de70..6cd98fe254 100644 --- a/activerecord/test/cases/date_time_test.rb +++ b/activerecord/test/cases/date_time_test.rb @@ -58,4 +58,17 @@ class DateTimeTest < ActiveRecord::TestCase assert_equal now, task.starting end end + + def test_date_time_with_string_value_with_subsecond_precision + skip unless subsecond_precision_supported? + string_value = "2017-07-04 14:19:00.5" + topic = Topic.create(written_on: string_value) + assert_equal topic, Topic.find_by(written_on: string_value) + end + + def test_date_time_with_string_value_with_non_iso_format + string_value = "04/07/2017 2:19pm" + topic = Topic.create(written_on: string_value) + assert_equal topic, Topic.find_by(written_on: string_value) + end end diff --git a/activerecord/test/cases/finder_test.rb b/activerecord/test/cases/finder_test.rb index 420f552ef6..af21cd529f 100644 --- a/activerecord/test/cases/finder_test.rb +++ b/activerecord/test/cases/finder_test.rb @@ -1232,6 +1232,34 @@ class FinderTest < ActiveRecord::TestCase assert_equal tyre2, zyke.tyres.custom_find_by(id: tyre2.id) end + test "#skip_query_cache! for #exists?" do + Topic.cache do + assert_queries(1) do + Topic.exists? + Topic.exists? + end + + assert_queries(2) do + Topic.all.skip_query_cache!.exists? + Topic.all.skip_query_cache!.exists? + end + end + end + + test "#skip_query_cache! for #exists? with a limited eager load" do + Topic.cache do + assert_queries(2) do + Topic.eager_load(:replies).limit(1).exists? + Topic.eager_load(:replies).limit(1).exists? + end + + assert_queries(4) do + Topic.eager_load(:replies).limit(1).skip_query_cache!.exists? + Topic.eager_load(:replies).limit(1).skip_query_cache!.exists? + end + end + end + private def table_with_custom_primary_key yield(Class.new(Toy) do diff --git a/activerecord/test/cases/migration_test.rb b/activerecord/test/cases/migration_test.rb index 3a49a41580..eff6e09eb7 100644 --- a/activerecord/test/cases/migration_test.rb +++ b/activerecord/test/cases/migration_test.rb @@ -1015,8 +1015,8 @@ class CopyMigrationsTest < ActiveRecord::TestCase assert File.exist?(@migrations_path + "/4_currencies_have_symbols.bukkits.rb") assert_equal [@migrations_path + "/4_currencies_have_symbols.bukkits.rb"], copied.map(&:filename) - expected = "# coding: ISO-8859-15\n# This migration comes from bukkits (originally 1)" - assert_equal expected, IO.readlines(@migrations_path + "/4_currencies_have_symbols.bukkits.rb")[0..1].join.chomp + expected = "# frozen_string_literal: true\n# coding: ISO-8859-15\n# This migration comes from bukkits (originally 1)" + assert_equal expected, IO.readlines(@migrations_path + "/4_currencies_have_symbols.bukkits.rb")[0..2].join.chomp files_count = Dir[@migrations_path + "/*.rb"].length copied = ActiveRecord::Migration.copy(@migrations_path, bukkits: MIGRATIONS_ROOT + "/magic") diff --git a/activerecord/test/cases/relation/mutation_test.rb b/activerecord/test/cases/relation/mutation_test.rb index dea787c07f..8e73baa70a 100644 --- a/activerecord/test/cases/relation/mutation_test.rb +++ b/activerecord/test/cases/relation/mutation_test.rb @@ -90,7 +90,7 @@ module ActiveRecord assert_equal [], relation.extending_values end - (Relation::SINGLE_VALUE_METHODS - [:lock, :reordering, :reverse_order, :create_with]).each do |method| + (Relation::SINGLE_VALUE_METHODS - [:lock, :reordering, :reverse_order, :create_with, :skip_query_cache]).each do |method| test "##{method}!" do assert relation.public_send("#{method}!", :foo).equal?(relation) assert_equal :foo, relation.public_send("#{method}_value") @@ -162,5 +162,10 @@ module ActiveRecord relation.distinct! :foo assert_equal :foo, relation.distinct_value end + + test "skip_query_cache!" do + relation.skip_query_cache! + assert relation.skip_query_cache_value + end end end diff --git a/activerecord/test/cases/relations_test.rb b/activerecord/test/cases/relations_test.rb index 5767dec315..eb3449b331 100644 --- a/activerecord/test/cases/relations_test.rb +++ b/activerecord/test/cases/relations_test.rb @@ -2034,4 +2034,46 @@ class RelationTest < ActiveRecord::TestCase assert_equal 2, posts.to_a.length end + + test "#skip_query_cache!" do + Post.cache do + assert_queries(1) do + Post.all.load + Post.all.load + end + + assert_queries(2) do + Post.all.skip_query_cache!.load + Post.all.skip_query_cache!.load + end + end + end + + test "#skip_query_cache! with an eager load" do + Post.cache do + assert_queries(1) do + Post.eager_load(:comments).load + Post.eager_load(:comments).load + end + + assert_queries(2) do + Post.eager_load(:comments).skip_query_cache!.load + Post.eager_load(:comments).skip_query_cache!.load + end + end + end + + test "#skip_query_cache! with a preload" do + Post.cache do + assert_queries(2) do + Post.preload(:comments).load + Post.preload(:comments).load + end + + assert_queries(4) do + Post.preload(:comments).skip_query_cache!.load + Post.preload(:comments).skip_query_cache!.load + end + end + end end diff --git a/activerecord/test/migrations/magic/1_currencies_have_symbols.rb b/activerecord/test/migrations/magic/1_currencies_have_symbols.rb index d4b0e6cd95..2ba2875751 100644 --- a/activerecord/test/migrations/magic/1_currencies_have_symbols.rb +++ b/activerecord/test/migrations/magic/1_currencies_have_symbols.rb @@ -1,3 +1,4 @@ +# frozen_string_literal: true # coding: ISO-8859-15 class CurrenciesHaveSymbols < ActiveRecord::Migration::Current diff --git a/activerecord/test/models/club.rb b/activerecord/test/models/club.rb index 49d7b24a3b..3d441b1d48 100644 --- a/activerecord/test/models/club.rb +++ b/activerecord/test/models/club.rb @@ -8,6 +8,8 @@ class Club < ActiveRecord::Base has_many :favourites, -> { where(memberships: { favourite: true }) }, through: :memberships, source: :member + scope :general, -> { left_joins(:category).where(categories: { name: "General" }) } + private def private_method diff --git a/activerecord/test/models/member.rb b/activerecord/test/models/member.rb index 36f2461b84..b9597c6b9a 100644 --- a/activerecord/test/models/member.rb +++ b/activerecord/test/models/member.rb @@ -22,6 +22,7 @@ class Member < ActiveRecord::Base has_many :organization_member_details_2, through: :organization, source: :member_details has_one :club_category, through: :club, source: :category + has_one :general_club, -> { general }, through: :current_membership, source: :club has_many :current_memberships, -> { where favourite: true } has_many :clubs, through: :current_memberships |