diff options
Diffstat (limited to 'activerecord')
203 files changed, 3050 insertions, 1333 deletions
diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md index d1ae41ab97..96fd6a62c6 100644 --- a/activerecord/CHANGELOG.md +++ b/activerecord/CHANGELOG.md @@ -1,3 +1,163 @@ +* Added `index` option for `change_table` migration helpers. + With this change you can create indexes while adding new + columns into the existing tables. + + Example: + + change_table(:languages) do |t| + t.string :country_code, index: true + end + + *Mehmet Emin İNAÇ* + +* Fix `transaction` reverting for migrations. + + Before: Commands inside a `transaction` in a reverted migration ran uninverted. + Now: This change fixes that by reverting commands inside `transaction` block. + + *fatkodima*, *David Verhasselt* + +* Raise an error instead of scanning the filesystem root when `fixture_path` is blank. + + *Gannon McGibbon*, *Max Albrecht* + +* Allow `ActiveRecord::Base.configurations=` to be set with a symbolized hash. + + *Gannon McGibbon* + +* Don't update counter cache unless the record is actually saved. + + Fixes #31493, #33113, #33117. + + *Ryuta Kamizono* + +* Deprecate `ActiveRecord::Result#to_hash` in favor of `ActiveRecord::Result#to_a`. + + *Gannon McGibbon*, *Kevin Cheng* + +* SQLite3 adapter supports expression indexes. + + ``` + create_table :users do |t| + t.string :email + end + + add_index :users, 'lower(email)', name: 'index_users_on_email', unique: true + ``` + + *Gray Kemmey* + +* Allow subclasses to redefine autosave callbacks for associated records. + + Fixes #33305. + + *Andrey Subbota* + +* Bump minimum MySQL version to 5.5.8. + + *Yasuo Honda* + +* Use MySQL utf8mb4 character set by default. + + `utf8mb4` character set with 4-Byte encoding supports supplementary characters including emoji. + The previous default 3-Byte encoding character set `utf8` is not enough to support them. + + *Yasuo Honda* + +* Fix duplicated record creation when using nested attributes with `create_with`. + + *Darwin Wu* + +* Configuration item `config.filter_parameters` could also filter out + sensitive values of database columns when call `#inspect`. + We also added `ActiveRecord::Base::filter_attributes`/`=` in order to + specify sensitive attributes to specific model. + + ``` + Rails.application.config.filter_parameters += [:credit_card_number] + Account.last.inspect # => #<Account id: 123, name: "DHH", credit_card_number: [FILTERED] ...> + SecureAccount.filter_attributes += [:name] + SecureAccount.last.inspect # => #<SecureAccount id: 42, name: [FILTERED], credit_card_number: [FILTERED] ...> + ``` + + *Zhang Kang* + +* Deprecate `column_name_length`, `table_name_length`, `columns_per_table`, + `indexes_per_table`, `columns_per_multicolumn_index`, `sql_query_length`, + and `joins_per_query` methods in `DatabaseLimits`. + + *Ryuta Kamizono* + +* `ActiveRecord::Base.configurations` now returns an object. + + `ActiveRecord::Base.configurations` used to return a hash, but this + is an inflexible data model. In order to improve multiple-database + handling in Rails, we've changed this to return an object. Some methods + are provided to make the object behave hash-like in order to ease the + transition process. Since most applications don't manipulate the hash + we've decided to add backwards-compatible functionality that will throw + a deprecation warning if used, however calling `ActiveRecord::Base.configurations` + will use the new version internally and externally. + + For example, the following `database.yml`: + + ``` + development: + adapter: sqlite3 + database: db/development.sqlite3 + ``` + + Used to become a hash: + + ``` + { "development" => { "adapter" => "sqlite3", "database" => "db/development.sqlite3" } } + ``` + + Is now converted into the following object: + + ``` + #<ActiveRecord::DatabaseConfigurations:0x00007fd1acbdf800 @configurations=[ + #<ActiveRecord::DatabaseConfigurations::HashConfig:0x00007fd1acbded10 @env_name="development", + @spec_name="primary", @config={"adapter"=>"sqlite3", "database"=>"db/development.sqlite3"}> + ] + ``` + + Iterating over the database configurations has also changed. Instead of + calling hash methods on the `configurations` hash directly, a new method `configs_for` has + been provided that allows you to select the correct configuration. `env_name`, and + `spec_name` arguments are optional. For example these return an array of + database config objects for the requested environment and a single database config object + will be returned for the requested environment and specification name respectively. + + ``` + ActiveRecord::Base.configurations.configs_for(env_name: "development") + ActiveRecord::Base.configurations.configs_for(env_name: "development", spec_name: "primary") + ``` + + *Eileen M. Uchitelle*, *Aaron Patterson* + +* Add database configuration to disable advisory locks. + + ``` + production: + adapter: postgresql + advisory_locks: false + ``` + + *Guo Xiang* + +* SQLite3 adapter `alter_table` method restores foreign keys. + + *Yasuo Honda* + +* Allow `:to_table` option to `invert_remove_foreign_key`. + + Example: + + remove_foreign_key :accounts, to_table: :owners + + *Nikolay Epifanov*, *Rich Chen* + * Add environment & load_config dependency to `bin/rake db:seed` to enable seed load in environments without Rails and custom DB configuration @@ -64,8 +224,8 @@ *Eddie Lebow* -* Add ActiveRecord::Base.create_or_find_by/! to deal with the SELECT/INSERT race condition in - ActiveRecord::Base.find_or_create_by/! by leaning on unique constraints in the database. +* Add `ActiveRecord::Base.create_or_find_by`/`!` to deal with the SELECT/INSERT race condition in + `ActiveRecord::Base.find_or_create_by`/`!` by leaning on unique constraints in the database. *DHH* diff --git a/activerecord/Rakefile b/activerecord/Rakefile index 170c95b827..fae56a51bb 100644 --- a/activerecord/Rakefile +++ b/activerecord/Rakefile @@ -94,8 +94,8 @@ namespace :db do desc "Build the MySQL test databases" task :build do config = ARTest.config["connections"]["mysql2"] - %x( mysql --user=#{config["arunit"]["username"]} --password=#{config["arunit"]["password"]} -e "create DATABASE #{config["arunit"]["database"]} DEFAULT CHARACTER SET utf8 DEFAULT COLLATE utf8_unicode_ci ") - %x( mysql --user=#{config["arunit2"]["username"]} --password=#{config["arunit2"]["password"]} -e "create DATABASE #{config["arunit2"]["database"]} DEFAULT CHARACTER SET utf8 DEFAULT COLLATE utf8_unicode_ci ") + %x( mysql --user=#{config["arunit"]["username"]} --password=#{config["arunit"]["password"]} -e "create DATABASE #{config["arunit"]["database"]} DEFAULT CHARACTER SET utf8mb4" ) + %x( mysql --user=#{config["arunit2"]["username"]} --password=#{config["arunit2"]["password"]} -e "create DATABASE #{config["arunit2"]["database"]} DEFAULT CHARACTER SET utf8mb4" ) end desc "Drop the MySQL test databases" diff --git a/activerecord/examples/performance.rb b/activerecord/examples/performance.rb index 1a2c78f39b..024e503ec7 100644 --- a/activerecord/examples/performance.rb +++ b/activerecord/examples/performance.rb @@ -176,7 +176,7 @@ Benchmark.ips(TIME) do |x| end x.report "Model.log" do - Exhibit.connection.send(:log, "hello", "world") {} + Exhibit.connection.send(:log, "hello", "world") { } end x.report "AR.execute(query)" do diff --git a/activerecord/lib/active_record.rb b/activerecord/lib/active_record.rb index d198466dbf..d43378c64f 100644 --- a/activerecord/lib/active_record.rb +++ b/activerecord/lib/active_record.rb @@ -40,7 +40,6 @@ module ActiveRecord autoload :Core autoload :ConnectionHandling autoload :CounterCache - autoload :DatabaseConfigurations autoload :DynamicMatchers autoload :Enum autoload :InternalMetadata diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb index 1ee52945ea..ab1e7ad269 100644 --- a/activerecord/lib/active_record/associations.rb +++ b/activerecord/lib/active_record/associations.rb @@ -1524,6 +1524,7 @@ module ActiveRecord # 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. + # No modification or deletion of existing records takes place. # [build_association(attributes = {})] # Returns a new object of the associated type that has been instantiated # with +attributes+ and linked to this object through a foreign key, but has not yet been saved. @@ -1581,7 +1582,7 @@ module ActiveRecord # association will use "taggable_type" as the default <tt>:foreign_type</tt>. # [:primary_key] # Specify the method that returns the primary key of associated object used for the association. - # By default this is id. + # By default this is +id+. # [:dependent] # If set to <tt>:destroy</tt>, the associated object is destroyed when this object is. If set to # <tt>:delete</tt>, the associated object is deleted *without* calling its destroy method. @@ -1761,6 +1762,7 @@ module ActiveRecord # has_and_belongs_to_many :projects, -> { includes(:milestones, :manager) } # has_and_belongs_to_many :categories, ->(post) { # where("default_category = ?", post.default_category) + # } # # === Extensions # diff --git a/activerecord/lib/active_record/associations/belongs_to_association.rb b/activerecord/lib/active_record/associations/belongs_to_association.rb index 3d4ad1dd5b..3346725f2d 100644 --- a/activerecord/lib/active_record/associations/belongs_to_association.rb +++ b/activerecord/lib/active_record/associations/belongs_to_association.rb @@ -34,14 +34,28 @@ module ActiveRecord @updated end - def decrement_counters # :nodoc: + def decrement_counters update_counters(-1) end - def increment_counters # :nodoc: + def increment_counters update_counters(1) end + def decrement_counters_before_last_save + if reflection.polymorphic? + model_was = owner.attribute_before_last_save(reflection.foreign_type).try(:constantize) + else + model_was = klass + end + + foreign_key_was = owner.attribute_before_last_save(reflection.foreign_key) + + if foreign_key_was && model_was < ActiveRecord::Base + update_counters_via_scope(model_was, foreign_key_was, -1) + end + end + def target_changed? owner.saved_change_to_attribute?(reflection.foreign_key) end @@ -50,11 +64,8 @@ module ActiveRecord def replace(record) if record raise_on_type_mismatch!(record) - update_counters_on_replace(record) set_inverse_instance(record) @updated = true - else - decrement_counters end replace_keys(record) @@ -67,11 +78,16 @@ module ActiveRecord if target && !stale_target? target.increment!(reflection.counter_cache_column, by, touch: reflection.options[:touch]) else - counter_cache_target.update_counters(reflection.counter_cache_column => by, touch: reflection.options[:touch]) + update_counters_via_scope(klass, owner._read_attribute(reflection.foreign_key), by) end end end + def update_counters_via_scope(klass, foreign_key, by) + scope = klass.unscoped.where!(primary_key(klass) => foreign_key) + scope.update_counters(reflection.counter_cache_column => by, touch: reflection.options[:touch]) + end + def find_target? !loaded? && foreign_key_present? && klass end @@ -80,25 +96,12 @@ module ActiveRecord reflection.counter_cache_column && owner.persisted? end - def update_counters_on_replace(record) - if require_counter_update? && different_target?(record) - owner.instance_variable_set :@_after_replace_counter_called, true - record.increment!(reflection.counter_cache_column, touch: reflection.options[:touch]) - decrement_counters - end - end - - # Checks whether record is different to the current target, without loading it - def different_target?(record) - record._read_attribute(primary_key(record)) != owner._read_attribute(reflection.foreign_key) - end - def replace_keys(record) - owner[reflection.foreign_key] = record ? record._read_attribute(primary_key(record)) : nil + owner[reflection.foreign_key] = record ? record._read_attribute(primary_key(record.class)) : nil end - def primary_key(record) - reflection.association_primary_key(record.class) + def primary_key(klass) + reflection.association_primary_key(klass) end def foreign_key_present? @@ -112,11 +115,6 @@ module ActiveRecord inverse && inverse.has_one? end - def counter_cache_target - primary_key = reflection.association_primary_key(klass) - klass.unscoped.where!(primary_key => owner._read_attribute(reflection.foreign_key)) - end - def stale_state result = owner._read_attribute(reflection.foreign_key) { |n| owner.send(:missing_attribute, n, caller) } result && result.to_s diff --git a/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb b/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb index 3fd2fb5f67..9ae452e7a1 100644 --- a/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb +++ b/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb @@ -19,10 +19,6 @@ module ActiveRecord owner[reflection.foreign_type] = record ? record.class.polymorphic_name : nil end - def different_target?(record) - super || record.class != klass - end - def inverse_reflection_for(record) reflection.polymorphic_inverse_of(record.class) end diff --git a/activerecord/lib/active_record/associations/builder/belongs_to.rb b/activerecord/lib/active_record/associations/builder/belongs_to.rb index b5fb0092d7..fc00f1e900 100644 --- a/activerecord/lib/active_record/associations/builder/belongs_to.rb +++ b/activerecord/lib/active_record/associations/builder/belongs_to.rb @@ -21,56 +21,16 @@ module ActiveRecord::Associations::Builder # :nodoc: add_default_callbacks(model, reflection) if reflection.options[:default] end - def self.define_accessors(mixin, reflection) - super - add_counter_cache_methods mixin - end - - def self.add_counter_cache_methods(mixin) - return if mixin.method_defined? :belongs_to_counter_cache_after_update - - mixin.class_eval do - def belongs_to_counter_cache_after_update(reflection) - foreign_key = reflection.foreign_key - cache_column = reflection.counter_cache_column - - if (@_after_replace_counter_called ||= false) - @_after_replace_counter_called = false - elsif association(reflection.name).target_changed? - if reflection.polymorphic? - model = attribute_in_database(reflection.foreign_type).try(:constantize) - model_was = attribute_before_last_save(reflection.foreign_type).try(:constantize) - else - model = reflection.klass - model_was = reflection.klass - end - - foreign_key_was = attribute_before_last_save foreign_key - foreign_key = attribute_in_database foreign_key - - if foreign_key && model < ActiveRecord::Base - counter_cache_target(reflection, model, foreign_key).update_counters(cache_column => 1) - end - - if foreign_key_was && model_was < ActiveRecord::Base - counter_cache_target(reflection, model_was, foreign_key_was).update_counters(cache_column => -1) - end - end - end - - private - def counter_cache_target(reflection, model, foreign_key) - primary_key = reflection.association_primary_key(model) - model.unscoped.where!(primary_key => foreign_key) - end - end - end - def self.add_counter_cache_callbacks(model, reflection) cache_column = reflection.counter_cache_column model.after_update lambda { |record| - record.belongs_to_counter_cache_after_update(reflection) + association = association(reflection.name) + + if association.target_changed? + association.increment_counters + association.decrement_counters_before_last_save + end } klass = reflection.class_name.safe_constantize @@ -121,12 +81,18 @@ module ActiveRecord::Associations::Builder # :nodoc: BelongsTo.touch_record(record, record.send(changes_method), foreign_key, n, touch, belongs_to_touch_method) }} - unless reflection.counter_cache_column + if reflection.counter_cache_column + touch_callback = callback.(:saved_changes) + update_callback = lambda { |record| + instance_exec(record, &touch_callback) unless association(reflection.name).target_changed? + } + model.after_update update_callback, if: :saved_changes? + else model.after_create callback.(:saved_changes), if: :saved_changes? + model.after_update callback.(:saved_changes), if: :saved_changes? model.after_destroy callback.(:changes_to_save) end - model.after_update callback.(:saved_changes), if: :saved_changes? model.after_touch callback.(:changes_to_save) end diff --git a/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb b/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb index e3070e0472..0140aa15c8 100644 --- a/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb +++ b/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb @@ -63,7 +63,7 @@ module ActiveRecord::Associations::Builder # :nodoc: def middle_reflection(join_model) middle_name = [lhs_model.name.downcase.pluralize, - association_name].join("_".freeze).gsub("::".freeze, "_".freeze).to_sym + association_name].join("_").gsub("::", "_").to_sym middle_options = middle_options join_model HasMany.create_reflection(lhs_model, diff --git a/activerecord/lib/active_record/associations/preloader.rb b/activerecord/lib/active_record/associations/preloader.rb index 5c2ac5b374..a8f94b574d 100644 --- a/activerecord/lib/active_record/associations/preloader.rb +++ b/activerecord/lib/active_record/associations/preloader.rb @@ -88,7 +88,6 @@ module ActiveRecord if records.empty? [] else - records.uniq! Array.wrap(associations).flat_map { |association| preloaders_on association, records, preload_scope } @@ -99,12 +98,11 @@ module ActiveRecord # Loads all the given data into +records+ for the +association+. def preloaders_on(association, records, scope, polymorphic_parent = false) - case association - when Hash + if association.respond_to?(:to_hash) preloaders_for_hash(association, records, scope, polymorphic_parent) - when Symbol + elsif association.is_a?(Symbol) preloaders_for_one(association, records, scope, polymorphic_parent) - when String + elsif association.respond_to?(:to_str) preloaders_for_one(association.to_sym, records, scope, polymorphic_parent) else raise ArgumentError, "#{association.inspect} was not recognized for preload" diff --git a/activerecord/lib/active_record/attribute_methods.rb b/activerecord/lib/active_record/attribute_methods.rb index e4b8b1a330..221ebea8ea 100644 --- a/activerecord/lib/active_record/attribute_methods.rb +++ b/activerecord/lib/active_record/attribute_methods.rb @@ -31,7 +31,7 @@ module ActiveRecord end } - BLACKLISTED_CLASS_METHODS = %w(private public protected allocate new name parent superclass) + RESTRICTED_CLASS_METHODS = %w(private public protected allocate new name parent superclass) class GeneratedAttributeMethods < Module #:nodoc: include Mutex_m @@ -123,7 +123,7 @@ module ActiveRecord # A class method is 'dangerous' if it is already (re)defined by Active Record, but # not by any ancestors. (So 'puts' is not dangerous but 'new' is.) def dangerous_class_method?(method_name) - BLACKLISTED_CLASS_METHODS.include?(method_name.to_s) || class_method_defined_within?(method_name, Base) + RESTRICTED_CLASS_METHODS.include?(method_name.to_s) || class_method_defined_within?(method_name, Base) end def class_method_defined_within?(name, klass, superklass = klass.superclass) # :nodoc: @@ -167,12 +167,14 @@ module ActiveRecord end end - # Regexp whitelist. Matches the following: + # Regexp for column names (with or without a table name prefix). Matches + # the following: # "#{table_name}.#{column_name}" # "#{column_name}" - COLUMN_NAME_WHITELIST = /\A(?:\w+\.)?\w+\z/i + COLUMN_NAME = /\A(?:\w+\.)?\w+\z/i - # Regexp whitelist. Matches the following: + # Regexp for column names with order (with or without a table name + # prefix, with or without various order modifiers). Matches the following: # "#{table_name}.#{column_name}" # "#{table_name}.#{column_name} #{direction}" # "#{table_name}.#{column_name} #{direction} NULLS FIRST" @@ -181,7 +183,7 @@ module ActiveRecord # "#{column_name} #{direction}" # "#{column_name} #{direction} NULLS FIRST" # "#{column_name} NULLS LAST" - COLUMN_NAME_ORDER_WHITELIST = / + COLUMN_NAME_WITH_ORDER = / \A (?:\w+\.)? \w+ @@ -190,12 +192,10 @@ module ActiveRecord \z /ix - def enforce_raw_sql_whitelist(args, whitelist: COLUMN_NAME_WHITELIST) # :nodoc: + def disallow_raw_sql!(args, permit: COLUMN_NAME) # :nodoc: unexpected = args.reject do |arg| - arg.kind_of?(Arel::Node) || - arg.is_a?(Arel::Nodes::SqlLiteral) || - arg.is_a?(Arel::Attributes::Attribute) || - arg.to_s.split(/\s*,\s*/).all? { |part| whitelist.match?(part) } + Arel.arel_node?(arg) || + arg.to_s.split(/\s*,\s*/).all? { |part| permit.match?(part) } end return if unexpected.none? @@ -272,9 +272,9 @@ module ActiveRecord case name when :to_partial_path - name = "to_partial_path".freeze + name = "to_partial_path" when :to_model - name = "to_model".freeze + name = "to_model" else name = name.to_s end @@ -449,14 +449,6 @@ module ActiveRecord defined?(@attributes) && @attributes.key?(attr_name) end - def attributes_with_values_for_create(attribute_names) - attributes_with_values(attributes_for_create(attribute_names)) - end - - def attributes_with_values_for_update(attribute_names) - attributes_with_values(attributes_for_update(attribute_names)) - end - def attributes_with_values(attribute_names) attribute_names.each_with_object({}) do |name, attrs| attrs[name] = _read_attribute(name) @@ -465,7 +457,8 @@ module ActiveRecord # Filters the primary keys and readonly attributes from the attribute names. def attributes_for_update(attribute_names) - attribute_names.reject do |name| + attribute_names &= self.class.column_names + attribute_names.delete_if do |name| readonly_attribute?(name) end end @@ -473,7 +466,8 @@ module ActiveRecord # Filters out the primary keys, from the attribute names, when the primary # key is to be generated (e.g. the id attribute has no value). def attributes_for_create(attribute_names) - attribute_names.reject do |name| + attribute_names &= self.class.column_names + attribute_names.delete_if do |name| pk_attribute?(name) && id.nil? end end diff --git a/activerecord/lib/active_record/attribute_methods/dirty.rb b/activerecord/lib/active_record/attribute_methods/dirty.rb index 233ee29fac..ebc2252c50 100644 --- a/activerecord/lib/active_record/attribute_methods/dirty.rb +++ b/activerecord/lib/active_record/attribute_methods/dirty.rb @@ -16,9 +16,6 @@ module ActiveRecord class_attribute :partial_writes, instance_writer: false, default: true - after_create { changes_applied } - after_update { changes_applied } - # Attribute methods for "changed in last call to save?" attribute_method_affix(prefix: "saved_change_to_", suffix: "?") attribute_method_prefix("saved_change_to_") @@ -167,16 +164,20 @@ module ActiveRecord result end - def _update_record(*) - partial_writes? ? super(keys_for_partial_write) : super + def _update_record(attribute_names = attribute_names_for_partial_writes) + affected_rows = super + changes_applied + affected_rows end - def _create_record(*) - partial_writes? ? super(keys_for_partial_write) : super + def _create_record(attribute_names = attribute_names_for_partial_writes) + id = super + changes_applied + id end - def keys_for_partial_write - changed_attribute_names_to_save & self.class.column_names + def attribute_names_for_partial_writes + partial_writes? ? changed_attribute_names_to_save : attribute_names end end end diff --git a/activerecord/lib/active_record/attribute_methods/read.rb b/activerecord/lib/active_record/attribute_methods/read.rb index 0f7bcba564..903fe86e04 100644 --- a/activerecord/lib/active_record/attribute_methods/read.rb +++ b/activerecord/lib/active_record/attribute_methods/read.rb @@ -27,7 +27,7 @@ module ActiveRecord # Making it frozen means that it doesn't get duped when used to # key the @attributes in read_attribute. def define_method_attribute(name) - safe_name = name.unpack1("h*".freeze) + safe_name = name.unpack1("h*") temp_method = "__temp__#{safe_name}" ActiveRecord::AttributeMethods::AttrNames.set_name_cache safe_name, name @@ -59,7 +59,7 @@ module ActiveRecord end primary_key = self.class.primary_key - name = primary_key if name == "id".freeze && primary_key + name = primary_key if name == "id" && primary_key sync_with_transaction_state if name == primary_key _read_attribute(name, &block) 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 d2b7817b45..294a3dc32c 100644 --- a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb +++ b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb @@ -73,7 +73,7 @@ module ActiveRecord # `skip_time_zone_conversion_for_attributes` would not be picked up. subclass.class_eval do matcher = ->(name, type) { create_time_zone_conversion_attribute?(name, type) } - decorate_matching_attribute_types(matcher, :_time_zone_conversion) do |type| + decorate_matching_attribute_types(matcher, "_time_zone_conversion") do |type| TimeZoneConverter.new(type) end end diff --git a/activerecord/lib/active_record/attribute_methods/write.rb b/activerecord/lib/active_record/attribute_methods/write.rb index c7521422bb..62743bc9d8 100644 --- a/activerecord/lib/active_record/attribute_methods/write.rb +++ b/activerecord/lib/active_record/attribute_methods/write.rb @@ -13,7 +13,7 @@ module ActiveRecord private def define_method_attribute=(name) - safe_name = name.unpack1("h*".freeze) + safe_name = name.unpack1("h*") ActiveRecord::AttributeMethods::AttrNames.set_name_cache safe_name, name sync_with_transaction_state = "sync_with_transaction_state" if name == primary_key @@ -40,7 +40,7 @@ module ActiveRecord end primary_key = self.class.primary_key - name = primary_key if name == "id".freeze && primary_key + name = primary_key if name == "id" && primary_key sync_with_transaction_state if name == primary_key _write_attribute(name, value) end diff --git a/activerecord/lib/active_record/autosave_association.rb b/activerecord/lib/active_record/autosave_association.rb index a405f05e0b..d77d76cb1e 100644 --- a/activerecord/lib/active_record/autosave_association.rb +++ b/activerecord/lib/active_record/autosave_association.rb @@ -149,7 +149,7 @@ module ActiveRecord private def define_non_cyclic_method(name, &block) - return if method_defined?(name) + return if instance_methods(false).include?(name) define_method(name) do |*args| result = true; @_already_called ||= {} # Loop prevention for validation of associations @@ -392,7 +392,7 @@ module ActiveRecord records -= records_to_destroy end - records.each_with_index do |record, index| + records.each do |record| next if record.destroyed? saved = true @@ -401,11 +401,11 @@ module ActiveRecord if autosave saved = association.insert_record(record, false) elsif !reflection.nested? + association_saved = association.insert_record(record) + if reflection.validate? - valid = association_valid?(reflection, record, index) - saved = valid ? association.insert_record(record, false) : false - else - association.insert_record(record) + errors.add(reflection.name) unless association_saved + saved = association_saved end end elsif autosave diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb index 5169f312f5..db097cb930 100644 --- a/activerecord/lib/active_record/base.rb +++ b/activerecord/lib/active_record/base.rb @@ -22,6 +22,7 @@ require "active_record/explain_subscriber" require "active_record/relation/delegation" require "active_record/attributes" require "active_record/type_caster" +require "active_record/database_configurations" module ActiveRecord #:nodoc: # = Active Record @@ -291,7 +292,6 @@ module ActiveRecord #:nodoc: extend Aggregations::ClassMethods include Core - include DatabaseConfigurations include Persistence include ReadonlyAttributes include ModelSchema diff --git a/activerecord/lib/active_record/callbacks.rb b/activerecord/lib/active_record/callbacks.rb index b6852bfc71..1bffe89875 100644 --- a/activerecord/lib/active_record/callbacks.rb +++ b/activerecord/lib/active_record/callbacks.rb @@ -318,7 +318,7 @@ module ActiveRecord _run_touch_callbacks { super } end - def increment!(*, touch: nil) # :nodoc: + def increment!(attribute, by = 1, touch: nil) # :nodoc: touch ? _run_touch_callbacks { super } : super end diff --git a/activerecord/lib/active_record/collection_cache_key.rb b/activerecord/lib/active_record/collection_cache_key.rb index dfba78614e..61581b0451 100644 --- a/activerecord/lib/active_record/collection_cache_key.rb +++ b/activerecord/lib/active_record/collection_cache_key.rb @@ -16,7 +16,7 @@ module ActiveRecord collection = collection.send(:apply_join_dependency) end column_type = type_for_attribute(timestamp_column) - column = connection.column_name_from_arel_node(collection.arel_attribute(timestamp_column)) + column = connection.visitor.compile(collection.arel_attribute(timestamp_column)) select_values = "COUNT(*) AS #{connection.quote_column_name("size")}, MAX(%s) AS timestamp" if collection.has_limit_or_offset? diff --git a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb index f721e91203..99934a0e31 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb @@ -188,7 +188,9 @@ module ActiveRecord t0 = Time.now elapsed = 0 loop do - @cond.wait(timeout - elapsed) + ActiveSupport::Dependencies.interlock.permit_concurrent_loads do + @cond.wait(timeout - elapsed) + end return remove if any? @@ -729,7 +731,7 @@ module ActiveRecord # this block can't be easily moved into attempt_to_checkout_all_existing_connections's # rescue block, because doing so would put it outside of synchronize section, without # being in a critical section thread_report might become inaccurate - msg = "could not obtain ownership of all database connections in #{checkout_timeout} seconds".dup + msg = +"could not obtain ownership of all database connections in #{checkout_timeout} seconds" thread_report = [] @connections.each do |conn| diff --git a/activerecord/lib/active_record/connection_adapters/abstract/database_limits.rb b/activerecord/lib/active_record/connection_adapters/abstract/database_limits.rb index 7a9e7add24..1305216be2 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/database_limits.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/database_limits.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require "active_support/deprecation" + module ActiveRecord module ConnectionAdapters # :nodoc: module DatabaseLimits @@ -12,11 +14,13 @@ module ActiveRecord def column_name_length 64 end + deprecate :column_name_length # Returns the maximum length of a table name. def table_name_length 64 end + deprecate :table_name_length # Returns the maximum allowed length for an index name. This # limit is enforced by \Rails and is less than or equal to @@ -36,16 +40,19 @@ module ActiveRecord def columns_per_table 1024 end + deprecate :columns_per_table # Returns the maximum number of indexes per table. def indexes_per_table 16 end + deprecate :indexes_per_table # Returns the maximum number of columns in a multicolumn index. def columns_per_multicolumn_index 16 end + deprecate :columns_per_multicolumn_index # Returns the maximum number of elements in an IN (x,y,z) clause. # +nil+ means no limit. @@ -57,11 +64,18 @@ module ActiveRecord def sql_query_length 1048575 end + deprecate :sql_query_length # Returns maximum number of joins in a single query. def joins_per_query 256 end + deprecate :joins_per_query + + private + def bind_params_length + 65535 + end end end end 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 41553cfa83..feacdf6931 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb @@ -20,7 +20,7 @@ module ActiveRecord raise "Passing bind parameters with an arel AST is forbidden. " \ "The values must be stored on the AST directly" end - sql, binds = visitor.accept(arel_or_sql_string.ast, collector).value + sql, binds = visitor.compile(arel_or_sql_string.ast, collector) [sql.freeze, binds || []] else [arel_or_sql_string.dup.freeze, binds] @@ -32,11 +32,11 @@ module ActiveRecord # can be used to query the database repeatedly. def cacheable_query(klass, arel) # :nodoc: if prepared_statements - sql, binds = visitor.accept(arel.ast, collector).value + sql, binds = visitor.compile(arel.ast, collector) query = klass.query(sql) else - collector = PartialQueryCollector.new - parts, binds = visitor.accept(arel.ast, collector).value + collector = klass.partial_query_collector + parts, binds = visitor.compile(arel.ast, collector) query = klass.partial_query(parts) end [query, binds] @@ -46,11 +46,16 @@ module ActiveRecord def select_all(arel, name = nil, binds = [], preparable: nil) arel = arel_from_relation(arel) sql, binds = to_sql_and_binds(arel, binds) + if !prepared_statements || (arel.is_a?(String) && preparable.nil?) preparable = false + elsif binds.length > bind_params_length + sql, binds = unprepared_statement { to_sql_and_binds(arel) } + preparable = false else preparable = visitor.preparable end + if prepared_statements && preparable select_prepared(sql, name, binds) else @@ -259,7 +264,9 @@ module ActiveRecord attr_reader :transaction_manager #:nodoc: - delegate :within_new_transaction, :open_transactions, :current_transaction, :begin_transaction, :commit_transaction, :rollback_transaction, to: :transaction_manager + delegate :within_new_transaction, :open_transactions, :current_transaction, :begin_transaction, + :commit_transaction, :rollback_transaction, :materialize_transactions, + :disable_lazy_transactions!, :enable_lazy_transactions!, to: :transaction_manager def transaction_open? current_transaction.open? @@ -372,7 +379,7 @@ module ActiveRecord build_fixture_sql(fixtures, table_name) end.compact - table_deletes = tables_to_delete.map { |table| "DELETE FROM #{quote_table_name table}".dup } + table_deletes = tables_to_delete.map { |table| +"DELETE FROM #{quote_table_name table}" } total_sql = Array.wrap(combine_multi_statements(table_deletes + fixture_inserts)) disable_referential_integrity do @@ -500,28 +507,6 @@ module ActiveRecord value end end - - class PartialQueryCollector - def initialize - @parts = [] - @binds = [] - end - - def <<(str) - @parts << str - self - end - - def add_bind(obj) - @binds << obj - @parts << Arel::Nodes::BindParam.new(1) - self - end - - def value - [@parts, @binds] - end - end end end end diff --git a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb index 98b1348135..07e86afe9a 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb @@ -60,7 +60,7 @@ module ActiveRecord # Quotes a string, escaping any ' (single quote) and \ (backslash) # characters. def quote_string(s) - s.gsub('\\'.freeze, '\&\&'.freeze).gsub("'".freeze, "''".freeze) # ' (for ruby-mode) + s.gsub('\\', '\&\&').gsub("'", "''") # ' (for ruby-mode) end # Quotes the column name. Defaults to no quoting. @@ -95,7 +95,7 @@ module ActiveRecord end def quoted_true - "TRUE".freeze + "TRUE" end def unquoted_true @@ -103,7 +103,7 @@ module ActiveRecord end def quoted_false - "FALSE".freeze + "FALSE" end def unquoted_false 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 529c9d8ca6..9d9e8a4110 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_creation.rb @@ -21,7 +21,7 @@ module ActiveRecord private def visit_AlterTable(o) - sql = "ALTER TABLE #{quote_table_name(o.name)} ".dup + sql = +"ALTER TABLE #{quote_table_name(o.name)} " sql << o.adds.map { |col| accept col }.join(" ") sql << o.foreign_key_adds.map { |fk| visit_AddForeignKey fk }.join(" ") sql << o.foreign_key_drops.map { |fk| visit_DropForeignKey fk }.join(" ") @@ -29,17 +29,17 @@ module ActiveRecord def visit_ColumnDefinition(o) o.sql_type = type_to_sql(o.type, o.options) - column_sql = "#{quote_column_name(o.name)} #{o.sql_type}".dup + column_sql = +"#{quote_column_name(o.name)} #{o.sql_type}" add_column_options!(column_sql, column_options(o)) unless o.type == :primary_key column_sql end def visit_AddColumnDefinition(o) - "ADD #{accept(o.column)}".dup + +"ADD #{accept(o.column)}" end def visit_TableDefinition(o) - create_sql = "CREATE#{' TEMPORARY' if o.temporary} TABLE #{quote_table_name(o.name)} ".dup + create_sql = +"CREATE#{' TEMPORARY' if o.temporary} TABLE #{quote_table_name(o.name)} " statements = o.columns.map { |c| accept c } statements << accept(o.primary_keys) if o.primary_keys 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 582ac516c7..015204c056 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb @@ -527,7 +527,9 @@ module ActiveRecord # # See TableDefinition#column for details of the options you can use. def column(column_name, type, options = {}) + index_options = options.delete(:index) @base.add_column(name, column_name, type, options) + index(column_name, index_options.is_a?(Hash) ? index_options : {}) if index_options end # Checks to see if a column exists. 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 3be0906f2a..723d8c318d 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb @@ -211,13 +211,13 @@ module ActiveRecord # # ====== Add a backend specific option to the generated SQL (MySQL) # - # create_table(:suppliers, options: 'ENGINE=InnoDB DEFAULT CHARSET=utf8') + # create_table(:suppliers, options: 'ENGINE=InnoDB DEFAULT CHARSET=utf8mb4') # # generates: # # CREATE TABLE suppliers ( # id bigint auto_increment PRIMARY KEY - # ) ENGINE=InnoDB DEFAULT CHARSET=utf8 + # ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 # # ====== Rename the primary key column # @@ -522,6 +522,9 @@ module ActiveRecord # Specifies the precision for the <tt>:decimal</tt> and <tt>:numeric</tt> columns. # * <tt>:scale</tt> - # Specifies the scale for the <tt>:decimal</tt> and <tt>:numeric</tt> columns. + # * <tt>:collation</tt> - + # Specifies the collation for a <tt>:string</tt> or <tt>:text</tt> column. If not specified, the + # column will have the same collation as the table. # * <tt>:comment</tt> - # Specifies the comment for the column. This option is ignored by some backends. # @@ -599,6 +602,7 @@ module ActiveRecord # The +type+ and +options+ parameters will be ignored if present. It can be helpful # to provide these in a migration's +change+ method so it can be reverted. # In that case, +type+ and +options+ will be used by #add_column. + # Indexes on the column are automatically removed. def remove_column(table_name, column_name, type = nil, options = {}) execute "ALTER TABLE #{quote_table_name(table_name)} #{remove_column_for_alter(table_name, column_name, type, options)}" end @@ -980,11 +984,18 @@ module ActiveRecord # # remove_foreign_key :accounts, column: :owner_id # + # Removes the foreign key on +accounts.owner_id+. + # + # remove_foreign_key :accounts, to_table: :owners + # # Removes the foreign key named +special_fk_name+ on the +accounts+ table. # # remove_foreign_key :accounts, name: :special_fk_name # - # The +options+ hash accepts the same keys as SchemaStatements#add_foreign_key. + # The +options+ hash accepts the same keys as SchemaStatements#add_foreign_key + # with an addition of + # [<tt>:to_table</tt>] + # The name of the table that contains the referenced primary key. def remove_foreign_key(from_table, options_or_to_table = {}) return unless supports_foreign_keys? @@ -1369,7 +1380,7 @@ module ActiveRecord sm_table = quote_table_name(ActiveRecord::SchemaMigration.table_name) if versions.is_a?(Array) - sql = "INSERT INTO #{sm_table} (version) VALUES\n".dup + sql = +"INSERT INTO #{sm_table} (version) VALUES\n" sql << versions.map { |v| "(#{quote(v)})" }.join(",\n") sql << ";\n\n" sql diff --git a/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb b/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb index b59df2fff7..564b226b39 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb @@ -91,12 +91,14 @@ module ActiveRecord end class Transaction #:nodoc: - attr_reader :connection, :state, :records, :savepoint_name + attr_reader :connection, :state, :records, :savepoint_name, :isolation_level def initialize(connection, options, run_commit_callbacks: false) @connection = connection @state = TransactionState.new @records = [] + @isolation_level = options[:isolation] + @materialized = false @joinable = options.fetch(:joinable, true) @run_commit_callbacks = run_commit_callbacks end @@ -105,6 +107,14 @@ module ActiveRecord records << record end + def materialize! + @materialized = true + end + + def materialized? + @materialized + end + def rollback_records ite = records.uniq while record = ite.shift @@ -141,24 +151,30 @@ module ActiveRecord end class SavepointTransaction < Transaction - def initialize(connection, savepoint_name, parent_transaction, options, *args) - super(connection, options, *args) + def initialize(connection, savepoint_name, parent_transaction, *args) + super(connection, *args) parent_transaction.state.add_child(@state) - if options[:isolation] + if isolation_level raise ActiveRecord::TransactionIsolationError, "cannot set transaction isolation in a nested transaction" end - connection.create_savepoint(@savepoint_name = savepoint_name) + + @savepoint_name = savepoint_name + end + + def materialize! + connection.create_savepoint(savepoint_name) + super end def rollback - connection.rollback_to_savepoint(savepoint_name) + connection.rollback_to_savepoint(savepoint_name) if materialized? @state.rollback! end def commit - connection.release_savepoint(savepoint_name) + connection.release_savepoint(savepoint_name) if materialized? @state.commit! end @@ -166,22 +182,23 @@ module ActiveRecord end class RealTransaction < Transaction - def initialize(connection, options, *args) - super - if options[:isolation] - connection.begin_isolated_db_transaction(options[:isolation]) + def materialize! + if isolation_level + connection.begin_isolated_db_transaction(isolation_level) else connection.begin_db_transaction end + + super end def rollback - connection.rollback_db_transaction + connection.rollback_db_transaction if materialized? @state.full_rollback! end def commit - connection.commit_db_transaction + connection.commit_db_transaction if materialized? @state.full_commit! end end @@ -190,6 +207,9 @@ module ActiveRecord def initialize(connection) @stack = [] @connection = connection + @has_unmaterialized_transactions = false + @materializing_transactions = false + @lazy_transactions_enabled = true end def begin_transaction(options = {}) @@ -203,11 +223,41 @@ module ActiveRecord run_commit_callbacks: run_commit_callbacks) end + transaction.materialize! unless @connection.supports_lazy_transactions? && lazy_transactions_enabled? @stack.push(transaction) + @has_unmaterialized_transactions = true if @connection.supports_lazy_transactions? transaction end end + def disable_lazy_transactions! + materialize_transactions + @lazy_transactions_enabled = false + end + + def enable_lazy_transactions! + @lazy_transactions_enabled = true + end + + def lazy_transactions_enabled? + @lazy_transactions_enabled + end + + def materialize_transactions + return if @materializing_transactions + return unless @has_unmaterialized_transactions + + @connection.lock.synchronize do + begin + @materializing_transactions = true + @stack.each { |t| t.materialize! unless t.materialized? } + ensure + @materializing_transactions = false + end + @has_unmaterialized_transactions = false + end + end + def commit_transaction @connection.lock.synchronize do transaction = @stack.last diff --git a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb index a4748dbeda..fa10f18cb7 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb @@ -65,7 +65,7 @@ module ActiveRecord # Most of the methods in the adapter are useful during migrations. Most # notably, the instance methods provided by SchemaStatements are very useful. class AbstractAdapter - ADAPTER_NAME = "Abstract".freeze + ADAPTER_NAME = "Abstract" include ActiveSupport::Callbacks define_callbacks :checkout, :checkin @@ -80,6 +80,8 @@ module ActiveRecord attr_reader :schema_cache, :owner, :logger, :prepared_statements, :lock alias :in_use? :owner + set_callback :checkin, :after, :enable_lazy_transactions! + def self.type_cast_config_to_integer(config) if config.is_a?(Integer) config @@ -119,6 +121,14 @@ module ActiveRecord else @prepared_statements = false end + + @advisory_locks_enabled = self.class.type_cast_config_to_boolean( + config.fetch(:advisory_locks, true) + ) + end + + def replica? + @config[:replica] || false end def migrations_paths # :nodoc: @@ -152,7 +162,7 @@ module ActiveRecord # this method must only be called while holding connection pool's mutex def lease if in_use? - msg = "Cannot lease connection, ".dup + msg = +"Cannot lease connection, " if @owner == Thread.current msg << "it is already leased by the current thread." else @@ -338,6 +348,10 @@ module ActiveRecord false end + def supports_lazy_transactions? + false + end + # This is meant to be implemented by the adapters that support extensions def disable_extension(name) end @@ -346,6 +360,10 @@ module ActiveRecord def enable_extension(name) end + def advisory_locks_enabled? # :nodoc: + supports_advisory_locks? && @advisory_locks_enabled + end + # This is meant to be implemented by the adapters that support advisory # locks # @@ -449,6 +467,7 @@ module ActiveRecord # This is useful for when you need to call a proprietary method such as # PostgreSQL's lo_* methods. def raw_connection + disable_lazy_transactions! @connection end @@ -475,11 +494,7 @@ module ActiveRecord end def column_name_for_operation(operation, node) # :nodoc: - column_name_from_arel_node(node) - end - - def column_name_from_arel_node(node) # :nodoc: - visitor.accept(node, Arel::Collectors::SQLString.new).value + visitor.compile(node) end def default_index_type?(index) # :nodoc: 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 9de8242a58..09242a0f14 100644 --- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb @@ -43,9 +43,11 @@ module ActiveRecord } class StatementPool < ConnectionAdapters::StatementPool # :nodoc: - private def dealloc(stmt) - stmt.close - end + private + + def dealloc(stmt) + stmt.close + end end def initialize(connection, logger, connection_options, config) @@ -53,8 +55,8 @@ 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 (#{version_string}) is too old. Active Record supports MySQL >= 5.1.10." + if version < "5.5.8" + raise "Your version of MySQL (#{version_string}) is too old. Active Record supports MySQL >= 5.5.8." end end @@ -114,6 +116,14 @@ module ActiveRecord true end + def supports_longer_index_key_prefix? + if mariadb? + version >= "10.2.2" + else + version >= "5.7.9" + end + end + def get_advisory_lock(lock_name, timeout = 0) # :nodoc: query_value("SELECT GET_LOCK(#{quote(lock_name.to_s)}, #{timeout})") == 1 end @@ -127,7 +137,7 @@ module ActiveRecord end def index_algorithms - { default: "ALGORITHM = DEFAULT".dup, copy: "ALGORITHM = COPY".dup, inplace: "ALGORITHM = INPLACE".dup } + { default: +"ALGORITHM = DEFAULT", copy: +"ALGORITHM = COPY", inplace: +"ALGORITHM = INPLACE" } end # HELPER METHODS =========================================== @@ -180,6 +190,8 @@ module ActiveRecord # Executes the SQL statement in the context of this connection. def execute(sql, name = nil) + materialize_transactions + log(sql, name) do ActiveSupport::Dependencies.interlock.permit_concurrent_loads do @connection.query(sql) @@ -239,7 +251,7 @@ module ActiveRecord end # Create a new MySQL database with optional <tt>:charset</tt> and <tt>:collation</tt>. - # Charset defaults to utf8. + # Charset defaults to utf8mb4. # # Example: # create_database 'charset_test', charset: 'latin1', collation: 'latin1_bin' @@ -248,8 +260,12 @@ module ActiveRecord def create_database(name, options = {}) if options[:collation] execute "CREATE DATABASE #{quote_table_name(name)} DEFAULT COLLATE #{quote_table_name(options[:collation])}" + elsif options[:charset] + execute "CREATE DATABASE #{quote_table_name(name)} DEFAULT CHARACTER SET #{quote_table_name(options[:charset])}" + elsif supports_longer_index_key_prefix? + execute "CREATE DATABASE #{quote_table_name(name)} DEFAULT CHARACTER SET `utf8mb4`" else - execute "CREATE DATABASE #{quote_table_name(name)} DEFAULT CHARACTER SET #{quote_table_name(options[:charset] || 'utf8')}" + raise "Configure a supported :charset and ensure innodb_large_prefix is enabled to support indexes on varchar(255) string columns." end end @@ -376,7 +392,7 @@ module ActiveRecord def add_index(table_name, column_name, options = {}) #:nodoc: index_name, index_type, index_columns, _, index_algorithm, index_using, comment = add_index_options(table_name, column_name, options) - sql = "CREATE #{index_type} INDEX #{quote_column_name(index_name)} #{index_using} ON #{quote_table_name(table_name)} (#{index_columns}) #{index_algorithm}".dup + sql = +"CREATE #{index_type} INDEX #{quote_column_name(index_name)} #{index_using} ON #{quote_table_name(table_name)} (#{index_columns}) #{index_algorithm}" execute add_sql_comment!(sql, comment) end @@ -769,7 +785,7 @@ module ActiveRecord # https://dev.mysql.com/doc/refman/5.7/en/set-names.html # (trailing comma because variable_assignments will always have content) if @config[:encoding] - encoding = "NAMES #{@config[:encoding]}".dup + encoding = +"NAMES #{@config[:encoding]}" encoding << " COLLATE #{@config[:collation]}" if @config[:collation] encoding << ", " end diff --git a/activerecord/lib/active_record/connection_adapters/connection_specification.rb b/activerecord/lib/active_record/connection_adapters/connection_specification.rb index 204691006c..2e7a78215a 100644 --- a/activerecord/lib/active_record/connection_adapters/connection_specification.rb +++ b/activerecord/lib/active_record/connection_adapters/connection_specification.rb @@ -114,8 +114,7 @@ module ActiveRecord class Resolver # :nodoc: attr_reader :configurations - # Accepts a hash two layers deep, keys on the first layer represent - # environments such as "production". Keys must be strings. + # Accepts a list of db config objects. def initialize(configurations) @configurations = configurations end @@ -136,33 +135,14 @@ module ActiveRecord # Resolver.new(configurations).resolve(:production) # # => { "host" => "localhost", "database" => "foo", "adapter" => "postgresql" } # - def resolve(config) - if config - resolve_connection config - elsif env = ActiveRecord::ConnectionHandling::RAILS_ENV.call - resolve_symbol_connection env.to_sym + def resolve(config_or_env, pool_name = nil) + if config_or_env + resolve_connection config_or_env, pool_name else raise AdapterNotSpecified end end - # Expands each key in @configurations hash into fully resolved hash - def resolve_all - config = configurations.dup - - if env = ActiveRecord::ConnectionHandling::DEFAULT_ENV.call - env_config = config[env] if config[env].is_a?(Hash) && !(config[env].key?("adapter") || config[env].key?("url")) - end - - config.merge! env_config if env_config - - config.each do |key, value| - config[key] = resolve(value) if value - end - - config - end - # Returns an instance of ConnectionSpecification for a given adapter. # Accepts a hash one layer deep that contains all connection information. # @@ -176,7 +156,9 @@ module ActiveRecord # # => { "host" => "localhost", "database" => "foo", "adapter" => "sqlite3" } # def spec(config) - spec = resolve(config).symbolize_keys + pool_name = config if config.is_a?(Symbol) + + spec = resolve(config, pool_name).symbolize_keys raise(AdapterNotSpecified, "database configuration does not specify adapter") unless spec.key?(:adapter) @@ -211,7 +193,6 @@ module ActiveRecord end private - # Returns fully resolved connection, accepts hash, string or symbol. # Always returns a hash. # @@ -232,29 +213,42 @@ module ActiveRecord # Resolver.new({}).resolve_connection("postgresql://localhost/foo") # # => { "host" => "localhost", "database" => "foo", "adapter" => "postgresql" } # - def resolve_connection(spec) - case spec + def resolve_connection(config_or_env, pool_name = nil) + case config_or_env when Symbol - resolve_symbol_connection spec + resolve_symbol_connection config_or_env, pool_name when String - resolve_url_connection spec + resolve_url_connection config_or_env when Hash - resolve_hash_connection spec + resolve_hash_connection config_or_env + else + resolve_connection config_or_env end end - # Takes the environment such as +:production+ or +:development+. + # Takes the environment such as +:production+ or +:development+ and a + # pool name the corresponds to the name given by the connection pool + # to the connection. That pool name is merged into the hash with the + # name key. + # # This requires that the @configurations was initialized with a key that # matches. # - # Resolver.new("production" => {}).resolve_symbol_connection(:production) - # # => {} + # configurations = #<ActiveRecord::DatabaseConfigurations:0x00007fd9fdace3e0 + # @configurations=[ + # #<ActiveRecord::DatabaseConfigurations::HashConfig:0x00007fd9fdace250 + # @env_name="production", @spec_name="primary", @config={"database"=>"my_db"}> + # ]> # - def resolve_symbol_connection(spec) - if config = configurations[spec.to_s] - resolve_connection(config).merge("name" => spec.to_s) + # Resolver.new(configurations).resolve_symbol_connection(:production, "primary") + # # => { "database" => "my_db" } + def resolve_symbol_connection(env_name, pool_name) + db_config = configurations.find_db_config(env_name) + + if db_config + resolve_connection(db_config.config).merge("name" => pool_name.to_s) else - raise(AdapterNotSpecified, "'#{spec}' database is not configured. Available: #{configurations.keys.inspect}") + raise(AdapterNotSpecified, "'#{env_name}' database is not configured. Available: #{configurations.configurations.map(&:env_name).join(", ")}") end 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 d89eeb7f54..684c7042a7 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql/database_statements.rb @@ -29,6 +29,8 @@ module ActiveRecord end def exec_query(sql, name = "SQL", binds = [], prepare: false) + materialize_transactions + if without_prepared_statement?(binds) execute_and_free(sql, name) do |result| ActiveRecord::Result.new(result.fields, result.to_a) if result @@ -41,6 +43,8 @@ module ActiveRecord end def exec_delete(sql, name = nil, binds = []) + materialize_transactions + if without_prepared_statement?(binds) execute_and_free(sql, name) { @connection.affected_rows } else diff --git a/activerecord/lib/active_record/connection_adapters/mysql/quoting.rb b/activerecord/lib/active_record/connection_adapters/mysql/quoting.rb index be038403b8..75564a61d6 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql/quoting.rb @@ -5,7 +5,7 @@ module ActiveRecord module MySQL module Quoting # :nodoc: def quote_column_name(name) - @quoted_column_names[name] ||= "`#{super.gsub('`', '``')}`".freeze + @quoted_column_names[name] ||= "`#{super.gsub('`', '``')}`" end def quote_table_name(name) diff --git a/activerecord/lib/active_record/connection_adapters/mysql/schema_creation.rb b/activerecord/lib/active_record/connection_adapters/mysql/schema_creation.rb index c9ea653b77..82ed320617 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql/schema_creation.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql/schema_creation.rb @@ -17,7 +17,7 @@ module ActiveRecord end def visit_ChangeColumnDefinition(o) - change_column_sql = "CHANGE #{quote_column_name(o.name)} #{accept(o.column)}".dup + change_column_sql = +"CHANGE #{quote_column_name(o.name)} #{accept(o.column)}" add_column_position!(change_column_sql, column_options(o.column)) end @@ -64,7 +64,7 @@ module ActiveRecord def index_in_create(table_name, column_name, options) index_name, index_type, index_columns, _, _, index_using, comment = @conn.add_index_options(table_name, column_name, options) - add_sql_comment!("#{index_type} INDEX #{quote_column_name(index_name)} #{index_using} (#{index_columns})".dup, comment) + add_sql_comment!((+"#{index_type} INDEX #{quote_column_name(index_name)} #{index_using} (#{index_columns})"), comment) end end end diff --git a/activerecord/lib/active_record/connection_adapters/mysql/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/mysql/schema_statements.rb index 1cf210d85b..e167c01802 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql/schema_statements.rb @@ -121,7 +121,7 @@ module ActiveRecord def data_source_sql(name = nil, type: nil) scope = quoted_scope(name, type: type) - sql = "SELECT table_name FROM information_schema.tables".dup + sql = +"SELECT table_name FROM information_schema.tables" sql << " WHERE table_schema = #{scope[:schema]}" sql << " AND table_name = #{scope[:name]}" if scope[:name] sql << " AND table_type = #{scope[:type]}" if scope[:type] diff --git a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb index 544d720428..9bdaa00336 100644 --- a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb @@ -3,7 +3,7 @@ require "active_record/connection_adapters/abstract_mysql_adapter" require "active_record/connection_adapters/mysql/database_statements" -gem "mysql2", ">= 0.4.4", "< 0.6.0" +gem "mysql2", ">= 0.4.4" require "mysql2" module ActiveRecord @@ -14,7 +14,7 @@ module ActiveRecord config[:flags] ||= 0 if config[:flags].kind_of? Array - config[:flags].push "FOUND_ROWS".freeze + config[:flags].push "FOUND_ROWS" else config[:flags] |= Mysql2::Client::FOUND_ROWS end @@ -32,7 +32,7 @@ module ActiveRecord module ConnectionAdapters class Mysql2Adapter < AbstractMysqlAdapter - ADAPTER_NAME = "Mysql2".freeze + ADAPTER_NAME = "Mysql2" include MySQL::DatabaseStatements @@ -58,6 +58,10 @@ module ActiveRecord true end + def supports_lazy_transactions? + true + end + # HELPER METHODS =========================================== def each_hash(result) # :nodoc: 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 8db2a645af..6bd6b67165 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb @@ -58,6 +58,8 @@ module ActiveRecord # Queries the database and returns the results in an Array-like object def query(sql, name = nil) #:nodoc: + materialize_transactions + log(sql, name) do ActiveSupport::Dependencies.interlock.permit_concurrent_loads do result_as_array @connection.async_exec(sql) @@ -70,6 +72,8 @@ module ActiveRecord # Note: the PG::Result object is manually memory managed; if you don't # need it specifically, you may want consider the <tt>exec_query</tt> wrapper. def execute(sql, name = nil) + materialize_transactions + log(sql, name) do ActiveSupport::Dependencies.interlock.permit_concurrent_loads do @connection.async_exec(sql) diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb index 26abeea7ed..6fbeaa2b9e 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/array.rb @@ -33,7 +33,13 @@ module ActiveRecord def cast(value) if value.is_a?(::String) - value = @pg_decoder.decode(value) + value = begin + @pg_decoder.decode(value) + rescue TypeError + # malformed array string is treated as [], will raise in PG 2.0 gem + # this keeps a consistent implementation + [] + end end type_cast_array(value, :cast) end diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb index 231278c184..83c21ba6ea 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require "active_support/core_ext/array/extract" + module ActiveRecord module ConnectionAdapters module PostgreSQL @@ -16,12 +18,12 @@ module ActiveRecord def run(records) nodes = records.reject { |row| @store.key? row["oid"].to_i } - mapped, nodes = nodes.partition { |row| @store.key? row["typname"] } - ranges, nodes = nodes.partition { |row| row["typtype"] == "r".freeze } - enums, nodes = nodes.partition { |row| row["typtype"] == "e".freeze } - domains, nodes = nodes.partition { |row| row["typtype"] == "d".freeze } - arrays, nodes = nodes.partition { |row| row["typinput"] == "array_in".freeze } - composites, nodes = nodes.partition { |row| row["typelem"].to_i != 0 } + mapped = nodes.extract! { |row| @store.key? row["typname"] } + ranges = nodes.extract! { |row| row["typtype"] == "r" } + enums = nodes.extract! { |row| row["typtype"] == "e" } + domains = nodes.extract! { |row| row["typtype"] == "d" } + arrays = nodes.extract! { |row| row["typinput"] == "array_in" } + composites = nodes.extract! { |row| row["typelem"].to_i != 0 } mapped.each { |row| register_mapped_type(row) } enums.each { |row| register_enum_type(row) } 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 26b8113b0d..fae3ddbad4 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb @@ -686,7 +686,7 @@ module ActiveRecord def change_column_sql(table_name, column_name, type, options = {}) quoted_column_name = quote_column_name(column_name) sql_type = type_to_sql(type, options) - sql = "ALTER COLUMN #{quoted_column_name} TYPE #{sql_type}".dup + sql = +"ALTER COLUMN #{quoted_column_name} TYPE #{sql_type}" if options[:collation] sql << " COLLATE \"#{options[:collation]}\"" end @@ -700,6 +700,11 @@ module ActiveRecord sql end + def add_column_for_alter(table_name, column_name, type, options = {}) + return super unless options.key?(:comment) + [super, Proc.new { change_column_comment(table_name, column_name, options[:comment]) }] + end + def change_column_for_alter(table_name, column_name, type, options = {}) sqls = [change_column_sql(table_name, column_name, type, options)] sqls << change_column_default_for_alter(table_name, column_name, options[:default]) if options.key?(:default) @@ -708,7 +713,6 @@ module ActiveRecord sqls end - # Changes the default value of a table column. def change_column_default_for_alter(table_name, column_name, default_or_changes) # :nodoc: column = column_for(table_name, column_name) @@ -753,7 +757,7 @@ module ActiveRecord scope = quoted_scope(name, type: type) scope[:type] ||= "'r','v','m','p','f'" # (r)elation/table, (v)iew, (m)aterialized view, (p)artitioned table, (f)oreign table - sql = "SELECT c.relname FROM pg_class c LEFT JOIN pg_namespace n ON n.oid = c.relnamespace".dup + sql = +"SELECT c.relname FROM pg_class c LEFT JOIN pg_namespace n ON n.oid = c.relnamespace" sql << " WHERE n.nspname = #{scope[:schema]}" sql << " AND c.relname = #{scope[:name]}" if scope[:name] sql << " AND c.relkind IN (#{scope[:type]})" diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/type_metadata.rb b/activerecord/lib/active_record/connection_adapters/postgresql/type_metadata.rb index ffd3be26b0..cd69d28139 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql/type_metadata.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql/type_metadata.rb @@ -17,7 +17,7 @@ module ActiveRecord end def sql_type - super.gsub(/\[\]$/, "".freeze) + super.gsub(/\[\]$/, "") end def ==(other) diff --git a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb index fdf6f75108..bc6eb11572 100644 --- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb @@ -4,6 +4,14 @@ gem "pg", ">= 0.18", "< 2.0" require "pg" +# Use async_exec instead of exec_params on pg versions before 1.1 +class ::PG::Connection + unless self.public_method_defined?(:async_exec_params) + remove_method :exec_params + alias exec_params async_exec + end +end + require "active_record/connection_adapters/abstract_adapter" require "active_record/connection_adapters/statement_pool" require "active_record/connection_adapters/postgresql/column" @@ -70,7 +78,7 @@ module ActiveRecord # In addition, default connection parameters of libpq can be set per environment variables. # See https://www.postgresql.org/docs/current/static/libpq-envars.html . class PostgreSQLAdapter < AbstractAdapter - ADAPTER_NAME = "PostgreSQL".freeze + ADAPTER_NAME = "PostgreSQL" NATIVE_DATABASE_TYPES = { primary_key: "bigserial primary key", @@ -326,6 +334,10 @@ module ActiveRecord postgresql_version >= 90400 end + def supports_lazy_transactions? + true + end + def get_advisory_lock(lock_id) # :nodoc: unless lock_id.is_a?(Integer) && lock_id.bit_length <= 63 raise(ArgumentError, "PostgreSQL requires advisory lock ids to be a signed 64 bit integer") @@ -436,7 +448,7 @@ module ActiveRecord end end - def get_oid_type(oid, fmod, column_name, sql_type = "".freeze) + def get_oid_type(oid, fmod, column_name, sql_type = "") if !type_map.key?(oid) load_additional_types([oid]) end @@ -525,13 +537,13 @@ module ActiveRecord # Quoted types when /\A[\(B]?'(.*)'.*::"?([\w. ]+)"?(?:\[\])?\z/m # The default 'now'::date is CURRENT_DATE - if $1 == "now".freeze && $2 == "date".freeze + if $1 == "now" && $2 == "date" nil else - $1.gsub("''".freeze, "'".freeze) + $1.gsub("''", "'") end # Boolean types - when "true".freeze, "false".freeze + when "true", "false" default # Numeric types when /\A\(?(-?\d+(\.\d*)?)\)?(::bigint)?\z/ @@ -597,15 +609,19 @@ module ActiveRecord end def exec_no_cache(sql, name, binds) + materialize_transactions + type_casted_binds = type_casted_binds(binds) log(sql, name, binds, type_casted_binds) do ActiveSupport::Dependencies.interlock.permit_concurrent_loads do - @connection.async_exec(sql, type_casted_binds) + @connection.exec_params(sql, type_casted_binds) end end end def exec_cache(sql, name, binds) + materialize_transactions + stmt_key = prepare_statement(sql) type_casted_binds = type_casted_binds(binds) @@ -639,7 +655,7 @@ module ActiveRecord # # Check here for more details: # https://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/utils/cache/plancache.c#l573 - CACHED_PLAN_HEURISTIC = "cached plan must not change result type".freeze + CACHED_PLAN_HEURISTIC = "cached plan must not change result type" def is_cached_plan_failure?(e) pgerror = e.cause code = pgerror.result.result_error_field(PG::PG_DIAG_SQLSTATE) diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3/quoting.rb b/activerecord/lib/active_record/connection_adapters/sqlite3/quoting.rb index abedf01f10..b2dcdb5373 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3/quoting.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3/quoting.rb @@ -13,7 +13,7 @@ module ActiveRecord end def quote_column_name(name) - @quoted_column_names[name] ||= %Q("#{super.gsub('"', '""')}").freeze + @quoted_column_names[name] ||= %Q("#{super.gsub('"', '""')}") end def quoted_time(value) @@ -26,19 +26,19 @@ module ActiveRecord end def quoted_true - ActiveRecord::ConnectionAdapters::SQLite3Adapter.represent_boolean_as_integer ? "1".freeze : "'t'".freeze + ActiveRecord::ConnectionAdapters::SQLite3Adapter.represent_boolean_as_integer ? "1" : "'t'" end def unquoted_true - ActiveRecord::ConnectionAdapters::SQLite3Adapter.represent_boolean_as_integer ? 1 : "t".freeze + ActiveRecord::ConnectionAdapters::SQLite3Adapter.represent_boolean_as_integer ? 1 : "t" end def quoted_false - ActiveRecord::ConnectionAdapters::SQLite3Adapter.represent_boolean_as_integer ? "0".freeze : "'f'".freeze + ActiveRecord::ConnectionAdapters::SQLite3Adapter.represent_boolean_as_integer ? "0" : "'f'" end def unquoted_false - ActiveRecord::ConnectionAdapters::SQLite3Adapter.represent_boolean_as_integer ? 0 : "f".freeze + ActiveRecord::ConnectionAdapters::SQLite3Adapter.represent_boolean_as_integer ? 0 : "f" end private diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3/schema_statements.rb b/activerecord/lib/active_record/connection_adapters/sqlite3/schema_statements.rb index 24e7bc65fa..48277f0ae2 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3/schema_statements.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3/schema_statements.rb @@ -21,19 +21,24 @@ module ActiveRecord WHERE name = #{quote(row['name'])} AND type = 'index' SQL - /\sWHERE\s+(?<where>.+)$/i =~ index_sql + /\bON\b\s*"?(\w+?)"?\s*\((?<expressions>.+?)\)(?:\s*WHERE\b\s*(?<where>.+))?\z/i =~ index_sql columns = exec_query("PRAGMA index_info(#{quote(row['name'])})", "SCHEMA").map do |col| col["name"] end - # Add info on sort order for columns (only desc order is explicitly specified, asc is - # the default) orders = {} - if index_sql # index_sql can be null in case of primary key indexes - index_sql.scan(/"(\w+)" DESC/).flatten.each { |order_column| - orders[order_column] = :desc - } + + if columns.any?(&:nil?) # index created with an expression + columns = expressions + else + # Add info on sort order for columns (only desc order is explicitly specified, + # asc is the default) + if index_sql # index_sql can be null in case of primary key indexes + index_sql.scan(/"(\w+)" DESC/).flatten.each { |order_column| + orders[order_column] = :desc + } + end end IndexDefinition.new( @@ -81,7 +86,7 @@ module ActiveRecord scope = quoted_scope(name, type: type) scope[:type] ||= "'table','view'" - sql = "SELECT name FROM sqlite_master WHERE name <> 'sqlite_sequence'".dup + sql = +"SELECT name FROM sqlite_master WHERE name <> 'sqlite_sequence'" sql << " AND name = #{scope[:name]}" if scope[:name] sql << " AND type IN (#{scope[:type]})" sql diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb index bee74dc33d..81882f6cc1 100644 --- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb +++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb @@ -56,7 +56,7 @@ module ActiveRecord # # * <tt>:database</tt> - Path to the database file. class SQLite3Adapter < AbstractAdapter - ADAPTER_NAME = "SQLite".freeze + ADAPTER_NAME = "SQLite" include SQLite3::Quoting include SQLite3::SchemaStatements @@ -125,6 +125,10 @@ module ActiveRecord true end + def supports_expression_index? + sqlite_version >= "3.9.0" + end + def requires_reloading? true end @@ -186,6 +190,10 @@ module ActiveRecord true end + def supports_lazy_transactions? + true + end + # REFERENTIAL INTEGRITY ==================================== def disable_referential_integrity # :nodoc: @@ -212,6 +220,8 @@ module ActiveRecord end def exec_query(sql, name = nil, binds = [], prepare: false) + materialize_transactions + type_casted_binds = type_casted_binds(binds) log(sql, name, binds, type_casted_binds) do @@ -252,6 +262,8 @@ module ActiveRecord end def execute(sql, name = nil) #:nodoc: + materialize_transactions + log(sql, name) do ActiveSupport::Dependencies.interlock.permit_concurrent_loads do @connection.execute(sql) @@ -409,12 +421,23 @@ module ActiveRecord def alter_table(table_name, options = {}) altered_table_name = "a#{table_name}" - caller = lambda { |definition| yield definition if block_given? } + foreign_keys = foreign_keys(table_name) + + caller = lambda do |definition| + rename = options[:rename] || {} + foreign_keys.each do |fk| + if column = rename[fk.options[:column]] + fk.options[:column] = column + end + definition.foreign_key(fk.to_table, fk.options) + end + + yield definition if block_given? + end transaction do disable_referential_integrity do - move_table(table_name, altered_table_name, - options.merge(temporary: true)) + move_table(table_name, altered_table_name, options.merge(temporary: true)) move_table(altered_table_name, table_name, &caller) end end @@ -446,6 +469,7 @@ module ActiveRecord primary_key: column_name == from_primary_key ) end + yield @definition if block_given? end copy_table_indexes(from, to, options[:rename] || {}) @@ -463,9 +487,12 @@ module ActiveRecord name = name[1..-1] end - to_column_names = columns(to).map(&:name) - columns = index.columns.map { |c| rename[c] || c }.select do |column| - to_column_names.include?(column) + columns = index.columns + if columns.is_a?(Array) + to_column_names = columns(to).map(&:name) + columns = columns.map { |c| rename[c] || c }.select do |column| + to_column_names.include?(column) + end end unless columns.empty? @@ -549,7 +576,7 @@ module ActiveRecord column end else - basic_structure.to_hash + basic_structure.to_a end end diff --git a/activerecord/lib/active_record/connection_handling.rb b/activerecord/lib/active_record/connection_handling.rb index ee0e651912..18114f9e1c 100644 --- a/activerecord/lib/active_record/connection_handling.rb +++ b/activerecord/lib/active_record/connection_handling.rb @@ -46,45 +46,18 @@ module ActiveRecord # # The exceptions AdapterNotSpecified, AdapterNotFound and +ArgumentError+ # may be returned on an error. - def establish_connection(config = nil) + def establish_connection(config_or_env = nil) raise "Anonymous class is not allowed." unless name - config ||= DEFAULT_ENV.call.to_sym - spec_name = self == Base ? "primary" : name - self.connection_specification_name = spec_name + config_or_env ||= DEFAULT_ENV.call.to_sym + pool_name = self == Base ? "primary" : name + self.connection_specification_name = pool_name resolver = ConnectionAdapters::ConnectionSpecification::Resolver.new(Base.configurations) - spec = resolver.resolve(config).symbolize_keys - spec[:name] = spec_name + config_hash = resolver.resolve(config_or_env, pool_name).symbolize_keys + config_hash[:name] = pool_name - # use the primary config if a config is not passed in and - # it's a three tier config - spec = spec[spec_name.to_sym] if spec[spec_name.to_sym] - - connection_handler.establish_connection(spec) - end - - class MergeAndResolveDefaultUrlConfig # :nodoc: - def initialize(raw_configurations) - @raw_config = raw_configurations.dup - @env = DEFAULT_ENV.call.to_s - end - - # Returns fully resolved connection hashes. - # Merges connection information from `ENV['DATABASE_URL']` if available. - def resolve - ConnectionAdapters::ConnectionSpecification::Resolver.new(config).resolve_all - end - - private - def config - @raw_config.dup.tap do |cfg| - if url = ENV["DATABASE_URL"] - cfg[@env] ||= {} - cfg[@env]["url"] ||= url - end - end - end + connection_handler.establish_connection(config_hash) end # Returns the connection currently associated with the class. This can diff --git a/activerecord/lib/active_record/core.rb b/activerecord/lib/active_record/core.rb index c983bc0d93..392602bc0f 100644 --- a/activerecord/lib/active_record/core.rb +++ b/activerecord/lib/active_record/core.rb @@ -3,11 +3,14 @@ require "active_support/core_ext/hash/indifferent_access" require "active_support/core_ext/string/filters" require "concurrent/map" +require "set" module ActiveRecord module Core extend ActiveSupport::Concern + FILTERED = "[FILTERED]" # :nodoc: + included do ## # :singleton-method: @@ -26,7 +29,7 @@ module ActiveRecord ## # Contains the database configuration - as is typically stored in config/database.yml - - # as a Hash. + # as an ActiveRecord::DatabaseConfigurations object. # # For example, the following database.yml... # @@ -40,22 +43,18 @@ module ActiveRecord # # ...would result in ActiveRecord::Base.configurations to look like this: # - # { - # 'development' => { - # 'adapter' => 'sqlite3', - # 'database' => 'db/development.sqlite3' - # }, - # 'production' => { - # 'adapter' => 'sqlite3', - # 'database' => 'db/production.sqlite3' - # } - # } + # #<ActiveRecord::DatabaseConfigurations:0x00007fd1acbdf800 @configurations=[ + # #<ActiveRecord::DatabaseConfigurations::HashConfig:0x00007fd1acbded10 @env_name="development", + # @spec_name="primary", @config={"adapter"=>"sqlite3", "database"=>"db/development.sqlite3"}>, + # #<ActiveRecord::DatabaseConfigurations::HashConfig:0x00007fd1acbdea90 @env_name="production", + # @spec_name="primary", @config={"adapter"=>"mysql2", "database"=>"db/production.sqlite3"}> + # ]> def self.configurations=(config) - @@configurations = ActiveRecord::ConnectionHandling::MergeAndResolveDefaultUrlConfig.new(config).resolve + @@configurations = ActiveRecord::DatabaseConfigurations.new(config) end self.configurations = {} - # Returns fully resolved configurations hash + # Returns fully resolved ActiveRecord::DatabaseConfigurations object def self.configurations @@configurations end @@ -127,6 +126,8 @@ module ActiveRecord class_attribute :default_connection_handler, instance_writer: false + self.filter_attributes = [] + def self.connection_handler ActiveRecord::RuntimeRegistry.connection_handler || default_connection_handler end @@ -138,7 +139,7 @@ module ActiveRecord self.default_connection_handler = ConnectionAdapters::ConnectionHandler.new end - module ClassMethods # :nodoc: + module ClassMethods def initialize_find_by_cache # :nodoc: @find_by_statement_cache = { true => Concurrent::Map.new, false => Concurrent::Map.new } end @@ -215,7 +216,7 @@ module ActiveRecord generated_association_methods end - def generated_association_methods + def generated_association_methods # :nodoc: @generated_association_methods ||= begin mod = const_set(:GeneratedAssociationMethods, Module.new) private_constant :GeneratedAssociationMethods @@ -225,8 +226,22 @@ module ActiveRecord end end + # Returns columns which shouldn't be exposed while calling +#inspect+. + def filter_attributes + if defined?(@filter_attributes) + @filter_attributes + else + superclass.filter_attributes + end + end + + # Specifies columns which shouldn't be exposed while calling +#inspect+. + def filter_attributes=(attributes_names) + @filter_attributes = attributes_names.map(&:to_s).to_set + end + # Returns a string like 'Post(id:integer, title:string, body:text)' - def inspect + def inspect # :nodoc: if self == Base super elsif abstract_class? @@ -242,7 +257,7 @@ module ActiveRecord end # Overwrite the default class equality method to provide support for decorated models. - def ===(object) + def ===(object) # :nodoc: object.is_a?(self) end @@ -496,7 +511,11 @@ module ActiveRecord inspection = if defined?(@attributes) && @attributes self.class.attribute_names.collect do |name| if has_attribute?(name) - "#{name}: #{attribute_for_inspect(name)}" + if filter_attribute?(name) + "#{name}: #{ActiveRecord::Core::FILTERED}" + else + "#{name}: #{attribute_for_inspect(name)}" + end end end.compact.join(", ") else @@ -514,13 +533,16 @@ module ActiveRecord if defined?(@attributes) && @attributes column_names = self.class.column_names.select { |name| has_attribute?(name) || new_record? } pp.seplist(column_names, proc { pp.text "," }) do |column_name| - column_value = read_attribute(column_name) pp.breakable " " pp.group(1) do pp.text column_name pp.text ":" pp.breakable - pp.pp column_value + if filter_attribute?(column_name) + pp.text ActiveRecord::Core::FILTERED + else + pp.pp read_attribute(column_name) + end end end else @@ -571,5 +593,9 @@ module ActiveRecord def custom_inspect_method_defined? self.class.instance_method(:inspect).owner != ActiveRecord::Base.instance_method(:inspect).owner end + + def filter_attribute?(attribute_name) + self.class.filter_attributes.include?(attribute_name) && !read_attribute(attribute_name).nil? + end end end diff --git a/activerecord/lib/active_record/counter_cache.rb b/activerecord/lib/active_record/counter_cache.rb index c7f0077a76..27c1b7a311 100644 --- a/activerecord/lib/active_record/counter_cache.rb +++ b/activerecord/lib/active_record/counter_cache.rb @@ -159,14 +159,11 @@ module ActiveRecord end private - - def _create_record(*) + def _create_record(attribute_names = self.attribute_names) id = super each_counter_cached_associations do |association| - if send(association.reflection.name) - association.increment_counters - end + association.increment_counters end id @@ -179,9 +176,7 @@ module ActiveRecord each_counter_cached_associations do |association| foreign_key = association.reflection.foreign_key.to_sym unless destroyed_by_association && destroyed_by_association.foreign_key.to_sym == foreign_key - if send(association.reflection.name) - association.decrement_counters - end + association.decrement_counters end end end diff --git a/activerecord/lib/active_record/database_configurations.rb b/activerecord/lib/active_record/database_configurations.rb index ffeed45030..fa1589511e 100644 --- a/activerecord/lib/active_record/database_configurations.rb +++ b/activerecord/lib/active_record/database_configurations.rb @@ -1,63 +1,186 @@ # frozen_string_literal: true +require "active_record/database_configurations/database_config" +require "active_record/database_configurations/hash_config" +require "active_record/database_configurations/url_config" + module ActiveRecord - module DatabaseConfigurations # :nodoc: - class DatabaseConfig - attr_reader :env_name, :spec_name, :config + # ActiveRecord::DatabaseConfigurations returns an array of DatabaseConfig + # objects (either a HashConfig or UrlConfig) that are constructed from the + # application's database configuration hash or url string. + class DatabaseConfigurations + attr_reader :configurations + delegate :any?, to: :configurations + + def initialize(configurations = {}) + @configurations = build_configs(configurations) + end - def initialize(env_name, spec_name, config) - @env_name = env_name - @spec_name = spec_name - @config = config + # Collects the configs for the environment and optionally the specification + # name passed in. To include replica configurations pass `include_replicas: true`. + # + # If a spec name is provided a single DatabaseConfig object will be + # returned, otherwise an array of DatabaseConfig objects will be + # returned that corresponds with the environment and type requested. + # + # Options: + # + # <tt>env_name:</tt> The environment name. Defaults to nil which will collect + # configs for all environments. + # <tt>spec_name:</tt> The specification name (ie primary, animals, etc.). Defaults + # to +nil+. + # <tt>include_replicas:</tt> Determines whether to include replicas in the + # the returned list. Most of the time we're only iterating over the write + # connection (i.e. migrations don't need to run for the write and read connection). + # Defaults to +false+. + def configs_for(env_name: nil, spec_name: nil, include_replicas: false) + configs = env_with_configs(env_name) + + unless include_replicas + configs = configs.select do |db_config| + !db_config.replica? + end + end + + if spec_name + configs.find do |db_config| + db_config.spec_name == spec_name + end + else + configs end end - # Selects the config for the specified environment and specification name + # Returns the config hash that corresponds with the environment + # + # If the application has multiple databases `default_hash` will + # return the first config hash for the environment. + # + # { database: "my_db", adapter: "mysql2" } + def default_hash(env = ActiveRecord::ConnectionHandling::DEFAULT_ENV.call.to_s) + default = find_db_config(env) + default.config if default + end + alias :[] :default_hash + + # Returns a single DatabaseConfig object based on the requested environment. # - # For example if passed :development, and :animals it will select the database - # under the :development and :animals configuration level - def self.config_for_env_and_spec(environment, specification_name, configs = ActiveRecord::Base.configurations) # :nodoc: - configs_for(environment, configs).find do |db_config| - db_config.spec_name == specification_name + # If the application has multiple databases `find_db_config` will return + # the first DatabaseConfig for the environment. + def find_db_config(env) + configurations.find do |db_config| + db_config.env_name == env.to_s || + (db_config.for_current_env? && db_config.spec_name == env.to_s) end end - # Collects the configs for the environment passed in. + # Returns the DatabaseConfigurations object as a Hash. + def to_h + configs = configurations.reverse.inject({}) do |memo, db_config| + memo.merge(db_config.to_legacy_hash) + end + + Hash[configs.to_a.reverse] + end + + # Checks if the application's configurations are empty. # - # If a block is given returns the specification name and configuration - # otherwise returns an array of DatabaseConfig structs for the environment. - def self.configs_for(env, configs = ActiveRecord::Base.configurations, &blk) # :nodoc: - env_with_configs = db_configs(configs).select do |db_config| - db_config.env_name == env + # Aliased to blank? + def empty? + configurations.empty? + end + alias :blank? :empty? + + private + def env_with_configs(env = nil) + if env + configurations.select { |db_config| db_config.env_name == env } + else + configurations + end end - if block_given? - env_with_configs.each do |env_with_config| - yield env_with_config.spec_name, env_with_config.config + def build_configs(configs) + return configs.configurations if configs.is_a?(DatabaseConfigurations) + + build_db_config = configs.each_pair.flat_map do |env_name, config| + walk_configs(env_name.to_s, "primary", config) + end.compact + + if url = ENV["DATABASE_URL"] + build_url_config(url, build_db_config) + else + build_db_config end - else - env_with_configs end - end - # Given an env, spec and config creates DatabaseConfig structs with - # each attribute set. - def self.walk_configs(env_name, spec_name, config) # :nodoc: - if config["database"] || config["url"] || config["adapter"] - DatabaseConfig.new(env_name, spec_name, config) - else - config.each_pair.map do |sub_spec_name, sub_config| - walk_configs(env_name, sub_spec_name, sub_config) + def walk_configs(env_name, spec_name, config) + case config + when String + build_db_config_from_string(env_name, spec_name, config) + when Hash + build_db_config_from_hash(env_name, spec_name, config.stringify_keys) end end - end - # Walks all the configs passed in and returns an array - # of DatabaseConfig structs for each configuration. - def self.db_configs(configs = ActiveRecord::Base.configurations) # :nodoc: - configs.each_pair.flat_map do |env_name, config| - walk_configs(env_name, "primary", config) + def build_db_config_from_string(env_name, spec_name, config) + begin + url = config + uri = URI.parse(url) + if uri.try(:scheme) + ActiveRecord::DatabaseConfigurations::UrlConfig.new(env_name, spec_name, url) + end + rescue URI::InvalidURIError + ActiveRecord::DatabaseConfigurations::HashConfig.new(env_name, spec_name, config) + end + end + + def build_db_config_from_hash(env_name, spec_name, config) + if url = config["url"] + config_without_url = config.dup + config_without_url.delete "url" + ActiveRecord::DatabaseConfigurations::UrlConfig.new(env_name, spec_name, url, config_without_url) + elsif config["database"] || (config.size == 1 && config.values.all? { |v| v.is_a? String }) + ActiveRecord::DatabaseConfigurations::HashConfig.new(env_name, spec_name, config) + else + config.each_pair.map do |sub_spec_name, sub_config| + walk_configs(env_name, sub_spec_name, sub_config) + end + end + end + + def build_url_config(url, configs) + env = ActiveRecord::ConnectionHandling::DEFAULT_ENV.call.to_s + + if original_config = configs.find(&:for_current_env?) + if original_config.url_config? + configs + else + configs.map do |config| + ActiveRecord::DatabaseConfigurations::UrlConfig.new(env, config.spec_name, url, config.config) + end + end + else + configs + [ActiveRecord::DatabaseConfigurations::UrlConfig.new(env, "primary", url)] + end + end + + def method_missing(method, *args, &blk) + if Hash.method_defined?(method) + ActiveSupport::Deprecation.warn \ + "Returning a hash from ActiveRecord::Base.configurations is deprecated. Therefore calling `#{method}` on the hash is also deprecated. Please switch to using the `configs_for` method instead to collect and iterate over database configurations." + end + + case method + when :each, :first + configurations.send(method, *args, &blk) + when :fetch + configs_for(env_name: args.first) + when :values + configurations.map(&:config) + else + super + end end - end end end diff --git a/activerecord/lib/active_record/database_configurations/database_config.rb b/activerecord/lib/active_record/database_configurations/database_config.rb new file mode 100644 index 0000000000..adc37cc439 --- /dev/null +++ b/activerecord/lib/active_record/database_configurations/database_config.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +module ActiveRecord + class DatabaseConfigurations + # ActiveRecord::Base.configurations will return either a HashConfig or + # UrlConfig respectively. It will never return a DatabaseConfig object, + # as this is the parent class for the types of database configuration objects. + class DatabaseConfig # :nodoc: + attr_reader :env_name, :spec_name + + def initialize(env_name, spec_name) + @env_name = env_name + @spec_name = spec_name + end + + def replica? + raise NotImplementedError + end + + def migrations_paths + raise NotImplementedError + end + + def url_config? + false + end + + def to_legacy_hash + { env_name => config } + end + + def for_current_env? + env_name == ActiveRecord::ConnectionHandling::DEFAULT_ENV.call + end + end + end +end diff --git a/activerecord/lib/active_record/database_configurations/hash_config.rb b/activerecord/lib/active_record/database_configurations/hash_config.rb new file mode 100644 index 0000000000..c176a62458 --- /dev/null +++ b/activerecord/lib/active_record/database_configurations/hash_config.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +module ActiveRecord + class DatabaseConfigurations + # A HashConfig object is created for each database configuration entry that + # is created from a hash. + # + # A hash config: + # + # { "development" => { "database" => "db_name" } } + # + # Becomes: + # + # #<ActiveRecord::DatabaseConfigurations::HashConfig:0x00007fd1acbded10 + # @env_name="development", @spec_name="primary", @config={"database"=>"db_name"}> + # + # Options are: + # + # <tt>:env_name</tt> - The Rails environment, ie "development" + # <tt>:spec_name</tt> - The specification name. In a standard two-tier + # database configuration this will default to "primary". In a multiple + # database three-tier database configuration this corresponds to the name + # used in the second tier, for example "primary_readonly". + # <tt>:config</tt> - The config hash. This is the hash that contains the + # database adapter, name, and other important information for database + # connections. + class HashConfig < DatabaseConfig + attr_reader :config + + def initialize(env_name, spec_name, config) + super(env_name, spec_name) + @config = config + end + + # Determines whether a database configuration is for a replica / readonly + # connection. If the `replica` key is present in the config, `replica?` will + # return +true+. + def replica? + config["replica"] + end + + # The migrations paths for a database configuration. If the + # `migrations_paths` key is present in the config, `migrations_paths` + # will return its value. + def migrations_paths + config["migrations_paths"] + end + end + end +end diff --git a/activerecord/lib/active_record/database_configurations/url_config.rb b/activerecord/lib/active_record/database_configurations/url_config.rb new file mode 100644 index 0000000000..81917fc4c1 --- /dev/null +++ b/activerecord/lib/active_record/database_configurations/url_config.rb @@ -0,0 +1,74 @@ +# frozen_string_literal: true + +module ActiveRecord + class DatabaseConfigurations + # A UrlConfig object is created for each database configuration + # entry that is created from a URL. This can either be a URL string + # or a hash with a URL in place of the config hash. + # + # A URL config: + # + # postgres://localhost/foo + # + # Becomes: + # + # #<ActiveRecord::DatabaseConfigurations::UrlConfig:0x00007fdc3238f340 + # @env_name="default_env", @spec_name="primary", + # @config={"adapter"=>"postgresql", "database"=>"foo", "host"=>"localhost"}, + # @url="postgres://localhost/foo"> + # + # Options are: + # + # <tt>:env_name</tt> - The Rails environment, ie "development" + # <tt>:spec_name</tt> - The specification name. In a standard two-tier + # database configuration this will default to "primary". In a multiple + # database three-tier database configuration this corresponds to the name + # used in the second tier, for example "primary_readonly". + # <tt>:url</tt> - The database URL. + # <tt>:config</tt> - The config hash. This is the hash that contains the + # database adapter, name, and other important information for database + # connections. + class UrlConfig < DatabaseConfig + attr_reader :url, :config + + def initialize(env_name, spec_name, url, config = {}) + super(env_name, spec_name) + @config = build_config(config, url) + @url = url + end + + def url_config? # :nodoc: + true + end + + # Determines whether a database configuration is for a replica / readonly + # connection. If the `replica` key is present in the config, `replica?` will + # return +true+. + def replica? + config["replica"] + end + + # The migrations paths for a database configuration. If the + # `migrations_paths` key is present in the config, `migrations_paths` + # will return its value. + def migrations_paths + config["migrations_paths"] + end + + private + def build_config(original_config, url) + if /^jdbc:/.match?(url) + hash = { "url" => url } + else + hash = ActiveRecord::ConnectionAdapters::ConnectionSpecification::ConnectionUrlResolver.new(url).to_hash + end + + if original_config[env_name] + original_config[env_name].merge(hash) + else + original_config.merge(hash) + end + end + end + end +end diff --git a/activerecord/lib/active_record/explain.rb b/activerecord/lib/active_record/explain.rb index 7ccb938888..919e96cd7a 100644 --- a/activerecord/lib/active_record/explain.rb +++ b/activerecord/lib/active_record/explain.rb @@ -18,7 +18,7 @@ module ActiveRecord # Returns a formatted string ready to be logged. def exec_explain(queries) # :nodoc: str = queries.map do |sql, binds| - msg = "EXPLAIN for: #{sql}".dup + msg = +"EXPLAIN for: #{sql}" unless binds.empty? msg << " " msg << binds.map { |attr| render_bind(attr) }.inspect diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb index 6e0f1a0dfb..0d1fdcfb28 100644 --- a/activerecord/lib/active_record/fixtures.rb +++ b/activerecord/lib/active_record/fixtures.rb @@ -892,6 +892,7 @@ module ActiveRecord def fixtures(*fixture_set_names) if fixture_set_names.first == :all + raise StandardError, "No fixture path found. Please set `#{self}.fixture_path`." if fixture_path.blank? fixture_set_names = Dir["#{fixture_path}/{**,*}/*.{yml}"].uniq fixture_set_names.map! { |f| f[(fixture_path.to_s.size + 1)..-5] } else diff --git a/activerecord/lib/active_record/inheritance.rb b/activerecord/lib/active_record/inheritance.rb index 6891c575c7..138fd1cf53 100644 --- a/activerecord/lib/active_record/inheritance.rb +++ b/activerecord/lib/active_record/inheritance.rb @@ -55,6 +55,10 @@ module ActiveRecord if has_attribute?(inheritance_column) subclass = subclass_from_attributes(attributes) + if subclass.nil? && scope_attributes = current_scope&.scope_for_create + subclass = subclass_from_attributes(scope_attributes) + end + if subclass.nil? && base_class? subclass = subclass_from_attributes(column_defaults) end @@ -176,7 +180,7 @@ module ActiveRecord # Returns the class type of the record using the current module as a prefix. So descendants of # MyApp::Business::Account would appear as MyApp::Business::AccountSubclass. def compute_type(type_name) - if type_name.start_with?("::".freeze) + if type_name.start_with?("::") # If the type is prefixed with a scope operator then we assume that # the type_name is an absolute reference. ActiveSupport::Dependencies.constantize(type_name) diff --git a/activerecord/lib/active_record/locking/optimistic.rb b/activerecord/lib/active_record/locking/optimistic.rb index 7f096bb532..4a3a31fc95 100644 --- a/activerecord/lib/active_record/locking/optimistic.rb +++ b/activerecord/lib/active_record/locking/optimistic.rb @@ -61,7 +61,7 @@ module ActiveRecord end private - def _create_record(attribute_names = self.attribute_names, *) + def _create_record(attribute_names = self.attribute_names) if locking_enabled? # We always want to persist the locking version, even if we don't detect # a change from the default, since the database might have no default @@ -165,7 +165,7 @@ module ActiveRecord def inherited(subclass) subclass.class_eval do is_lock_column = ->(name, _) { lock_optimistically && name == locking_column } - decorate_matching_attribute_types(is_lock_column, :_optimistic_locking) do |type| + decorate_matching_attribute_types(is_lock_column, "_optimistic_locking") do |type| LockingType.new(type) end end diff --git a/activerecord/lib/active_record/log_subscriber.rb b/activerecord/lib/active_record/log_subscriber.rb index 1ae6840921..6b84431343 100644 --- a/activerecord/lib/active_record/log_subscriber.rb +++ b/activerecord/lib/active_record/log_subscriber.rb @@ -4,6 +4,8 @@ module ActiveRecord class LogSubscriber < ActiveSupport::LogSubscriber IGNORE_PAYLOAD_NAMES = ["SCHEMA", "EXPLAIN"] + class_attribute :backtrace_cleaner, default: ActiveSupport::BacktraceCleaner.new + def self.runtime=(value) ActiveRecord::RuntimeRegistry.sql_runtime = value end @@ -100,21 +102,15 @@ module ActiveRecord end def log_query_source - location = extract_query_source_location(caller_locations) - - if location - source = "#{location.path}:#{location.lineno}" - source = source.sub("#{::Rails.root}/", "") if defined?(::Rails.root) + source = extract_query_source_location(caller) + if source logger.debug(" ↳ #{source}") end end - RAILS_GEM_ROOT = File.expand_path("../../..", __dir__) + "/" - PATHS_TO_IGNORE = /\A(#{RAILS_GEM_ROOT}|#{RbConfig::CONFIG["rubylibdir"]})/ - def extract_query_source_location(locations) - locations.find { |line| line.absolute_path && !line.absolute_path.match?(PATHS_TO_IGNORE) } + backtrace_cleaner.clean(locations).first end end end diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb index d68ed3cad9..6e5a610642 100644 --- a/activerecord/lib/active_record/migration.rb +++ b/activerecord/lib/active_record/migration.rb @@ -140,8 +140,8 @@ module ActiveRecord end class ConcurrentMigrationError < MigrationError #:nodoc: - DEFAULT_MESSAGE = "Cannot run migrations because another migration process is currently running.".freeze - RELEASE_LOCK_FAILED_MESSAGE = "Failed to release advisory lock".freeze + DEFAULT_MESSAGE = "Cannot run migrations because another migration process is currently running." + RELEASE_LOCK_FAILED_MESSAGE = "Failed to release advisory lock" def initialize(message = DEFAULT_MESSAGE) super @@ -161,7 +161,7 @@ module ActiveRecord class ProtectedEnvironmentError < ActiveRecordError #:nodoc: def initialize(env = "production") - msg = "You are attempting to run a destructive action against your '#{env}' database.\n".dup + msg = +"You are attempting to run a destructive action against your '#{env}' database.\n" msg << "If you are sure you want to continue, run the same command with the environment variable:\n" msg << "DISABLE_DATABASE_ENVIRONMENT_CHECK=1" super(msg) @@ -170,7 +170,7 @@ module ActiveRecord class EnvironmentMismatchError < ActiveRecordError def initialize(current: nil, stored: nil) - msg = "You are attempting to modify a database that was last run in `#{ stored }` environment.\n".dup + msg = +"You are attempting to modify a database that was last run in `#{ stored }` environment.\n" msg << "You are running in `#{ current }` environment. " msg << "If you are sure you want to continue, first set the environment using:\n\n" msg << " rails db:environment:set" @@ -678,15 +678,13 @@ module ActiveRecord if connection.respond_to? :revert connection.revert { yield } else - recorder = CommandRecorder.new(connection) + recorder = command_recorder @connection = recorder suppress_messages do connection.revert { yield } end @connection = recorder.delegate - recorder.commands.each do |cmd, args, block| - send(cmd, *args, &block) - end + recorder.replay(self) end end end @@ -891,7 +889,7 @@ module ActiveRecord source_migrations.each do |migration| source = File.binread(migration.filename) inserted_comment = "# This migration comes from #{scope} (originally #{migration.version})\n" - magic_comments = "".dup + magic_comments = +"" 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) @@ -962,6 +960,10 @@ module ActiveRecord yield end end + + def command_recorder + CommandRecorder.new(connection) + end end # MigrationProxy is used to defer loading of the actual migration classes @@ -1169,7 +1171,7 @@ module ActiveRecord def migrations_path=(path) ActiveSupport::Deprecation.warn \ - "ActiveRecord::Migrator.migrations_paths= is now deprecated and will be removed in Rails 6.0." \ + "`ActiveRecord::Migrator.migrations_path=` is now deprecated and will be removed in Rails 6.0. " \ "You can set the `migrations_paths` on the `connection` instead through the `database.yml`." self.migrations_paths = [path] end @@ -1299,7 +1301,7 @@ module ActiveRecord record_version_state_after_migrating(migration.version) end rescue => e - msg = "An error has occurred, ".dup + msg = +"An error has occurred, " msg << "this and " if use_transaction?(migration) msg << "all later migrations canceled:\n\n#{e}" raise StandardError, msg, e.backtrace @@ -1357,7 +1359,7 @@ module ActiveRecord end def use_advisory_lock? - Base.connection.supports_advisory_locks? + Base.connection.advisory_locks_enabled? end def with_advisory_lock diff --git a/activerecord/lib/active_record/migration/command_recorder.rb b/activerecord/lib/active_record/migration/command_recorder.rb index 087632b10f..82f5121d94 100644 --- a/activerecord/lib/active_record/migration/command_recorder.rb +++ b/activerecord/lib/active_record/migration/command_recorder.rb @@ -108,11 +108,17 @@ module ActiveRecord yield delegate.update_table_definition(table_name, self) end + def replay(migration) + commands.each do |cmd, args, block| + migration.send(cmd, *args, &block) + end + end + private module StraightReversions # :nodoc: private - { transaction: :transaction, + { execute_block: :execute_block, create_table: :drop_table, create_join_table: :drop_join_table, @@ -133,6 +139,17 @@ module ActiveRecord include StraightReversions + def invert_transaction(args) + sub_recorder = CommandRecorder.new(delegate) + sub_recorder.revert { yield } + + invertions_proc = proc { + sub_recorder.replay(self) + } + + [:transaction, args, invertions_proc] + end + def invert_drop_table(args, &block) if args.size == 1 && block == nil raise ActiveRecord::IrreversibleMigration, "To avoid mistakes, drop_table is only reversible if given options or a block (can be empty)." @@ -214,11 +231,24 @@ module ActiveRecord end def invert_remove_foreign_key(args) - from_table, to_table, remove_options = args - raise ActiveRecord::IrreversibleMigration, "remove_foreign_key is only reversible if given a second table" if to_table.nil? || to_table.is_a?(Hash) + from_table, options_or_to_table, options_or_nil = args + + to_table = if options_or_to_table.is_a?(Hash) + options_or_to_table[:to_table] + else + options_or_to_table + end + + remove_options = if options_or_to_table.is_a?(Hash) + options_or_to_table.except(:to_table) + else + options_or_nil + end + + raise ActiveRecord::IrreversibleMigration, "remove_foreign_key is only reversible if given a second table" if to_table.nil? reversed_args = [from_table, to_table] - reversed_args << remove_options if remove_options + reversed_args << remove_options if remove_options.present? [:add_foreign_key, reversed_args] end diff --git a/activerecord/lib/active_record/migration/compatibility.rb b/activerecord/lib/active_record/migration/compatibility.rb index 0edaaa0cf9..8f6fcfcaea 100644 --- a/activerecord/lib/active_record/migration/compatibility.rb +++ b/activerecord/lib/active_record/migration/compatibility.rb @@ -16,6 +16,21 @@ module ActiveRecord V6_0 = Current class V5_2 < V6_0 + module CommandRecorder + def invert_transaction(args, &block) + [:transaction, args, block] + end + end + + private + + def command_recorder + recorder = super + class << recorder + prepend CommandRecorder + end + recorder + end end class V5_1 < V5_2 diff --git a/activerecord/lib/active_record/model_schema.rb b/activerecord/lib/active_record/model_schema.rb index 694ff85fa1..9b985e049b 100644 --- a/activerecord/lib/active_record/model_schema.rb +++ b/activerecord/lib/active_record/model_schema.rb @@ -375,7 +375,7 @@ module ActiveRecord # default values when instantiating the Active Record object for this table. def column_defaults load_schema - @column_defaults ||= _default_attributes.to_hash + @column_defaults ||= _default_attributes.deep_dup.to_hash end def _default_attributes # :nodoc: diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb index 05963e5546..6eb2bfb452 100644 --- a/activerecord/lib/active_record/persistence.rb +++ b/activerecord/lib/active_record/persistence.rb @@ -70,17 +70,6 @@ module ActiveRecord instantiate_instance_of(klass, attributes, column_types, &block) end - # Given a class, an attributes hash, +instantiate_instance_of+ returns a - # new instance of the class. Accepts only keys as strings. - # - # This is private, don't call it. :) - # - # :nodoc: - def instantiate_instance_of(klass, attributes, column_types = {}, &block) - attributes = klass.attributes_builder.build_from_database(attributes, column_types) - klass.allocate.init_from_db(attributes, &block) - end - # Updates an object (or multiple objects) and saves it to the database, if validations pass. # The resulting object is returned whether the object was saved successfully to the database or not. # @@ -216,6 +205,13 @@ module ActiveRecord end private + # Given a class, an attributes hash, +instantiate_instance_of+ returns a + # new instance of the class. Accepts only keys as strings. + def instantiate_instance_of(klass, attributes, column_types = {}, &block) + attributes = klass.attributes_builder.build_from_database(attributes, column_types) + klass.allocate.init_from_db(attributes, &block) + end + # Called by +instantiate+ to decide which class to use for a new # record instance. # @@ -718,7 +714,6 @@ module ActiveRecord # Updates the associated record with values matching those of the instance attributes. # Returns the number of affected rows. def _update_record(attribute_names = self.attribute_names) - attribute_names &= self.class.column_names attribute_names = attributes_for_update(attribute_names) if attribute_names.empty? @@ -737,10 +732,12 @@ module ActiveRecord # Creates a record with values matching those of the instance attributes # and returns its id. def _create_record(attribute_names = self.attribute_names) - attribute_names &= self.class.column_names - attributes_values = attributes_with_values_for_create(attribute_names) + attribute_names = attributes_for_create(attribute_names) + + new_id = self.class._insert_record( + attributes_with_values(attribute_names) + ) - new_id = self.class._insert_record(attributes_values) self.id ||= new_id if self.class.primary_key @new_record = false diff --git a/activerecord/lib/active_record/railtie.rb b/activerecord/lib/active_record/railtie.rb index 009f412234..81ad9ef3a2 100644 --- a/activerecord/lib/active_record/railtie.rb +++ b/activerecord/lib/active_record/railtie.rb @@ -77,6 +77,10 @@ module ActiveRecord ActiveSupport.on_load(:active_record) { self.logger ||= ::Rails.logger } end + initializer "active_record.backtrace_cleaner" do + ActiveSupport.on_load(:active_record) { LogSubscriber.backtrace_cleaner = ::Rails.backtrace_cleaner } + end + initializer "active_record.migration_error" do if config.active_record.delete(:migration_error) == :page_load config.app_middleware.insert_after ::ActionDispatch::Callbacks, @@ -84,6 +88,31 @@ module ActiveRecord end end + initializer "Check for cache versioning support" do + config.after_initialize do |app| + ActiveSupport.on_load(:active_record) do + if app.config.active_record.cache_versioning && Rails.cache + unless Rails.cache.class.try(:supports_cache_versioning?) + raise <<-end_error + +You're using a cache store that doesn't support native cache versioning. +Your best option is to upgrade to a newer version of #{Rails.cache.class} +that supports cache versioning (#{Rails.cache.class}.supports_cache_versioning? #=> true). + +Next best, switch to a different cache store that does support cache versioning: +https://guides.rubyonrails.org/caching_with_rails.html#cache-stores. + +To keep using the current cache store, you can turn off cache versioning entirely: + + config.active_record.cache_versioning = false + +end_error + end + end + end + end + end + initializer "active_record.check_schema_cache_dump" do if config.active_record.delete(:use_schema_cache_dump) config.after_initialize do |app| @@ -108,6 +137,14 @@ module ActiveRecord end end + initializer "active_record.define_attribute_methods" do |app| + config.after_initialize do + ActiveSupport.on_load(:active_record) do + descendants.each(&:define_attribute_methods) if app.config.eager_load + end + end + end + initializer "active_record.warn_on_records_fetched_greater_than" do if config.active_record.warn_on_records_fetched_greater_than ActiveSupport.on_load(:active_record) do @@ -176,9 +213,7 @@ end_warning end initializer "active_record.set_executor_hooks" do - ActiveSupport.on_load(:active_record) do - ActiveRecord::QueryCache.install_executor_hooks - end + ActiveRecord::QueryCache.install_executor_hooks end initializer "active_record.add_watchable_files" do |app| @@ -231,5 +266,11 @@ MSG end end end + + initializer "active_record.set_filter_attributes" do + ActiveSupport.on_load(:active_record) do + self.filter_attributes += Rails.application.config.filter_parameters + end + end end end diff --git a/activerecord/lib/active_record/railties/databases.rake b/activerecord/lib/active_record/railties/databases.rake index 8b7d18fb3d..be5ef350a9 100644 --- a/activerecord/lib/active_record/railties/databases.rake +++ b/activerecord/lib/active_record/railties/databases.rake @@ -26,7 +26,7 @@ db_namespace = namespace :db do ActiveRecord::Tasks::DatabaseTasks.for_each do |spec_name| desc "Create #{spec_name} database for current environment" task spec_name => :load_config do - db_config = ActiveRecord::DatabaseConfigurations.config_for_env_and_spec(Rails.env, spec_name) + db_config = ActiveRecord::Base.configurations.configs_for(env_name: Rails.env, spec_name: spec_name) ActiveRecord::Tasks::DatabaseTasks.create(db_config.config) end end @@ -45,7 +45,7 @@ db_namespace = namespace :db do ActiveRecord::Tasks::DatabaseTasks.for_each do |spec_name| desc "Drop #{spec_name} database for current environment" task spec_name => [:load_config, :check_protected_environments] do - db_config = ActiveRecord::DatabaseConfigurations.config_for_env_and_spec(Rails.env, spec_name) + db_config = ActiveRecord::Base.configurations.configs_for(env_name: Rails.env, spec_name: spec_name) ActiveRecord::Tasks::DatabaseTasks.drop(db_config.config) end end @@ -73,8 +73,8 @@ db_namespace = namespace :db do desc "Migrate the database (options: VERSION=x, VERBOSE=false, SCOPE=blog)." task migrate: :load_config do - ActiveRecord::DatabaseConfigurations.configs_for(Rails.env) do |spec_name, config| - ActiveRecord::Base.establish_connection(config) + ActiveRecord::Base.configurations.configs_for(env_name: Rails.env).each do |db_config| + ActiveRecord::Base.establish_connection(db_config.config) ActiveRecord::Tasks::DatabaseTasks.migrate end db_namespace["_dump"].invoke @@ -99,7 +99,7 @@ db_namespace = namespace :db do ActiveRecord::Tasks::DatabaseTasks.for_each do |spec_name| desc "Migrate #{spec_name} database for current environment" task spec_name => :load_config do - db_config = ActiveRecord::DatabaseConfigurations.config_for_env_and_spec(Rails.env, spec_name) + db_config = ActiveRecord::Base.configurations.configs_for(env_name: Rails.env, spec_name: spec_name) ActiveRecord::Base.establish_connection(db_config.config) ActiveRecord::Tasks::DatabaseTasks.migrate end @@ -274,11 +274,10 @@ db_namespace = namespace :db do desc "Creates a db/schema.rb file that is portable against any DB supported by Active Record" task dump: :load_config do require "active_record/schema_dumper" - - ActiveRecord::DatabaseConfigurations.configs_for(Rails.env) do |spec_name, config| - filename = ActiveRecord::Tasks::DatabaseTasks.dump_filename(spec_name, :ruby) + ActiveRecord::Base.configurations.configs_for(env_name: Rails.env).each do |db_config| + filename = ActiveRecord::Tasks::DatabaseTasks.dump_filename(db_config.spec_name, :ruby) File.open(filename, "w:utf-8") do |file| - ActiveRecord::Base.establish_connection(config) + ActiveRecord::Base.establish_connection(db_config.config) ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, file) end end @@ -314,11 +313,10 @@ db_namespace = namespace :db do namespace :structure do desc "Dumps the database structure to db/structure.sql. Specify another file with SCHEMA=db/my_structure.sql" task dump: :load_config do - ActiveRecord::DatabaseConfigurations.configs_for(Rails.env) do |spec_name, config| - ActiveRecord::Base.establish_connection(config) - filename = ActiveRecord::Tasks::DatabaseTasks.dump_filename(spec_name, :sql) - ActiveRecord::Tasks::DatabaseTasks.structure_dump(config, filename) - + ActiveRecord::Base.configurations.configs_for(env_name: Rails.env).each do |db_config| + ActiveRecord::Base.establish_connection(db_config.config) + filename = ActiveRecord::Tasks::DatabaseTasks.dump_filename(db_config.spec_name, :sql) + ActiveRecord::Tasks::DatabaseTasks.structure_dump(db_config.config, filename) if ActiveRecord::SchemaMigration.table_exists? File.open(filename, "a") do |f| f.puts ActiveRecord::Base.connection.dump_schema_information @@ -356,22 +354,30 @@ db_namespace = namespace :db do begin should_reconnect = ActiveRecord::Base.connection_pool.active_connection? ActiveRecord::Schema.verbose = false - ActiveRecord::Tasks::DatabaseTasks.load_schema ActiveRecord::Base.configurations["test"], :ruby, ENV["SCHEMA"], "test" + ActiveRecord::Base.configurations.configs_for(env_name: "test").each do |db_config| + filename = ActiveRecord::Tasks::DatabaseTasks.dump_filename(db_config.spec_name, :ruby) + ActiveRecord::Tasks::DatabaseTasks.load_schema(db_config.config, :ruby, filename, "test") + end ensure if should_reconnect - ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations[ActiveRecord::Tasks::DatabaseTasks.env]) + ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations.default_hash(ActiveRecord::Tasks::DatabaseTasks.env)) end end end # desc "Recreate the test database from an existent structure.sql file" task load_structure: %w(db:test:purge) do - ActiveRecord::Tasks::DatabaseTasks.load_schema ActiveRecord::Base.configurations["test"], :sql, ENV["SCHEMA"], "test" + ActiveRecord::Base.configurations.configs_for(env_name: "test").each do |db_config| + filename = ActiveRecord::Tasks::DatabaseTasks.dump_filename(db_config.spec_name, :sql) + ActiveRecord::Tasks::DatabaseTasks.load_schema(db_config.config, :sql, filename, "test") + end end # desc "Empty the test database" task purge: %w(load_config check_protected_environments) do - ActiveRecord::Tasks::DatabaseTasks.purge ActiveRecord::Base.configurations["test"] + ActiveRecord::Base.configurations.configs_for(env_name: "test").each do |db_config| + ActiveRecord::Tasks::DatabaseTasks.purge(db_config.config) + end end # desc 'Load the test schema' diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb index 6d2f75a3ae..b2110f727c 100644 --- a/activerecord/lib/active_record/reflection.rb +++ b/activerecord/lib/active_record/reflection.rb @@ -612,9 +612,21 @@ module ActiveRecord # returns either +nil+ or the inverse association name that it finds. def automatic_inverse_of - if can_find_inverse_of_automatically?(self) - inverse_name = ActiveSupport::Inflector.underscore(options[:as] || active_record.name.demodulize).to_sym + return unless can_find_inverse_of_automatically?(self) + inverse_name_candidates = + if options[:as] + [options[:as]] + else + active_record_name = active_record.name.demodulize + [active_record_name, ActiveSupport::Inflector.pluralize(active_record_name)] + end + + inverse_name_candidates.map! do |candidate| + ActiveSupport::Inflector.underscore(candidate).to_sym + end + + inverse_name_candidates.detect do |inverse_name| begin reflection = klass._reflect_on_association(inverse_name) rescue NameError @@ -623,9 +635,7 @@ module ActiveRecord reflection = false end - if valid_inverse_reflection?(reflection) - return inverse_name - end + valid_inverse_reflection?(reflection) end end diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb index 2d3e1eaa08..d8cff30b88 100644 --- a/activerecord/lib/active_record/relation.rb +++ b/activerecord/lib/active_record/relation.rb @@ -62,7 +62,7 @@ module ActiveRecord # user = users.new { |user| user.name = 'Oscar' } # user.name # => Oscar def new(attributes = nil, &block) - scoping { klass.new(scope_for_create(attributes), &block) } + scoping { klass.new(attributes, &block) } end alias build new @@ -87,11 +87,7 @@ module ActiveRecord # users.create(name: nil) # validation on name # # => #<User id: nil, name: nil, ...> def create(attributes = nil, &block) - if attributes.is_a?(Array) - attributes.collect { |attr| create(attr, &block) } - else - scoping { klass.create(scope_for_create(attributes), &block) } - end + scoping { klass.create(attributes, &block) } end # Similar to #create, but calls @@ -101,11 +97,7 @@ module ActiveRecord # Expects arguments in the same format as # {ActiveRecord::Base.create!}[rdoc-ref:Persistence::ClassMethods#create!]. def create!(attributes = nil, &block) - if attributes.is_a?(Array) - attributes.collect { |attr| create!(attr, &block) } - else - scoping { klass.create!(scope_for_create(attributes), &block) } - end + scoping { klass.create!(attributes, &block) } end def first_or_create(attributes = nil, &block) # :nodoc: @@ -315,10 +307,7 @@ module ActiveRecord # Please check unscoped if you want to remove all previous scopes (including # the default_scope) during the execution of a block. def scoping - previous, klass.current_scope = klass.current_scope(true), self unless @delegate_to_klass - yield - ensure - klass.current_scope = previous unless @delegate_to_klass + @delegate_to_klass ? yield : klass._scoping(self) { yield } end def _exec_scope(*args, &block) # :nodoc: @@ -359,15 +348,20 @@ module ActiveRecord end stmt = Arel::UpdateManager.new - - stmt.set Arel.sql(@klass.sanitize_sql_for_assignment(updates)) stmt.table(table) - if has_join_values? || offset_value + if updates.is_a?(Hash) + stmt.set _substitute_values(updates) + else + stmt.set Arel.sql(klass.sanitize_sql_for_assignment(updates, table.name)) + end + + if has_join_values? @klass.connection.join_to_update(stmt, arel, arel_attribute(primary_key)) else stmt.key = arel_attribute(primary_key) stmt.take(arel.limit) + stmt.offset(arel.offset) stmt.order(*arel.orders) stmt.wheres = arel.constraints end @@ -375,26 +369,33 @@ module ActiveRecord @klass.connection.update stmt, "#{@klass} Update All" end - def update(attributes) # :nodoc: - each { |record| record.update(attributes) } + def update(id = :all, attributes) # :nodoc: + if id == :all + each { |record| record.update(attributes) } + else + klass.update(id, attributes) + end end def update_counters(counters) # :nodoc: touch = counters.delete(:touch) - updates = counters.map do |counter_name, value| - operator = value < 0 ? "-" : "+" - quoted_column = connection.quote_column_name(counter_name) - "#{quoted_column} = COALESCE(#{quoted_column}, 0) #{operator} #{value.abs}" + updates = {} + counters.each do |counter_name, value| + attr = arel_attribute(counter_name) + bind = predicate_builder.build_bind_attribute(attr.name, value.abs) + expr = table.coalesce(Arel::Nodes::UnqualifiedColumn.new(attr), 0) + expr = value < 0 ? expr - bind : expr + bind + updates[counter_name] = expr.expr end if touch names = touch if touch != true touch_updates = klass.touch_attributes_with_time(*names) - updates << klass.sanitize_sql_for_assignment(touch_updates) unless touch_updates.empty? + updates.merge!(touch_updates) unless touch_updates.empty? end - update_all updates.join(", ") + update_all updates end # Touches all records in the current relation without instantiating records first with the updated_at/on attributes @@ -421,14 +422,12 @@ module ActiveRecord # Person.where(name: 'David').touch_all # # => "UPDATE \"people\" SET \"updated_at\" = '2018-01-04 22:55:23.132670' WHERE \"people\".\"name\" = 'David'" def touch_all(*names, time: nil) - updates = touch_attributes_with_time(*names, time: time) - if klass.locking_enabled? - quoted_locking_column = connection.quote_column_name(klass.locking_column) - updates = sanitize_sql_for_assignment(updates) + ", #{quoted_locking_column} = COALESCE(#{quoted_locking_column}, 0) + 1" + names << { time: time } + update_counters(klass.locking_column => 1, touch: names) + else + update_all klass.touch_attributes_with_time(*names, time: time) end - - update_all(updates) end # Destroys the records by instantiating each @@ -486,9 +485,13 @@ module ActiveRecord stmt = Arel::DeleteManager.new stmt.from(table) - if has_join_values? || has_limit_or_offset? + if has_join_values? @klass.connection.join_to_delete(stmt, arel, arel_attribute(primary_key)) else + stmt.key = arel_attribute(primary_key) + stmt.take(arel.limit) + stmt.offset(arel.offset) + stmt.order(*arel.orders) stmt.wheres = arel.constraints end @@ -550,10 +553,8 @@ module ActiveRecord where_clause.to_h(relation_table_name) end - def scope_for_create(attributes = nil) - scope = where_values_hash.merge!(create_with_value.stringify_keys) - scope.merge!(attributes) if attributes - scope + def scope_for_create + where_values_hash.merge!(create_with_value.stringify_keys) end # Returns true if relation needs eager loading. @@ -636,6 +637,16 @@ module ActiveRecord end private + def _substitute_values(values) + values.map do |name, value| + attr = arel_attribute(name) + unless Arel.arel_node?(value) + type = klass.type_for_attribute(attr.name) + value = predicate_builder.build_bind_attribute(attr.name, type.cast(value)) + end + [attr, value] + end + end def has_join_values? joins_values.any? || left_outer_joins_values.any? diff --git a/activerecord/lib/active_record/relation/calculations.rb b/activerecord/lib/active_record/relation/calculations.rb index 40fe39fa9d..0fa5ba2e50 100644 --- a/activerecord/lib/active_record/relation/calculations.rb +++ b/activerecord/lib/active_record/relation/calculations.rb @@ -190,7 +190,7 @@ module ActiveRecord relation = apply_join_dependency relation.pluck(*column_names) else - enforce_raw_sql_whitelist(column_names) + disallow_raw_sql!(column_names) relation = spawn relation.select_values = column_names.map { |cn| @klass.has_attribute?(cn) || @klass.attribute_alias?(cn) ? arel_attribute(cn) : cn diff --git a/activerecord/lib/active_record/relation/delegation.rb b/activerecord/lib/active_record/relation/delegation.rb index 488f71cdde..8f657840f5 100644 --- a/activerecord/lib/active_record/relation/delegation.rb +++ b/activerecord/lib/active_record/relation/delegation.rb @@ -17,7 +17,7 @@ module ActiveRecord delegate = Class.new(klass) { include ClassSpecificRelation } - mangled_name = klass.name.gsub("::".freeze, "_".freeze) + mangled_name = klass.name.gsub("::", "_") const_set mangled_name, delegate private_constant mangled_name diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb index c5562c1ff0..6f420fe6bb 100644 --- a/activerecord/lib/active_record/relation/finder_methods.rb +++ b/activerecord/lib/active_record/relation/finder_methods.rb @@ -338,14 +338,14 @@ module ActiveRecord name = @klass.name if ids.nil? - error = "Couldn't find #{name}".dup + error = +"Couldn't find #{name}" error << " with#{conditions}" if conditions raise RecordNotFound.new(error, name, key) elsif Array(ids).size == 1 error = "Couldn't find #{name} with '#{key}'=#{ids}#{conditions}" raise RecordNotFound.new(error, name, key, ids) else - error = "Couldn't find all #{name.pluralize} with '#{key}': ".dup + error = +"Couldn't find all #{name.pluralize} with '#{key}': " error << "(#{ids.join(", ")})#{conditions} (found #{result_size} results, but was looking for #{expected_size})." error << " Couldn't find #{name.pluralize(not_found_ids.size)} with #{key.to_s.pluralize(not_found_ids.size)} #{not_found_ids.join(', ')}." if not_found_ids raise RecordNotFound.new(error, name, key, ids) @@ -398,7 +398,7 @@ module ActiveRecord def limited_ids_for(relation) values = @klass.connection.columns_for_distinct( - connection.column_name_from_arel_node(arel_attribute(primary_key)), + connection.visitor.compile(arel_attribute(primary_key)), relation.order_values ) @@ -416,7 +416,7 @@ module ActiveRecord raise UnknownPrimaryKey.new(@klass) if primary_key.nil? expects_array = ids.first.kind_of?(Array) - return ids.first if expects_array && ids.first.empty? + return [] if expects_array && ids.first.empty? ids = ids.flatten.compact.uniq diff --git a/activerecord/lib/active_record/relation/merger.rb b/activerecord/lib/active_record/relation/merger.rb index 10e4779ca4..4de7465128 100644 --- a/activerecord/lib/active_record/relation/merger.rb +++ b/activerecord/lib/active_record/relation/merger.rb @@ -152,10 +152,10 @@ module ActiveRecord def merge_multi_values if other.reordering_value # override any order specified in the original relation - relation.reorder! other.order_values + relation.reorder!(*other.order_values) elsif other.order_values.any? # merge in order_values from relation - relation.order! other.order_values + relation.order!(*other.order_values) end extensions = other.extensions - relation.extensions @@ -171,9 +171,7 @@ module ActiveRecord end def merge_clauses - if relation.from_clause.empty? && !other.from_clause.empty? - relation.from_clause = other.from_clause - end + relation.from_clause = other.from_clause if replace_from_clause? where_clause = relation.where_clause.merge(other.where_clause) relation.where_clause = where_clause unless where_clause.empty? @@ -181,6 +179,11 @@ module ActiveRecord having_clause = relation.having_clause.merge(other.having_clause) relation.having_clause = having_clause unless having_clause.empty? end + + def replace_from_clause? + relation.from_clause.empty? && !other.from_clause.empty? && + relation.klass.base_class == other.klass.base_class + 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 f734cd0ad8..b59ff912fe 100644 --- a/activerecord/lib/active_record/relation/predicate_builder.rb +++ b/activerecord/lib/active_record/relation/predicate_builder.rb @@ -27,7 +27,7 @@ module ActiveRecord key else key = key.to_s - key.split(".".freeze).first if key.include?(".".freeze) + key.split(".").first if key.include?(".") end end.compact end @@ -115,11 +115,11 @@ module ActiveRecord def convert_dot_notation_to_hash(attributes) dot_notation = attributes.select do |k, v| - k.include?(".".freeze) && !v.is_a?(Hash) + k.include?(".") && !v.is_a?(Hash) end dot_notation.each_key do |key| - table_name, column_name = key.split(".".freeze) + table_name, column_name = key.split(".") value = attributes.delete(key) attributes[table_name] ||= {} diff --git a/activerecord/lib/active_record/relation/predicate_builder/array_handler.rb b/activerecord/lib/active_record/relation/predicate_builder/array_handler.rb index 64bf83e3c1..fadb3c420d 100644 --- a/activerecord/lib/active_record/relation/predicate_builder/array_handler.rb +++ b/activerecord/lib/active_record/relation/predicate_builder/array_handler.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require "active_support/core_ext/array/extract" + module ActiveRecord class PredicateBuilder class ArrayHandler # :nodoc: @@ -11,18 +13,19 @@ module ActiveRecord return attribute.in([]) if value.empty? values = value.map { |x| x.is_a?(Base) ? x.id : x } - nils, values = values.partition(&:nil?) - ranges, values = values.partition { |v| v.is_a?(Range) } + nils = values.extract!(&:nil?) + ranges = values.extract! { |v| v.is_a?(Range) } values_predicate = case values.length when 0 then NullPredicate when 1 then predicate_builder.build(attribute, values.first) else - bind_values = values.map do |v| - predicate_builder.build_bind_attribute(attribute.name, v) - end - attribute.in(bind_values) + values.map! do |v| + bind = predicate_builder.build_bind_attribute(attribute.name, v) + bind if bind.value.boundable? + end.compact! + values.empty? ? NullPredicate : attribute.in(values) end unless nils.empty? diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb index 74103d6cdd..b12497fd0a 100644 --- a/activerecord/lib/active_record/relation/query_methods.rb +++ b/activerecord/lib/active_record/relation/query_methods.rb @@ -953,7 +953,7 @@ module ActiveRecord arel.having(having_clause.ast) unless having_clause.empty? if limit_value limit_attribute = ActiveModel::Attribute.with_cast_value( - "LIMIT".freeze, + "LIMIT", connection.sanitize_limit(limit_value), Type.default_value, ) @@ -961,7 +961,7 @@ module ActiveRecord end if offset_value offset_attribute = ActiveModel::Attribute.with_cast_value( - "OFFSET".freeze, + "OFFSET", offset_value.to_i, Type.default_value, ) @@ -1147,9 +1147,9 @@ module ActiveRecord end order_args.flatten! - @klass.enforce_raw_sql_whitelist( + @klass.disallow_raw_sql!( order_args.flat_map { |a| a.is_a?(Hash) ? a.keys : a }, - whitelist: AttributeMethods::ClassMethods::COLUMN_NAME_ORDER_WHITELIST + permit: AttributeMethods::ClassMethods::COLUMN_NAME_WITH_ORDER ) validate_order_args(order_args) diff --git a/activerecord/lib/active_record/result.rb b/activerecord/lib/active_record/result.rb index 7f1c2fd7eb..da6d10b6ec 100644 --- a/activerecord/lib/active_record/result.rb +++ b/activerecord/lib/active_record/result.rb @@ -21,7 +21,7 @@ module ActiveRecord # ] # # # Get an array of hashes representing the result (column => value): - # result.to_hash + # result.to_a # # => [{"id" => 1, "title" => "title_1", "body" => "body_1"}, # {"id" => 2, "title" => "title_2", "body" => "body_2"}, # ... @@ -65,9 +65,12 @@ module ActiveRecord end end - # Returns an array of hashes representing each row record. def to_hash - hash_rows + ActiveSupport::Deprecation.warn(<<-MSG.squish) + `ActiveRecord::Result#to_hash` has been renamed to `to_a`. + `to_hash` is deprecated and will be removed in Rails 6.1. + MSG + to_a end alias :map! :map @@ -83,6 +86,8 @@ module ActiveRecord hash_rows end + alias :to_a :to_ary + def [](idx) hash_rows[idx] end @@ -140,6 +145,8 @@ module ActiveRecord # We freeze the strings to prevent them getting duped when # used as keys in ActiveRecord::Base's @attributes hash columns = @columns.map(&:-@) + length = columns.length + @rows.map { |row| # In the past we used Hash[columns.zip(row)] # though elegant, the verbose way is much more efficient @@ -148,8 +155,6 @@ module ActiveRecord hash = {} index = 0 - length = columns.length - while index < length hash[columns[index]] = row[index] index += 1 diff --git a/activerecord/lib/active_record/sanitization.rb b/activerecord/lib/active_record/sanitization.rb index c6c268855e..3485d9e557 100644 --- a/activerecord/lib/active_record/sanitization.rb +++ b/activerecord/lib/active_record/sanitization.rb @@ -61,8 +61,8 @@ module ActiveRecord # # => "id ASC" def sanitize_sql_for_order(condition) if condition.is_a?(Array) && condition.first.to_s.include?("?") - enforce_raw_sql_whitelist([condition.first], - whitelist: AttributeMethods::ClassMethods::COLUMN_NAME_ORDER_WHITELIST + disallow_raw_sql!([condition.first], + permit: AttributeMethods::ClassMethods::COLUMN_NAME_WITH_ORDER ) # Ensure we aren't dealing with a subclass of String that might diff --git a/activerecord/lib/active_record/scoping.rb b/activerecord/lib/active_record/scoping.rb index 01ac56570a..9eba1254a4 100644 --- a/activerecord/lib/active_record/scoping.rb +++ b/activerecord/lib/active_record/scoping.rb @@ -12,14 +12,6 @@ module ActiveRecord end module ClassMethods # :nodoc: - def current_scope(skip_inherited_scope = false) - ScopeRegistry.value_for(:current_scope, self, skip_inherited_scope) - end - - def current_scope=(scope) - ScopeRegistry.set_value_for(:current_scope, self, scope) - end - # Collects attributes from scopes that should be applied when creating # an AR instance for the particular class this is called on. def scope_attributes @@ -30,6 +22,15 @@ module ActiveRecord def scope_attributes? current_scope end + + private + def current_scope(skip_inherited_scope = false) + ScopeRegistry.value_for(:current_scope, self, skip_inherited_scope) + end + + def current_scope=(scope) + ScopeRegistry.set_value_for(:current_scope, self, scope) + end end def populate_with_current_scope_attributes # :nodoc: diff --git a/activerecord/lib/active_record/scoping/default.rb b/activerecord/lib/active_record/scoping/default.rb index 8c612df27a..6caf9b3251 100644 --- a/activerecord/lib/active_record/scoping/default.rb +++ b/activerecord/lib/active_record/scoping/default.rb @@ -31,7 +31,14 @@ module ActiveRecord # Post.limit(10) # Fires "SELECT * FROM posts LIMIT 10" # } def unscoped - block_given? ? relation.scoping { yield } : relation + block_given? ? _scoping(relation) { yield } : relation + end + + def _scoping(relation) # :nodoc: + previous, self.current_scope = current_scope(true), relation + yield + ensure + self.current_scope = previous end # Are there attributes associated with this scope? diff --git a/activerecord/lib/active_record/scoping/named.rb b/activerecord/lib/active_record/scoping/named.rb index a784001587..573d97b819 100644 --- a/activerecord/lib/active_record/scoping/named.rb +++ b/activerecord/lib/active_record/scoping/named.rb @@ -24,13 +24,13 @@ module ActiveRecord # You can define a scope that applies to all finders using # {default_scope}[rdoc-ref:Scoping::Default::ClassMethods#default_scope]. def all - current_scope = self.current_scope + scope = current_scope - if current_scope - if self == current_scope.klass - current_scope.clone + if scope + if self == scope.klass + scope.clone else - relation.merge!(current_scope) + relation.merge!(scope) end else default_scoped @@ -38,9 +38,7 @@ module ActiveRecord end def scope_for_association(scope = relation) # :nodoc: - current_scope = self.current_scope - - if current_scope && current_scope.empty_scope? + if current_scope&.empty_scope? scope else default_scoped(scope) @@ -182,15 +180,13 @@ module ActiveRecord if body.respond_to?(:to_proc) singleton_class.send(:define_method, name) do |*args| - scope = all - scope = scope._exec_scope(*args, &body) + scope = all._exec_scope(*args, &body) scope = scope.extending(extension) if extension scope end else singleton_class.send(:define_method, name) do |*args| - scope = all - scope = scope.scoping { body.call(*args) || scope } + scope = body.call(*args) || all scope = scope.extending(extension) if extension scope end diff --git a/activerecord/lib/active_record/statement_cache.rb b/activerecord/lib/active_record/statement_cache.rb index b41d3504fd..1b1736dcab 100644 --- a/activerecord/lib/active_record/statement_cache.rb +++ b/activerecord/lib/active_record/statement_cache.rb @@ -44,7 +44,7 @@ module ActiveRecord def initialize(values) @values = values @indexes = values.each_with_index.find_all { |thing, i| - Arel::Nodes::BindParam === thing + Substitute === thing }.map(&:last) end @@ -56,6 +56,28 @@ module ActiveRecord end end + class PartialQueryCollector + def initialize + @parts = [] + @binds = [] + end + + def <<(str) + @parts << str + self + end + + def add_bind(obj) + @binds << obj + @parts << Substitute.new + self + end + + def value + [@parts, @binds] + end + end + def self.query(sql) Query.new(sql) end @@ -64,6 +86,10 @@ module ActiveRecord PartialQuery.new(values) end + def self.partial_query_collector + PartialQueryCollector.new + end + class Params # :nodoc: def bind; Substitute.new; end end diff --git a/activerecord/lib/active_record/tasks/database_tasks.rb b/activerecord/lib/active_record/tasks/database_tasks.rb index fd36c0abd2..5e29085aff 100644 --- a/activerecord/lib/active_record/tasks/database_tasks.rb +++ b/activerecord/lib/active_record/tasks/database_tasks.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require "active_record/database_configurations" + module ActiveRecord module Tasks # :nodoc: class DatabaseAlreadyExists < StandardError; end # :nodoc: @@ -101,16 +103,21 @@ module ActiveRecord @env ||= Rails.env end + def spec + @spec ||= "primary" + end + def seed_loader @seed_loader ||= Rails.application end def current_config(options = {}) options.reverse_merge! env: env + options[:spec] ||= "primary" if options.has_key?(:config) @current_config = options[:config] else - @current_config ||= ActiveRecord::Base.configurations[options[:env]] + @current_config ||= ActiveRecord::Base.configurations.configs_for(env_name: options[:env], spec_name: options[:spec]).config end end @@ -122,7 +129,7 @@ module ActiveRecord $stderr.puts "Database '#{configuration['database']}' already exists" if verbose? rescue Exception => error $stderr.puts error - $stderr.puts "Couldn't create database for #{configuration.inspect}" + $stderr.puts "Couldn't create '#{configuration['database']}' database. Please check your configuration." raise end @@ -135,8 +142,8 @@ module ActiveRecord end def for_each - databases = Rails.application.config.load_database_yaml - database_configs = ActiveRecord::DatabaseConfigurations.configs_for(Rails.env, databases) + databases = Rails.application.config.database_configuration + database_configs = ActiveRecord::DatabaseConfigurations.new(databases).configs_for(env_name: Rails.env) # if this is a single database application we don't want tasks for each primary database return if database_configs.count == 1 @@ -180,9 +187,11 @@ module ActiveRecord scope = ENV["SCOPE"] verbose_was, Migration.verbose = Migration.verbose, verbose? + Base.connection.migration_context.migrate(target_version) do |migration| scope.blank? || scope == migration.scope end + ActiveRecord::Base.clear_cache! ensure Migration.verbose = verbose_was @@ -198,8 +207,8 @@ module ActiveRecord ENV["VERSION"].to_i if ENV["VERSION"] && !ENV["VERSION"].empty? end - def charset_current(environment = env) - charset ActiveRecord::Base.configurations[environment] + def charset_current(environment = env, specification_name = spec) + charset ActiveRecord::Base.configurations.configs_for(env_name: environment, spec_name: specification_name).config end def charset(*arguments) @@ -207,8 +216,8 @@ module ActiveRecord class_for_adapter(configuration["adapter"]).new(*arguments).charset end - def collation_current(environment = env) - collation ActiveRecord::Base.configurations[environment] + def collation_current(environment = env, specification_name = spec) + collation ActiveRecord::Base.configurations.configs_for(env_name: environment, spec_name: specification_name).config end def collation(*arguments) @@ -298,7 +307,7 @@ module ActiveRecord def check_schema_file(filename) unless File.exist?(filename) - message = %{#{filename} doesn't exist yet. Run `rails db:migrate` to create it, then try again.}.dup + message = +%{#{filename} doesn't exist yet. Run `rails db:migrate` to create it, then try again.} message << %{ If you do not intend to use a database, you should instead alter #{Rails.root}/config/application.rb to limit the frameworks that will be loaded.} if defined?(::Rails.root) Kernel.abort message end @@ -342,14 +351,15 @@ module ActiveRecord environments << "test" if environment == "development" environments.each do |env| - ActiveRecord::DatabaseConfigurations.configs_for(env) do |spec_name, configuration| - yield configuration, spec_name, env + ActiveRecord::Base.configurations.configs_for(env_name: env).each do |db_config| + yield db_config.config, db_config.spec_name, env end end end def each_local_configuration - ActiveRecord::Base.configurations.each_value do |configuration| + ActiveRecord::Base.configurations.configs_for.each do |db_config| + configuration = db_config.config next unless configuration["database"] if local_database?(configuration) diff --git a/activerecord/lib/active_record/tasks/mysql_database_tasks.rb b/activerecord/lib/active_record/tasks/mysql_database_tasks.rb index eddc6fa223..1c1b29b5e1 100644 --- a/activerecord/lib/active_record/tasks/mysql_database_tasks.rb +++ b/activerecord/lib/active_record/tasks/mysql_database_tasks.rb @@ -104,7 +104,7 @@ module ActiveRecord end def run_cmd_error(cmd, args, action) - msg = "failed to execute: `#{cmd}`\n".dup + msg = +"failed to execute: `#{cmd}`\n" msg << "Please check the output above for any errors and make sure that `#{cmd}` is installed in your PATH and has proper permissions.\n\n" msg end diff --git a/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb b/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb index 533e6953a4..8acb11f75f 100644 --- a/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb +++ b/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb @@ -6,8 +6,8 @@ module ActiveRecord module Tasks # :nodoc: class PostgreSQLDatabaseTasks # :nodoc: DEFAULT_ENCODING = ENV["CHARSET"] || "utf8" - ON_ERROR_STOP_1 = "ON_ERROR_STOP=1".freeze - SQL_COMMENT_BEGIN = "--".freeze + ON_ERROR_STOP_1 = "ON_ERROR_STOP=1" + SQL_COMMENT_BEGIN = "--" delegate :connection, :establish_connection, :clear_active_connections!, to: ActiveRecord::Base @@ -82,7 +82,7 @@ module ActiveRecord def structure_load(filename, extra_flags) set_psql_env - args = ["-v", ON_ERROR_STOP_1, "-q", "-f", filename] + args = ["-v", ON_ERROR_STOP_1, "-q", "-X", "-f", filename] args.concat(Array(extra_flags)) if extra_flags args << configuration["database"] run_cmd("psql", args, "loading") @@ -115,7 +115,7 @@ module ActiveRecord end def run_cmd_error(cmd, args, action) - msg = "failed to execute:\n".dup + msg = +"failed to execute:\n" msg << "#{cmd} #{args.join(' ')}\n\n" msg << "Please check the output above for any errors and make sure that `#{cmd}` is installed in your PATH and has proper permissions.\n\n" msg diff --git a/activerecord/lib/active_record/tasks/sqlite_database_tasks.rb b/activerecord/lib/active_record/tasks/sqlite_database_tasks.rb index d0bad05176..a82cea80ca 100644 --- a/activerecord/lib/active_record/tasks/sqlite_database_tasks.rb +++ b/activerecord/lib/active_record/tasks/sqlite_database_tasks.rb @@ -67,7 +67,7 @@ module ActiveRecord end def run_cmd_error(cmd, args) - msg = "failed to execute:\n".dup + msg = +"failed to execute:\n" msg << "#{cmd} #{args.join(' ')}\n\n" msg << "Please check the output above for any errors and make sure that `#{cmd}` is installed in your PATH and has proper permissions.\n\n" msg diff --git a/activerecord/lib/active_record/test_databases.rb b/activerecord/lib/active_record/test_databases.rb index 5b4efe22c9..999830ba79 100644 --- a/activerecord/lib/active_record/test_databases.rb +++ b/activerecord/lib/active_record/test_databases.rb @@ -5,31 +5,32 @@ require "active_support/testing/parallelization" module ActiveRecord module TestDatabases # :nodoc: ActiveSupport::Testing::Parallelization.after_fork_hook do |i| - create_and_load_schema(i, spec_name: Rails.env) + create_and_load_schema(i, env_name: Rails.env) end - ActiveSupport::Testing::Parallelization.run_cleanup_hook do |_| - drop(spec_name: Rails.env) + ActiveSupport::Testing::Parallelization.run_cleanup_hook do + drop(env_name: Rails.env) end - def self.create_and_load_schema(i, spec_name:) + def self.create_and_load_schema(i, env_name:) old, ENV["VERBOSE"] = ENV["VERBOSE"], "false" - connection_spec = ActiveRecord::Base.configurations[spec_name] - - connection_spec["database"] += "-#{i}" - ActiveRecord::Tasks::DatabaseTasks.create(connection_spec) - ActiveRecord::Tasks::DatabaseTasks.load_schema(connection_spec) + ActiveRecord::Base.configurations.configs_for(env_name: env_name).each do |db_config| + db_config.config["database"] += "-#{i}" + ActiveRecord::Tasks::DatabaseTasks.create(db_config.config) + ActiveRecord::Tasks::DatabaseTasks.load_schema(db_config.config, ActiveRecord::Base.schema_format, nil, env_name, db_config.spec_name) + end ensure - ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations[Rails.env]) + ActiveRecord::Base.establish_connection(Rails.env.to_sym) ENV["VERBOSE"] = old end - def self.drop(spec_name:) + def self.drop(env_name:) old, ENV["VERBOSE"] = ENV["VERBOSE"], "false" - connection_spec = ActiveRecord::Base.configurations[spec_name] - ActiveRecord::Tasks::DatabaseTasks.drop(connection_spec) + ActiveRecord::Base.configurations.configs_for(env_name: env_name).each do |db_config| + ActiveRecord::Tasks::DatabaseTasks.drop(db_config.config) + end ensure ENV["VERBOSE"] = old end diff --git a/activerecord/lib/arel.rb b/activerecord/lib/arel.rb index 7d04e1cac6..dab785738e 100644 --- a/activerecord/lib/arel.rb +++ b/activerecord/lib/arel.rb @@ -35,6 +35,11 @@ module Arel # :nodoc: all def self.star sql "*" end + + def self.arel_node?(value) + value.is_a?(Arel::Node) || value.is_a?(Arel::Attribute) || value.is_a?(Arel::Nodes::SqlLiteral) + end + ## Convenience Alias Node = Arel::Nodes::Node end diff --git a/activerecord/lib/arel/collectors/composite.rb b/activerecord/lib/arel/collectors/composite.rb index d040d8598d..0533544993 100644 --- a/activerecord/lib/arel/collectors/composite.rb +++ b/activerecord/lib/arel/collectors/composite.rb @@ -24,8 +24,7 @@ module Arel # :nodoc: all [left.value, right.value] end - protected - + private attr_reader :left, :right end end diff --git a/activerecord/lib/arel/collectors/plain_string.rb b/activerecord/lib/arel/collectors/plain_string.rb index 687d7fbf2f..c0e9fff399 100644 --- a/activerecord/lib/arel/collectors/plain_string.rb +++ b/activerecord/lib/arel/collectors/plain_string.rb @@ -4,7 +4,7 @@ module Arel # :nodoc: all module Collectors class PlainString def initialize - @str = "".dup + @str = +"" end def value diff --git a/activerecord/lib/arel/collectors/sql_string.rb b/activerecord/lib/arel/collectors/sql_string.rb index c293a89a74..54e1e562c2 100644 --- a/activerecord/lib/arel/collectors/sql_string.rb +++ b/activerecord/lib/arel/collectors/sql_string.rb @@ -15,10 +15,6 @@ module Arel # :nodoc: all @bind_index += 1 self end - - def compile(bvs) - value - end end end end diff --git a/activerecord/lib/arel/collectors/substitute_binds.rb b/activerecord/lib/arel/collectors/substitute_binds.rb index 3f40eec8a8..4b894bc4b1 100644 --- a/activerecord/lib/arel/collectors/substitute_binds.rb +++ b/activerecord/lib/arel/collectors/substitute_binds.rb @@ -21,8 +21,7 @@ module Arel # :nodoc: all delegate.value end - protected - + private attr_reader :quoter, :delegate end end diff --git a/activerecord/lib/arel/delete_manager.rb b/activerecord/lib/arel/delete_manager.rb index 2def581009..fdba937d64 100644 --- a/activerecord/lib/arel/delete_manager.rb +++ b/activerecord/lib/arel/delete_manager.rb @@ -2,6 +2,8 @@ module Arel # :nodoc: all class DeleteManager < Arel::TreeManager + include TreeManager::StatementMethods + def initialize super @ast = Nodes::DeleteStatement.new @@ -12,14 +14,5 @@ module Arel # :nodoc: all @ast.relation = relation self end - - def take(limit) - @ast.limit = Nodes::Limit.new(Nodes.build_quoted(limit)) if limit - self - end - - def wheres=(list) - @ast.wheres = list - end end end diff --git a/activerecord/lib/arel/factory_methods.rb b/activerecord/lib/arel/factory_methods.rb index b828bc274e..83ec23e403 100644 --- a/activerecord/lib/arel/factory_methods.rb +++ b/activerecord/lib/arel/factory_methods.rb @@ -41,5 +41,9 @@ module Arel # :nodoc: all def lower(column) Nodes::NamedFunction.new "LOWER", [Nodes.build_quoted(column)] end + + def coalesce(*exprs) + Nodes::NamedFunction.new "COALESCE", exprs + end end end diff --git a/activerecord/lib/arel/nodes/bind_param.rb b/activerecord/lib/arel/nodes/bind_param.rb index 53c5563d93..91e9b2b70f 100644 --- a/activerecord/lib/arel/nodes/bind_param.rb +++ b/activerecord/lib/arel/nodes/bind_param.rb @@ -3,7 +3,7 @@ module Arel # :nodoc: all module Nodes class BindParam < Node - attr_accessor :value + attr_reader :value def initialize(value) @value = value diff --git a/activerecord/lib/arel/nodes/delete_statement.rb b/activerecord/lib/arel/nodes/delete_statement.rb index eaac05e2f6..a419975335 100644 --- a/activerecord/lib/arel/nodes/delete_statement.rb +++ b/activerecord/lib/arel/nodes/delete_statement.rb @@ -3,8 +3,7 @@ module Arel # :nodoc: all module Nodes class DeleteStatement < Arel::Nodes::Node - attr_accessor :left, :right - attr_accessor :limit + attr_accessor :left, :right, :orders, :limit, :offset, :key alias :relation :left alias :relation= :left= @@ -15,6 +14,10 @@ module Arel # :nodoc: all super() @left = relation @right = wheres + @orders = [] + @limit = nil + @offset = nil + @key = nil end def initialize_copy(other) @@ -24,13 +27,17 @@ module Arel # :nodoc: all end def hash - [self.class, @left, @right].hash + [self.class, @left, @right, @orders, @limit, @offset, @key].hash end def eql?(other) self.class == other.class && self.left == other.left && - self.right == other.right + self.right == other.right && + self.orders == other.orders && + self.limit == other.limit && + self.offset == other.offset && + self.key == other.key end alias :== :eql? end diff --git a/activerecord/lib/arel/nodes/node.rb b/activerecord/lib/arel/nodes/node.rb index 2b9b1e9828..8086102bde 100644 --- a/activerecord/lib/arel/nodes/node.rb +++ b/activerecord/lib/arel/nodes/node.rb @@ -8,16 +8,6 @@ module Arel # :nodoc: all include Arel::FactoryMethods include Enumerable - if $DEBUG - def _caller - @caller - end - - def initialize - @caller = caller.dup - end - end - ### # Factory method to create a Nodes::Not node that has the recipient of # the caller as a child. diff --git a/activerecord/lib/arel/nodes/select_core.rb b/activerecord/lib/arel/nodes/select_core.rb index 2defe61974..73461ff683 100644 --- a/activerecord/lib/arel/nodes/select_core.rb +++ b/activerecord/lib/arel/nodes/select_core.rb @@ -3,13 +3,12 @@ module Arel # :nodoc: all module Nodes class SelectCore < Arel::Nodes::Node - attr_accessor :top, :projections, :wheres, :groups, :windows + attr_accessor :projections, :wheres, :groups, :windows attr_accessor :havings, :source, :set_quantifier def initialize super() @source = JoinSource.new nil - @top = nil # https://ronsavage.github.io/SQL/sql-92.bnf.html#set%20quantifier @set_quantifier = nil @@ -43,7 +42,7 @@ module Arel # :nodoc: all def hash [ - @source, @top, @set_quantifier, @projections, + @source, @set_quantifier, @projections, @wheres, @groups, @havings, @windows ].hash end @@ -51,7 +50,6 @@ module Arel # :nodoc: all def eql?(other) self.class == other.class && self.source == other.source && - self.top == other.top && self.set_quantifier == other.set_quantifier && self.projections == other.projections && self.wheres == other.wheres && diff --git a/activerecord/lib/arel/nodes/unary.rb b/activerecord/lib/arel/nodes/unary.rb index a3c0045897..00639304e4 100644 --- a/activerecord/lib/arel/nodes/unary.rb +++ b/activerecord/lib/arel/nodes/unary.rb @@ -37,7 +37,6 @@ module Arel # :nodoc: all On Ordering RollUp - Top }.each do |name| const_set(name, Class.new(Unary)) end diff --git a/activerecord/lib/arel/nodes/update_statement.rb b/activerecord/lib/arel/nodes/update_statement.rb index 5184b1180f..cfaa19e392 100644 --- a/activerecord/lib/arel/nodes/update_statement.rb +++ b/activerecord/lib/arel/nodes/update_statement.rb @@ -3,8 +3,7 @@ module Arel # :nodoc: all module Nodes class UpdateStatement < Arel::Nodes::Node - attr_accessor :relation, :wheres, :values, :orders, :limit - attr_accessor :key + attr_accessor :relation, :wheres, :values, :orders, :limit, :offset, :key def initialize @relation = nil @@ -12,6 +11,7 @@ module Arel # :nodoc: all @values = [] @orders = [] @limit = nil + @offset = nil @key = nil end @@ -22,7 +22,7 @@ module Arel # :nodoc: all end def hash - [@relation, @wheres, @values, @orders, @limit, @key].hash + [@relation, @wheres, @values, @orders, @limit, @offset, @key].hash end def eql?(other) @@ -32,6 +32,7 @@ module Arel # :nodoc: all self.values == other.values && self.orders == other.orders && self.limit == other.limit && + self.offset == other.offset && self.key == other.key end alias :== :eql? diff --git a/activerecord/lib/arel/select_manager.rb b/activerecord/lib/arel/select_manager.rb index 22a04b00c6..a2b2838a3d 100644 --- a/activerecord/lib/arel/select_manager.rb +++ b/activerecord/lib/arel/select_manager.rb @@ -222,10 +222,8 @@ module Arel # :nodoc: all def take(limit) if limit @ast.limit = Nodes::Limit.new(limit) - @ctx.top = Nodes::Top.new(limit) else @ast.limit = nil - @ctx.top = nil end self end @@ -252,9 +250,9 @@ module Arel # :nodoc: all end private - def collapse(exprs, existing = nil) - exprs = exprs.unshift(existing.expr) if existing - exprs = exprs.compact.map { |expr| + def collapse(exprs) + exprs = exprs.compact + exprs.map! { |expr| if String === expr # FIXME: Don't do this automatically Arel.sql(expr) diff --git a/activerecord/lib/arel/table.rb b/activerecord/lib/arel/table.rb index 686fcdf962..c40c68715a 100644 --- a/activerecord/lib/arel/table.rb +++ b/activerecord/lib/arel/table.rb @@ -104,8 +104,7 @@ module Arel # :nodoc: all !type_caster.nil? end - protected - + private attr_reader :type_caster end end diff --git a/activerecord/lib/arel/tree_manager.rb b/activerecord/lib/arel/tree_manager.rb index ed47b09a37..0476399618 100644 --- a/activerecord/lib/arel/tree_manager.rb +++ b/activerecord/lib/arel/tree_manager.rb @@ -4,6 +4,40 @@ module Arel # :nodoc: all class TreeManager include Arel::FactoryMethods + module StatementMethods + def take(limit) + @ast.limit = Nodes::Limit.new(Nodes.build_quoted(limit)) if limit + self + end + + def offset(offset) + @ast.offset = Nodes::Offset.new(Nodes.build_quoted(offset)) if offset + self + end + + def order(*expr) + @ast.orders = expr + self + end + + def key=(key) + @ast.key = Nodes.build_quoted(key) + end + + def key + @ast.key + end + + def wheres=(exprs) + @ast.wheres = exprs + end + + def where(expr) + @ast.wheres << expr + self + end + end + attr_reader :ast def initialize diff --git a/activerecord/lib/arel/update_manager.rb b/activerecord/lib/arel/update_manager.rb index fe444343ba..a809dbb307 100644 --- a/activerecord/lib/arel/update_manager.rb +++ b/activerecord/lib/arel/update_manager.rb @@ -2,30 +2,14 @@ module Arel # :nodoc: all class UpdateManager < Arel::TreeManager + include TreeManager::StatementMethods + def initialize super @ast = Nodes::UpdateStatement.new @ctx = @ast end - def take(limit) - @ast.limit = Nodes::Limit.new(Nodes.build_quoted(limit)) if limit - self - end - - def key=(key) - @ast.key = Nodes.build_quoted(key) - end - - def key - @ast.key - end - - def order(*expr) - @ast.orders = expr - self - end - ### # UPDATE +table+ def table(table) @@ -33,15 +17,6 @@ module Arel # :nodoc: all self end - def wheres=(exprs) - @ast.wheres = exprs - end - - def where(expr) - @ast.wheres << expr - self - end - def set(values) if String === values @ast.values = [values] diff --git a/activerecord/lib/arel/visitors/depth_first.rb b/activerecord/lib/arel/visitors/depth_first.rb index bcf8f8f980..8f65d303ac 100644 --- a/activerecord/lib/arel/visitors/depth_first.rb +++ b/activerecord/lib/arel/visitors/depth_first.rb @@ -34,7 +34,6 @@ module Arel # :nodoc: all alias :visit_Arel_Nodes_Ordering :unary alias :visit_Arel_Nodes_Ascending :unary alias :visit_Arel_Nodes_Descending :unary - alias :visit_Arel_Nodes_Top :unary alias :visit_Arel_Nodes_UnqualifiedColumn :unary def function(o) @@ -136,12 +135,10 @@ module Arel # :nodoc: all alias :visit_Arel_Nodes_True :terminal alias :visit_Arel_Nodes_False :terminal alias :visit_BigDecimal :terminal - alias :visit_Bignum :terminal alias :visit_Class :terminal alias :visit_Date :terminal alias :visit_DateTime :terminal alias :visit_FalseClass :terminal - alias :visit_Fixnum :terminal alias :visit_Float :terminal alias :visit_Integer :terminal alias :visit_NilClass :terminal diff --git a/activerecord/lib/arel/visitors/dot.rb b/activerecord/lib/arel/visitors/dot.rb index d352b81914..9054f0159b 100644 --- a/activerecord/lib/arel/visitors/dot.rb +++ b/activerecord/lib/arel/visitors/dot.rb @@ -81,7 +81,6 @@ module Arel # :nodoc: all alias :visit_Arel_Nodes_Not :unary alias :visit_Arel_Nodes_Offset :unary alias :visit_Arel_Nodes_On :unary - alias :visit_Arel_Nodes_Top :unary alias :visit_Arel_Nodes_UnqualifiedColumn :unary alias :visit_Arel_Nodes_Preceding :unary alias :visit_Arel_Nodes_Following :unary @@ -212,7 +211,6 @@ module Arel # :nodoc: all alias :visit_TrueClass :visit_String alias :visit_FalseClass :visit_String alias :visit_Integer :visit_String - alias :visit_Fixnum :visit_String alias :visit_BigDecimal :visit_String alias :visit_Float :visit_String alias :visit_Symbol :visit_String diff --git a/activerecord/lib/arel/visitors/mssql.rb b/activerecord/lib/arel/visitors/mssql.rb index 9aedc51d15..d564e19089 100644 --- a/activerecord/lib/arel/visitors/mssql.rb +++ b/activerecord/lib/arel/visitors/mssql.rb @@ -12,13 +12,6 @@ module Arel # :nodoc: all private - # `top` wouldn't really work here. I.e. User.select("distinct first_name").limit(10) would generate - # "select top 10 distinct first_name from users", which is invalid query! it should be - # "select distinct top 10 first_name from users" - def visit_Arel_Nodes_Top(o) - "" - end - def visit_Arel_Visitors_MSSQL_RowNumber(o, collector) collector << "ROW_NUMBER() OVER (ORDER BY " inject_join(o.children, collector, ", ") << ") as _row_num" diff --git a/activerecord/lib/arel/visitors/mysql.rb b/activerecord/lib/arel/visitors/mysql.rb index 37bfb661f0..eb8a449079 100644 --- a/activerecord/lib/arel/visitors/mysql.rb +++ b/activerecord/lib/arel/visitors/mysql.rb @@ -37,6 +37,10 @@ module Arel # :nodoc: all visit o.expr, collector end + def visit_Arel_Nodes_UnqualifiedColumn(o, collector) + visit o.expr, collector + end + ### # :'( # http://dev.mysql.com/doc/refman/5.0/en/select.html#id3482214 @@ -52,14 +56,31 @@ module Arel # :nodoc: all super end - def visit_Arel_Nodes_UpdateStatement(o, collector) - collector << "UPDATE " - collector = visit o.relation, collector + def visit_Arel_Nodes_Concat(o, collector) + collector << " CONCAT(" + visit o.left, collector + collector << ", " + visit o.right, collector + collector << ") " + collector + end + + def build_subselect(key, o) + subselect = super - unless o.values.empty? - collector << " SET " - collector = inject_join o.values, collector, ", " + # Materialize subquery by adding distinct + # to work with MySQL 5.7.6 which sets optimizer_switch='derived_merge=on' + subselect.distinct unless subselect.limit || subselect.offset || subselect.orders.any? + + Nodes::SelectStatement.new.tap do |stmt| + core = stmt.cores.last + core.froms = Nodes::Grouping.new(subselect).as("__active_record_temp") + core.projections = [Arel.sql(quote_column_name(key.name))] end + end + + def collect_where_for(o, collector) + return super if o.offset unless o.wheres.empty? collector << " WHERE " @@ -73,15 +94,6 @@ module Arel # :nodoc: all maybe_visit o.limit, collector end - - def visit_Arel_Nodes_Concat(o, collector) - collector << " CONCAT(" - visit o.left, collector - collector << ", " - visit o.right, collector - collector << ") " - collector - end end end end diff --git a/activerecord/lib/arel/visitors/to_sql.rb b/activerecord/lib/arel/visitors/to_sql.rb index 5986fd5576..0172204fc8 100644 --- a/activerecord/lib/arel/visitors/to_sql.rb +++ b/activerecord/lib/arel/visitors/to_sql.rb @@ -67,8 +67,8 @@ module Arel # :nodoc: all @connection = connection end - def compile(node, &block) - accept(node, Arel::Collectors::SQLString.new, &block).value + def compile(node, collector = Arel::Collectors::SQLString.new) + accept(node, collector).value end private @@ -76,12 +76,8 @@ module Arel # :nodoc: all def visit_Arel_Nodes_DeleteStatement(o, collector) collector << "DELETE FROM " collector = visit o.relation, collector - if o.wheres.any? - collector << WHERE - collector = inject_join o.wheres, collector, AND - end - maybe_visit o.limit, collector + collect_where_for(o, collector) end # FIXME: we should probably have a 2-pass visitor for this @@ -92,17 +88,12 @@ module Arel # :nodoc: all core.wheres = o.wheres core.projections = [key] stmt.limit = o.limit + stmt.offset = o.offset stmt.orders = o.orders stmt end def visit_Arel_Nodes_UpdateStatement(o, collector) - if o.orders.empty? && o.limit.nil? - wheres = o.wheres - else - wheres = [Nodes::In.new(o.key, [build_subselect(o.key, o)])] - end - collector << "UPDATE " collector = visit o.relation, collector unless o.values.empty? @@ -110,12 +101,7 @@ module Arel # :nodoc: all collector = inject_join o.values, collector, ", " end - unless wheres.empty? - collector << " WHERE " - collector = inject_join wheres, collector, " AND " - end - - collector + collect_where_for(o, collector) end def visit_Arel_Nodes_InsertStatement(o, collector) @@ -237,8 +223,6 @@ module Arel # :nodoc: all def visit_Arel_Nodes_SelectCore(o, collector) collector << "SELECT" - collector = maybe_visit o.top, collector - collector = maybe_visit o.set_quantifier, collector collect_nodes_for o.projections, collector, SPACE @@ -405,11 +389,6 @@ module Arel # :nodoc: all visit o.expr, collector end - # FIXME: this does nothing on most databases, but does on MSSQL - def visit_Arel_Nodes_Top(o, collector) - collector - end - def visit_Arel_Nodes_Lock(o, collector) visit o.expr, collector end @@ -644,7 +623,7 @@ module Arel # :nodoc: all def visit_Arel_Nodes_Assignment(o, collector) case o.right - when Arel::Nodes::UnqualifiedColumn, Arel::Attributes::Attribute, Arel::Nodes::BindParam + when Arel::Nodes::Node, Arel::Attributes::Attribute collector = visit o.left, collector collector << " = " visit o.right, collector @@ -739,8 +718,6 @@ module Arel # :nodoc: all end alias :visit_Arel_Nodes_SqlLiteral :literal - alias :visit_Bignum :literal - alias :visit_Fixnum :literal alias :visit_Integer :literal def quoted(o, a) @@ -823,6 +800,21 @@ module Arel # :nodoc: all } end + def collect_where_for(o, collector) + if o.orders.empty? && o.limit.nil? && o.offset.nil? + wheres = o.wheres + else + wheres = [Nodes::In.new(o.key, [build_subselect(o.key, o)])] + end + + unless wheres.empty? + collector << " WHERE " + collector = inject_join wheres, collector, " AND " + end + + collector + end + def infix_value(o, collector, value) collector = visit o.left, collector collector << value diff --git a/activerecord/lib/rails/generators/active_record/migration.rb b/activerecord/lib/rails/generators/active_record/migration.rb index 4ceb502c5d..cbb88d571d 100644 --- a/activerecord/lib/rails/generators/active_record/migration.rb +++ b/activerecord/lib/rails/generators/active_record/migration.rb @@ -25,11 +25,24 @@ module ActiveRecord def db_migrate_path if defined?(Rails.application) && Rails.application - Rails.application.config.paths["db/migrate"].to_ary.first + configured_migrate_path || default_migrate_path else "db/migrate" end end + + def default_migrate_path + Rails.application.config.paths["db/migrate"].to_ary.first + end + + def configured_migrate_path + return unless database = options[:database] + config = ActiveRecord::Base.configurations.configs_for( + env_name: Rails.env, + spec_name: database, + ) + config&.migrations_paths + end end end end diff --git a/activerecord/lib/rails/generators/active_record/migration/migration_generator.rb b/activerecord/lib/rails/generators/active_record/migration/migration_generator.rb index a07b00ef79..dd79bcf542 100644 --- a/activerecord/lib/rails/generators/active_record/migration/migration_generator.rb +++ b/activerecord/lib/rails/generators/active_record/migration/migration_generator.rb @@ -8,6 +8,7 @@ module ActiveRecord argument :attributes, type: :array, default: [], banner: "field[:type][:index] field[:type][:index]" class_option :primary_key_type, type: :string, desc: "The type for primary key" + class_option :database, type: :string, aliases: %i(db), desc: "The database for your migration. By default, the current environment's primary database is used." def create_migration_file set_local_assigns! diff --git a/activerecord/lib/rails/generators/active_record/model/model_generator.rb b/activerecord/lib/rails/generators/active_record/model/model_generator.rb index 25e54f3ac8..eac504f9f1 100644 --- a/activerecord/lib/rails/generators/active_record/model/model_generator.rb +++ b/activerecord/lib/rails/generators/active_record/model/model_generator.rb @@ -14,6 +14,7 @@ module ActiveRecord class_option :parent, type: :string, desc: "The parent class for the generated model" class_option :indexes, type: :boolean, default: true, desc: "Add indexes for references and belongs_to columns" class_option :primary_key_type, type: :string, desc: "The type for primary key" + class_option :database, type: :string, aliases: %i(db), desc: "The database for your model's migration. By default, the current environment's primary database is used." # creates the migration file for the model. def create_migration_file diff --git a/activerecord/test/cases/adapter_test.rb b/activerecord/test/cases/adapter_test.rb index 59b99351d1..64c2b51f83 100644 --- a/activerecord/test/cases/adapter_test.rb +++ b/activerecord/test/cases/adapter_test.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require "cases/helper" +require "support/connection_helper" require "models/book" require "models/post" require "models/author" @@ -10,6 +11,7 @@ module ActiveRecord class AdapterTest < ActiveRecord::TestCase def setup @connection = ActiveRecord::Base.connection + @connection.materialize_transactions end ## @@ -225,7 +227,7 @@ module ActiveRecord post = Post.create!(title: "foo", body: "bar") expected = @connection.select_all("SELECT * FROM posts WHERE id = #{post.id}") result = @connection.select_all("SELECT * FROM posts WHERE id = #{Arel::Nodes::BindParam.new(nil).to_sql}", nil, [[nil, post.id]]) - assert_equal expected.to_hash, result.to_hash + assert_equal expected.to_a, result.to_a end def test_insert_update_delete_with_legacy_binds @@ -288,7 +290,7 @@ module ActiveRecord def test_log_invalid_encoding error = assert_raises RuntimeError do @connection.send :log, "SELECT 'ы' FROM DUAL" do - raise "ы".dup.force_encoding(Encoding::ASCII_8BIT) + raise (+"ы").force_encoding(Encoding::ASCII_8BIT) end end @@ -299,6 +301,34 @@ module ActiveRecord def test_supports_multi_insert_is_deprecated assert_deprecated { @connection.supports_multi_insert? } end + + def test_column_name_length_is_deprecated + assert_deprecated { @connection.column_name_length } + end + + def test_table_name_length_is_deprecated + assert_deprecated { @connection.table_name_length } + end + + def test_columns_per_table_is_deprecated + assert_deprecated { @connection.columns_per_table } + end + + def test_indexes_per_table_is_deprecated + assert_deprecated { @connection.indexes_per_table } + end + + def test_columns_per_multicolumn_index_is_deprecated + assert_deprecated { @connection.columns_per_multicolumn_index } + end + + def test_sql_query_length_is_deprecated + assert_deprecated { @connection.sql_query_length } + end + + def test_joins_per_query_is_deprecated + assert_deprecated { @connection.joins_per_query } + end end class AdapterForeignKeyTest < ActiveRecord::TestCase @@ -417,3 +447,27 @@ module ActiveRecord end end end + +if ActiveRecord::Base.connection.supports_advisory_locks? + class AdvisoryLocksEnabledTest < ActiveRecord::TestCase + include ConnectionHelper + + def test_advisory_locks_enabled? + assert ActiveRecord::Base.connection.advisory_locks_enabled? + + run_without_connection do |orig_connection| + ActiveRecord::Base.establish_connection( + orig_connection.merge(advisory_locks: false) + ) + + assert_not ActiveRecord::Base.connection.advisory_locks_enabled? + + ActiveRecord::Base.establish_connection( + orig_connection.merge(advisory_locks: true) + ) + + assert ActiveRecord::Base.connection.advisory_locks_enabled? + end + end + end +end diff --git a/activerecord/test/cases/adapters/mysql2/active_schema_test.rb b/activerecord/test/cases/adapters/mysql2/active_schema_test.rb index 6fc9df5083..261fee13eb 100644 --- a/activerecord/test/cases/adapters/mysql2/active_schema_test.rb +++ b/activerecord/test/cases/adapters/mysql2/active_schema_test.rb @@ -106,7 +106,7 @@ class Mysql2ActiveSchemaTest < ActiveRecord::Mysql2TestCase end def test_create_mysql_database_with_encoding - assert_equal "CREATE DATABASE `matt` DEFAULT CHARACTER SET `utf8`", create_database(:matt) + assert_equal "CREATE DATABASE `matt` DEFAULT CHARACTER SET `utf8mb4`", create_database(:matt) assert_equal "CREATE DATABASE `aimonetti` DEFAULT CHARACTER SET `latin1`", create_database(:aimonetti, charset: "latin1") assert_equal "CREATE DATABASE `matt_aimonetti` DEFAULT COLLATE `utf8mb4_bin`", create_database(:matt_aimonetti, collation: "utf8mb4_bin") end diff --git a/activerecord/test/cases/adapters/mysql2/case_sensitivity_test.rb b/activerecord/test/cases/adapters/mysql2/case_sensitivity_test.rb index aa870349be..c32475c683 100644 --- a/activerecord/test/cases/adapters/mysql2/case_sensitivity_test.rb +++ b/activerecord/test/cases/adapters/mysql2/case_sensitivity_test.rb @@ -9,8 +9,8 @@ class Mysql2CaseSensitivityTest < ActiveRecord::Mysql2TestCase repair_validations(CollationTest) def test_columns_include_collation_different_from_table - assert_equal "utf8_bin", CollationTest.columns_hash["string_cs_column"].collation - assert_equal "utf8_general_ci", CollationTest.columns_hash["string_ci_column"].collation + assert_equal "utf8mb4_bin", CollationTest.columns_hash["string_cs_column"].collation + assert_equal "utf8mb4_general_ci", CollationTest.columns_hash["string_ci_column"].collation end def test_case_sensitive diff --git a/activerecord/test/cases/adapters/mysql2/charset_collation_test.rb b/activerecord/test/cases/adapters/mysql2/charset_collation_test.rb index d0c57de65d..0bdbefdfb9 100644 --- a/activerecord/test/cases/adapters/mysql2/charset_collation_test.rb +++ b/activerecord/test/cases/adapters/mysql2/charset_collation_test.rb @@ -32,20 +32,20 @@ class Mysql2CharsetCollationTest < ActiveRecord::Mysql2TestCase end test "add column with charset and collation" do - @connection.add_column :charset_collations, :title, :string, charset: "utf8", collation: "utf8_bin" + @connection.add_column :charset_collations, :title, :string, charset: "utf8mb4", collation: "utf8mb4_bin" column = @connection.columns(:charset_collations).find { |c| c.name == "title" } assert_equal :string, column.type - assert_equal "utf8_bin", column.collation + assert_equal "utf8mb4_bin", column.collation end test "change column with charset and collation" do - @connection.add_column :charset_collations, :description, :string, charset: "utf8", collation: "utf8_unicode_ci" - @connection.change_column :charset_collations, :description, :text, charset: "utf8", collation: "utf8_general_ci" + @connection.add_column :charset_collations, :description, :string, charset: "utf8mb4", collation: "utf8mb4_unicode_ci" + @connection.change_column :charset_collations, :description, :text, charset: "utf8mb4", collation: "utf8mb4_general_ci" column = @connection.columns(:charset_collations).find { |c| c.name == "description" } assert_equal :text, column.type - assert_equal "utf8_general_ci", column.collation + assert_equal "utf8mb4_general_ci", column.collation end test "schema dump includes collation" do diff --git a/activerecord/test/cases/adapters/mysql2/connection_test.rb b/activerecord/test/cases/adapters/mysql2/connection_test.rb index 726f58d58e..3103589186 100644 --- a/activerecord/test/cases/adapters/mysql2/connection_test.rb +++ b/activerecord/test/cases/adapters/mysql2/connection_test.rb @@ -104,8 +104,8 @@ class Mysql2ConnectionTest < ActiveRecord::Mysql2TestCase end def test_mysql_connection_collation_is_configured - assert_equal "utf8_unicode_ci", @connection.show_variable("collation_connection") - assert_equal "utf8_general_ci", ARUnit2Model.connection.show_variable("collation_connection") + assert_equal "utf8mb4_unicode_ci", @connection.show_variable("collation_connection") + assert_equal "utf8mb4_general_ci", ARUnit2Model.connection.show_variable("collation_connection") end def test_mysql_default_in_strict_mode @@ -170,6 +170,8 @@ class Mysql2ConnectionTest < ActiveRecord::Mysql2TestCase end def test_logs_name_show_variable + ActiveRecord::Base.connection.materialize_transactions + @subscriber.logged.clear @connection.show_variable "foo" assert_equal "SCHEMA", @subscriber.logged[0][1] end diff --git a/activerecord/test/cases/adapters/postgresql/active_schema_test.rb b/activerecord/test/cases/adapters/postgresql/active_schema_test.rb index 308ad1d854..afd422881b 100644 --- a/activerecord/test/cases/adapters/postgresql/active_schema_test.rb +++ b/activerecord/test/cases/adapters/postgresql/active_schema_test.rb @@ -4,6 +4,8 @@ require "cases/helper" class PostgresqlActiveSchemaTest < ActiveRecord::PostgreSQLTestCase def setup + ActiveRecord::Base.connection.materialize_transactions + ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.class_eval do def execute(sql, name = nil) sql end end diff --git a/activerecord/test/cases/adapters/postgresql/bytea_test.rb b/activerecord/test/cases/adapters/postgresql/bytea_test.rb index 64bb6906cd..3988c2adca 100644 --- a/activerecord/test/cases/adapters/postgresql/bytea_test.rb +++ b/activerecord/test/cases/adapters/postgresql/bytea_test.rb @@ -49,7 +49,7 @@ class PostgresqlByteaTest < ActiveRecord::PostgreSQLTestCase end def test_type_cast_binary_value - data = "\u001F\x8B".dup.force_encoding("BINARY") + data = (+"\u001F\x8B").force_encoding("BINARY") assert_equal(data, @type.deserialize(data)) end diff --git a/activerecord/test/cases/adapters/postgresql/connection_test.rb b/activerecord/test/cases/adapters/postgresql/connection_test.rb index 54b0dde7dc..70aa189893 100644 --- a/activerecord/test/cases/adapters/postgresql/connection_test.rb +++ b/activerecord/test/cases/adapters/postgresql/connection_test.rb @@ -15,8 +15,9 @@ module ActiveRecord def setup super @subscriber = SQLSubscriber.new - @subscription = ActiveSupport::Notifications.subscribe("sql.active_record", @subscriber) @connection = ActiveRecord::Base.connection + @connection.materialize_transactions + @subscription = ActiveSupport::Notifications.subscribe("sql.active_record", @subscriber) end def teardown diff --git a/activerecord/test/cases/adapters/postgresql/money_test.rb b/activerecord/test/cases/adapters/postgresql/money_test.rb index 61e75e772d..75e5aaed53 100644 --- a/activerecord/test/cases/adapters/postgresql/money_test.rb +++ b/activerecord/test/cases/adapters/postgresql/money_test.rb @@ -52,10 +52,10 @@ class PostgresqlMoneyTest < ActiveRecord::PostgreSQLTestCase def test_money_type_cast type = PostgresqlMoney.type_for_attribute("wealth") - assert_equal(12345678.12, type.cast("$12,345,678.12".dup)) - assert_equal(12345678.12, type.cast("$12.345.678,12".dup)) - assert_equal(-1.15, type.cast("-$1.15".dup)) - assert_equal(-2.25, type.cast("($2.25)".dup)) + assert_equal(12345678.12, type.cast(+"$12,345,678.12")) + assert_equal(12345678.12, type.cast(+"$12.345.678,12")) + assert_equal(-1.15, type.cast(+"-$1.15")) + assert_equal(-2.25, type.cast(+"($2.25)")) end def test_schema_dumping @@ -65,7 +65,7 @@ class PostgresqlMoneyTest < ActiveRecord::PostgreSQLTestCase end def test_create_and_update_money - money = PostgresqlMoney.create(wealth: "987.65".dup) + money = PostgresqlMoney.create(wealth: +"987.65") assert_equal 987.65, money.wealth new_value = BigDecimal("123.45") diff --git a/activerecord/test/cases/adapters/postgresql/referential_integrity_test.rb b/activerecord/test/cases/adapters/postgresql/referential_integrity_test.rb index 0bcc214c24..ba477c63f4 100644 --- a/activerecord/test/cases/adapters/postgresql/referential_integrity_test.rb +++ b/activerecord/test/cases/adapters/postgresql/referential_integrity_test.rb @@ -101,7 +101,7 @@ class PostgreSQLReferentialIntegrityTest < ActiveRecord::PostgreSQLTestCase @connection.extend ProgrammerMistake assert_raises ArgumentError do - @connection.disable_referential_integrity {} + @connection.disable_referential_integrity { } end end diff --git a/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb b/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb index d1d4d545a3..89052019f8 100644 --- a/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb +++ b/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb @@ -87,7 +87,7 @@ module ActiveRecord def test_connection_no_db assert_raises(ArgumentError) do - Base.sqlite3_connection {} + Base.sqlite3_connection { } end end @@ -167,7 +167,7 @@ module ActiveRecord data binary ) eosql - str = "\x80".dup.force_encoding("ASCII-8BIT") + str = (+"\x80").force_encoding("ASCII-8BIT") binary = DualEncoding.new name: "いただきます!", data: str binary.save! assert_equal str, binary.data @@ -176,7 +176,7 @@ module ActiveRecord end def test_type_cast_should_not_mutate_encoding - name = "hello".dup.force_encoding(Encoding::ASCII_8BIT) + name = (+"hello").force_encoding(Encoding::ASCII_8BIT) Owner.create(name: name) assert_equal Encoding::ASCII_8BIT, name.encoding ensure @@ -345,6 +345,42 @@ module ActiveRecord end end + if ActiveRecord::Base.connection.supports_expression_index? + def test_expression_index + with_example_table do + @conn.add_index "ex", "max(id, number)", name: "expression" + index = @conn.indexes("ex").find { |idx| idx.name == "expression" } + assert_equal "max(id, number)", index.columns + end + end + + def test_expression_index_with_where + with_example_table do + @conn.add_index "ex", "id % 10, max(id, number)", name: "expression", where: "id > 1000" + index = @conn.indexes("ex").find { |idx| idx.name == "expression" } + assert_equal "id % 10, max(id, number)", index.columns + assert_equal "id > 1000", index.where + end + end + + def test_complicated_expression + with_example_table do + @conn.execute "CREATE INDEX expression ON ex (id % 10, (CASE WHEN number > 0 THEN max(id, number) END))WHERE(id > 1000)" + index = @conn.indexes("ex").find { |idx| idx.name == "expression" } + assert_equal "id % 10, (CASE WHEN number > 0 THEN max(id, number) END)", index.columns + assert_equal "(id > 1000)", index.where + end + end + + def test_not_everything_an_expression + with_example_table do + @conn.add_index "ex", "id, max(id, number)", name: "expression" + index = @conn.indexes("ex").find { |idx| idx.name == "expression" } + assert_equal "id, max(id, number)", index.columns + end + end + end + def test_primary_key with_example_table do assert_equal "id", @conn.primary_key("ex") diff --git a/activerecord/test/cases/arel/collectors/sql_string_test.rb b/activerecord/test/cases/arel/collectors/sql_string_test.rb index 380573494d..443c7eb54b 100644 --- a/activerecord/test/cases/arel/collectors/sql_string_test.rb +++ b/activerecord/test/cases/arel/collectors/sql_string_test.rb @@ -19,27 +19,21 @@ module Arel collect(node).value end - def ast_with_binds(bv) + def ast_with_binds table = Table.new(:users) manager = Arel::SelectManager.new table - manager.where(table[:age].eq(bv)) - manager.where(table[:name].eq(bv)) + manager.where(table[:age].eq(Nodes::BindParam.new("hello"))) + manager.where(table[:name].eq(Nodes::BindParam.new("world"))) manager.ast end def test_compile - bv = Nodes::BindParam.new(1) - collector = collect ast_with_binds bv - - sql = collector.compile ["hello", "world"] + sql = compile(ast_with_binds) assert_equal 'SELECT FROM "users" WHERE "users"."age" = ? AND "users"."name" = ?', sql end def test_returned_sql_uses_utf8_encoding - bv = Nodes::BindParam.new(1) - collector = collect ast_with_binds bv - - sql = collector.compile ["hello", "world"] + sql = compile(ast_with_binds) assert_equal sql.encoding, Encoding::UTF_8 end end diff --git a/activerecord/test/cases/arel/delete_manager_test.rb b/activerecord/test/cases/arel/delete_manager_test.rb index 17a5271039..0bad02f4d2 100644 --- a/activerecord/test/cases/arel/delete_manager_test.rb +++ b/activerecord/test/cases/arel/delete_manager_test.rb @@ -15,6 +15,7 @@ module Arel dm = Arel::DeleteManager.new dm.take 10 dm.from table + dm.key = table[:id] assert_match(/LIMIT 10/, dm.to_sql) end diff --git a/activerecord/test/cases/arel/select_manager_test.rb b/activerecord/test/cases/arel/select_manager_test.rb index c17487ae88..5220950905 100644 --- a/activerecord/test/cases/arel/select_manager_test.rb +++ b/activerecord/test/cases/arel/select_manager_test.rb @@ -131,7 +131,7 @@ module Arel right = table.alias mgr = table.from mgr.join(right).on("omg") - mgr.to_sql.must_be_like %{ SELECT FROM "users" INNER JOIN "users" "users_2" ON omg } + mgr.to_sql.must_be_like %{ SELECT FROM "users" INNER JOIN "users" "users_2" ON omg } end it "converts to sqlliterals with multiple items" do @@ -139,7 +139,7 @@ module Arel right = table.alias mgr = table.from mgr.join(right).on("omg", "123") - mgr.to_sql.must_be_like %{ SELECT FROM "users" INNER JOIN "users" "users_2" ON omg AND 123 } + mgr.to_sql.must_be_like %{ SELECT FROM "users" INNER JOIN "users" "users_2" ON omg AND 123 } end end end diff --git a/activerecord/test/cases/arel/visitors/depth_first_test.rb b/activerecord/test/cases/arel/visitors/depth_first_test.rb index 3baccc7316..f94ad521d7 100644 --- a/activerecord/test/cases/arel/visitors/depth_first_test.rb +++ b/activerecord/test/cases/arel/visitors/depth_first_test.rb @@ -33,7 +33,6 @@ module Arel Arel::Nodes::Ordering, Arel::Nodes::StringJoin, Arel::Nodes::UnqualifiedColumn, - Arel::Nodes::Top, Arel::Nodes::Limit, Arel::Nodes::Else, ].each do |klass| diff --git a/activerecord/test/cases/arel/visitors/dot_test.rb b/activerecord/test/cases/arel/visitors/dot_test.rb index 98f3bab620..6b3c132f83 100644 --- a/activerecord/test/cases/arel/visitors/dot_test.rb +++ b/activerecord/test/cases/arel/visitors/dot_test.rb @@ -37,7 +37,6 @@ module Arel Arel::Nodes::Offset, Arel::Nodes::Ordering, Arel::Nodes::UnqualifiedColumn, - Arel::Nodes::Top, Arel::Nodes::Limit, ].each do |klass| define_method("test_#{klass.name.gsub('::', '_')}") do diff --git a/activerecord/test/cases/arel/visitors/to_sql_test.rb b/activerecord/test/cases/arel/visitors/to_sql_test.rb index e8ac50bfa3..b6426a211e 100644 --- a/activerecord/test/cases/arel/visitors/to_sql_test.rb +++ b/activerecord/test/cases/arel/visitors/to_sql_test.rb @@ -221,7 +221,7 @@ module Arel sql.must_be_like "foo AS bar" end - it "should visit_Bignum" do + it "should visit_Integer" do compile 8787878092 end diff --git a/activerecord/test/cases/associations/belongs_to_associations_test.rb b/activerecord/test/cases/associations/belongs_to_associations_test.rb index 0cc4ed7127..1fca1be181 100644 --- a/activerecord/test/cases/associations/belongs_to_associations_test.rb +++ b/activerecord/test/cases/associations/belongs_to_associations_test.rb @@ -452,15 +452,39 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase end def test_belongs_to_counter_with_assigning_nil - post = Post.find(1) - comment = Comment.find(1) + topic = Topic.create!(title: "debate") + reply = Reply.create!(title: "blah!", content: "world around!", topic: topic) - assert_equal post.id, comment.post_id - assert_equal 2, Post.find(post.id).comments.size + assert_equal topic.id, reply.parent_id + assert_equal 1, topic.reload.replies.size - comment.post = nil + reply.topic = nil + reply.reload - assert_equal 1, Post.find(post.id).comments.size + assert_equal topic.id, reply.parent_id + assert_equal 1, topic.reload.replies.size + + reply.topic = nil + reply.save! + + assert_equal 0, topic.reload.replies.size + end + + def test_belongs_to_counter_with_assigning_new_object + topic = Topic.create!(title: "debate") + reply = Reply.create!(title: "blah!", content: "world around!", topic: topic) + + assert_equal topic.id, reply.parent_id + assert_equal 1, topic.reload.replies_count + + topic2 = reply.build_topic(title: "debate2") + reply.save! + + assert_not_equal topic.id, reply.parent_id + assert_equal topic2.id, reply.parent_id + + assert_equal 0, topic.reload.replies_count + assert_equal 1, topic2.reload.replies_count end def test_belongs_to_with_primary_key_counter @@ -485,11 +509,13 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase assert_equal 0, debate2.reload.replies_count reply.topic_with_primary_key = debate2 + reply.save! assert_equal 0, debate.reload.replies_count assert_equal 1, debate2.reload.replies_count reply.topic_with_primary_key = nil + reply.save! assert_equal 0, debate.reload.replies_count assert_equal 0, debate2.reload.replies_count @@ -516,11 +542,13 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase assert_equal 1, Topic.find(topic2.id).replies.size reply1.topic = nil + reply1.save! assert_equal 0, Topic.find(topic1.id).replies.size assert_equal 0, Topic.find(topic2.id).replies.size reply1.topic = topic1 + reply1.save! assert_equal 1, Topic.find(topic1.id).replies.size assert_equal 0, Topic.find(topic2.id).replies.size @@ -550,7 +578,11 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase def test_belongs_to_counter_after_save topic = Topic.create!(title: "monday night") - topic.replies.create!(title: "re: monday night", content: "football") + + assert_queries(2) do + topic.replies.create!(title: "re: monday night", content: "football") + end + assert_equal 1, Topic.find(topic.id)[:replies_count] topic.save! @@ -584,8 +616,10 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase debate.touch(time: time) debate2.touch(time: time) - reply.parent_title = "debate" - reply.save! + assert_queries(3) do + reply.parent_title = "debate" + reply.save! + end assert_operator debate.reload.updated_at, :>, time assert_operator debate2.reload.updated_at, :>, time @@ -593,7 +627,10 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase debate.touch(time: time) debate2.touch(time: time) - reply.topic_with_primary_key = debate2 + assert_queries(3) do + reply.topic_with_primary_key = debate2 + reply.save! + end assert_operator debate.reload.updated_at, :>, time assert_operator debate2.reload.updated_at, :>, time @@ -772,6 +809,7 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase reply = Reply.create(title: "re: zoom", content: "speedy quick!") reply.topic = topic + reply.save! assert_equal 1, topic.reload[:replies_count] assert_equal 1, topic.replies.size @@ -827,6 +865,7 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase silly = SillyReply.create(title: "gaga", content: "boo-boo") silly.reply = reply + silly.save! assert_equal 1, reply.reload[:replies_count] assert_equal 1, reply.replies.size diff --git a/activerecord/test/cases/associations/cascaded_eager_loading_test.rb b/activerecord/test/cases/associations/cascaded_eager_loading_test.rb index e717621928..ba2104eb26 100644 --- a/activerecord/test/cases/associations/cascaded_eager_loading_test.rb +++ b/activerecord/test/cases/associations/cascaded_eager_loading_test.rb @@ -117,9 +117,8 @@ class CascadedEagerLoadingTest < ActiveRecord::TestCase end def test_eager_association_loading_with_has_many_sti_and_subclasses - silly = SillyReply.new(title: "gaga", content: "boo-boo", parent_id: 1) - silly.parent_id = 1 - assert silly.save + reply = Reply.new(title: "gaga", content: "boo-boo", parent_id: 1) + assert reply.save topics = Topic.all.merge!(includes: :replies, order: ["topics.id", "replies_topics.id"]).to_a assert_no_queries do diff --git a/activerecord/test/cases/associations/eager_test.rb b/activerecord/test/cases/associations/eager_test.rb index 5b8d4722af..79b3b4a6ad 100644 --- a/activerecord/test/cases/associations/eager_test.rb +++ b/activerecord/test/cases/associations/eager_test.rb @@ -18,6 +18,7 @@ require "models/job" require "models/subscriber" require "models/subscription" require "models/book" +require "models/citation" require "models/developer" require "models/computer" require "models/project" @@ -29,6 +30,18 @@ require "models/sponsor" require "models/mentor" require "models/contract" +class EagerLoadingTooManyIdsTest < ActiveRecord::TestCase + fixtures :citations + + def test_preloading_too_many_ids + assert_equal Citation.count, Citation.preload(:citations).to_a.size + end + + def test_eager_loading_too_may_ids + assert_equal Citation.count, Citation.eager_load(:citations).offset(0).size + end +end + class EagerAssociationTest < ActiveRecord::TestCase fixtures :posts, :comments, :authors, :essays, :author_addresses, :categories, :categories_posts, :companies, :accounts, :tags, :taggings, :people, :readers, :categorizations, @@ -1571,8 +1584,9 @@ class EagerAssociationTest < ActiveRecord::TestCase # CollectionProxy#reader is expensive, so the preloader avoids calling it. test "preloading has_many_through association avoids calling association.reader" do - ActiveRecord::Associations::HasManyAssociation.any_instance.expects(:reader).never - Author.preload(:readonly_comments).first! + assert_not_called_on_instance_of(ActiveRecord::Associations::HasManyAssociation, :reader) do + Author.preload(:readonly_comments).first! + end end test "preloading through a polymorphic association doesn't require the association to exist" do @@ -1604,6 +1618,32 @@ class EagerAssociationTest < ActiveRecord::TestCase end end + # Associations::Preloader#preloaders_on works with hash-like objects + test "preloading works with an object that responds to :to_hash" do + CustomHash = Class.new(Hash) + + assert_nothing_raised do + Post.preload(CustomHash.new(comments: [{ author: :essays }])).first + end + end + + # Associations::Preloader#preloaders_on works with string-like objects + test "preloading works with an object that responds to :to_str" do + CustomString = Class.new(String) + + assert_nothing_raised do + Post.preload(CustomString.new("comments")).first + end + end + + # Associations::Preloader#preloaders_on does not work with ranges + test "preloading fails when Range is passed" do + exception = assert_raises(ArgumentError) do + Post.preload(1..10).first + end + assert_equal("1..10 was not recognized for preload", exception.message) + end + private def find_all_ordered(klass, include = nil) klass.order("#{klass.table_name}.#{klass.primary_key}").includes(include).to_a diff --git a/activerecord/test/cases/associations/extension_test.rb b/activerecord/test/cases/associations/extension_test.rb index 5eacb5a3d8..aef8f31112 100644 --- a/activerecord/test/cases/associations/extension_test.rb +++ b/activerecord/test/cases/associations/extension_test.rb @@ -89,6 +89,6 @@ class AssociationsExtensionsTest < ActiveRecord::TestCase private def extend!(model) - ActiveRecord::Associations::Builder::HasMany.define_extensions(model, :association_name) {} + ActiveRecord::Associations::Builder::HasMany.define_extensions(model, :association_name) { } end end 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 f414fbf64b..482302055d 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 @@ -781,7 +781,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase end def test_has_many_through_polymorphic_has_manys_works - assert_equal [10, 20].to_set, pirates(:redbeard).treasure_estimates.map(&:price).to_set + assert_equal ["$10.00", "$20.00"].to_set, pirates(:redbeard).treasure_estimates.map(&:price).to_set end def test_symbols_as_keys diff --git a/activerecord/test/cases/associations/has_many_associations_test.rb b/activerecord/test/cases/associations/has_many_associations_test.rb index 0ca902385a..0b44515e00 100644 --- a/activerecord/test/cases/associations/has_many_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_associations_test.rb @@ -27,7 +27,6 @@ require "models/categorization" require "models/minivan" require "models/speedometer" require "models/reference" -require "models/job" require "models/college" require "models/student" require "models/pirate" @@ -114,7 +113,7 @@ end class HasManyAssociationsTest < ActiveRecord::TestCase fixtures :accounts, :categories, :companies, :developers, :projects, :developers_projects, :topics, :authors, :author_addresses, :comments, - :posts, :readers, :taggings, :cars, :jobs, :tags, + :posts, :readers, :taggings, :cars, :tags, :categorizations, :zines, :interests def setup @@ -377,6 +376,27 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_equal invoice.id, line_item.invoice_id end + class SpecialAuthor < ActiveRecord::Base + self.table_name = "authors" + has_many :books, class_name: "SpecialBook", foreign_key: :author_id + end + + class SpecialBook < ActiveRecord::Base + self.table_name = "books" + + belongs_to :author + enum read_status: { unread: 0, reading: 2, read: 3, forgotten: nil } + end + + def test_association_enum_works_properly + author = SpecialAuthor.create!(name: "Test") + book = SpecialBook.create!(read_status: "reading") + author.books << book + + assert_equal "reading", book.read_status + assert_not_equal 0, SpecialAuthor.joins(:books).where(books: { read_status: "reading" }).count + end + # When creating objects on the association, we must not do it within a scope (even though it # would be convenient), because this would cause that scope to be applied to any callbacks etc. def test_build_and_create_should_not_happen_within_scope @@ -1177,6 +1197,38 @@ class HasManyAssociationsTest < ActiveRecord::TestCase assert_equal 2, topic.reload.replies.size end + def test_counter_cache_updates_in_memory_after_update_with_inverse_of_disabled + topic = Topic.create!(title: "Zoom-zoom-zoom") + + assert_equal 0, topic.replies_count + + reply1 = Reply.create!(title: "re: zoom", content: "speedy quick!") + reply2 = Reply.create!(title: "re: zoom 2", content: "OMG lol!") + + assert_queries(4) do + topic.replies << [reply1, reply2] + end + + assert_equal 2, topic.replies_count + assert_equal 2, topic.reload.replies_count + end + + def test_counter_cache_updates_in_memory_after_update_with_inverse_of_enabled + category = Category.create!(name: "Counter Cache") + + assert_nil category.categorizations_count + + categorization1 = Categorization.create! + categorization2 = Categorization.create! + + assert_queries(4) do + category.categorizations << [categorization1, categorization2] + end + + assert_equal 2, category.categorizations_count + assert_equal 2, category.reload.categorizations_count + end + def test_pushing_association_updates_counter_cache topic = Topic.order("id ASC").first reply = Reply.create! @@ -2134,21 +2186,29 @@ class HasManyAssociationsTest < ActiveRecord::TestCase end def test_defining_has_many_association_with_delete_all_dependency_lazily_evaluates_target_class - ActiveRecord::Reflection::AssociationReflection.any_instance.expects(:class_name).never - class_eval(<<-EOF, __FILE__, __LINE__ + 1) - class DeleteAllModel < ActiveRecord::Base - has_many :nonentities, :dependent => :delete_all - end - EOF + assert_not_called_on_instance_of( + ActiveRecord::Reflection::AssociationReflection, + :class_name, + ) do + class_eval(<<-EOF, __FILE__, __LINE__ + 1) + class DeleteAllModel < ActiveRecord::Base + has_many :nonentities, :dependent => :delete_all + end + EOF + end end def test_defining_has_many_association_with_nullify_dependency_lazily_evaluates_target_class - ActiveRecord::Reflection::AssociationReflection.any_instance.expects(:class_name).never - class_eval(<<-EOF, __FILE__, __LINE__ + 1) - class NullifyModel < ActiveRecord::Base - has_many :nonentities, :dependent => :nullify - end - EOF + assert_not_called_on_instance_of( + ActiveRecord::Reflection::AssociationReflection, + :class_name, + ) do + class_eval(<<-EOF, __FILE__, __LINE__ + 1) + class NullifyModel < ActiveRecord::Base + has_many :nonentities, :dependent => :nullify + end + EOF + end end def test_attributes_are_being_set_when_initialized_from_has_many_association_with_where_clause 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 d5573b6d02..442f4a93d4 100644 --- a/activerecord/test/cases/associations/has_many_through_associations_test.rb +++ b/activerecord/test/cases/associations/has_many_through_associations_test.rb @@ -71,6 +71,15 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase club1.members.sort_by(&:id) end + def test_preload_multiple_instances_of_the_same_record + club = Club.create!(name: "Aaron cool banana club") + Membership.create! club: club, member: Member.create!(name: "Aaron") + Membership.create! club: club, member: Member.create!(name: "Bob") + + preloaded_clubs = Club.joins(:memberships).preload(:membership).to_a + assert_no_queries { preloaded_clubs.each(&:membership) } + end + def test_ordered_has_many_through person_prime = Class.new(ActiveRecord::Base) do def self.name; "Person"; end diff --git a/activerecord/test/cases/associations/has_one_associations_test.rb b/activerecord/test/cases/associations/has_one_associations_test.rb index d7e898a1c0..9eea34d2b9 100644 --- a/activerecord/test/cases/associations/has_one_associations_test.rb +++ b/activerecord/test/cases/associations/has_one_associations_test.rb @@ -661,6 +661,8 @@ class HasOneAssociationsTest < ActiveRecord::TestCase self.table_name = "books" belongs_to :author, class_name: "SpecialAuthor" has_one :subscription, class_name: "SpecialSupscription", foreign_key: "subscriber_id" + + enum status: [:proposed, :written, :published] end class SpecialAuthor < ActiveRecord::Base @@ -678,6 +680,7 @@ class HasOneAssociationsTest < ActiveRecord::TestCase book = SpecialBook.create!(status: "published") author.book = book + assert_equal "published", book.status assert_not_equal 0, SpecialAuthor.joins(:book).where(books: { status: "published" }).count end diff --git a/activerecord/test/cases/associations/inverse_associations_test.rb b/activerecord/test/cases/associations/inverse_associations_test.rb index da3a42e2b5..eb4dc73423 100644 --- a/activerecord/test/cases/associations/inverse_associations_test.rb +++ b/activerecord/test/cases/associations/inverse_associations_test.rb @@ -20,6 +20,8 @@ require "models/company" require "models/project" require "models/author" require "models/post" +require "models/department" +require "models/hotel" class AutomaticInverseFindingTests < ActiveRecord::TestCase fixtures :ratings, :comments, :cars @@ -724,6 +726,16 @@ class InversePolymorphicBelongsToTests < ActiveRecord::TestCase # fails because Interest does have the correct inverse_of assert_raise(ActiveRecord::InverseOfAssociationNotFoundError) { Face.first.polymorphic_man = Interest.first } end + + def test_favors_has_one_associations_for_inverse_of + inverse_name = Post.reflect_on_association(:author).inverse_of.name + assert_equal :post, inverse_name + end + + def test_finds_inverse_of_for_plural_associations + inverse_name = Department.reflect_on_association(:hotel).inverse_of.name + assert_equal :departments, inverse_name + end end # NOTE - these tests might not be meaningful, ripped as they were from the parental_control plugin diff --git a/activerecord/test/cases/associations_test.rb b/activerecord/test/cases/associations_test.rb index 739eb02e0c..081da95df7 100644 --- a/activerecord/test/cases/associations_test.rb +++ b/activerecord/test/cases/associations_test.rb @@ -80,7 +80,7 @@ class AssociationsTest < ActiveRecord::TestCase def test_force_reload firm = Firm.new("name" => "A New Firm, Inc") firm.save - firm.clients.each {} # forcing to load all clients + firm.clients.each { } # forcing to load all clients assert firm.clients.empty?, "New firm shouldn't have client objects" assert_equal 0, firm.clients.size, "New firm should have 0 clients" diff --git a/activerecord/test/cases/attributes_test.rb b/activerecord/test/cases/attributes_test.rb index 3bc56694be..2632aec7ab 100644 --- a/activerecord/test/cases/attributes_test.rb +++ b/activerecord/test/cases/attributes_test.rb @@ -148,6 +148,20 @@ module ActiveRecord assert_equal 2, klass.new.counter end + test "procs for default values are evaluated even after column_defaults is called" do + klass = Class.new(OverloadedType) do + @@counter = 0 + attribute :counter, :integer, default: -> { @@counter += 1 } + end + + assert_equal 1, klass.new.counter + + # column_defaults will increment the counter since the proc is called + klass.column_defaults + + assert_equal 3, klass.new.counter + end + test "procs are memoized before type casting" do klass = Class.new(OverloadedType) do @@counter = 0 diff --git a/activerecord/test/cases/autosave_association_test.rb b/activerecord/test/cases/autosave_association_test.rb index ade1f4b44d..db3a58eba9 100644 --- a/activerecord/test/cases/autosave_association_test.rb +++ b/activerecord/test/cases/autosave_association_test.rb @@ -14,6 +14,7 @@ require "models/line_item" require "models/order" require "models/parrot" require "models/pirate" +require "models/project" require "models/ship" require "models/ship_part" require "models/tag" @@ -558,6 +559,13 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociation < ActiveRecord::TestCa assert_equal no_of_clients + 1, Client.count end + def test_parent_should_save_children_record_with_foreign_key_validation_set_in_before_save_callback + company = NewlyContractedCompany.new(name: "test") + + assert company.save + assert_not_empty company.reload.new_contracts + end + def test_parent_should_not_get_saved_with_duplicate_children_records assert_no_difference "Reply.count" do assert_no_difference "SillyUniqueReply.count" do @@ -568,7 +576,13 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociation < ActiveRecord::TestCa ]) assert_not reply.save - assert_not_empty reply.errors + assert_equal ["is invalid"], reply.errors[:silly_unique_replies] + assert_empty reply.silly_unique_replies.first.errors + + assert_equal( + ["has already been taken"], + reply.silly_unique_replies.last.errors[:content] + ) end end end @@ -1774,3 +1788,21 @@ class TestAutosaveAssociationOnAHasManyAssociationWithInverse < ActiveRecord::Te assert_equal 1, comment.post_comments_count end end + +class TestAutosaveAssociationOnAHasManyAssociationDefinedInSubclassWithAcceptsNestedAttributes < ActiveRecord::TestCase + def test_should_update_children_when_asssociation_redefined_in_subclass + agency = Agency.create!(name: "Agency") + valid_project = Project.create!(firm: agency, name: "Initial") + agency.update!( + "projects_attributes" => { + "0" => { + "name" => "Updated", + "id" => valid_project.id + } + } + ) + valid_project.reload + + assert_equal "Updated", valid_project.name + end +end diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb index d216fe16fa..09e5517449 100644 --- a/activerecord/test/cases/base_test.rb +++ b/activerecord/test/cases/base_test.rb @@ -282,11 +282,13 @@ class BasicsTest < ActiveRecord::TestCase end def test_initialize_with_invalid_attribute - Topic.new("title" => "test", - "last_read(1i)" => "2005", "last_read(2i)" => "2", "last_read(3i)" => "31") - rescue ActiveRecord::MultiparameterAssignmentErrors => ex + ex = assert_raise(ActiveRecord::MultiparameterAssignmentErrors) do + Topic.new("title" => "test", + "written_on(4i)" => "16", "written_on(5i)" => "24", "written_on(6i)" => "00") + end + assert_equal(1, ex.errors.size) - assert_equal("last_read", ex.errors[0].attribute) + assert_equal("written_on", ex.errors[0].attribute) end def test_create_after_initialize_without_block @@ -853,8 +855,7 @@ class BasicsTest < ActiveRecord::TestCase assert_equal company, Company.find(company.id) end - # TODO: extend defaults tests to other databases! - if current_adapter?(:PostgreSQLAdapter) + if current_adapter?(:PostgreSQLAdapter, :Mysql2Adapter, :SQLite3Adapter) def test_default with_timezone_config default: :local do default = Default.new @@ -866,7 +867,10 @@ class BasicsTest < ActiveRecord::TestCase # char types assert_equal "Y", default.char1 assert_equal "a varchar field", default.char2 - assert_equal "a text field", default.char3 + # Mysql text type can't have default value + unless current_adapter?(:Mysql2Adapter) + assert_equal "a text field", default.char3 + end end end end diff --git a/activerecord/test/cases/batches_test.rb b/activerecord/test/cases/batches_test.rb index c8163901c6..d21218a997 100644 --- a/activerecord/test/cases/batches_test.rb +++ b/activerecord/test/cases/batches_test.rb @@ -24,7 +24,7 @@ class EachTest < ActiveRecord::TestCase def test_each_should_not_return_query_chain_and_execute_only_one_query assert_queries(1) do - result = Post.find_each(batch_size: 100000) {} + result = Post.find_each(batch_size: 100000) { } assert_nil result end end @@ -155,7 +155,7 @@ class EachTest < ActiveRecord::TestCase end def test_find_in_batches_should_not_use_records_after_yielding_them_in_case_original_array_is_modified - not_a_post = "not a post".dup + not_a_post = +"not a post" def not_a_post.id; end not_a_post.stub(:id, -> { raise StandardError.new("not_a_post had #id called on it") }) do assert_nothing_raised do @@ -183,7 +183,7 @@ class EachTest < ActiveRecord::TestCase def test_find_in_batches_should_error_on_ignore_the_order assert_raise(ArgumentError) do - PostWithDefaultScope.find_in_batches(error_on_ignore: true) {} + PostWithDefaultScope.find_in_batches(error_on_ignore: true) { } end end @@ -192,7 +192,7 @@ class EachTest < ActiveRecord::TestCase prev = ActiveRecord::Base.error_on_ignored_order ActiveRecord::Base.error_on_ignored_order = true assert_nothing_raised do - PostWithDefaultScope.find_in_batches(error_on_ignore: false) {} + PostWithDefaultScope.find_in_batches(error_on_ignore: false) { } end ensure # Set back to default @@ -204,7 +204,7 @@ class EachTest < ActiveRecord::TestCase prev = ActiveRecord::Base.error_on_ignored_order ActiveRecord::Base.error_on_ignored_order = true assert_raise(ArgumentError) do - PostWithDefaultScope.find_in_batches() {} + PostWithDefaultScope.find_in_batches() { } end ensure # Set back to default @@ -213,7 +213,7 @@ class EachTest < ActiveRecord::TestCase def test_find_in_batches_should_not_error_by_default assert_nothing_raised do - PostWithDefaultScope.find_in_batches() {} + PostWithDefaultScope.find_in_batches() { } end end @@ -228,7 +228,7 @@ class EachTest < ActiveRecord::TestCase def test_find_in_batches_should_not_modify_passed_options assert_nothing_raised do - Post.find_in_batches({ batch_size: 42, start: 1 }.freeze) {} + Post.find_in_batches({ batch_size: 42, start: 1 }.freeze) { } end end @@ -420,7 +420,7 @@ class EachTest < ActiveRecord::TestCase end def test_in_batches_should_not_use_records_after_yielding_them_in_case_original_array_is_modified - not_a_post = "not a post".dup + not_a_post = +"not a post" def not_a_post.id raise StandardError.new("not_a_post had #id called on it") end @@ -446,7 +446,7 @@ class EachTest < ActiveRecord::TestCase def test_in_batches_should_not_modify_passed_options assert_nothing_raised do - Post.in_batches({ of: 42, start: 1 }.freeze) {} + Post.in_batches({ of: 42, start: 1 }.freeze) { } end end @@ -597,15 +597,15 @@ class EachTest < ActiveRecord::TestCase table: table_alias, predicate_builder: predicate_builder ) - posts.find_each {} + posts.find_each { } end 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 {} + Post.find_each { } + Post.find_each { } end end end @@ -624,8 +624,8 @@ class EachTest < ActiveRecord::TestCase 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 {} + Post.find_in_batches { } + Post.find_in_batches { } end end end @@ -644,8 +644,8 @@ class EachTest < ActiveRecord::TestCase 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 {} + Post.in_batches { } + Post.in_batches { } end end end diff --git a/activerecord/test/cases/binary_test.rb b/activerecord/test/cases/binary_test.rb index d5376ece69..58abdb47f7 100644 --- a/activerecord/test/cases/binary_test.rb +++ b/activerecord/test/cases/binary_test.rb @@ -12,7 +12,7 @@ unless current_adapter?(:DB2Adapter) FIXTURES = %w(flowers.jpg example.log test.txt) def test_mixed_encoding - str = "\x80".dup + str = +"\x80" str.force_encoding("ASCII-8BIT") binary = Binary.new name: "いただきます!", data: str diff --git a/activerecord/test/cases/bind_parameter_test.rb b/activerecord/test/cases/bind_parameter_test.rb index 91cc49385c..9c1f7aaef2 100644 --- a/activerecord/test/cases/bind_parameter_test.rb +++ b/activerecord/test/cases/bind_parameter_test.rb @@ -34,6 +34,12 @@ if ActiveRecord::Base.connection.prepared_statements ActiveSupport::Notifications.unsubscribe(@subscription) end + def test_too_many_binds + bind_params_length = @connection.send(:bind_params_length) + topics = Topic.where(id: (1 .. bind_params_length + 1).to_a) + assert_equal Topic.count, topics.count + end + def test_bind_from_join_in_subquery subquery = Author.joins(:thinking_posts).where(name: "David") scope = Author.from(subquery, "authors").where(id: 1) diff --git a/activerecord/test/cases/callbacks_test.rb b/activerecord/test/cases/callbacks_test.rb index 253c3099d6..0ea3fb86a6 100644 --- a/activerecord/test/cases/callbacks_test.rb +++ b/activerecord/test/cases/callbacks_test.rb @@ -480,7 +480,7 @@ class CallbacksTest < ActiveRecord::TestCase def test_before_save_doesnt_allow_on_option exception = assert_raises ArgumentError do Class.new(ActiveRecord::Base) do - before_save(on: :create) {} + before_save(on: :create) { } end end assert_equal "Unknown key: :on. Valid keys are: :if, :unless, :prepend", exception.message @@ -489,7 +489,7 @@ class CallbacksTest < ActiveRecord::TestCase def test_around_save_doesnt_allow_on_option exception = assert_raises ArgumentError do Class.new(ActiveRecord::Base) do - around_save(on: :create) {} + around_save(on: :create) { } end end assert_equal "Unknown key: :on. Valid keys are: :if, :unless, :prepend", exception.message @@ -498,7 +498,7 @@ class CallbacksTest < ActiveRecord::TestCase def test_after_save_doesnt_allow_on_option exception = assert_raises ArgumentError do Class.new(ActiveRecord::Base) do - after_save(on: :create) {} + after_save(on: :create) { } end end assert_equal "Unknown key: :on. Valid keys are: :if, :unless, :prepend", exception.message diff --git a/activerecord/test/cases/connection_adapters/connection_handler_test.rb b/activerecord/test/cases/connection_adapters/connection_handler_test.rb index b8e623f17b..5e3447efde 100644 --- a/activerecord/test/cases/connection_adapters/connection_handler_test.rb +++ b/activerecord/test/cases/connection_adapters/connection_handler_test.rb @@ -28,13 +28,16 @@ module ActiveRecord end def test_establish_connection_uses_spec_name + old_config = ActiveRecord::Base.configurations config = { "readonly" => { "adapter" => "sqlite3" } } - resolver = ConnectionAdapters::ConnectionSpecification::Resolver.new(config) + ActiveRecord::Base.configurations = config + resolver = ConnectionAdapters::ConnectionSpecification::Resolver.new(ActiveRecord::Base.configurations) spec = resolver.spec(:readonly) @handler.establish_connection(spec.to_hash) assert_not_nil @handler.retrieve_connection_pool("readonly") ensure + ActiveRecord::Base.configurations = old_config @handler.remove_connection("readonly") end @@ -148,6 +151,30 @@ module ActiveRecord ActiveRecord::Base.configurations = @prev_configs end + def test_symbolized_configurations_assignment + @prev_configs = ActiveRecord::Base.configurations + config = { + development: { + primary: { + adapter: "sqlite3", + database: "db/development.sqlite3", + }, + }, + test: { + primary: { + adapter: "sqlite3", + database: "db/test.sqlite3", + }, + }, + } + ActiveRecord::Base.configurations = config + ActiveRecord::Base.configurations.configs_for.each do |db_config| + assert_instance_of ActiveRecord::DatabaseConfigurations::HashConfig, db_config + end + ensure + ActiveRecord::Base.configurations = @prev_configs + end + def test_retrieve_connection assert @handler.retrieve_connection(@spec_name) end diff --git a/activerecord/test/cases/connection_adapters/merge_and_resolve_default_url_config_test.rb b/activerecord/test/cases/connection_adapters/merge_and_resolve_default_url_config_test.rb index 1b64324cc4..06c1c51724 100644 --- a/activerecord/test/cases/connection_adapters/merge_and_resolve_default_url_config_test.rb +++ b/activerecord/test/cases/connection_adapters/merge_and_resolve_default_url_config_test.rb @@ -18,11 +18,14 @@ module ActiveRecord end def resolve_config(config) - ActiveRecord::ConnectionHandling::MergeAndResolveDefaultUrlConfig.new(config).resolve + configs = ActiveRecord::DatabaseConfigurations.new(config) + configs.to_h end def resolve_spec(spec, config) - ConnectionSpecification::Resolver.new(resolve_config(config)).resolve(spec) + configs = ActiveRecord::DatabaseConfigurations.new(config) + resolver = ConnectionAdapters::ConnectionSpecification::Resolver.new(configs) + resolver.resolve(spec, spec) end def test_resolver_with_database_uri_and_current_env_symbol_key diff --git a/activerecord/test/cases/connection_management_test.rb b/activerecord/test/cases/connection_management_test.rb index 0941ee3309..b9b5cc0e28 100644 --- a/activerecord/test/cases/connection_management_test.rb +++ b/activerecord/test/cases/connection_management_test.rb @@ -106,7 +106,7 @@ module ActiveRecord def middleware(app) lambda do |env| a, b, c = executor.wrap { app.call(env) } - [a, b, Rack::BodyProxy.new(c) {}] + [a, b, Rack::BodyProxy.new(c) { }] end end end diff --git a/activerecord/test/cases/connection_pool_test.rb b/activerecord/test/cases/connection_pool_test.rb index 9ac03629c3..633d56e479 100644 --- a/activerecord/test/cases/connection_pool_test.rb +++ b/activerecord/test/cases/connection_pool_test.rb @@ -91,7 +91,9 @@ module ActiveRecord end def test_full_pool_exception + @pool.checkout_timeout = 0.001 # no need to delay test suite by waiting the whole full default timeout @pool.size.times { assert @pool.checkout } + assert_raises(ConnectionTimeoutError) do @pool.checkout end @@ -109,6 +111,44 @@ module ActiveRecord assert_equal connection, t.join.value end + def test_full_pool_blocking_shares_load_interlock + @pool.instance_variable_set(:@size, 1) + + load_interlock_latch = Concurrent::CountDownLatch.new + connection_latch = Concurrent::CountDownLatch.new + + able_to_get_connection = false + able_to_load = false + + thread_with_load_interlock = Thread.new do + ActiveSupport::Dependencies.interlock.running do + load_interlock_latch.count_down + connection_latch.wait + + @pool.with_connection do + able_to_get_connection = true + end + end + end + + thread_with_last_connection = Thread.new do + @pool.with_connection do + connection_latch.count_down + load_interlock_latch.wait + + ActiveSupport::Dependencies.interlock.loading do + able_to_load = true + end + end + end + + thread_with_load_interlock.join + thread_with_last_connection.join + + assert able_to_get_connection + assert able_to_load + end + def test_removing_releases_latch cs = @pool.size.times.map { @pool.checkout } t = Thread.new { @pool.checkout } @@ -156,6 +196,48 @@ module ActiveRecord @pool.connections.each { |conn| conn.close if conn.in_use? } end + def test_idle_timeout_configuration + @pool.disconnect! + spec = ActiveRecord::Base.connection_pool.spec + spec.config.merge!(idle_timeout: "0.02") + @pool = ConnectionPool.new(spec) + idle_conn = @pool.checkout + @pool.checkin(idle_conn) + + idle_conn.instance_variable_set( + :@idle_since, + Concurrent.monotonic_time - 0.01 + ) + + @pool.flush + assert_equal 1, @pool.connections.length + + idle_conn.instance_variable_set( + :@idle_since, + Concurrent.monotonic_time - 0.02 + ) + + @pool.flush + assert_equal 0, @pool.connections.length + end + + def test_disable_flush + @pool.disconnect! + spec = ActiveRecord::Base.connection_pool.spec + spec.config.merge!(idle_timeout: -5) + @pool = ConnectionPool.new(spec) + idle_conn = @pool.checkout + @pool.checkin(idle_conn) + + idle_conn.instance_variable_set( + :@idle_since, + Concurrent.monotonic_time - 1 + ) + + @pool.flush + assert_equal 1, @pool.connections.length + end + def test_flush idle_conn = @pool.checkout recent_conn = @pool.checkout @@ -166,9 +248,10 @@ module ActiveRecord assert_equal 3, @pool.connections.length - def idle_conn.seconds_idle - 1000 - end + idle_conn.instance_variable_set( + :@idle_since, + Concurrent.monotonic_time - 1000 + ) @pool.flush(30) @@ -578,7 +661,7 @@ module ActiveRecord end stuck_thread = Thread.new do - pool.with_connection {} + pool.with_connection { } end # wait for stuck_thread to get in queue diff --git a/activerecord/test/cases/connection_specification/resolver_test.rb b/activerecord/test/cases/connection_specification/resolver_test.rb index 5b80f16a44..72be14f507 100644 --- a/activerecord/test/cases/connection_specification/resolver_test.rb +++ b/activerecord/test/cases/connection_specification/resolver_test.rb @@ -7,11 +7,15 @@ module ActiveRecord class ConnectionSpecification class ResolverTest < ActiveRecord::TestCase def resolve(spec, config = {}) - Resolver.new(config).resolve(spec) + configs = ActiveRecord::DatabaseConfigurations.new(config) + resolver = ConnectionAdapters::ConnectionSpecification::Resolver.new(configs) + resolver.resolve(spec, spec) end def spec(spec, config = {}) - Resolver.new(config).spec(spec) + configs = ActiveRecord::DatabaseConfigurations.new(config) + resolver = ConnectionAdapters::ConnectionSpecification::Resolver.new(configs) + resolver.spec(spec) end def test_url_invalid_adapter diff --git a/activerecord/test/cases/core_test.rb b/activerecord/test/cases/core_test.rb index 6e7ae2efb4..f7fbf3ee8a 100644 --- a/activerecord/test/cases/core_test.rb +++ b/activerecord/test/cases/core_test.rb @@ -36,7 +36,7 @@ class CoreTest < ActiveRecord::TestCase def test_pretty_print_new topic = Topic.new - actual = "".dup + actual = +"" PP.pp(topic, StringIO.new(actual)) expected = <<~PRETTY #<Topic:0xXXXXXX @@ -65,7 +65,7 @@ class CoreTest < ActiveRecord::TestCase def test_pretty_print_persisted topic = topics(:first) - actual = "".dup + actual = +"" PP.pp(topic, StringIO.new(actual)) expected = <<~PRETTY #<Topic:0x\\w+ @@ -93,7 +93,7 @@ class CoreTest < ActiveRecord::TestCase def test_pretty_print_uninitialized topic = Topic.allocate - actual = "".dup + actual = +"" PP.pp(topic, StringIO.new(actual)) expected = "#<Topic:XXXXXX not initialized>\n" assert actual.start_with?(expected.split("XXXXXX").first) @@ -106,7 +106,7 @@ class CoreTest < ActiveRecord::TestCase "inspecting topic" end end - actual = "".dup + actual = +"" PP.pp(subtopic.new, StringIO.new(actual)) assert_equal "inspecting topic\n", actual end diff --git a/activerecord/test/cases/dirty_test.rb b/activerecord/test/cases/dirty_test.rb index 83cc2aa319..b1ebd20d6b 100644 --- a/activerecord/test/cases/dirty_test.rb +++ b/activerecord/test/cases/dirty_test.rb @@ -567,8 +567,6 @@ class DirtyTest < ActiveRecord::TestCase assert_not_nil pirate.previous_changes["updated_on"][1] assert_not pirate.previous_changes.key?("parrot_id") assert_not pirate.previous_changes.key?("created_on") - ensure - travel_back end class Testings < ActiveRecord::Base; end @@ -879,6 +877,26 @@ class DirtyTest < ActiveRecord::TestCase raise "changed? should be false" if changed? raise "has_changes_to_save? should be false" if has_changes_to_save? raise "saved_changes? should be true" unless saved_changes? + raise "id_in_database should not be nil" if id_in_database.nil? + end + end + + person = klass.create!(first_name: "Sean") + assert_not_predicate person, :changed? + end + + test "changed? in around callbacks after yield returns false" do + klass = Class.new(ActiveRecord::Base) do + self.table_name = "people" + + around_create :check_around + + def check_around + yield + raise "changed? should be false" if changed? + raise "has_changes_to_save? should be false" if has_changes_to_save? + raise "saved_changes? should be true" unless saved_changes? + raise "id_in_database should not be nil" if id_in_database.nil? end end diff --git a/activerecord/test/cases/explain_subscriber_test.rb b/activerecord/test/cases/explain_subscriber_test.rb index 82cc891970..79a0630193 100644 --- a/activerecord/test/cases/explain_subscriber_test.rb +++ b/activerecord/test/cases/explain_subscriber_test.rb @@ -40,7 +40,7 @@ if ActiveRecord::Base.connection.supports_explain? assert_equal binds, queries[0][1] end - def test_collects_nothing_if_the_statement_is_not_whitelisted + def test_collects_nothing_if_the_statement_is_not_explainable SUBSCRIBER.finish(nil, nil, name: "SQL", sql: "SHOW max_identifier_length") assert_empty queries end diff --git a/activerecord/test/cases/filter_attributes_test.rb b/activerecord/test/cases/filter_attributes_test.rb new file mode 100644 index 0000000000..af5badd87d --- /dev/null +++ b/activerecord/test/cases/filter_attributes_test.rb @@ -0,0 +1,84 @@ +# frozen_string_literal: true + +require "cases/helper" +require "models/admin" +require "models/admin/user" +require "models/admin/account" +require "pp" + +class FilterAttributesTest < ActiveRecord::TestCase + fixtures :"admin/users", :"admin/accounts" + + setup do + @previous_filter_attributes = ActiveRecord::Base.filter_attributes + ActiveRecord::Base.filter_attributes = [:name] + end + + teardown do + ActiveRecord::Base.filter_attributes = @previous_filter_attributes + end + + test "filter_attributes" do + Admin::User.all.each do |user| + assert_includes user.inspect, "name: [FILTERED]" + assert_equal 1, user.inspect.scan("[FILTERED]").length + end + + Admin::Account.all.each do |account| + assert_includes account.inspect, "name: [FILTERED]" + assert_equal 1, account.inspect.scan("[FILTERED]").length + end + end + + test "filter_attributes could be overwritten by models" do + Admin::Account.all.each do |account| + assert_includes account.inspect, "name: [FILTERED]" + assert_equal 1, account.inspect.scan("[FILTERED]").length + end + + begin + previous_account_filter_attributes = Admin::Account.filter_attributes + Admin::Account.filter_attributes = [] + + # Above changes should not impact other models + Admin::User.all.each do |user| + assert_includes user.inspect, "name: [FILTERED]" + assert_equal 1, user.inspect.scan("[FILTERED]").length + end + + Admin::Account.all.each do |account| + assert_not_includes account.inspect, "name: [FILTERED]" + assert_equal 0, account.inspect.scan("[FILTERED]").length + end + ensure + Admin::Account.filter_attributes = previous_account_filter_attributes + end + end + + test "filter_attributes should not filter nil value" do + account = Admin::Account.new + + assert_includes account.inspect, "name: nil" + assert_not_includes account.inspect, "name: [FILTERED]" + assert_equal 0, account.inspect.scan("[FILTERED]").length + end + + test "filter_attributes on pretty_print" do + user = admin_users(:david) + actual = "".dup + PP.pp(user, StringIO.new(actual)) + + assert_includes actual, "name: [FILTERED]" + assert_equal 1, actual.scan("[FILTERED]").length + end + + test "filter_attributes on pretty_print should not filter nil value" do + user = Admin::User.new + actual = "".dup + PP.pp(user, StringIO.new(actual)) + + assert_includes actual, "name: nil" + assert_not_includes actual, "name: [FILTERED]" + assert_equal 0, actual.scan("[FILTERED]").length + end +end diff --git a/activerecord/test/cases/finder_respond_to_test.rb b/activerecord/test/cases/finder_respond_to_test.rb index 59af4e6961..e0acd30c22 100644 --- a/activerecord/test/cases/finder_respond_to_test.rb +++ b/activerecord/test/cases/finder_respond_to_test.rb @@ -12,7 +12,7 @@ class FinderRespondToTest < ActiveRecord::TestCase end def test_should_preserve_normal_respond_to_behaviour_and_respond_to_newly_added_method - class << Topic; self; end.send(:define_method, :method_added_for_finder_respond_to_test) {} + class << Topic; self; end.send(:define_method, :method_added_for_finder_respond_to_test) { } assert_respond_to Topic, :method_added_for_finder_respond_to_test ensure class << Topic; self; end.send(:remove_method, :method_added_for_finder_respond_to_test) diff --git a/activerecord/test/cases/finder_test.rb b/activerecord/test/cases/finder_test.rb index e73c88dd5d..355fb4517f 100644 --- a/activerecord/test/cases/finder_test.rb +++ b/activerecord/test/cases/finder_test.rb @@ -371,7 +371,10 @@ class FinderTest < ActiveRecord::TestCase end def test_find_an_empty_array - assert_equal [], Topic.find([]) + empty_array = [] + result = Topic.find(empty_array) + assert_equal [], result + assert_not_same empty_array, result end def test_find_doesnt_have_implicit_ordering diff --git a/activerecord/test/cases/fixtures_test.rb b/activerecord/test/cases/fixtures_test.rb index c65523d8c1..82ca15b415 100644 --- a/activerecord/test/cases/fixtures_test.rb +++ b/activerecord/test/cases/fixtures_test.rb @@ -974,7 +974,7 @@ class TransactionalFixturesOnConnectionNotification < ActiveRecord::TestCase connection_id: connection.object_id } - message_bus.instrument("!connection.active_record", payload) {} + message_bus.instrument("!connection.active_record", payload) { } end end end @@ -1344,3 +1344,19 @@ class SameNameDifferentDatabaseFixturesTest < ActiveRecord::TestCase assert_kind_of OtherDog, other_dogs(:lassie) end end + +class NilFixturePathTest < ActiveRecord::TestCase + test "raises an error when all fixtures loaded" do + error = assert_raises(StandardError) do + TestCase = Class.new(ActiveRecord::TestCase) + TestCase.class_eval do + self.fixture_path = nil + fixtures :all + end + end + assert_equal <<~MSG.squish, error.message + No fixture path found. + Please set `NilFixturePathTest::TestCase.fixture_path`. + MSG + end +end diff --git a/activerecord/test/cases/helper.rb b/activerecord/test/cases/helper.rb index 66f11fe5bd..68be685e4b 100644 --- a/activerecord/test/cases/helper.rb +++ b/activerecord/test/cases/helper.rb @@ -183,5 +183,3 @@ module InTimeZone ActiveRecord::Base.time_zone_aware_attributes = old_tz end end - -require "mocha/minitest" # FIXME: stop using mocha diff --git a/activerecord/test/cases/invertible_migration_test.rb b/activerecord/test/cases/invertible_migration_test.rb index 363beb4780..6cf17ac15d 100644 --- a/activerecord/test/cases/invertible_migration_test.rb +++ b/activerecord/test/cases/invertible_migration_test.rb @@ -22,6 +22,14 @@ module ActiveRecord end end + class InvertibleTransactionMigration < InvertibleMigration + def change + transaction do + super + end + end + end + class InvertibleRevertMigration < SilentMigration def change revert do @@ -271,6 +279,14 @@ module ActiveRecord assert_not revert.connection.table_exists?("horses") end + def test_migrate_revert_transaction + migration = InvertibleTransactionMigration.new + migration.migrate :up + assert migration.connection.table_exists?("horses") + migration.migrate :down + assert_not migration.connection.table_exists?("horses") + end + def test_migrate_revert_change_column_default migration1 = ChangeColumnDefault1.new migration1.migrate(:up) diff --git a/activerecord/test/cases/legacy_configurations_test.rb b/activerecord/test/cases/legacy_configurations_test.rb new file mode 100644 index 0000000000..c36feb5116 --- /dev/null +++ b/activerecord/test/cases/legacy_configurations_test.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +require "cases/helper" + +module ActiveRecord + class LegacyConfigurationsTest < ActiveRecord::TestCase + def test_can_turn_configurations_into_a_hash + assert ActiveRecord::Base.configurations.to_h.is_a?(Hash), "expected to be a hash but was not." + assert_equal ["arunit", "arunit2", "arunit_without_prepared_statements"].sort, ActiveRecord::Base.configurations.to_h.keys.sort + end + + def test_each_is_deprecated + assert_deprecated do + ActiveRecord::Base.configurations.each do |db_config| + assert_equal "primary", db_config.spec_name + end + end + end + + def test_first_is_deprecated + assert_deprecated do + db_config = ActiveRecord::Base.configurations.first + assert_equal "arunit", db_config.env_name + assert_equal "primary", db_config.spec_name + end + end + + def test_fetch_is_deprecated + assert_deprecated do + db_config = ActiveRecord::Base.configurations.fetch("arunit").first + assert_equal "arunit", db_config.env_name + assert_equal "primary", db_config.spec_name + end + end + + def test_values_are_deprecated + config_hashes = ActiveRecord::Base.configurations.configurations.map(&:config) + assert_deprecated do + assert_equal config_hashes, ActiveRecord::Base.configurations.values + end + end + end +end diff --git a/activerecord/test/cases/log_subscriber_test.rb b/activerecord/test/cases/log_subscriber_test.rb index f0126fdb0d..ae2597adc8 100644 --- a/activerecord/test/cases/log_subscriber_test.rb +++ b/activerecord/test/cases/log_subscriber_test.rb @@ -44,6 +44,7 @@ class LogSubscriberTest < ActiveRecord::TestCase def setup @old_logger = ActiveRecord::Base.logger Developer.primary_key + ActiveRecord::Base.connection.materialize_transactions super ActiveRecord::LogSubscriber.attach_to(:active_record) end diff --git a/activerecord/test/cases/migration/change_table_test.rb b/activerecord/test/cases/migration/change_table_test.rb index 034bf32165..c108d372d1 100644 --- a/activerecord/test/cases/migration/change_table_test.rb +++ b/activerecord/test/cases/migration/change_table_test.rb @@ -164,6 +164,14 @@ module ActiveRecord end end + def test_column_creates_column_with_index + with_change_table do |t| + @connection.expect :add_column, nil, [:delete_me, :bar, :integer, {}] + @connection.expect :add_index, nil, [:delete_me, :bar, {}] + t.column :bar, :integer, index: true + end + end + def test_index_creates_index with_change_table do |t| @connection.expect :add_index, nil, [:delete_me, :bar, {}] diff --git a/activerecord/test/cases/migration/command_recorder_test.rb b/activerecord/test/cases/migration/command_recorder_test.rb index 3a11bb081b..01f8628fc5 100644 --- a/activerecord/test/cases/migration/command_recorder_test.rb +++ b/activerecord/test/cases/migration/command_recorder_test.rb @@ -117,13 +117,13 @@ module ActiveRecord end def test_invert_create_table_with_options_and_block - block = Proc.new {} + block = Proc.new { } drop_table = @recorder.inverse_of :create_table, [:people_reminders, id: false], &block assert_equal [:drop_table, [:people_reminders, id: false], block], drop_table end def test_invert_drop_table - block = Proc.new {} + block = Proc.new { } create_table = @recorder.inverse_of :drop_table, [:people_reminders, id: false], &block assert_equal [:create_table, [:people_reminders, id: false], block], create_table end @@ -145,7 +145,7 @@ module ActiveRecord end def test_invert_drop_join_table - block = Proc.new {} + block = Proc.new { } create_join_table = @recorder.inverse_of :drop_join_table, [:musics, :artists, table_name: :catalog], &block assert_equal [:create_join_table, [:musics, :artists, table_name: :catalog], block], create_join_table end @@ -329,11 +329,24 @@ module ActiveRecord assert_equal [:add_foreign_key, [:dogs, :people, primary_key: "person_id"]], enable end + def test_invert_remove_foreign_key_with_primary_key_and_to_table_in_options + enable = @recorder.inverse_of :remove_foreign_key, [:dogs, to_table: :people, primary_key: "uuid"] + assert_equal [:add_foreign_key, [:dogs, :people, primary_key: "uuid"]], enable + end + def test_invert_remove_foreign_key_with_on_delete_on_update enable = @recorder.inverse_of :remove_foreign_key, [:dogs, :people, on_delete: :nullify, on_update: :cascade] assert_equal [:add_foreign_key, [:dogs, :people, on_delete: :nullify, on_update: :cascade]], enable end + def test_invert_remove_foreign_key_with_to_table_in_options + enable = @recorder.inverse_of :remove_foreign_key, [:dogs, to_table: :people] + assert_equal [:add_foreign_key, [:dogs, :people]], enable + + enable = @recorder.inverse_of :remove_foreign_key, [:dogs, to_table: :people, column: :owner_id] + assert_equal [:add_foreign_key, [:dogs, :people, column: :owner_id]], enable + end + def test_invert_remove_foreign_key_is_irreversible_without_to_table assert_raises ActiveRecord::IrreversibleMigration do @recorder.inverse_of :remove_foreign_key, [:dogs, column: "owner_id"] @@ -347,6 +360,16 @@ module ActiveRecord @recorder.inverse_of :remove_foreign_key, [:dogs] end end + + def test_invert_transaction_with_irreversible_inside_is_irreversible + assert_raises(ActiveRecord::IrreversibleMigration) do + @recorder.revert do + @recorder.transaction do + @recorder.execute "some sql" + end + end + end + end end end end diff --git a/activerecord/test/cases/migration/compatibility_test.rb b/activerecord/test/cases/migration/compatibility_test.rb index 69a50674af..017ee7951e 100644 --- a/activerecord/test/cases/migration/compatibility_test.rb +++ b/activerecord/test/cases/migration/compatibility_test.rb @@ -127,6 +127,20 @@ module ActiveRecord assert_match(/LegacyMigration < ActiveRecord::Migration\[4\.2\]/, e.message) end + def test_legacy_migrations_not_raise_exception_on_reverting_transaction + migration = Class.new(ActiveRecord::Migration[5.2]) { + def change + transaction do + execute "select 1" + end + end + }.new + + assert_nothing_raised do + migration.migrate(:down) + end + end + if current_adapter?(:PostgreSQLAdapter) class Testing < ActiveRecord::Base end diff --git a/activerecord/test/cases/migration/foreign_key_test.rb b/activerecord/test/cases/migration/foreign_key_test.rb index c471dd1106..bb233fbf74 100644 --- a/activerecord/test/cases/migration/foreign_key_test.rb +++ b/activerecord/test/cases/migration/foreign_key_test.rb @@ -53,16 +53,49 @@ if ActiveRecord::Base.connection.supports_foreign_keys_in_create? end def test_change_column_of_parent_table - foreign_keys = ActiveRecord::Base.connection.foreign_keys("astronauts") rocket = Rocket.create!(name: "myrocket") rocket.astronauts << Astronaut.create! @connection.change_column_null :rockets, :name, false + foreign_keys = @connection.foreign_keys("astronauts") + assert_equal 1, foreign_keys.size + + fk = foreign_keys.first + assert_equal "myrocket", Rocket.first.name + assert_equal "astronauts", fk.from_table + assert_equal "rockets", fk.to_table + end + + def test_rename_column_of_child_table + rocket = Rocket.create!(name: "myrocket") + rocket.astronauts << Astronaut.create! + + @connection.rename_column :astronauts, :name, :astronaut_name + + foreign_keys = @connection.foreign_keys("astronauts") + assert_equal 1, foreign_keys.size + + fk = foreign_keys.first + assert_equal "myrocket", Rocket.first.name + assert_equal "astronauts", fk.from_table + assert_equal "rockets", fk.to_table + end + + def test_rename_reference_column_of_child_table + rocket = Rocket.create!(name: "myrocket") + rocket.astronauts << Astronaut.create! + + @connection.rename_column :astronauts, :rocket_id, :new_rocket_id + + foreign_keys = @connection.foreign_keys("astronauts") + assert_equal 1, foreign_keys.size + fk = foreign_keys.first assert_equal "myrocket", Rocket.first.name assert_equal "astronauts", fk.from_table assert_equal "rockets", fk.to_table + assert_equal "new_rocket_id", fk.options[:column] end end end diff --git a/activerecord/test/cases/migration/pending_migrations_test.rb b/activerecord/test/cases/migration/pending_migrations_test.rb index dedb5ea502..119bfd372a 100644 --- a/activerecord/test/cases/migration/pending_migrations_test.rb +++ b/activerecord/test/cases/migration/pending_migrations_test.rb @@ -25,7 +25,7 @@ module ActiveRecord ActiveRecord::Base.connection.drop_table "schema_migrations", if_exists: true assert_raises ActiveRecord::PendingMigrationError do - CheckPending.new(Proc.new {}).call({}) + CheckPending.new(Proc.new { }).call({}) end end @@ -34,7 +34,7 @@ module ActiveRecord migrator = Base.connection.migration_context capture(:stdout) { migrator.migrate } - assert_nil CheckPending.new(Proc.new {}).call({}) + assert_nil CheckPending.new(Proc.new { }).call({}) end end end diff --git a/activerecord/test/cases/migration_test.rb b/activerecord/test/cases/migration_test.rb index d1292dc53d..5d060c8899 100644 --- a/activerecord/test/cases/migration_test.rb +++ b/activerecord/test/cases/migration_test.rb @@ -87,7 +87,6 @@ class MigrationTest < ActiveRecord::TestCase def test_migrator_versions migrations_path = MIGRATIONS_ROOT + "/valid" - old_path = ActiveRecord::Migrator.migrations_paths migrator = ActiveRecord::MigrationContext.new(migrations_path) migrator.up @@ -100,24 +99,18 @@ class MigrationTest < ActiveRecord::TestCase ActiveRecord::SchemaMigration.create!(version: 3) assert_equal true, migrator.needs_migration? - ensure - ActiveRecord::MigrationContext.new(old_path) end def test_migration_detection_without_schema_migration_table ActiveRecord::Base.connection.drop_table "schema_migrations", if_exists: true migrations_path = MIGRATIONS_ROOT + "/valid" - old_path = ActiveRecord::Migrator.migrations_paths migrator = ActiveRecord::MigrationContext.new(migrations_path) assert_equal true, migrator.needs_migration? - ensure - ActiveRecord::MigrationContext.new(old_path) end def test_any_migrations - old_path = ActiveRecord::Migrator.migrations_paths migrator = ActiveRecord::MigrationContext.new(MIGRATIONS_ROOT + "/valid") assert_predicate migrator, :any_migrations? @@ -125,8 +118,6 @@ class MigrationTest < ActiveRecord::TestCase migrator_empty = ActiveRecord::MigrationContext.new(MIGRATIONS_ROOT + "/empty") assert_not_predicate migrator_empty, :any_migrations? - ensure - ActiveRecord::MigrationContext.new(old_path) end def test_migration_version @@ -393,7 +384,6 @@ class MigrationTest < ActiveRecord::TestCase def test_internal_metadata_stores_environment current_env = ActiveRecord::ConnectionHandling::DEFAULT_ENV.call migrations_path = MIGRATIONS_ROOT + "/valid" - old_path = ActiveRecord::Migrator.migrations_paths migrator = ActiveRecord::MigrationContext.new(migrations_path) migrator.up @@ -410,7 +400,6 @@ class MigrationTest < ActiveRecord::TestCase migrator.up assert_equal new_env, ActiveRecord::InternalMetadata[:environment] ensure - migrator = ActiveRecord::MigrationContext.new(old_path) ENV["RAILS_ENV"] = original_rails_env ENV["RACK_ENV"] = original_rack_env migrator.up @@ -422,16 +411,12 @@ class MigrationTest < ActiveRecord::TestCase current_env = ActiveRecord::ConnectionHandling::DEFAULT_ENV.call migrations_path = MIGRATIONS_ROOT + "/valid" - old_path = ActiveRecord::Migrator.migrations_paths current_env = ActiveRecord::ConnectionHandling::DEFAULT_ENV.call migrator = ActiveRecord::MigrationContext.new(migrations_path) migrator.up assert_equal current_env, ActiveRecord::InternalMetadata[:environment] assert_equal "bar", ActiveRecord::InternalMetadata[:foo] - ensure - migrator = ActiveRecord::MigrationContext.new(old_path) - migrator.up end def test_proper_table_name_on_migration @@ -793,12 +778,20 @@ if ActiveRecord::Base.connection.supports_bulk_alter? end def test_adding_multiple_columns - assert_queries(1) do + classname = ActiveRecord::Base.connection.class.name[/[^:]*$/] + expected_query_count = { + "Mysql2Adapter" => 1, + "PostgreSQLAdapter" => 2, # one for bulk change, one for comment + }.fetch(classname) { + raise "need an expected query count for #{classname}" + } + + assert_queries(expected_query_count) do with_bulk_change_table do |t| t.column :name, :string t.string :qualification, :experience t.integer :age, default: 0 - t.date :birthdate + t.date :birthdate, comment: "This is a comment" t.timestamps null: true end end @@ -806,6 +799,7 @@ if ActiveRecord::Base.connection.supports_bulk_alter? assert_equal 8, columns.size [:name, :qualification, :experience].each { |s| assert_equal :string, column(s).type } assert_equal "0", column(:age).default + assert_equal "This is a comment", column(:birthdate).comment end def test_removing_columns @@ -1150,7 +1144,7 @@ class CopyMigrationsTest < ActiveRecord::TestCase def test_check_pending_with_stdlib_logger old, ActiveRecord::Base.logger = ActiveRecord::Base.logger, ::Logger.new($stdout) quietly do - assert_nothing_raised { ActiveRecord::Migration::CheckPending.new(Proc.new {}).call({}) } + assert_nothing_raised { ActiveRecord::Migration::CheckPending.new(Proc.new { }).call({}) } end ensure ActiveRecord::Base.logger = old diff --git a/activerecord/test/cases/migrator_test.rb b/activerecord/test/cases/migrator_test.rb index 873455cf67..30e199f1c5 100644 --- a/activerecord/test/cases/migrator_test.rb +++ b/activerecord/test/cases/migrator_test.rb @@ -100,7 +100,6 @@ class MigratorTest < ActiveRecord::TestCase def test_finds_migrations_in_subdirectories migrations = ActiveRecord::MigrationContext.new(MIGRATIONS_ROOT + "/valid_with_subdirectories").migrations - [[1, "ValidPeopleHaveLastNames"], [2, "WeNeedReminders"], [3, "InnocentJointable"]].each_with_index do |pair, i| assert_equal migrations[i].version, pair.first assert_equal migrations[i].name, pair.last diff --git a/activerecord/test/cases/nested_attributes_test.rb b/activerecord/test/cases/nested_attributes_test.rb index aa519fd332..bb1c1ea17d 100644 --- a/activerecord/test/cases/nested_attributes_test.rb +++ b/activerecord/test/cases/nested_attributes_test.rb @@ -217,6 +217,18 @@ class TestNestedAttributesInGeneral < ActiveRecord::TestCase mean_pirate.parrot_attributes = { name: "James" } assert_equal "James", mean_pirate.parrot.name end + + def test_should_not_create_duplicates_with_create_with + Man.accepts_nested_attributes_for(:interests) + + assert_difference("Interest.count", 1) do + Man.create_with( + interests_attributes: [{ topic: "Pirate king" }] + ).find_or_create_by!( + name: "Monkey D. Luffy" + ) + end + end end class TestNestedAttributesOnAHasOneAssociation < ActiveRecord::TestCase diff --git a/activerecord/test/cases/persistence_test.rb b/activerecord/test/cases/persistence_test.rb index 7348a22dd3..8073cabae6 100644 --- a/activerecord/test/cases/persistence_test.rb +++ b/activerecord/test/cases/persistence_test.rb @@ -13,90 +13,15 @@ require "models/developer" require "models/computer" require "models/project" require "models/minimalistic" -require "models/warehouse_thing" require "models/parrot" require "models/minivan" -require "models/owner" require "models/person" -require "models/pet" require "models/ship" -require "models/toy" require "models/admin" require "models/admin/user" -require "rexml/document" class PersistenceTest < ActiveRecord::TestCase - fixtures :topics, :companies, :developers, :projects, :computers, :accounts, :minimalistics, "warehouse-things", :authors, :author_addresses, :categorizations, :categories, :posts, :minivans, :pets, :toys - - # Oracle UPDATE does not support ORDER BY - unless current_adapter?(:OracleAdapter) - def test_update_all_ignores_order_without_limit_from_association - author = authors(:david) - assert_nothing_raised do - assert_equal author.posts_with_comments_and_categories.length, author.posts_with_comments_and_categories.update_all([ "body = ?", "bulk update!" ]) - end - end - - def test_update_all_doesnt_ignore_order - assert_equal authors(:david).id + 1, authors(:mary).id # make sure there is going to be a duplicate PK error - test_update_with_order_succeeds = lambda do |order| - begin - Author.order(order).update_all("id = id + 1") - rescue ActiveRecord::ActiveRecordError - false - end - end - - if test_update_with_order_succeeds.call("id DESC") - assert_not test_update_with_order_succeeds.call("id ASC") # test that this wasn't a fluke and using an incorrect order results in an exception - else - # test that we're failing because the current Arel's engine doesn't support UPDATE ORDER BY queries is using subselects instead - assert_sql(/\AUPDATE .+ \(SELECT .* ORDER BY id DESC\)\z/i) do - test_update_with_order_succeeds.call("id DESC") - end - end - end - - def test_update_all_with_order_and_limit_updates_subset_only - author = authors(:david) - limited_posts = author.posts_sorted_by_id_limited - assert_equal 1, limited_posts.size - assert_equal 2, limited_posts.limit(2).size - assert_equal 1, limited_posts.update_all([ "body = ?", "bulk update!" ]) - assert_equal "bulk update!", posts(:welcome).body - assert_not_equal "bulk update!", posts(:thinking).body - end - - def test_update_all_with_order_and_limit_and_offset_updates_subset_only - author = authors(:david) - limited_posts = author.posts_sorted_by_id_limited.offset(1) - assert_equal 1, limited_posts.size - assert_equal 2, limited_posts.limit(2).size - assert_equal 1, limited_posts.update_all([ "body = ?", "bulk update!" ]) - assert_equal "bulk update!", posts(:thinking).body - assert_not_equal "bulk update!", posts(:welcome).body - end - - def test_delete_all_with_order_and_limit_deletes_subset_only - author = authors(:david) - limited_posts = Post.where(author: author).order(:id).limit(1) - assert_equal 1, limited_posts.size - assert_equal 2, limited_posts.limit(2).size - assert_equal 1, limited_posts.delete_all - assert_raise(ActiveRecord::RecordNotFound) { posts(:welcome) } - assert posts(:thinking) - end - - def test_delete_all_with_order_and_limit_and_offset_deletes_subset_only - author = authors(:david) - limited_posts = Post.where(author: author).order(:id).limit(1).offset(1) - assert_equal 1, limited_posts.size - assert_equal 2, limited_posts.limit(2).size - assert_equal 1, limited_posts.delete_all - assert_raise(ActiveRecord::RecordNotFound) { posts(:thinking) } - assert posts(:welcome) - end - end + fixtures :topics, :companies, :developers, :accounts, :minimalistics, :authors, :author_addresses, :posts, :minivans def test_update_many topic_data = { 1 => { "content" => "1 updated" }, 2 => { "content" => "2 updated" } } @@ -145,34 +70,6 @@ class PersistenceTest < ActiveRecord::TestCase assert_equal Topic.count, Topic.delete_all end - def test_delete_all_with_joins_and_where_part_is_hash - pets = Pet.joins(:toys).where(toys: { name: "Bone" }) - - assert_equal true, pets.exists? - assert_equal pets.count, pets.delete_all - end - - def test_delete_all_with_joins_and_where_part_is_not_hash - pets = Pet.joins(:toys).where("toys.name = ?", "Bone") - - assert_equal true, pets.exists? - assert_equal pets.count, pets.delete_all - end - - def test_delete_all_with_left_joins - pets = Pet.left_joins(:toys).where(toys: { name: "Bone" }) - - assert_equal true, pets.exists? - assert_equal pets.count, pets.delete_all - end - - def test_delete_all_with_includes - pets = Pet.includes(:toys).where(toys: { name: "Bone" }) - - assert_equal true, pets.exists? - assert_equal pets.count, pets.delete_all - end - def test_increment_attribute assert_equal 50, accounts(:signals37).credit_limit accounts(:signals37).increment! :credit_limit @@ -230,16 +127,9 @@ class PersistenceTest < ActiveRecord::TestCase assert_operator previously_written_on, :<, topic.written_on end - def test_destroy_all - conditions = "author_name = 'Mary'" - topics_by_mary = Topic.all.merge!(where: conditions, order: "id").to_a - assert_not_empty topics_by_mary - - assert_difference("Topic.count", -topics_by_mary.size) do - destroyed = Topic.where(conditions).destroy_all.sort_by(&:id) - assert_equal topics_by_mary, destroyed - assert destroyed.all?(&:frozen?), "destroyed topics should be frozen" - end + def test_increment_with_no_arg + topic = topics(:first) + assert_raises(ArgumentError) { topic.increment! } end def test_destroy_many @@ -638,32 +528,6 @@ class PersistenceTest < ActiveRecord::TestCase assert_nil Topic.find(2).last_read end - def test_update_all_with_joins - pets = Pet.joins(:toys).where(toys: { name: "Bone" }) - - assert_equal true, pets.exists? - assert_equal pets.count, pets.update_all(name: "Bob") - end - - def test_update_all_with_left_joins - pets = Pet.left_joins(:toys).where(toys: { name: "Bone" }) - - assert_equal true, pets.exists? - assert_equal pets.count, pets.update_all(name: "Bob") - end - - def test_update_all_with_includes - pets = Pet.includes(:toys).where(toys: { name: "Bone" }) - - assert_equal true, pets.exists? - assert_equal pets.count, pets.update_all(name: "Bob") - end - - def test_update_all_with_non_standard_table_name - assert_equal 1, WarehouseThing.where(id: 1).update_all(["value = ?", 0]) - assert_equal 0, WarehouseThing.find(1).value - end - def test_delete_new_record client = Client.new(name: "37signals") client.delete @@ -1185,21 +1049,19 @@ class PersistenceTest < ActiveRecord::TestCase ActiveRecord::Base.connection.disable_query_cache! end - class SaveTest < ActiveRecord::TestCase - def test_save_touch_false - pet = Pet.create!( - name: "Bob", - created_at: 1.day.ago, - updated_at: 1.day.ago) + def test_save_touch_false + parrot = Parrot.create!( + name: "Bob", + created_at: 1.day.ago, + updated_at: 1.day.ago) - created_at = pet.created_at - updated_at = pet.updated_at + created_at = parrot.created_at + updated_at = parrot.updated_at - pet.name = "Barb" - pet.save!(touch: false) - assert_equal pet.created_at, created_at - assert_equal pet.updated_at, updated_at - end + parrot.name = "Barb" + parrot.save!(touch: false) + assert_equal parrot.created_at, created_at + assert_equal parrot.updated_at, updated_at end def test_reset_column_information_resets_children diff --git a/activerecord/test/cases/query_cache_test.rb b/activerecord/test/cases/query_cache_test.rb index 69be091869..3eb4e04cb7 100644 --- a/activerecord/test/cases/query_cache_test.rb +++ b/activerecord/test/cases/query_cache_test.rb @@ -55,16 +55,6 @@ class QueryCacheTest < ActiveRecord::TestCase assert_cache :off end - private def with_temporary_connection_pool - old_pool = ActiveRecord::Base.connection_handler.retrieve_connection_pool(ActiveRecord::Base.connection_specification_name) - new_pool = ActiveRecord::ConnectionAdapters::ConnectionPool.new ActiveRecord::Base.connection_pool.spec - ActiveRecord::Base.connection_handler.send(:owner_to_pool)["primary"] = new_pool - - yield - ensure - ActiveRecord::Base.connection_handler.send(:owner_to_pool)["primary"] = old_pool - end - def test_query_cache_across_threads with_temporary_connection_pool do begin @@ -495,6 +485,17 @@ class QueryCacheTest < ActiveRecord::TestCase end private + + def with_temporary_connection_pool + old_pool = ActiveRecord::Base.connection_handler.retrieve_connection_pool(ActiveRecord::Base.connection_specification_name) + new_pool = ActiveRecord::ConnectionAdapters::ConnectionPool.new ActiveRecord::Base.connection_pool.spec + ActiveRecord::Base.connection_handler.send(:owner_to_pool)["primary"] = new_pool + + yield + ensure + ActiveRecord::Base.connection_handler.send(:owner_to_pool)["primary"] = old_pool + end + def middleware(&app) executor = Class.new(ActiveSupport::Executor) ActiveRecord::QueryCache.install_executor_hooks executor diff --git a/activerecord/test/cases/reaper_test.rb b/activerecord/test/cases/reaper_test.rb index b034fe3e3b..b630f782bc 100644 --- a/activerecord/test/cases/reaper_test.rb +++ b/activerecord/test/cases/reaper_test.rb @@ -61,9 +61,9 @@ module ActiveRecord def test_reaping_frequency_configuration spec = ActiveRecord::Base.connection_pool.spec.dup - spec.config[:reaping_frequency] = 100 + spec.config[:reaping_frequency] = "10.01" pool = ConnectionPool.new spec - assert_equal 100, pool.reaper.frequency + assert_equal 10.01, pool.reaper.frequency end def test_connection_pool_starts_reaper diff --git a/activerecord/test/cases/relation/delegation_test.rb b/activerecord/test/cases/relation/delegation_test.rb index 3f3d41980c..a8030c2d64 100644 --- a/activerecord/test/cases/relation/delegation_test.rb +++ b/activerecord/test/cases/relation/delegation_test.rb @@ -5,7 +5,7 @@ require "models/post" require "models/comment" module ActiveRecord - module DelegationWhitelistTests + module ArrayDelegationTests ARRAY_DELEGATES = [ :+, :-, :|, :&, :[], :shuffle, :all?, :collect, :compact, :detect, :each, :each_cons, :each_with_index, @@ -38,7 +38,7 @@ module ActiveRecord end class DelegationAssociationTest < ActiveRecord::TestCase - include DelegationWhitelistTests + include ArrayDelegationTests include DeprecatedArelDelegationTests def target @@ -47,7 +47,7 @@ module ActiveRecord end class DelegationRelationTest < ActiveRecord::TestCase - include DelegationWhitelistTests + include ArrayDelegationTests include DeprecatedArelDelegationTests def target diff --git a/activerecord/test/cases/relation/delete_all_test.rb b/activerecord/test/cases/relation/delete_all_test.rb new file mode 100644 index 0000000000..446d7621ea --- /dev/null +++ b/activerecord/test/cases/relation/delete_all_test.rb @@ -0,0 +1,104 @@ +# frozen_string_literal: true + +require "cases/helper" +require "models/author" +require "models/post" +require "models/pet" +require "models/toy" + +class DeleteAllTest < ActiveRecord::TestCase + fixtures :authors, :author_addresses, :posts, :pets, :toys + + def test_destroy_all + davids = Author.where(name: "David") + + # Force load + assert_equal [authors(:david)], davids.to_a + assert_predicate davids, :loaded? + + assert_difference("Author.count", -1) do + destroyed = davids.destroy_all + assert_equal [authors(:david)], destroyed + assert_predicate destroyed.first, :frozen? + end + + assert_equal [], davids.to_a + assert_predicate davids, :loaded? + end + + def test_delete_all + davids = Author.where(name: "David") + + assert_difference("Author.count", -1) { davids.delete_all } + assert_not_predicate davids, :loaded? + end + + def test_delete_all_loaded + davids = Author.where(name: "David") + + # Force load + assert_equal [authors(:david)], davids.to_a + assert_predicate davids, :loaded? + + assert_difference("Author.count", -1) { davids.delete_all } + + assert_equal [], davids.to_a + assert_predicate davids, :loaded? + end + + def test_delete_all_with_unpermitted_relation_raises_error + assert_raises(ActiveRecord::ActiveRecordError) { Author.distinct.delete_all } + assert_raises(ActiveRecord::ActiveRecordError) { Author.group(:name).delete_all } + assert_raises(ActiveRecord::ActiveRecordError) { Author.having("SUM(id) < 3").delete_all } + end + + def test_delete_all_with_joins_and_where_part_is_hash + pets = Pet.joins(:toys).where(toys: { name: "Bone" }) + + assert_equal true, pets.exists? + assert_equal pets.count, pets.delete_all + end + + def test_delete_all_with_joins_and_where_part_is_not_hash + pets = Pet.joins(:toys).where("toys.name = ?", "Bone") + + assert_equal true, pets.exists? + assert_equal pets.count, pets.delete_all + end + + def test_delete_all_with_left_joins + pets = Pet.left_joins(:toys).where(toys: { name: "Bone" }) + + assert_equal true, pets.exists? + assert_equal pets.count, pets.delete_all + end + + def test_delete_all_with_includes + pets = Pet.includes(:toys).where(toys: { name: "Bone" }) + + assert_equal true, pets.exists? + assert_equal pets.count, pets.delete_all + end + + unless current_adapter?(:OracleAdapter) + def test_delete_all_with_order_and_limit_deletes_subset_only + author = authors(:david) + limited_posts = Post.where(author: author).order(:id).limit(1) + assert_equal 1, limited_posts.size + assert_equal 2, limited_posts.limit(2).size + assert_equal 1, limited_posts.delete_all + assert_raise(ActiveRecord::RecordNotFound) { posts(:welcome) } + assert posts(:thinking) + end + + def test_delete_all_with_order_and_limit_and_offset_deletes_subset_only + author = authors(:david) + limited_posts = Post.where(author: author).order(:id).limit(1).offset(1) + assert_equal 1, limited_posts.size + assert_equal 2, limited_posts.limit(2).size + assert_equal 1, limited_posts.delete_all + assert_raise(ActiveRecord::RecordNotFound) { posts(:thinking) } + assert posts(:welcome) + end + end +end diff --git a/activerecord/test/cases/relation/merging_test.rb b/activerecord/test/cases/relation/merging_test.rb index f53ef1fe35..224e4f39a8 100644 --- a/activerecord/test/cases/relation/merging_test.rb +++ b/activerecord/test/cases/relation/merging_test.rb @@ -121,6 +121,20 @@ class RelationMergingTest < ActiveRecord::TestCase relation = relation.merge(Post.from("posts")) assert_not_empty relation.from_clause end + + def test_merging_with_from_clause_on_different_class + assert Comment.joins(:post).merge(Post.from("posts")).first + end + + def test_merging_with_order_with_binds + relation = Post.all.merge(Post.order([Arel.sql("title LIKE ?"), "%suffix"])) + assert_equal ["title LIKE '%suffix'"], relation.order_values + end + + def test_merging_with_order_without_binds + relation = Post.all.merge(Post.order(Arel.sql("title LIKE '%?'"))) + assert_equal ["title LIKE '%?'"], relation.order_values + end end class MergingDifferentRelationsTest < ActiveRecord::TestCase diff --git a/activerecord/test/cases/relation/update_all_test.rb b/activerecord/test/cases/relation/update_all_test.rb new file mode 100644 index 0000000000..09c365f31b --- /dev/null +++ b/activerecord/test/cases/relation/update_all_test.rb @@ -0,0 +1,239 @@ +# frozen_string_literal: true + +require "cases/helper" +require "models/author" +require "models/category" +require "models/comment" +require "models/computer" +require "models/developer" +require "models/post" +require "models/person" +require "models/pet" +require "models/toy" +require "models/topic" +require "models/tag" +require "models/tagging" +require "models/warehouse_thing" + +class UpdateAllTest < ActiveRecord::TestCase + fixtures :authors, :author_addresses, :comments, :developers, :posts, :people, :pets, :toys, :tags, :taggings, "warehouse-things" + + class TopicWithCallbacks < ActiveRecord::Base + self.table_name = :topics + cattr_accessor :topic_count + before_update { |topic| topic.author_name = "David" if topic.author_name.blank? } + after_update { |topic| topic.class.topic_count = topic.class.count } + end + + def test_update_all_with_scope + tag = Tag.first + Post.tagged_with(tag.id).update_all(title: "rofl") + posts = Post.tagged_with(tag.id).all.to_a + assert_operator posts.length, :>, 0 + posts.each { |post| assert_equal "rofl", post.title } + end + + def test_update_all_with_non_standard_table_name + assert_equal 1, WarehouseThing.where(id: 1).update_all(["value = ?", 0]) + assert_equal 0, WarehouseThing.find(1).value + end + + def test_update_all_with_blank_argument + assert_raises(ArgumentError) { Comment.update_all({}) } + end + + def test_update_all_with_joins + pets = Pet.joins(:toys).where(toys: { name: "Bone" }) + + assert_equal true, pets.exists? + assert_equal pets.count, pets.update_all(name: "Bob") + end + + def test_update_all_with_left_joins + pets = Pet.left_joins(:toys).where(toys: { name: "Bone" }) + + assert_equal true, pets.exists? + assert_equal pets.count, pets.update_all(name: "Bob") + end + + def test_update_all_with_includes + pets = Pet.includes(:toys).where(toys: { name: "Bone" }) + + assert_equal true, pets.exists? + assert_equal pets.count, pets.update_all(name: "Bob") + end + + def test_update_all_with_joins_and_limit + comments = Comment.joins(:post).where("posts.id" => posts(:welcome).id).limit(1) + assert_equal 1, comments.update_all(post_id: posts(:thinking).id) + assert_equal posts(:thinking), comments(:greetings).post + end + + def test_update_all_with_joins_and_limit_and_order + comments = Comment.joins(:post).where("posts.id" => posts(:welcome).id).order("comments.id").limit(1) + assert_equal 1, comments.update_all(post_id: posts(:thinking).id) + assert_equal posts(:thinking), comments(:greetings).post + assert_equal posts(:welcome), comments(:more_greetings).post + end + + def test_update_all_with_joins_and_offset + all_comments = Comment.joins(:post).where("posts.id" => posts(:welcome).id) + count = all_comments.count + comments = all_comments.offset(1) + + assert_equal count - 1, comments.update_all(post_id: posts(:thinking).id) + end + + def test_update_all_with_joins_and_offset_and_order + all_comments = Comment.joins(:post).where("posts.id" => posts(:welcome).id).order("posts.id", "comments.id") + count = all_comments.count + comments = all_comments.offset(1) + + assert_equal count - 1, comments.update_all(post_id: posts(:thinking).id) + assert_equal posts(:thinking), comments(:more_greetings).post + assert_equal posts(:welcome), comments(:greetings).post + end + + def test_update_counters_with_joins + assert_nil pets(:parrot).integer + + Pet.joins(:toys).where(toys: { name: "Bone" }).update_counters(integer: 1) + + assert_equal 1, pets(:parrot).reload.integer + end + + def test_touch_all_updates_records_timestamps + david = developers(:david) + david_previously_updated_at = david.updated_at + jamis = developers(:jamis) + jamis_previously_updated_at = jamis.updated_at + Developer.where(name: "David").touch_all + + assert_not_equal david_previously_updated_at, david.reload.updated_at + assert_equal jamis_previously_updated_at, jamis.reload.updated_at + end + + def test_touch_all_with_custom_timestamp + developer = developers(:david) + previously_created_at = developer.created_at + previously_updated_at = developer.updated_at + Developer.where(name: "David").touch_all(:created_at) + developer.reload + + assert_not_equal previously_created_at, developer.created_at + assert_not_equal previously_updated_at, developer.updated_at + end + + def test_touch_all_with_given_time + developer = developers(:david) + previously_created_at = developer.created_at + previously_updated_at = developer.updated_at + new_time = Time.utc(2015, 2, 16, 4, 54, 0) + Developer.where(name: "David").touch_all(:created_at, time: new_time) + developer.reload + + assert_not_equal previously_created_at, developer.created_at + assert_not_equal previously_updated_at, developer.updated_at + assert_equal new_time, developer.created_at + assert_equal new_time, developer.updated_at + end + + def test_touch_all_updates_locking_column + person = people(:david) + + assert_difference -> { person.reload.lock_version }, +1 do + Person.where(first_name: "David").touch_all + end + end + + def test_update_on_relation + topic1 = TopicWithCallbacks.create! title: "arel", author_name: nil + topic2 = TopicWithCallbacks.create! title: "activerecord", author_name: nil + topics = TopicWithCallbacks.where(id: [topic1.id, topic2.id]) + topics.update(title: "adequaterecord") + + assert_equal TopicWithCallbacks.count, TopicWithCallbacks.topic_count + + assert_equal "adequaterecord", topic1.reload.title + assert_equal "adequaterecord", topic2.reload.title + # Testing that the before_update callbacks have run + assert_equal "David", topic1.reload.author_name + assert_equal "David", topic2.reload.author_name + end + + def test_update_with_ids_on_relation + topic1 = TopicWithCallbacks.create!(title: "arel", author_name: nil) + topic2 = TopicWithCallbacks.create!(title: "activerecord", author_name: nil) + topics = TopicWithCallbacks.none + topics.update( + [topic1.id, topic2.id], + [{ title: "adequaterecord" }, { title: "adequaterecord" }] + ) + + assert_equal TopicWithCallbacks.count, TopicWithCallbacks.topic_count + + assert_equal "adequaterecord", topic1.reload.title + assert_equal "adequaterecord", topic2.reload.title + # Testing that the before_update callbacks have run + assert_equal "David", topic1.reload.author_name + assert_equal "David", topic2.reload.author_name + end + + def test_update_on_relation_passing_active_record_object_is_not_permitted + topic = Topic.create!(title: "Foo", author_name: nil) + assert_raises(ArgumentError) do + Topic.where(id: topic.id).update(topic, title: "Bar") + end + end + + # Oracle UPDATE does not support ORDER BY + unless current_adapter?(:OracleAdapter) + def test_update_all_ignores_order_without_limit_from_association + author = authors(:david) + assert_nothing_raised do + assert_equal author.posts_with_comments_and_categories.length, author.posts_with_comments_and_categories.update_all([ "body = ?", "bulk update!" ]) + end + end + + def test_update_all_doesnt_ignore_order + assert_equal authors(:david).id + 1, authors(:mary).id # make sure there is going to be a duplicate PK error + test_update_with_order_succeeds = lambda do |order| + begin + Author.order(order).update_all("id = id + 1") + rescue ActiveRecord::ActiveRecordError + false + end + end + + if test_update_with_order_succeeds.call("id DESC") + # test that this wasn't a fluke and using an incorrect order results in an exception + assert_not test_update_with_order_succeeds.call("id ASC") + else + # test that we're failing because the current Arel's engine doesn't support UPDATE ORDER BY queries is using subselects instead + assert_sql(/\AUPDATE .+ \(SELECT .* ORDER BY id DESC\)\z/i) do + test_update_with_order_succeeds.call("id DESC") + end + end + end + + def test_update_all_with_order_and_limit_updates_subset_only + author = authors(:david) + limited_posts = author.posts_sorted_by_id_limited + assert_equal 1, limited_posts.size + assert_equal 2, limited_posts.limit(2).size + assert_equal 1, limited_posts.update_all([ "body = ?", "bulk update!" ]) + assert_equal "bulk update!", posts(:welcome).body + assert_not_equal "bulk update!", posts(:thinking).body + end + + def test_update_all_with_order_and_limit_and_offset_updates_subset_only + author = authors(:david) + limited_posts = author.posts_sorted_by_id_limited.offset(1) + assert_equal 1, limited_posts.size + assert_equal 2, limited_posts.limit(2).size + assert_equal 1, limited_posts.update_all([ "body = ?", "bulk update!" ]) + assert_equal "bulk update!", posts(:thinking).body + assert_not_equal "bulk update!", posts(:welcome).body + end + end +end diff --git a/activerecord/test/cases/relations_test.rb b/activerecord/test/cases/relations_test.rb index 5412ab5def..9914a61033 100644 --- a/activerecord/test/cases/relations_test.rb +++ b/activerecord/test/cases/relations_test.rb @@ -9,6 +9,7 @@ require "models/comment" require "models/author" require "models/entrant" require "models/developer" +require "models/project" require "models/person" require "models/computer" require "models/reply" @@ -28,13 +29,6 @@ require "models/subscriber" class RelationTest < ActiveRecord::TestCase fixtures :authors, :author_addresses, :topics, :entrants, :developers, :people, :companies, :developers_projects, :accounts, :categories, :categorizations, :categories_posts, :posts, :comments, :tags, :taggings, :cars, :minivans - class TopicWithCallbacks < ActiveRecord::Base - self.table_name = :topics - cattr_accessor :topic_count - before_update { |topic| topic.author_name = "David" if topic.author_name.blank? } - after_update { |topic| topic.class.topic_count = topic.class.count } - end - def test_do_not_double_quote_string_id van = Minivan.last assert van @@ -862,45 +856,6 @@ class RelationTest < ActiveRecord::TestCase assert_equal authors(:bob), authors.last end - def test_destroy_all - davids = Author.where(name: "David") - - # Force load - assert_equal [authors(:david)], davids.to_a - assert_predicate davids, :loaded? - - assert_difference("Author.count", -1) { davids.destroy_all } - - assert_equal [], davids.to_a - assert_predicate davids, :loaded? - end - - def test_delete_all - davids = Author.where(name: "David") - - assert_difference("Author.count", -1) { davids.delete_all } - assert_not_predicate davids, :loaded? - end - - def test_delete_all_loaded - davids = Author.where(name: "David") - - # Force load - assert_equal [authors(:david)], davids.to_a - assert_predicate davids, :loaded? - - assert_difference("Author.count", -1) { davids.delete_all } - - assert_equal [], davids.to_a - assert_predicate davids, :loaded? - end - - def test_delete_all_with_unpermitted_relation_raises_error - assert_raises(ActiveRecord::ActiveRecordError) { Author.distinct.delete_all } - assert_raises(ActiveRecord::ActiveRecordError) { Author.group(:name).delete_all } - assert_raises(ActiveRecord::ActiveRecordError) { Author.having("SUM(id) < 3").delete_all } - end - def test_select_with_aggregates posts = Post.select(:title, :body) @@ -985,14 +940,6 @@ class RelationTest < ActiveRecord::TestCase assert_queries(1) { assert_equal 11, posts.load.size } end - def test_update_all_with_scope - tag = Tag.first - Post.tagged_with(tag.id).update_all title: "rofl" - list = Post.tagged_with(tag.id).all.to_a - assert_operator list.length, :>, 0 - list.each { |post| assert_equal "rofl", post.title } - end - def test_count_explicit_columns Post.update_all(comments_count: nil) posts = Post.all @@ -1405,6 +1352,16 @@ class RelationTest < ActiveRecord::TestCase assert_equal "cock", hens.new.name end + def test_create_with_nested_attributes + assert_difference("Project.count", 1) do + developers = Developer.where(name: "Aaron") + developers = developers.create_with( + projects_attributes: [{ name: "p1" }] + ) + developers.create! + end + end + def test_except relation = Post.where(author_id: 1).order("id ASC").limit(1) assert_equal [posts(:welcome)], relation.to_a @@ -1486,114 +1443,6 @@ class RelationTest < ActiveRecord::TestCase assert_equal authors(:david), Author.order("id DESC , name DESC").last end - def test_update_all_with_blank_argument - assert_raises(ArgumentError) { Comment.update_all({}) } - end - - def test_update_all_with_joins - comments = Comment.joins(:post).where("posts.id" => posts(:welcome).id) - count = comments.count - - assert_equal count, comments.update_all(post_id: posts(:thinking).id) - assert_equal posts(:thinking), comments(:greetings).post - end - - def test_update_all_with_joins_and_limit - comments = Comment.joins(:post).where("posts.id" => posts(:welcome).id).limit(1) - assert_equal 1, comments.update_all(post_id: posts(:thinking).id) - end - - def test_update_all_with_joins_and_limit_and_order - comments = Comment.joins(:post).where("posts.id" => posts(:welcome).id).order("comments.id").limit(1) - assert_equal 1, comments.update_all(post_id: posts(:thinking).id) - assert_equal posts(:thinking), comments(:greetings).post - assert_equal posts(:welcome), comments(:more_greetings).post - end - - def test_update_all_with_joins_and_offset - all_comments = Comment.joins(:post).where("posts.id" => posts(:welcome).id) - count = all_comments.count - comments = all_comments.offset(1) - - assert_equal count - 1, comments.update_all(post_id: posts(:thinking).id) - end - - def test_update_all_with_joins_and_offset_and_order - all_comments = Comment.joins(:post).where("posts.id" => posts(:welcome).id).order("posts.id", "comments.id") - count = all_comments.count - comments = all_comments.offset(1) - - assert_equal count - 1, comments.update_all(post_id: posts(:thinking).id) - assert_equal posts(:thinking), comments(:more_greetings).post - assert_equal posts(:welcome), comments(:greetings).post - end - - def test_touch_all_updates_records_timestamps - david = developers(:david) - david_previously_updated_at = david.updated_at - jamis = developers(:jamis) - jamis_previously_updated_at = jamis.updated_at - Developer.where(name: "David").touch_all - - assert_not_equal david_previously_updated_at, david.reload.updated_at - assert_equal jamis_previously_updated_at, jamis.reload.updated_at - end - - def test_touch_all_with_custom_timestamp - developer = developers(:david) - previously_created_at = developer.created_at - previously_updated_at = developer.updated_at - Developer.where(name: "David").touch_all(:created_at) - developer = developer.reload - - assert_not_equal previously_created_at, developer.created_at - assert_not_equal previously_updated_at, developer.updated_at - end - - def test_touch_all_with_given_time - developer = developers(:david) - previously_created_at = developer.created_at - previously_updated_at = developer.updated_at - new_time = Time.utc(2015, 2, 16, 4, 54, 0) - Developer.where(name: "David").touch_all(:created_at, time: new_time) - developer = developer.reload - - assert_not_equal previously_created_at, developer.created_at - assert_not_equal previously_updated_at, developer.updated_at - assert_equal new_time, developer.created_at - assert_equal new_time, developer.updated_at - end - - def test_touch_all_updates_locking_column - person = people(:david) - - assert_difference -> { person.reload.lock_version }, +1 do - Person.where(first_name: "David").touch_all - end - end - - def test_update_on_relation - topic1 = TopicWithCallbacks.create! title: "arel", author_name: nil - topic2 = TopicWithCallbacks.create! title: "activerecord", author_name: nil - topics = TopicWithCallbacks.where(id: [topic1.id, topic2.id]) - topics.update(title: "adequaterecord") - - assert_equal TopicWithCallbacks.count, TopicWithCallbacks.topic_count - - assert_equal "adequaterecord", topic1.reload.title - assert_equal "adequaterecord", topic2.reload.title - # Testing that the before_update callbacks have run - assert_equal "David", topic1.reload.author_name - assert_equal "David", topic2.reload.author_name - end - - def test_update_on_relation_passing_active_record_object_is_not_permitted - topic = Topic.create!(title: "Foo", author_name: nil) - assert_raises(ArgumentError) do - Topic.where(id: topic.id).update(topic, title: "Bar") - end - end - def test_distinct tag1 = Tag.create(name: "Foo") tag2 = Tag.create(name: "Foo") diff --git a/activerecord/test/cases/result_test.rb b/activerecord/test/cases/result_test.rb index 68fcafb682..825aee2423 100644 --- a/activerecord/test/cases/result_test.rb +++ b/activerecord/test/cases/result_test.rb @@ -21,12 +21,22 @@ module ActiveRecord assert_equal 3, result.length end - test "to_hash returns row_hashes" do + test "to_a returns row_hashes" do assert_equal [ { "col_1" => "row 1 col 1", "col_2" => "row 1 col 2" }, { "col_1" => "row 2 col 1", "col_2" => "row 2 col 2" }, { "col_1" => "row 3 col 1", "col_2" => "row 3 col 2" }, - ], result.to_hash + ], result.to_a + end + + test "to_hash (deprecated) returns row_hashes" do + assert_deprecated do + assert_equal [ + { "col_1" => "row 1 col 1", "col_2" => "row 1 col 2" }, + { "col_1" => "row 2 col 1", "col_2" => "row 2 col 2" }, + { "col_1" => "row 3 col 1", "col_2" => "row 3 col 2" }, + ], result.to_hash + end end test "first returns first row as a hash" do diff --git a/activerecord/test/cases/schema_dumper_test.rb b/activerecord/test/cases/schema_dumper_test.rb index 75b68b521c..db13f20a39 100644 --- a/activerecord/test/cases/schema_dumper_test.rb +++ b/activerecord/test/cases/schema_dumper_test.rb @@ -226,6 +226,20 @@ class SchemaDumperTest < ActiveRecord::TestCase assert_match %r{t\.float\s+"temperature"$}, output end + if ActiveRecord::Base.connection.supports_expression_index? + def test_schema_dump_expression_indices + index_definition = dump_table_schema("companies").split(/\n/).grep(/t\.index.*company_expression_index/).first.strip + + if current_adapter?(:PostgreSQLAdapter) + assert_match %r{CASE.+lower\(\(name\)::text\)}i, index_definition + elsif current_adapter?(:SQLite3Adapter) + assert_match %r{CASE.+lower\(name\)}i, index_definition + else + assert false + end + end + end + if current_adapter?(:Mysql2Adapter) def test_schema_dump_includes_length_for_mysql_binary_fields output = standard_dump @@ -278,11 +292,6 @@ class SchemaDumperTest < ActiveRecord::TestCase assert_match %r{t\.decimal\s+"decimal_array_default",\s+default: \["1.23", "3.45"\],\s+array: true}, output end - def test_schema_dump_expression_indices - index_definition = dump_table_schema("companies").split(/\n/).grep(/t\.index.*company_expression_index/).first.strip - assert_match %r{CASE.+lower\(\(name\)::text\)}i, index_definition - end - def test_schema_dump_interval_type output = dump_table_schema "postgresql_times" assert_match %r{t\.interval\s+"time_interval"$}, output diff --git a/activerecord/test/cases/scoping/default_scoping_test.rb b/activerecord/test/cases/scoping/default_scoping_test.rb index e3a34aa50d..6281712df6 100644 --- a/activerecord/test/cases/scoping/default_scoping_test.rb +++ b/activerecord/test/cases/scoping/default_scoping_test.rb @@ -4,6 +4,7 @@ require "cases/helper" require "models/post" require "models/comment" require "models/developer" +require "models/project" require "models/computer" require "models/vehicle" require "models/cat" @@ -366,6 +367,21 @@ class DefaultScopingTest < ActiveRecord::TestCase assert_equal "Jamis", jamis.name end + def test_create_with_takes_precedence_over_where + developer = Developer.where(name: nil).create_with(name: "Aaron").new + assert_equal "Aaron", developer.name + end + + def test_create_with_nested_attributes + assert_difference("Project.count", 1) do + Developer.create_with( + projects_attributes: [{ name: "p1" }] + ).scoping do + Developer.create!(name: "Aaron") + end + end + end + # FIXME: I don't know if this is *desired* behavior, but it is *today's* # behavior. def test_create_with_empty_hash_will_not_reset diff --git a/activerecord/test/cases/scoping/named_scoping_test.rb b/activerecord/test/cases/scoping/named_scoping_test.rb index 4214f347fb..f707951a16 100644 --- a/activerecord/test/cases/scoping/named_scoping_test.rb +++ b/activerecord/test/cases/scoping/named_scoping_test.rb @@ -489,7 +489,7 @@ class NamedScopingTest < ActiveRecord::TestCase [:public_method, :protected_method, :private_method].each do |reserved_method| assert Topic.respond_to?(reserved_method, true) assert_called(ActiveRecord::Base.logger, :warn) do - silence_warnings { Topic.scope(reserved_method, -> {}) } + silence_warnings { Topic.scope(reserved_method, -> { }) } end end end diff --git a/activerecord/test/cases/scoping/relation_scoping_test.rb b/activerecord/test/cases/scoping/relation_scoping_test.rb index 544adc9b39..b4f4379e5e 100644 --- a/activerecord/test/cases/scoping/relation_scoping_test.rb +++ b/activerecord/test/cases/scoping/relation_scoping_test.rb @@ -236,8 +236,8 @@ class RelationScopingTest < ActiveRecord::TestCase SpecialComment.unscoped.created end - assert_nil Comment.current_scope - assert_nil SpecialComment.current_scope + assert_nil Comment.send(:current_scope) + assert_nil SpecialComment.send(:current_scope) end def test_scoping_respects_current_class diff --git a/activerecord/test/cases/serialized_attribute_test.rb b/activerecord/test/cases/serialized_attribute_test.rb index 4cd4515c3b..1192b30b14 100644 --- a/activerecord/test/cases/serialized_attribute_test.rb +++ b/activerecord/test/cases/serialized_attribute_test.rb @@ -322,7 +322,7 @@ class SerializedAttributeTest < ActiveRecord::TestCase topic = Topic.create!(content: {}) topic2 = Topic.create!(content: nil) - assert_equal [topic, topic2], Topic.where(content: nil) + assert_equal [topic, topic2], Topic.where(content: nil).sort_by(&:id) end def test_nil_is_always_persisted_as_null diff --git a/activerecord/test/cases/suppressor_test.rb b/activerecord/test/cases/suppressor_test.rb index b68f0033d9..9be5356901 100644 --- a/activerecord/test/cases/suppressor_test.rb +++ b/activerecord/test/cases/suppressor_test.rb @@ -66,7 +66,7 @@ class SuppressorTest < ActiveRecord::TestCase def test_suppresses_when_nested_multiple_times assert_no_difference -> { Notification.count } do Notification.suppress do - Notification.suppress {} + Notification.suppress { } Notification.create Notification.create! Notification.new.save diff --git a/activerecord/test/cases/tasks/database_tasks_test.rb b/activerecord/test/cases/tasks/database_tasks_test.rb index a6efe3fa5e..d674bd562f 100644 --- a/activerecord/test/cases/tasks/database_tasks_test.rb +++ b/activerecord/test/cases/tasks/database_tasks_test.rb @@ -46,35 +46,46 @@ module ActiveRecord class DatabaseTasksUtilsTask < ActiveRecord::TestCase def test_raises_an_error_when_called_with_protected_environment - ActiveRecord::MigrationContext.any_instance.stubs(:current_version).returns(1) - protected_environments = ActiveRecord::Base.protected_environments current_env = ActiveRecord::Base.connection.migration_context.current_environment - assert_not_includes protected_environments, current_env - # Assert no error - ActiveRecord::Tasks::DatabaseTasks.check_protected_environments! - - ActiveRecord::Base.protected_environments = [current_env] - assert_raise(ActiveRecord::ProtectedEnvironmentError) do + assert_called_on_instance_of( + ActiveRecord::MigrationContext, + :current_version, + times: 6, + returns: 1 + ) do + assert_not_includes protected_environments, current_env + # Assert no error ActiveRecord::Tasks::DatabaseTasks.check_protected_environments! + + ActiveRecord::Base.protected_environments = [current_env] + + assert_raise(ActiveRecord::ProtectedEnvironmentError) do + ActiveRecord::Tasks::DatabaseTasks.check_protected_environments! + end end ensure ActiveRecord::Base.protected_environments = protected_environments end def test_raises_an_error_when_called_with_protected_environment_which_name_is_a_symbol - ActiveRecord::MigrationContext.any_instance.stubs(:current_version).returns(1) - protected_environments = ActiveRecord::Base.protected_environments current_env = ActiveRecord::Base.connection.migration_context.current_environment - assert_not_includes protected_environments, current_env - # Assert no error - ActiveRecord::Tasks::DatabaseTasks.check_protected_environments! - - ActiveRecord::Base.protected_environments = [current_env.to_sym] - assert_raise(ActiveRecord::ProtectedEnvironmentError) do + assert_called_on_instance_of( + ActiveRecord::MigrationContext, + :current_version, + times: 6, + returns: 1 + ) do + assert_not_includes protected_environments, current_env + # Assert no error ActiveRecord::Tasks::DatabaseTasks.check_protected_environments! + + ActiveRecord::Base.protected_environments = [current_env.to_sym] + assert_raise(ActiveRecord::ProtectedEnvironmentError) do + ActiveRecord::Tasks::DatabaseTasks.check_protected_environments! + end end ensure ActiveRecord::Base.protected_environments = protected_environments @@ -82,10 +93,14 @@ module ActiveRecord def test_raises_an_error_if_no_migrations_have_been_made ActiveRecord::InternalMetadata.stub(:table_exists?, false) do - ActiveRecord::MigrationContext.any_instance.stubs(:current_version).returns(1) - - assert_raise(ActiveRecord::NoEnvironmentInSchemaError) do - ActiveRecord::Tasks::DatabaseTasks.check_protected_environments! + assert_called_on_instance_of( + ActiveRecord::MigrationContext, + :current_version, + returns: 1 + ) do + assert_raise(ActiveRecord::NoEnvironmentInSchemaError) do + ActiveRecord::Tasks::DatabaseTasks.check_protected_environments! + end end end end @@ -213,18 +228,17 @@ module ActiveRecord end private - def with_stubbed_configurations_establish_connection - ActiveRecord::Base.stub(:configurations, @configurations) do - # To refrain from connecting to a newly created empty DB in - # sqlite3_mem tests - ActiveRecord::Base.connection_handler.stub( - :establish_connection, - nil - ) do - yield - end + old_configurations = ActiveRecord::Base.configurations + ActiveRecord::Base.configurations = @configurations + + # To refrain from connecting to a newly created empty DB in + # sqlite3_mem tests + ActiveRecord::Base.connection_handler.stub(:establish_connection, nil) do + yield end + ensure + ActiveRecord::Base.configurations = old_configurations end end @@ -233,7 +247,7 @@ module ActiveRecord @configurations = { "development" => { "database" => "dev-db" }, "test" => { "database" => "test-db" }, - "production" => { "url" => "prod-db-url" } + "production" => { "url" => "abstract://prod-db-host/prod-db" } } end @@ -256,7 +270,7 @@ module ActiveRecord assert_called_with( ActiveRecord::Tasks::DatabaseTasks, :create, - ["url" => "prod-db-url"], + ["adapter" => "abstract", "database" => "prod-db", "host" => "prod-db-host"], ) do ActiveRecord::Tasks::DatabaseTasks.create_current( ActiveSupport::StringInquirer.new("production") @@ -315,13 +329,15 @@ module ActiveRecord end private - def with_stubbed_configurations_establish_connection - ActiveRecord::Base.stub(:configurations, @configurations) do - ActiveRecord::Base.stub(:establish_connection, nil) do - yield - end + old_configurations = ActiveRecord::Base.configurations + ActiveRecord::Base.configurations = @configurations + + ActiveRecord::Base.connection_handler.stub(:establish_connection, nil) do + yield end + ensure + ActiveRecord::Base.configurations = old_configurations end end @@ -330,7 +346,7 @@ module ActiveRecord @configurations = { "development" => { "primary" => { "database" => "dev-db" }, "secondary" => { "database" => "secondary-dev-db" } }, "test" => { "primary" => { "database" => "test-db" }, "secondary" => { "database" => "secondary-test-db" } }, - "production" => { "primary" => { "url" => "prod-db-url" }, "secondary" => { "url" => "secondary-prod-db-url" } } + "production" => { "primary" => { "url" => "abstract://prod-db-host/prod-db" }, "secondary" => { "url" => "abstract://secondary-prod-db-host/secondary-prod-db" } } } end @@ -357,8 +373,8 @@ module ActiveRecord ActiveRecord::Tasks::DatabaseTasks, :create, [ - ["url" => "prod-db-url"], - ["url" => "secondary-prod-db-url"] + ["adapter" => "abstract", "database" => "prod-db", "host" => "prod-db-host"], + ["adapter" => "abstract", "database" => "secondary-prod-db", "host" => "secondary-prod-db-host"] ] ) do ActiveRecord::Tasks::DatabaseTasks.create_current( @@ -426,13 +442,15 @@ module ActiveRecord end private - def with_stubbed_configurations_establish_connection - ActiveRecord::Base.stub(:configurations, @configurations) do - ActiveRecord::Base.stub(:establish_connection, nil) do - yield - end + old_configurations = ActiveRecord::Base.configurations + ActiveRecord::Base.configurations = @configurations + + ActiveRecord::Base.connection_handler.stub(:establish_connection, nil) do + yield end + ensure + ActiveRecord::Base.configurations = old_configurations end end @@ -465,7 +483,7 @@ module ActiveRecord def test_ignores_configurations_without_databases @configurations[:development]["database"] = nil - ActiveRecord::Base.stub(:configurations, @configurations) do + with_stubbed_configurations do assert_not_called(ActiveRecord::Tasks::DatabaseTasks, :drop) do ActiveRecord::Tasks::DatabaseTasks.drop_all end @@ -475,7 +493,7 @@ module ActiveRecord def test_ignores_remote_databases @configurations[:development]["host"] = "my.server.tld" - ActiveRecord::Base.stub(:configurations, @configurations) do + with_stubbed_configurations do assert_not_called(ActiveRecord::Tasks::DatabaseTasks, :drop) do ActiveRecord::Tasks::DatabaseTasks.drop_all end @@ -485,7 +503,7 @@ module ActiveRecord def test_warning_for_remote_databases @configurations[:development]["host"] = "my.server.tld" - ActiveRecord::Base.stub(:configurations, @configurations) do + with_stubbed_configurations do ActiveRecord::Tasks::DatabaseTasks.drop_all assert_match "This task only modifies local databases. my-db is on a remote host.", @@ -496,7 +514,7 @@ module ActiveRecord def test_drops_configurations_with_local_ip @configurations[:development]["host"] = "127.0.0.1" - ActiveRecord::Base.stub(:configurations, @configurations) do + with_stubbed_configurations do assert_called(ActiveRecord::Tasks::DatabaseTasks, :drop) do ActiveRecord::Tasks::DatabaseTasks.drop_all end @@ -506,7 +524,7 @@ module ActiveRecord def test_drops_configurations_with_local_host @configurations[:development]["host"] = "localhost" - ActiveRecord::Base.stub(:configurations, @configurations) do + with_stubbed_configurations do assert_called(ActiveRecord::Tasks::DatabaseTasks, :drop) do ActiveRecord::Tasks::DatabaseTasks.drop_all end @@ -516,12 +534,22 @@ module ActiveRecord def test_drops_configurations_with_blank_hosts @configurations[:development]["host"] = nil - ActiveRecord::Base.stub(:configurations, @configurations) do + with_stubbed_configurations do assert_called(ActiveRecord::Tasks::DatabaseTasks, :drop) do ActiveRecord::Tasks::DatabaseTasks.drop_all end end end + + private + def with_stubbed_configurations + old_configurations = ActiveRecord::Base.configurations + ActiveRecord::Base.configurations = @configurations + + yield + ensure + ActiveRecord::Base.configurations = old_configurations + end end class DatabaseTasksDropCurrentTest < ActiveRecord::TestCase @@ -529,12 +557,12 @@ module ActiveRecord @configurations = { "development" => { "database" => "dev-db" }, "test" => { "database" => "test-db" }, - "production" => { "url" => "prod-db-url" } + "production" => { "url" => "abstract://prod-db-host/prod-db" } } end def test_drops_current_environment_database - ActiveRecord::Base.stub(:configurations, @configurations) do + with_stubbed_configurations do assert_called_with(ActiveRecord::Tasks::DatabaseTasks, :drop, ["database" => "test-db"]) do ActiveRecord::Tasks::DatabaseTasks.drop_current( @@ -545,9 +573,9 @@ module ActiveRecord end def test_drops_current_environment_database_with_url - ActiveRecord::Base.stub(:configurations, @configurations) do + with_stubbed_configurations do assert_called_with(ActiveRecord::Tasks::DatabaseTasks, :drop, - ["url" => "prod-db-url"]) do + ["adapter" => "abstract", "database" => "prod-db", "host" => "prod-db-host"]) do ActiveRecord::Tasks::DatabaseTasks.drop_current( ActiveSupport::StringInquirer.new("production") ) @@ -556,7 +584,7 @@ module ActiveRecord end def test_drops_test_and_development_databases_when_env_was_not_specified - ActiveRecord::Base.stub(:configurations, @configurations) do + with_stubbed_configurations do assert_called_with( ActiveRecord::Tasks::DatabaseTasks, :drop, @@ -576,7 +604,7 @@ module ActiveRecord old_env = ENV["RAILS_ENV"] ENV["RAILS_ENV"] = "development" - ActiveRecord::Base.stub(:configurations, @configurations) do + with_stubbed_configurations do assert_called_with( ActiveRecord::Tasks::DatabaseTasks, :drop, @@ -593,6 +621,16 @@ module ActiveRecord ensure ENV["RAILS_ENV"] = old_env end + + private + def with_stubbed_configurations + old_configurations = ActiveRecord::Base.configurations + ActiveRecord::Base.configurations = @configurations + + yield + ensure + ActiveRecord::Base.configurations = old_configurations + end end class DatabaseTasksDropCurrentThreeTierTest < ActiveRecord::TestCase @@ -600,12 +638,12 @@ module ActiveRecord @configurations = { "development" => { "primary" => { "database" => "dev-db" }, "secondary" => { "database" => "secondary-dev-db" } }, "test" => { "primary" => { "database" => "test-db" }, "secondary" => { "database" => "secondary-test-db" } }, - "production" => { "primary" => { "url" => "prod-db-url" }, "secondary" => { "url" => "secondary-prod-db-url" } } + "production" => { "primary" => { "url" => "abstract://prod-db-host/prod-db" }, "secondary" => { "url" => "abstract://secondary-prod-db-host/secondary-prod-db" } } } end def test_drops_current_environment_database - ActiveRecord::Base.stub(:configurations, @configurations) do + with_stubbed_configurations do assert_called_with( ActiveRecord::Tasks::DatabaseTasks, :drop, @@ -622,13 +660,13 @@ module ActiveRecord end def test_drops_current_environment_database_with_url - ActiveRecord::Base.stub(:configurations, @configurations) do + with_stubbed_configurations do assert_called_with( ActiveRecord::Tasks::DatabaseTasks, :drop, [ - ["url" => "prod-db-url"], - ["url" => "secondary-prod-db-url"] + ["adapter" => "abstract", "database" => "prod-db", "host" => "prod-db-host"], + ["adapter" => "abstract", "database" => "secondary-prod-db", "host" => "secondary-prod-db-host"] ] ) do ActiveRecord::Tasks::DatabaseTasks.drop_current( @@ -639,7 +677,7 @@ module ActiveRecord end def test_drops_test_and_development_databases_when_env_was_not_specified - ActiveRecord::Base.stub(:configurations, @configurations) do + with_stubbed_configurations do assert_called_with( ActiveRecord::Tasks::DatabaseTasks, :drop, @@ -661,7 +699,7 @@ module ActiveRecord old_env = ENV["RAILS_ENV"] ENV["RAILS_ENV"] = "development" - ActiveRecord::Base.stub(:configurations, @configurations) do + with_stubbed_configurations do assert_called_with( ActiveRecord::Tasks::DatabaseTasks, :drop, @@ -680,6 +718,16 @@ module ActiveRecord ensure ENV["RAILS_ENV"] = old_env end + + private + def with_stubbed_configurations + old_configurations = ActiveRecord::Base.configurations + ActiveRecord::Base.configurations = @configurations + + yield + ensure + ActiveRecord::Base.configurations = old_configurations + end end if current_adapter?(:SQLite3Adapter) && !in_memory_db? @@ -833,37 +881,44 @@ module ActiveRecord class DatabaseTasksPurgeCurrentTest < ActiveRecord::TestCase def test_purges_current_environment_database + old_configurations = ActiveRecord::Base.configurations configurations = { "development" => { "database" => "dev-db" }, "test" => { "database" => "test-db" }, "production" => { "database" => "prod-db" } } - ActiveRecord::Base.stub(:configurations, configurations) do - assert_called_with( - ActiveRecord::Tasks::DatabaseTasks, - :purge, - ["database" => "prod-db"] - ) do - assert_called_with(ActiveRecord::Base, :establish_connection, [:production]) do - ActiveRecord::Tasks::DatabaseTasks.purge_current("production") - end + + ActiveRecord::Base.configurations = configurations + + assert_called_with( + ActiveRecord::Tasks::DatabaseTasks, + :purge, + ["database" => "prod-db"] + ) do + assert_called_with(ActiveRecord::Base, :establish_connection, [:production]) do + ActiveRecord::Tasks::DatabaseTasks.purge_current("production") end end + ensure + ActiveRecord::Base.configurations = old_configurations end end class DatabaseTasksPurgeAllTest < ActiveRecord::TestCase def test_purge_all_local_configurations + old_configurations = ActiveRecord::Base.configurations configurations = { development: { "database" => "my-db" } } - ActiveRecord::Base.stub(:configurations, configurations) do - assert_called_with( - ActiveRecord::Tasks::DatabaseTasks, - :purge, - ["database" => "my-db"] - ) do - ActiveRecord::Tasks::DatabaseTasks.purge_all - end + ActiveRecord::Base.configurations = configurations + + assert_called_with( + ActiveRecord::Tasks::DatabaseTasks, + :purge, + ["database" => "my-db"] + ) do + ActiveRecord::Tasks::DatabaseTasks.purge_all end + ensure + ActiveRecord::Base.configurations = old_configurations end end diff --git a/activerecord/test/cases/tasks/mysql_rake_test.rb b/activerecord/test/cases/tasks/mysql_rake_test.rb index eeb4222d97..4d6dff68f9 100644 --- a/activerecord/test/cases/tasks/mysql_rake_test.rb +++ b/activerecord/test/cases/tasks/mysql_rake_test.rb @@ -152,10 +152,14 @@ if current_adapter?(:Mysql2Adapter) end def test_establishes_connection_to_mysql_database - with_stubbed_connection_establish_connection do - ActiveRecord::Base.expects(:establish_connection).with @configuration - - ActiveRecord::Tasks::DatabaseTasks.drop @configuration + ActiveRecord::Base.stub(:connection, @connection) do + assert_called_with( + ActiveRecord::Base, + :establish_connection, + [@configuration] + ) do + ActiveRecord::Tasks::DatabaseTasks.drop @configuration + end end end @@ -196,10 +200,14 @@ if current_adapter?(:Mysql2Adapter) end def test_establishes_connection_to_the_appropriate_database - with_stubbed_connection_establish_connection do - ActiveRecord::Base.expects(:establish_connection).with(@configuration) - - ActiveRecord::Tasks::DatabaseTasks.purge @configuration + ActiveRecord::Base.stub(:connection, @connection) do + assert_called_with( + ActiveRecord::Base, + :establish_connection, + [@configuration] + ) do + ActiveRecord::Tasks::DatabaseTasks.purge @configuration + end end end diff --git a/activerecord/test/cases/tasks/postgresql_rake_test.rb b/activerecord/test/cases/tasks/postgresql_rake_test.rb index 00005e7a0d..065ba7734c 100644 --- a/activerecord/test/cases/tasks/postgresql_rake_test.rb +++ b/activerecord/test/cases/tasks/postgresql_rake_test.rb @@ -112,7 +112,7 @@ if current_adapter?(:PostgreSQLAdapter) ActiveRecord::Base.stub(:connection, @connection) do ActiveRecord::Base.stub(:establish_connection, -> * { raise Exception }) do assert_raises(Exception) { ActiveRecord::Tasks::DatabaseTasks.create @configuration } - assert_match "Couldn't create database for #{@configuration.inspect}", $stderr.string + assert_match "Couldn't create '#{@configuration['database']}' database. Please check your configuration.", $stderr.string end end end @@ -166,12 +166,17 @@ if current_adapter?(:PostgreSQLAdapter) def test_establishes_connection_to_postgresql_database ActiveRecord::Base.stub(:connection, @connection) do - ActiveRecord::Base.expects(:establish_connection).with( - "adapter" => "postgresql", - "database" => "postgres", - "schema_search_path" => "public" - ) - ActiveRecord::Tasks::DatabaseTasks.drop @configuration + assert_called_with( + ActiveRecord::Base, + :establish_connection, + [ + "adapter" => "postgresql", + "database" => "postgres", + "schema_search_path" => "public" + ] + ) do + ActiveRecord::Tasks::DatabaseTasks.drop @configuration + end end end @@ -491,7 +496,7 @@ if current_adapter?(:PostgreSQLAdapter) assert_called_with( Kernel, :system, - ["psql", "-v", "ON_ERROR_STOP=1", "-q", "-f", filename, @configuration["database"]], + ["psql", "-v", "ON_ERROR_STOP=1", "-q", "-X", "-f", filename, @configuration["database"]], returns: true ) do ActiveRecord::Tasks::DatabaseTasks.structure_load(@configuration, filename) @@ -500,7 +505,7 @@ if current_adapter?(:PostgreSQLAdapter) def test_structure_load_with_extra_flags filename = "awesome-file.sql" - expected_command = ["psql", "-v", "ON_ERROR_STOP=1", "-q", "-f", filename, "--noop", @configuration["database"]] + expected_command = ["psql", "-v", "ON_ERROR_STOP=1", "-q", "-X", "-f", filename, "--noop", @configuration["database"]] assert_called_with(Kernel, :system, expected_command, returns: true) do with_structure_load_flags(["--noop"]) do @@ -514,7 +519,7 @@ if current_adapter?(:PostgreSQLAdapter) assert_called_with( Kernel, :system, - ["psql", "-v", "ON_ERROR_STOP=1", "-q", "-f", filename, @configuration["database"]], + ["psql", "-v", "ON_ERROR_STOP=1", "-q", "-X", "-f", filename, @configuration["database"]], returns: true ) do ActiveRecord::Tasks::DatabaseTasks.structure_load(@configuration, filename) diff --git a/activerecord/test/cases/tasks/sqlite_rake_test.rb b/activerecord/test/cases/tasks/sqlite_rake_test.rb index 7eb062b456..c1092b97c1 100644 --- a/activerecord/test/cases/tasks/sqlite_rake_test.rb +++ b/activerecord/test/cases/tasks/sqlite_rake_test.rb @@ -47,9 +47,9 @@ if current_adapter?(:SQLite3Adapter) def test_db_create_with_file_does_nothing File.stub(:exist?, true) do - ActiveRecord::Base.expects(:establish_connection).never - - ActiveRecord::Tasks::DatabaseTasks.create @configuration, "/rails/root" + assert_not_called(ActiveRecord::Base, :establish_connection) do + ActiveRecord::Tasks::DatabaseTasks.create @configuration, "/rails/root" + end end end @@ -62,7 +62,7 @@ if current_adapter?(:SQLite3Adapter) def test_db_create_with_error_prints_message ActiveRecord::Base.stub(:establish_connection, proc { raise Exception }) do assert_raises(Exception) { ActiveRecord::Tasks::DatabaseTasks.create @configuration, "/rails/root" } - assert_match "Couldn't create database for #{@configuration.inspect}", $stderr.string + assert_match "Couldn't create '#{@configuration['database']}' database. Please check your configuration.", $stderr.string end end end diff --git a/activerecord/test/cases/test_case.rb b/activerecord/test/cases/test_case.rb index 409b07e56c..40947767f3 100644 --- a/activerecord/test/cases/test_case.rb +++ b/activerecord/test/cases/test_case.rb @@ -31,6 +31,7 @@ module ActiveRecord end def capture_sql + ActiveRecord::Base.connection.materialize_transactions SQLCounter.clear_log yield SQLCounter.log_all.dup @@ -48,6 +49,7 @@ module ActiveRecord def assert_queries(num = 1, options = {}) ignore_none = options.fetch(:ignore_none) { num == :any } + ActiveRecord::Base.connection.materialize_transactions SQLCounter.clear_log x = yield the_log = ignore_none ? SQLCounter.log_all : SQLCounter.log diff --git a/activerecord/test/cases/transaction_isolation_test.rb b/activerecord/test/cases/transaction_isolation_test.rb index eaafd13360..2932969412 100644 --- a/activerecord/test/cases/transaction_isolation_test.rb +++ b/activerecord/test/cases/transaction_isolation_test.rb @@ -11,7 +11,7 @@ unless ActiveRecord::Base.connection.supports_transaction_isolation? test "setting the isolation level raises an error" do assert_raises(ActiveRecord::TransactionIsolationError) do - Tag.transaction(isolation: :serializable) {} + Tag.transaction(isolation: :serializable) { Tag.connection.materialize_transactions } end end end @@ -90,7 +90,7 @@ else test "setting isolation when joining a transaction raises an error" do Tag.transaction do assert_raises(ActiveRecord::TransactionIsolationError) do - Tag.transaction(isolation: :serializable) {} + Tag.transaction(isolation: :serializable) { } end end end @@ -98,7 +98,7 @@ else test "setting isolation when starting a nested transaction raises error" do Tag.transaction do assert_raises(ActiveRecord::TransactionIsolationError) do - Tag.transaction(requires_new: true, isolation: :serializable) {} + Tag.transaction(requires_new: true, isolation: :serializable) { } end end end diff --git a/activerecord/test/cases/transactions_test.rb b/activerecord/test/cases/transactions_test.rb index 46463ac414..50740054f7 100644 --- a/activerecord/test/cases/transactions_test.rb +++ b/activerecord/test/cases/transactions_test.rb @@ -575,7 +575,7 @@ class TransactionTest < ActiveRecord::TestCase assert_called(Topic.connection, :rollback_db_transaction) do e = assert_raise RuntimeError do Topic.transaction do - # do nothing + Topic.connection.materialize_transactions end end assert_equal "OH NOES", e.message @@ -930,7 +930,7 @@ class TransactionTest < ActiveRecord::TestCase klass = Class.new(ActiveRecord::Base) do self.table_name = "transaction_without_primary_keys" - after_commit {} # necessary to trigger the has_transactional_callbacks branch + after_commit { } # necessary to trigger the has_transactional_callbacks branch end assert_no_difference(-> { klass.count }) do @@ -943,6 +943,76 @@ class TransactionTest < ActiveRecord::TestCase connection.drop_table "transaction_without_primary_keys", if_exists: true end + def test_empty_transaction_is_not_materialized + assert_no_queries do + Topic.transaction { } + end + end + + def test_unprepared_statement_materializes_transaction + assert_sql(/BEGIN/i, /COMMIT/i) do + Topic.transaction { Topic.where("1=1").first } + end + end + + if ActiveRecord::Base.connection.prepared_statements + def test_prepared_statement_materializes_transaction + Topic.first + + assert_sql(/BEGIN/i, /COMMIT/i) do + Topic.transaction { Topic.first } + end + end + end + + def test_savepoint_does_not_materialize_transaction + assert_no_queries do + Topic.transaction do + Topic.transaction(requires_new: true) { } + end + end + end + + def test_raising_does_not_materialize_transaction + assert_raise(RuntimeError) do + assert_no_queries do + Topic.transaction { raise } + end + end + end + + def test_accessing_raw_connection_materializes_transaction + assert_sql(/BEGIN/i, /COMMIT/i) do + Topic.transaction { Topic.connection.raw_connection } + end + end + + def test_accessing_raw_connection_disables_lazy_transactions + Topic.connection.raw_connection + + assert_sql(/BEGIN/i, /COMMIT/i) do + Topic.transaction { } + end + end + + def test_checking_in_connection_reenables_lazy_transactions + connection = Topic.connection_pool.checkout + connection.raw_connection + Topic.connection_pool.checkin connection + + assert_no_queries do + connection.transaction { } + end + end + + def test_transactions_can_be_manually_materialized + assert_sql(/BEGIN/i, /COMMIT/i) do + Topic.transaction do + Topic.connection.materialize_transactions + end + end + end + private %w(validation save destroy).each do |filter| diff --git a/activerecord/test/cases/type/type_map_test.rb b/activerecord/test/cases/type/type_map_test.rb index f3699c11a2..1ce515a90c 100644 --- a/activerecord/test/cases/type/type_map_test.rb +++ b/activerecord/test/cases/type/type_map_test.rb @@ -32,7 +32,7 @@ module ActiveRecord end def test_fuzzy_lookup - string = String.new + string = +"" mapping = TypeMap.new mapping.register_type(/varchar/i, string) @@ -41,7 +41,7 @@ module ActiveRecord end def test_aliasing_types - string = String.new + string = +"" mapping = TypeMap.new mapping.register_type(/string/i, string) @@ -73,7 +73,7 @@ module ActiveRecord end def test_register_proc - string = String.new + string = +"" binary = Binary.new mapping = TypeMap.new diff --git a/activerecord/test/cases/validations_test.rb b/activerecord/test/cases/validations_test.rb index a33877f43a..66763c727f 100644 --- a/activerecord/test/cases/validations_test.rb +++ b/activerecord/test/cases/validations_test.rb @@ -3,11 +3,11 @@ require "cases/helper" require "models/topic" require "models/reply" -require "models/person" require "models/developer" require "models/computer" require "models/parrot" require "models/company" +require "models/price_estimate" class ValidationsTest < ActiveRecord::TestCase fixtures :topics, :developers @@ -183,6 +183,22 @@ class ValidationsTest < ActiveRecord::TestCase assert_not_predicate klass.new(wibble: BigDecimal("97.179")), :valid? end + def test_numericality_validator_wont_be_affected_by_custom_getter + price_estimate = PriceEstimate.new(price: 50) + + assert_equal "$50.00", price_estimate.price + assert_equal 50, price_estimate.price_before_type_cast + assert_equal 50, price_estimate.read_attribute(:price) + + assert_predicate price_estimate, :price_came_from_user? + assert_predicate price_estimate, :valid? + + price_estimate.save! + + assert_not_predicate price_estimate, :price_came_from_user? + assert_predicate price_estimate, :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/config.example.yml b/activerecord/test/config.example.yml index 4bcb2aeea6..be337ddcd8 100644 --- a/activerecord/test/config.example.yml +++ b/activerecord/test/config.example.yml @@ -54,11 +54,11 @@ connections: mysql2: arunit: username: rails - encoding: utf8 - collation: utf8_unicode_ci + encoding: utf8mb4 + collation: utf8mb4_unicode_ci arunit2: username: rails - encoding: utf8 + encoding: utf8mb4 oracle: arunit: diff --git a/activerecord/test/fixtures/citations.yml b/activerecord/test/fixtures/citations.yml new file mode 100644 index 0000000000..d31cb8efa1 --- /dev/null +++ b/activerecord/test/fixtures/citations.yml @@ -0,0 +1,4 @@ +<% 65536.times do |i| %> +fixture_no_<%= i %>: + id: <%= i %> +<% end %> diff --git a/activerecord/test/models/author.rb b/activerecord/test/models/author.rb index 75932c7eb6..8b5a2fa0c8 100644 --- a/activerecord/test/models/author.rb +++ b/activerecord/test/models/author.rb @@ -81,7 +81,7 @@ class Author < ActiveRecord::Base after_add: [:log_after_adding, Proc.new { |o, r| o.post_log << "after_adding_proc#{r.id || '<new>'}" }] has_many :unchangeable_posts, class_name: "Post", before_add: :raise_exception, after_add: :log_after_adding - has_many :categorizations, -> {} + has_many :categorizations, -> { } has_many :categories, through: :categorizations has_many :named_categories, through: :categorizations diff --git a/activerecord/test/models/citation.rb b/activerecord/test/models/citation.rb index 3d786f27eb..cee3d18173 100644 --- a/activerecord/test/models/citation.rb +++ b/activerecord/test/models/citation.rb @@ -2,4 +2,5 @@ class Citation < ActiveRecord::Base belongs_to :reference_of, class_name: "Book", foreign_key: :book2_id + has_many :citations end diff --git a/activerecord/test/models/company.rb b/activerecord/test/models/company.rb index d4d5275b78..838f515aad 100644 --- a/activerecord/test/models/company.rb +++ b/activerecord/test/models/company.rb @@ -122,6 +122,12 @@ class RestrictedWithErrorFirm < Company has_many :companies, -> { order("id") }, foreign_key: "client_of", dependent: :restrict_with_error end +class Agency < Firm + has_many :projects, foreign_key: :firm_id + + accepts_nested_attributes_for :projects +end + class Client < Company belongs_to :firm, foreign_key: "client_of" belongs_to :firm_with_basic_id, class_name: "Firm", foreign_key: "firm_id" @@ -204,4 +210,12 @@ end class VerySpecialClient < SpecialClient end +class NewlyContractedCompany < Company + has_many :new_contracts, foreign_key: "company_id" + + before_save do + self.new_contracts << NewContract.new + end +end + require "models/account" diff --git a/activerecord/test/models/contract.rb b/activerecord/test/models/contract.rb index f273badd85..3f663375c4 100644 --- a/activerecord/test/models/contract.rb +++ b/activerecord/test/models/contract.rb @@ -20,3 +20,7 @@ class Contract < ActiveRecord::Base @bye_count += 1 end end + +class NewContract < Contract + validates :company_id, presence: true +end diff --git a/activerecord/test/models/post.rb b/activerecord/test/models/post.rb index 640cdb33b4..528585fb75 100644 --- a/activerecord/test/models/post.rb +++ b/activerecord/test/models/post.rb @@ -324,7 +324,7 @@ class FakeKlass table[name] end - def enforce_raw_sql_whitelist(*args) + def disallow_raw_sql!(*args) # noop end diff --git a/activerecord/test/models/price_estimate.rb b/activerecord/test/models/price_estimate.rb index f1f88d8d8d..669d0991f7 100644 --- a/activerecord/test/models/price_estimate.rb +++ b/activerecord/test/models/price_estimate.rb @@ -1,6 +1,14 @@ # frozen_string_literal: true class PriceEstimate < ActiveRecord::Base + include ActiveSupport::NumberHelper + belongs_to :estimate_of, polymorphic: true belongs_to :thing, polymorphic: true + + validates_numericality_of :price + + def price + number_to_currency super + end end diff --git a/activerecord/test/models/reply.rb b/activerecord/test/models/reply.rb index 0ea110f4f8..0807bcf875 100644 --- a/activerecord/test/models/reply.rb +++ b/activerecord/test/models/reply.rb @@ -9,6 +9,10 @@ class Reply < Topic has_many :silly_unique_replies, dependent: :destroy, foreign_key: "parent_id" end +class SillyReply < Topic + belongs_to :reply, foreign_key: "parent_id", counter_cache: :replies_count +end + class UniqueReply < Reply belongs_to :topic, foreign_key: "parent_id", counter_cache: true validates_uniqueness_of :content, scope: "parent_id" @@ -54,10 +58,6 @@ class WrongReply < Reply end end -class SillyReply < Reply - belongs_to :reply, foreign_key: "parent_id", counter_cache: :replies_count -end - module Web class Reply < Web::Topic belongs_to :topic, foreign_key: "parent_id", counter_cache: true, class_name: "Web::Topic" diff --git a/activerecord/test/models/topic.rb b/activerecord/test/models/topic.rb index 72699046f9..4aad6a4498 100644 --- a/activerecord/test/models/topic.rb +++ b/activerecord/test/models/topic.rb @@ -24,7 +24,7 @@ class Topic < ActiveRecord::Base end scope "approved_as_string", -> { where(approved: true) } - scope :anonymous_extension, -> {} do + scope :anonymous_extension, -> { } do def one 1 end diff --git a/activerecord/test/schema/mysql2_specific_schema.rb b/activerecord/test/schema/mysql2_specific_schema.rb index 8371ba9528..499280cb0c 100644 --- a/activerecord/test/schema/mysql2_specific_schema.rb +++ b/activerecord/test/schema/mysql2_specific_schema.rb @@ -14,6 +14,13 @@ ActiveRecord::Schema.define do end end + create_table :defaults, force: true do |t| + t.date :fixed_date, default: "2004-01-01" + t.datetime :fixed_time, default: "2004-01-01 00:00:00" + t.column :char1, "char(1)", default: "Y" + t.string :char2, limit: 50, default: "a varchar field" + end + create_table :binary_fields, force: true do |t| t.binary :var_binary, limit: 255 t.binary :var_binary_large, limit: 4095 @@ -29,7 +36,7 @@ ActiveRecord::Schema.define do t.index :var_binary end - create_table :key_tests, force: true, options: "ENGINE=MyISAM" do |t| + create_table :key_tests, force: true do |t| t.string :awesome t.string :pizza t.string :snacks @@ -39,8 +46,8 @@ ActiveRecord::Schema.define do end create_table :collation_tests, id: false, force: true do |t| - t.string :string_cs_column, limit: 1, collation: "utf8_bin" - t.string :string_ci_column, limit: 1, collation: "utf8_general_ci" + t.string :string_cs_column, limit: 1, collation: "utf8mb4_bin" + t.string :string_ci_column, limit: 1, collation: "utf8mb4_general_ci" t.binary :binary_column, limit: 1 end diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb index 266e55f682..2aaf393009 100644 --- a/activerecord/test/schema/schema.rb +++ b/activerecord/test/schema/schema.rb @@ -160,6 +160,7 @@ ActiveRecord::Schema.define do create_table :citations, force: true do |t| t.column :book1_id, :integer t.column :book2_id, :integer + t.references :citation end create_table :clubs, force: true do |t| @@ -946,7 +947,7 @@ ActiveRecord::Schema.define do end [:circles, :squares, :triangles, :non_poly_ones, :non_poly_twos].each do |t| - create_table(t, force: true) {} + create_table(t, force: true) { } end create_table :men, force: true do |t| diff --git a/activerecord/test/schema/sqlite_specific_schema.rb b/activerecord/test/schema/sqlite_specific_schema.rb new file mode 100644 index 0000000000..18192292e4 --- /dev/null +++ b/activerecord/test/schema/sqlite_specific_schema.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +ActiveRecord::Schema.define do + create_table :defaults, force: true do |t| + t.date :fixed_date, default: "2004-01-01" + t.datetime :fixed_time, default: "2004-01-01 00:00:00" + t.column :char1, "char(1)", default: "Y" + t.string :char2, limit: 50, default: "a varchar field" + t.text :char3, limit: 50, default: "a text field" + end +end |