From 18bb9a0e2a46519d9ac980dc26d5ddb7ad6a5ce4 Mon Sep 17 00:00:00 2001 From: Anton Savicky Date: Mon, 28 Aug 2023 18:39:21 +0200 Subject: [PATCH 01/52] style(Rails/TimeZoneAssignment): manual (#103) Co-authored-by: Johnny Shields <27655+johnnyshields@users.noreply.github.com> --- .rubocop_todo.yml | 9 --------- spec/integration/criteria/raw_value_spec.rb | 1 - spec/spec_helper.rb | 4 ++++ spec/support/macros.rb | 5 +---- spec/support/shared/time.rb | 8 +------- 5 files changed, 6 insertions(+), 21 deletions(-) diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index 8f1bd42e1..c9573dfd4 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -800,15 +800,6 @@ Rails/TimeZone: - 'spec/mongoid/touchable_spec.rb' - 'spec/support/models/post.rb' -# Offense count: 5 -# Configuration parameters: Include. -# Include: spec/**/*.rb, test/**/*.rb -Rails/TimeZoneAssignment: - Exclude: - - 'spec/integration/criteria/raw_value_spec.rb' - - 'spec/support/macros.rb' - - 'spec/support/shared/time.rb' - # Offense count: 5 Style/MultilineBlockChain: Exclude: diff --git a/spec/integration/criteria/raw_value_spec.rb b/spec/integration/criteria/raw_value_spec.rb index be873646b..2ab4f8914 100644 --- a/spec/integration/criteria/raw_value_spec.rb +++ b/spec/integration/criteria/raw_value_spec.rb @@ -3,7 +3,6 @@ require 'spec_helper' describe 'Queries with Mongoid::RawValue criteria' do - before { Time.zone = 'UTC' } let(:now_utc) { Time.utc(2020, 1, 1, 16, 0, 0, 0) } let(:today) { Date.new(2020, 1, 1) } diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index e1e9f4112..12c06b0bb 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -163,4 +163,8 @@ class Query timeout_lib.timeout(30) { example.run } end end + + config.around do |example| + Time.use_zone('UTC') { example.run } + end end diff --git a/spec/support/macros.rb b/spec/support/macros.rb index 3b2366c30..a99b331ef 100644 --- a/spec/support/macros.rb +++ b/spec/support/macros.rb @@ -104,10 +104,7 @@ def persistence_context_override(component, value) def time_zone_override(time_zone) around do |example| - old_time_zone = Time.zone - Time.zone = time_zone - example.run - Time.zone = old_time_zone + Time.use_zone(time_zone) { example.run } end end diff --git a/spec/support/shared/time.rb b/spec/support/shared/time.rb index e30384c0c..286714414 100644 --- a/spec/support/shared/time.rb +++ b/spec/support/shared/time.rb @@ -1,13 +1,7 @@ # frozen_string_literal: true shared_context 'setting ActiveSupport time zone' do - before do - Time.zone = 'Tokyo' - end - - after do - Time.zone = nil - end + time_zone_override 'Tokyo' end shared_examples_for 'mongoizes to AS::TimeWithZone' do From f674f2ec2ed4656575faac00ba2e4e07b3043ff9 Mon Sep 17 00:00:00 2001 From: Johnny Shields <27655+johnnyshields@users.noreply.github.com> Date: Tue, 29 Aug 2023 06:53:59 +0900 Subject: [PATCH 02/52] MONGOID-5653 - Move Hash#__nested__ monkey patch method to new module Mongoid::Attributes::Embedded.traverse (#5692) * Move Hash#__nested__ monkey patch method to new module Mongoid::Attributes::Embedded.traverse * Fix whitespace * Fix rubocop whitespace warning * Update embedded.rb * minor changes to test names --------- Co-authored-by: Jamis Buck --- lib/mongoid/attributes.rb | 3 +- lib/mongoid/attributes/embedded.rb | 34 +++++++ lib/mongoid/attributes/nested.rb | 2 +- lib/mongoid/extensions/hash.rb | 22 ----- lib/mongoid/reloadable.rb | 22 +---- lib/mongoid/utils.rb | 14 --- spec/mongoid/attributes/embedded_spec.rb | 118 +++++++++++++++++++++++ spec/mongoid/extensions/hash_spec.rb | 62 ------------ 8 files changed, 158 insertions(+), 119 deletions(-) create mode 100644 lib/mongoid/attributes/embedded.rb create mode 100644 spec/mongoid/attributes/embedded_spec.rb diff --git a/lib/mongoid/attributes.rb b/lib/mongoid/attributes.rb index 888d2c8fd..063cfd3ca 100644 --- a/lib/mongoid/attributes.rb +++ b/lib/mongoid/attributes.rb @@ -3,6 +3,7 @@ require "active_model/attribute_methods" require "mongoid/attributes/dynamic" +require "mongoid/attributes/embedded" require "mongoid/attributes/nested" require "mongoid/attributes/processing" require "mongoid/attributes/projector" @@ -299,7 +300,7 @@ def read_raw_attribute(name) if fields.key?(normalized) attributes[normalized] else - attributes.__nested__(normalized) + Embedded.traverse(attributes, normalized) end else attributes[normalized] diff --git a/lib/mongoid/attributes/embedded.rb b/lib/mongoid/attributes/embedded.rb new file mode 100644 index 000000000..7269f909d --- /dev/null +++ b/lib/mongoid/attributes/embedded.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +module Mongoid + module Attributes + # Utility module for working with embedded attributes. + module Embedded + extend self + + # Fetch an embedded value or subset of attributes via dot notation. + # + # @example Fetch an embedded value via dot notation. + # Embedded.traverse({ 'name' => { 'en' => 'test' } }, 'name.en') + # #=> 'test' + # + # @param [ Hash ] attributes The document attributes. + # @param [ String ] path The dot notation string. + # + # @return [ Object | nil ] The attributes at the given path, + # or nil if the path doesn't exist. + def traverse(attributes, path) + path.split('.').each do |key| + break if attributes.nil? + + attributes = if attributes.try(:key?, key) + attributes[key] + elsif attributes.respond_to?(:each) && key.match?(/\A\d+\z/) + attributes[key.to_i] + end + end + attributes + end + end + end +end diff --git a/lib/mongoid/attributes/nested.rb b/lib/mongoid/attributes/nested.rb index 75eca7e54..e57d07460 100644 --- a/lib/mongoid/attributes/nested.rb +++ b/lib/mongoid/attributes/nested.rb @@ -4,7 +4,7 @@ module Mongoid module Attributes - # Defines behavior around that lovel Rails feature nested attributes. + # Defines behavior for the Rails nested attributes feature. module Nested extend ActiveSupport::Concern diff --git a/lib/mongoid/extensions/hash.rb b/lib/mongoid/extensions/hash.rb index 02f49097b..00104c38a 100644 --- a/lib/mongoid/extensions/hash.rb +++ b/lib/mongoid/extensions/hash.rb @@ -127,28 +127,6 @@ def extract_id self["_id"] || self[:_id] || self["id"] || self[:id] end - # Fetch a nested value via dot syntax. - # - # @example Fetch a nested value via dot syntax. - # { "name" => { "en" => "test" }}.__nested__("name.en") - # - # @param [ String ] string the dot syntax string. - # - # @return [ Object ] The matching value. - def __nested__(string) - keys = string.split(".") - value = self - keys.each do |key| - return nil if value.nil? - value_for_key = value[key] - if value_for_key.nil? && key.to_i.to_s == key - value_for_key = value[key.to_i] - end - value = value_for_key - end - value - end - # Turn the object from the ruby type we deal with to a Mongo friendly # type. # diff --git a/lib/mongoid/reloadable.rb b/lib/mongoid/reloadable.rb index 7a4d27a29..e90b72168 100644 --- a/lib/mongoid/reloadable.rb +++ b/lib/mongoid/reloadable.rb @@ -91,26 +91,10 @@ def reload_root_document # # @return [ Hash ] The reloaded attributes. def reload_embedded_document - extract_embedded_attributes( - collection(_root).find(_root.atomic_selector, session: _session).read(mode: :primary).first + Mongoid::Attributes::Embedded.traverse( + collection(_root).find(_root.atomic_selector, session: _session).read(mode: :primary).first, + atomic_position ) end - - # Extract only the desired embedded document from the attributes. - # - # @example Extract the embedded document. - # document.extract_embedded_attributes(attributes) - # - # @param [ Hash ] attributes The document in the db. - # - # @return [ Hash | nil ] The document's extracted attributes or nil if the - # document doesn't exist. - def extract_embedded_attributes(attributes) - # rubocop:disable Lint/UnmodifiedReduceAccumulator - atomic_position.split('.').inject(attributes) do |attrs, part| - attrs[Utils.maybe_integer(part)] - end - # rubocop:enable Lint/UnmodifiedReduceAccumulator - end end end diff --git a/lib/mongoid/utils.rb b/lib/mongoid/utils.rb index 3c43e4f27..b3ac76141 100644 --- a/lib/mongoid/utils.rb +++ b/lib/mongoid/utils.rb @@ -22,20 +22,6 @@ def placeholder?(value) value == PLACEHOLDER end - # If value can be coerced to an integer, return it as an integer. - # Otherwise, return the value itself. - # - # @param [ String ] value the string to possibly coerce. - # - # @return [ String | Integer ] the result of the coercion. - def maybe_integer(value) - if value.match?(/^\d/) - value.to_i - else - value - end - end - # This function should be used if you need to measure time. # @example Calculate elapsed time. # starting = Utils.monotonic_time diff --git a/spec/mongoid/attributes/embedded_spec.rb b/spec/mongoid/attributes/embedded_spec.rb new file mode 100644 index 000000000..f06269d27 --- /dev/null +++ b/spec/mongoid/attributes/embedded_spec.rb @@ -0,0 +1,118 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Mongoid::Attributes::Embedded do + describe '.traverse' do + subject(:embedded) { described_class.traverse(attributes, path) } + + let(:path) { '100.name' } + + context 'when the attribute key is a string' do + let(:attributes) { { '100' => { 'name' => 'hundred' } } } + + it 'retrieves an embedded value under the provided key' do + expect(embedded).to eq 'hundred' + end + + context 'when the value is false' do + let(:attributes) { { '100' => { 'name' => false } } } + + it 'retrieves the embedded value under the provided key' do + expect(embedded).to be false + end + end + + context 'when the value does not exist' do + let(:attributes) { { '100' => { 0 => 'Please do not return this value!' } } } + + it 'returns nil' do + expect(embedded).to be_nil + end + end + end + + context 'when the attribute key is an integer' do + let(:attributes) { { 100 => { 'name' => 'hundred' } } } + + it 'retrieves an embedded value under the provided key' do + expect(embedded).to eq 'hundred' + end + end + + context 'when the attribute value is nil' do + let(:attributes) { { 100 => { 'name' => nil } } } + + it 'returns nil' do + expect(embedded).to be_nil + end + end + + context 'when both string and integer keys are present' do + let(:attributes) { { '100' => { 'name' => 'Fred' }, 100 => { 'name' => 'Daphne' } } } + + it 'returns the string key value' do + expect(embedded).to eq 'Fred' + end + + context 'when the string key value is nil' do + let(:attributes) { { '100' => nil, 100 => { 'name' => 'Daphne' } } } + + it 'returns nil' do + expect(embedded).to be_nil + end + end + end + + context 'when attributes is an array' do + let(:attributes) do + [ { 'name' => 'Fred' }, { 'name' => 'Daphne' }, { 'name' => 'Velma' }, { 'name' => 'Shaggy' } ] + end + let(:path) { '2.name' } + + it 'retrieves the nth value' do + expect(embedded).to eq 'Velma' + end + + context 'when the member does not exist' do + let(:attributes) { [ { 'name' => 'Fred' }, { 'name' => 'Daphne' } ] } + + it 'returns nil' do + expect(embedded).to be_nil + end + end + end + + context 'when the path includes a scalar value' do + let(:attributes) { { '100' => 'name' } } + + it 'returns nil' do + expect(embedded).to be_nil + end + end + + context 'when the parent key is not present' do + let(:attributes) { { '101' => { 'name' => 'hundred and one' } } } + + it 'returns nil' do + expect(embedded).to be_nil + end + end + + context 'when the attributes are deeply nested' do + let(:attributes) { { '100' => { 'name' => { 300 => %w[a b c] } } } } + + it 'retrieves the embedded subset of attributes' do + expect(embedded).to eq(300 => %w[a b c]) + end + + context 'when the path is deeply nested' do + let(:path) { '100.name.300.1' } + + it 'retrieves the embedded value' do + expect(embedded).to eq 'b' + end + end + end + end +end diff --git a/spec/mongoid/extensions/hash_spec.rb b/spec/mongoid/extensions/hash_spec.rb index c110b233f..cc4135a74 100644 --- a/spec/mongoid/extensions/hash_spec.rb +++ b/spec/mongoid/extensions/hash_spec.rb @@ -220,68 +220,6 @@ end end - context "when the hash key is a string" do - - let(:hash) do - { "100" => { "name" => "hundred" } } - end - - let(:nested) do - hash.__nested__("100.name") - end - - it "should retrieve a nested value under the provided key" do - expect(nested).to eq "hundred" - end - - context 'and the value is falsey' do - let(:hash) do - { "100" => { "name" => false } } - end - it "should retrieve the falsey nested value under the provided key" do - expect(nested).to eq false - end - end - - context 'and the value is nil' do - let(:hash) do - { "100" => { 0 => "Please don't return this value!" } } - end - it "should retrieve the nil nested value under the provided key" do - expect(nested).to eq nil - end - end - end - - context "when the hash key is an integer" do - let(:hash) do - { 100 => { "name" => "hundred" } } - end - - let(:nested) do - hash.__nested__("100.name") - end - - it "should retrieve a nested value under the provided key" do - expect(nested).to eq("hundred") - end - end - - context "when the parent key is not present" do - - let(:hash) do - { "101" => { "name" => "hundred and one" } } - end - - let(:nested) do - hash.__nested__("100.name") - end - - it "should return nil" do - expect(nested).to eq(nil) - end - end - describe ".demongoize" do let(:hash) do From 2840061e72af5ffba555fa82d6a5a90b038ad4f4 Mon Sep 17 00:00:00 2001 From: Johnny Shields <27655+johnnyshields@users.noreply.github.com> Date: Tue, 29 Aug 2023 07:18:43 +0900 Subject: [PATCH 03/52] Upgrade libmongocrypt-helper gem to latest (#5691) --- gemfiles/standard.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gemfiles/standard.rb b/gemfiles/standard.rb index 1428a0e7e..59cdf8d0f 100644 --- a/gemfiles/standard.rb +++ b/gemfiles/standard.rb @@ -50,6 +50,6 @@ def standard_dependencies end if ENV['FLE'] == 'helper' - gem 'libmongocrypt-helper', '~> 1.7.0' + gem 'libmongocrypt-helper', '~> 1.8.0' end end From cce678547a603658c4802c6c1b3f4479a4e76f23 Mon Sep 17 00:00:00 2001 From: Jamis Buck Date: Tue, 29 Aug 2023 11:30:17 -0600 Subject: [PATCH 04/52] MONGOID-5647 Allow #count to be used with #for_js (#5693) * MONGOID-5647 Allow #count to be used with #for_js * look for $where in nested hashes * fix typo in method name --- lib/mongoid/contextual/mongo.rb | 25 ++++++++++++++++++++++++- spec/mongoid/contextual/mongo_spec.rb | 10 ++++++++++ 2 files changed, 34 insertions(+), 1 deletion(-) diff --git a/lib/mongoid/contextual/mongo.rb b/lib/mongoid/contextual/mongo.rb index 14b85124a..8797b7efd 100644 --- a/lib/mongoid/contextual/mongo.rb +++ b/lib/mongoid/contextual/mongo.rb @@ -74,7 +74,12 @@ class Mongo # @return [ Integer ] The number of matches. def count(options = {}, &block) return super(&block) if block_given? - view.count_documents(options) + + if valid_for_count_documents? + view.count_documents(options) + else + view.count(options) + end end # Get the estimated number of documents matching the query. @@ -1038,6 +1043,24 @@ def process_raw_docs(raw_docs, limit) limit ? docs : docs.first end + # Queries whether the current context is valid for use with + # the #count_documents? predicate. A context is valid if it + # does not include a `$where` operator. + # + # @return [ true | false ] whether or not the current context + # excludes a `$where` operator. + def valid_for_count_documents?(hash = view.filter) + # Note that `view.filter` is a BSON::Document, and all keys in a + # BSON::Document are strings; we don't need to worry about symbol + # representations of `$where`. + hash.keys.each do |key| + return false if key == '$where' + return false if hash[key].is_a?(Hash) && !valid_for_count_documents?(hash[key]) + end + + true + end + def raise_document_not_found_error raise Errors::DocumentNotFound.new(klass, nil, nil) end diff --git a/spec/mongoid/contextual/mongo_spec.rb b/spec/mongoid/contextual/mongo_spec.rb index cf4c534ab..fd5583993 100644 --- a/spec/mongoid/contextual/mongo_spec.rb +++ b/spec/mongoid/contextual/mongo_spec.rb @@ -159,6 +159,16 @@ end end end + + context 'when for_js is present' do + let(:context) do + Band.for_js('this.name == "Depeche Mode"') + end + + it 'counts the expected records' do + expect(context.count).to eq(1) + end + end end describe "#estimated_count" do From 8b1829a7b53d2905d24e316406181c392472a399 Mon Sep 17 00:00:00 2001 From: Johnny Shields <27655+johnnyshields@users.noreply.github.com> Date: Fri, 1 Sep 2023 16:55:06 +0900 Subject: [PATCH 05/52] Add call for maintainers to README --- README.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index d10dae5c0..685a041aa 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,10 @@ Mongoid is the Ruby Object Document Mapper (ODM) for MongoDB. This fork of Mongoid is **not** endorsed by or affiliated with MongoDB Inc. 👍 +## 📣 Open Call for Community Steering Committee + +If you would like to help with governance and/or maintenance of this project, please [raise an issue](https://github.com/tablecheck/mongoid-ultra/issues/new). + ## Installation Replace `gem 'mongoid'` in your application's Gemfile with: @@ -133,7 +137,7 @@ The email should be encrypted with the following PGP public key: We appreciate your help to disclose security issues responsibly. -## Maintainership +## Project Governance Mongoid Ultra is shepherded by the team at TableCheck. TableCheck have been avid Mongoid users since 2013, contributing over 150 PRs to Mongoid and MongoDB Ruby projects. TableCheck uses Mongoid to power millions of From 30f46d47950e9d1facad302bd5cc5ae2284ecc2c Mon Sep 17 00:00:00 2001 From: Alex Bevilacqua Date: Fri, 1 Sep 2023 07:20:49 -0400 Subject: [PATCH 06/52] MONGOID-5649: added network compression docs (#5697) --- docs/reference/configuration.txt | 49 ++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/docs/reference/configuration.txt b/docs/reference/configuration.txt index 507511c55..ffb4e8e36 100644 --- a/docs/reference/configuration.txt +++ b/docs/reference/configuration.txt @@ -738,6 +738,55 @@ For more information about TLS context hooks, including best practices for assigning and removing them, see `the Ruby driver documentation `_. +Network Compression +=================== + +Mongoid supports compression of messages to and from MongoDB servers. This functionality is provided by +the Ruby driver, which implements the three algorithms that are supported by MongoDB servers: + +- `Snappy `_: ``snappy`` compression + can be used when connecting to MongoDB servers starting with the 3.4 release, + and requires the `snappy `_ library to be + installed. +- `Zlib `_: ``zlib`` compression can be used when + connecting to MongoDB servers starting with the 3.6 release. +- `Zstandard `_: ``zstd`` compression can be + used when connecting to MongoDB servers starting with the 4.2 release, and + requires the `zstd-ruby `_ library to + be installed. + +To use wire protocol compression, at least one compressor must be explicitly requested +using either the `compressors URI option `_, +or directly within the ``mongoid.yml``: + +.. code-block:: yaml + + development: + # Configure available database clients. (required) + clients: + # Define the default client. (required) + default: + # ... + options: + # These options are Ruby driver options, documented in + # https://mongodb.com/docs/ruby-driver/current/reference/create-client/ + # ... + # Compressors to use. (default is to not use compression) + # Valid values are zstd, zlib or snappy - or any combination of the three + compressors: ["zstd", "snappy"] + +If no compressors are explicitly requested, the driver will not use compression, +even if the required dependencies for one or more compressors are present on the +system. + +The driver chooses the first compressor of the ones requested that is also supported +by the server. The ``zstd`` compressor is recommended as it produces the highest +compression at the same CPU consumption compared to the other compressors. + +For maximum server compatibility all three compressors can be specified, e.g. +as ``compressors: ["zstd", "snappy", "zlib"]``. + + Client-Side Encryption ====================== From 60eb57d31e29710f8aeb378ff22ae14103c72c80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mustafa=20Ate=C5=9F=20Uzun?= Date: Fri, 8 Sep 2023 18:32:27 +0300 Subject: [PATCH 07/52] fix: Rakefile desc spelling (#5719) --- Rakefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Rakefile b/Rakefile index c7e009300..61323eae4 100644 --- a/Rakefile +++ b/Rakefile @@ -89,7 +89,7 @@ desc "Generate all documentation" task :docs => 'docs:yard' namespace :docs do - desc "Generate yard documention" + desc "Generate yard documentation" task :yard do out = File.join('yard-docs', Mongoid::VERSION) FileUtils.rm_rf(out) From c9e55f713149e44ee26e9c42b86c369e941840e5 Mon Sep 17 00:00:00 2001 From: Jamis Buck Date: Tue, 12 Sep 2023 08:02:49 -0600 Subject: [PATCH 08/52] convenience tasks for evergreen (#5720) --- Rakefile | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/Rakefile b/Rakefile index 61323eae4..0a02a9675 100644 --- a/Rakefile +++ b/Rakefile @@ -41,6 +41,32 @@ RSpec::Core::RakeTask.new('spec:progress') do |spec| spec.pattern = "spec/**/*_spec.rb" end +desc 'Build and validate the evergreen config' +task eg: %w[ eg:build eg:validate ] + +# 'eg' == 'evergreen', but evergreen is too many letters for convenience +namespace :eg do + desc 'Builds the .evergreen/config.yml file from the templates' + task :build do + ruby '.evergreen/update-evergreen-configs' + end + + desc 'Validates the .evergreen/config.yml file' + task :validate do + system 'evergreen validate --project mongoid .evergreen/config.yml' + end + + desc 'Updates the evergreen executable to the latest available version' + task :update do + system 'evergreen get-update --install' + end + + desc 'Runs the current branch as an evergreen patch' + task :patch do + system 'evergreen patch --uncommitted --project mongoid --browse --auto-description --yes' + end +end + CLASSIFIERS = [ [%r,^mongoid/attribute,, :attributes], [%r,^mongoid/association/[or],, :associations_referenced], From f40bb73c23c0aea6459009b9b0c24f7a878603fe Mon Sep 17 00:00:00 2001 From: Dmitry Rybakov Date: Thu, 14 Sep 2023 10:20:24 +0200 Subject: [PATCH 09/52] RUBY-5651 Deprecate for_js API (#5721) --- docs/release-notes/mongoid-9.0.txt | 6 ++++++ lib/mongoid/contextual/mongo.rb | 5 +++++ lib/mongoid/criteria.rb | 4 ++++ lib/mongoid/deprecation.rb | 9 ++++++--- spec/mongoid/criteria_spec.rb | 6 ++++++ 5 files changed, 27 insertions(+), 3 deletions(-) diff --git a/docs/release-notes/mongoid-9.0.txt b/docs/release-notes/mongoid-9.0.txt index 0a5ab664b..b47977010 100644 --- a/docs/release-notes/mongoid-9.0.txt +++ b/docs/release-notes/mongoid-9.0.txt @@ -18,6 +18,12 @@ please consult GitHub releases for detailed release notes and JIRA for the complete list of issues fixed in each release, including bug fixes. +``for_js`` method is deprecated +------------------------------- + +The ``for_js`` method is deprecated and will be removed in Mongoid 10.0. + + Deprecated options removed -------------------------- diff --git a/lib/mongoid/contextual/mongo.rb b/lib/mongoid/contextual/mongo.rb index 8797b7efd..916d8a2a7 100644 --- a/lib/mongoid/contextual/mongo.rb +++ b/lib/mongoid/contextual/mongo.rb @@ -78,6 +78,8 @@ def count(options = {}, &block) if valid_for_count_documents? view.count_documents(options) else + # TODO: Remove this when we remove the deprecated for_js API. + # https://jira.mongodb.org/browse/MONGOID-5681 view.count(options) end end @@ -1049,6 +1051,9 @@ def process_raw_docs(raw_docs, limit) # # @return [ true | false ] whether or not the current context # excludes a `$where` operator. + # + # TODO: Remove this method when we remove the deprecated for_js API. + # https://jira.mongodb.org/browse/MONGOID-5681 def valid_for_count_documents?(hash = view.filter) # Note that `view.filter` is a BSON::Document, and all keys in a # BSON::Document are strings; we don't need to worry about symbol diff --git a/lib/mongoid/criteria.rb b/lib/mongoid/criteria.rb index e90a7c751..ad365b5a8 100644 --- a/lib/mongoid/criteria.rb +++ b/lib/mongoid/criteria.rb @@ -41,6 +41,8 @@ class Criteria include Clients::Sessions include Options + Mongoid.deprecate(self, :for_js) + # Static array used to check with method missing - we only need to ever # instantiate once. CHECK = [] @@ -439,6 +441,8 @@ def without_options # @param [ Hash ] scope The scope for the code. # # @return [ Criteria ] The criteria. + # + # @deprecated def for_js(javascript, scope = {}) code = if scope.empty? # CodeWithScope is not supported for $where as of MongoDB 4.4 diff --git a/lib/mongoid/deprecation.rb b/lib/mongoid/deprecation.rb index c97c909f7..4fb299418 100644 --- a/lib/mongoid/deprecation.rb +++ b/lib/mongoid/deprecation.rb @@ -6,10 +6,13 @@ module Mongoid # Utility class for logging deprecation warnings. class Deprecation < ::ActiveSupport::Deprecation - @gem_name = 'Mongoid' + def initialize + # Per change policy, deprecations will be removed in the next major version. + deprecation_horizon = "#{Mongoid::VERSION.split('.').first.to_i + 1}.0".freeze + gem_name = 'Mongoid' + super(deprecation_horizon, gem_name) + end - # Per change policy, deprecations will be removed in the next major version. - @deprecation_horizon = "#{Mongoid::VERSION.split('.').first.to_i + 1}.0".freeze # Overrides default ActiveSupport::Deprecation behavior # to use Mongoid's logger. diff --git a/spec/mongoid/criteria_spec.rb b/spec/mongoid/criteria_spec.rb index a18da5a35..6996341e2 100644 --- a/spec/mongoid/criteria_spec.rb +++ b/spec/mongoid/criteria_spec.rb @@ -2977,6 +2977,12 @@ def self.ages; self; end Band.create!(name: "Depeche Mode") end + it 'is deprecated' do + expect(Mongoid.logger).to receive(:warn).with(/for_js is deprecated/).and_call_original + + Band.for_js("this.name == 'Depeche Mode'") + end + context "when the code has no scope" do let(:criteria) do From 8f74b689c2295616d4042725655f45917e062bb7 Mon Sep 17 00:00:00 2001 From: Jamis Buck Date: Thu, 14 Sep 2023 08:05:34 -0600 Subject: [PATCH 10/52] MONGOID-5656 Rails specs ought to enable FLE (#5718) * make sure and test the rails specs with fle enabled * only run FLE with Rails 7 (due to OS constraints with cmake/libmongocrypt-helper) * see about making GH actions build and use libmongocrypt-helper * make sure command-line parsing doesn't pick up arguments that weren't intended for it * a bit more information for deciphering what's wrong under GH actions * *grumble* use a real exception class * run `rake ci` instead, to better isolate the specs * disambiguate test names * use logger to report error in collection creation also, use log-levels to minimize log spam in tests, instead of stubbing the logger --- .evergreen/config.yml | 3 +- .evergreen/config/variants.yml.erb | 5 ++- .github/workflows/test.yml | 41 ++++++------------- lib/mongoid/tasks/database.rb | 3 ++ lib/mongoid/tasks/encryption.rake | 51 ++++++++++++++++-------- spec/mongoid/tasks/database_rake_spec.rb | 4 +- 6 files changed, 56 insertions(+), 51 deletions(-) diff --git a/.evergreen/config.yml b/.evergreen/config.yml index 1eda42aa3..3aff112ac 100644 --- a/.evergreen/config.yml +++ b/.evergreen/config.yml @@ -680,7 +680,8 @@ buildvariants: mongodb-version: "6.0" topology: "standalone" rails: ['7.0'] - os: debian11 + os: ubuntu-22.04 + fle: helper display_name: "${rails}, ${driver}, ${mongodb-version}" tasks: - name: "test" diff --git a/.evergreen/config/variants.yml.erb b/.evergreen/config/variants.yml.erb index 43098d9f1..b0d633844 100644 --- a/.evergreen/config/variants.yml.erb +++ b/.evergreen/config/variants.yml.erb @@ -116,8 +116,9 @@ buildvariants: mongodb-version: "6.0" topology: "standalone" rails: ['7.0'] - os: debian11 - display_name: "${rails}, ${driver}, ${mongodb-version}" + os: ubuntu-22.04 + fle: helper + display_name: "${rails}, ${driver}, ${mongodb-version} (FLE ${fle})" tasks: - name: "test" diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 70285f0ca..dc8961f5c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -7,8 +7,8 @@ name: Run Mongoid Tests - pull_request jobs: build: - name: "${{matrix.ruby}} driver-${{matrix.driver}} mongodb-${{matrix.mongodb}} - ${{matrix.topology}}" + name: "${{matrix.ruby}} drv:${{matrix.driver}} db:${{matrix.mongodb}} + rails:${{matrix.rails}} fle:${{matrix.fle}} ${{matrix.topology}}" env: CI: true TESTOPTS: "-v" @@ -24,8 +24,6 @@ jobs: os: ubuntu-20.04 task: test driver: current - rails: - i18n: gemfile: Gemfile experimental: false - mongodb: '6.0' @@ -34,8 +32,6 @@ jobs: os: ubuntu-20.04 task: test driver: current - rails: - i18n: gemfile: Gemfile experimental: false - mongodb: '6.0' @@ -44,8 +40,6 @@ jobs: os: ubuntu-20.04 task: test driver: current - rails: - i18n: gemfile: Gemfile experimental: false - mongodb: '6.0' @@ -54,8 +48,6 @@ jobs: os: ubuntu-20.04 task: test driver: master - rails: - i18n: gemfile: gemfiles/driver_master.gemfile experimental: true - mongodb: '6.0' @@ -64,8 +56,6 @@ jobs: os: ubuntu-20.04 task: test driver: stable - rails: - i18n: gemfile: gemfiles/driver_stable.gemfile experimental: false - mongodb: '6.0' @@ -75,7 +65,7 @@ jobs: task: test driver: current rails: '7.0' - i18n: + fle: helper gemfile: gemfiles/rails-7.0.gemfile experimental: false - mongodb: '6.0' @@ -85,7 +75,7 @@ jobs: task: test driver: current rails: '6.1' - i18n: + fle: helper gemfile: gemfiles/rails-6.1.gemfile experimental: false - mongodb: '6.0' @@ -95,7 +85,7 @@ jobs: task: test driver: current rails: '7.0' - i18n: + fle: helper gemfile: gemfiles/rails-7.0.gemfile experimental: false - mongodb: '6.0' @@ -105,7 +95,7 @@ jobs: task: test driver: current rails: '6.1' - i18n: + fle: helper gemfile: gemfiles/rails-6.1.gemfile experimental: false - mongodb: '6.0' @@ -115,7 +105,7 @@ jobs: task: test driver: current rails: '6.0' - i18n: + fle: helper gemfile: gemfiles/rails-6.0.gemfile experimental: false - mongodb: '6.0' @@ -125,7 +115,7 @@ jobs: task: test driver: current rails: '5.2' - i18n: + fle: helper gemfile: gemfiles/rails-5.2.gemfile experimental: false - mongodb: '6.0' @@ -135,7 +125,7 @@ jobs: task: test driver: current rails: '6.0' - i18n: + fle: helper gemfile: gemfiles/rails-6.0.gemfile experimental: false - mongodb: '5.0' @@ -144,8 +134,6 @@ jobs: os: ubuntu-20.04 task: test driver: current - rails: - i18n: gemfile: Gemfile experimental: false - mongodb: '4.4' @@ -154,8 +142,6 @@ jobs: os: ubuntu-20.04 task: test driver: current - rails: - i18n: gemfile: Gemfile experimental: false - mongodb: '4.0' @@ -164,8 +150,6 @@ jobs: os: ubuntu-20.04 task: test driver: current - rails: - i18n: gemfile: Gemfile experimental: false - mongodb: '3.6' @@ -174,8 +158,6 @@ jobs: os: ubuntu-20.04 task: test driver: current - rails: - i18n: gemfile: Gemfile experimental: false @@ -193,6 +175,7 @@ jobs: - name: load ruby uses: ruby/setup-ruby@v1 env: + FLE: "${{matrix.fle}}" BUNDLE_GEMFILE: "${{matrix.gemfile}}" with: ruby-version: "${{matrix.ruby}}" @@ -200,11 +183,13 @@ jobs: - name: bundle run: bundle install --jobs 4 --retry 3 env: + FLE: "${{matrix.fle}}" BUNDLE_GEMFILE: "${{matrix.gemfile}}" - name: test timeout-minutes: 60 continue-on-error: "${{matrix.experimental}}" - run: bundle exec rake spec + run: bundle exec rake ci env: BUNDLE_GEMFILE: "${{matrix.gemfile}}" + FLE: "${{matrix.fle}}" MONGODB_URI: "${{ steps.start-mongodb.outputs.cluster-uri }}" diff --git a/lib/mongoid/tasks/database.rb b/lib/mongoid/tasks/database.rb index ab39f86e1..a779b7cfb 100644 --- a/lib/mongoid/tasks/database.rb +++ b/lib/mongoid/tasks/database.rb @@ -26,6 +26,9 @@ def create_collections(models = ::Mongoid.models, force: false) else logger.info("MONGOID: collection options ignored on: #{model}, please define in the root model.") end + rescue Exception + logger.error "error while creating collection for #{model}" + raise end end diff --git a/lib/mongoid/tasks/encryption.rake b/lib/mongoid/tasks/encryption.rake index 2f9cf8cdf..84871201e 100644 --- a/lib/mongoid/tasks/encryption.rake +++ b/lib/mongoid/tasks/encryption.rake @@ -2,27 +2,45 @@ require 'optparse' -# rubocop:disable Metrics/BlockLength +def parse_data_key_options(argv = ARGV) + # The only way to use OptionParser to parse custom options in rake is + # to pass an empty argument ("--") before specifying them, e.g.: + # + # rake db:mongoid:encryption:create_data_key -- --client default + # + # Otherwise, rake complains about an unknown option. Thus, we can tell + # if the argument list is valid for us to parse by detecting this empty + # argument. + # + # (This works around an issue in the tests, where the specs are loading + # the tasks directly to test them, but the option parser is then picking + # up rspec command-line arguments and raising an exception). + return {} unless argv.include?('--') + + {}.tap do |options| + parser = OptionParser.new do |opts| + opts.on('-c', '--client CLIENT', 'Name of the client to use') do |v| + options[:client_name] = v + end + opts.on('-p', '--provider PROVIDER', 'KMS provider to use') do |v| + options[:kms_provider_name] = v + end + opts.on('-n', '--key-alt-name KEY_ALT_NAME', 'Alternate name for the key') do |v| + options[:key_alt_name] = v + end + end + # rubocop:disable Lint/EmptyBlock + parser.parse!(parser.order!(argv) {}) + # rubocop:enable Lint/EmptyBlock + end +end + namespace :db do namespace :mongoid do namespace :encryption do desc 'Create encryption key' task create_data_key: [ :environment ] do - options = {} - parser = OptionParser.new do |opts| - opts.on('-c', '--client CLIENT', 'Name of the client to use') do |v| - options[:client_name] = v - end - opts.on('-p', '--provider PROVIDER', 'KMS provider to use') do |v| - options[:kms_provider_name] = v - end - opts.on('-n', '--key-alt-name KEY_ALT_NAME', 'Alternate name for the key') do |v| - options[:key_alt_name] = v - end - end - # rubocop:disable Lint/EmptyBlock - parser.parse!(parser.order!(ARGV) {}) - # rubocop:enable Lint/EmptyBlock + options = parse_data_key_options result = Mongoid::Tasks::Encryption.create_data_key( client_name: options[:client_name], kms_provider_name: options[:kms_provider_name], @@ -39,4 +57,3 @@ namespace :db do end end end -# rubocop:enable Metrics/BlockLength diff --git a/spec/mongoid/tasks/database_rake_spec.rb b/spec/mongoid/tasks/database_rake_spec.rb index e920e99be..9340b9d48 100644 --- a/spec/mongoid/tasks/database_rake_spec.rb +++ b/spec/mongoid/tasks/database_rake_spec.rb @@ -11,9 +11,7 @@ let(:task_file) { "mongoid/tasks/database" } let(:logger) do - double("logger").tap do |log| - allow(log).to receive(:info) - end + Logger.new(STDOUT, level: :error, formatter: ->(_sev, _dt, _prog, msg) { msg }) end before do From 34f7a919fbf6882f6f35b02403d939144afe1add Mon Sep 17 00:00:00 2001 From: Jamis Buck Date: Wed, 27 Sep 2023 15:24:42 -0600 Subject: [PATCH 11/52] MONGOID-5601 Atlas Search Index Management (#5723) * let's see if this'll start up an atlas cluster... * need to add a variant so the spec will actually run * appease rubocop * run on a supported os * need .mod/drivers-evergreen-tools * this got put in the wrong directory * get specs to pass * rubocop * finish specs for atlas index management * docs for the search index stuff * rubocop * return the correct value * add missing documentation --- .evergreen/config.yml | 169 ++++++++++++++++------- .evergreen/config/axes.yml.erb | 5 +- .evergreen/config/commands.yml.erb | 142 +++++++++++++------ .evergreen/config/variants.yml.erb | 10 ++ .evergreen/run-tests-atlas-full.sh | 24 ++++ .gitmodules | 3 + .mod/drivers-evergreen-tools | 1 + docs/reference/indexes.txt | 61 ++++++++ docs/release-notes/mongoid-9.0.txt | 47 +++++++ lib/mongoid/composable.rb | 2 + lib/mongoid/railties/database.rake | 5 + lib/mongoid/search_indexable.rb | 167 ++++++++++++++++++++++ lib/mongoid/tasks/database.rake | 11 ++ lib/mongoid/tasks/database.rb | 56 ++++++++ lib/mongoid/utils.rb | 11 ++ spec/lite_spec_helper.rb | 45 ++++-- spec/mongoid/search_indexable_spec.rb | 147 ++++++++++++++++++++ spec/mongoid/tasks/database_rake_spec.rb | 70 ++++++++++ spec/mongoid/tasks/database_spec.rb | 60 +++++++- spec/spec_helper.rb | 39 +++--- spec/support/spec_config.rb | 8 +- 21 files changed, 965 insertions(+), 118 deletions(-) create mode 100755 .evergreen/run-tests-atlas-full.sh create mode 160000 .mod/drivers-evergreen-tools create mode 100644 lib/mongoid/search_indexable.rb create mode 100644 spec/mongoid/search_indexable_spec.rb diff --git a/.evergreen/config.yml b/.evergreen/config.yml index 3aff112ac..70e4ae92e 100644 --- a/.evergreen/config.yml +++ b/.evergreen/config.yml @@ -44,50 +44,56 @@ functions: params: working_dir: "src" script: | - # Get the current unique version of this checkout - if [ "${is_patch}" = "true" ]; then - CURRENT_VERSION=$(git describe)-patch-${version_id} - else - CURRENT_VERSION=latest - fi - - export DRIVERS_TOOLS="$(pwd)/../drivers-tools" - - export MONGO_ORCHESTRATION_HOME="$DRIVERS_TOOLS/.evergreen/orchestration" - export MONGODB_BINARIES="$DRIVERS_TOOLS/mongodb/bin" - export UPLOAD_BUCKET="${project}" - export PROJECT_DIRECTORY="$(pwd)" - - cat < expansion.yml - CURRENT_VERSION: "$CURRENT_VERSION" - DRIVERS_TOOLS: "$DRIVERS_TOOLS" - MONGO_ORCHESTRATION_HOME: "$MONGO_ORCHESTRATION_HOME" - MONGODB_BINARIES: "$MONGODB_BINARIES" - UPLOAD_BUCKET: "$UPLOAD_BUCKET" - PROJECT_DIRECTORY: "$PROJECT_DIRECTORY" - PREPARE_SHELL: | - set -o errexit - set -o xtrace - export DRIVERS_TOOLS="$DRIVERS_TOOLS" - export MONGO_ORCHESTRATION_HOME="$MONGO_ORCHESTRATION_HOME" - export MONGODB_BINARIES="$MONGODB_BINARIES" - export UPLOAD_BUCKET="$UPLOAD_BUCKET" - export PROJECT_DIRECTORY="$PROJECT_DIRECTORY" - - export TMPDIR="$MONGO_ORCHESTRATION_HOME/db" - export PATH="$MONGODB_BINARIES:$PATH" - export PROJECT="${project}" - - export MONGODB_VERSION=${VERSION} - export TOPOLOGY=${TOPOLOGY} - export SINGLE_MONGOS=${SINGLE_MONGOS} - export AUTH=${AUTH} - export SSL=${SSL} - export APP_TESTS=${APP_TESTS} - export DOCKER_DISTRO=${DOCKER_DISTRO} - EOT - # See what we've done - cat expansion.yml + # Get the current unique version of this checkout + if [ "${is_patch}" = "true" ]; then + CURRENT_VERSION=$(git describe)-patch-${version_id} + else + CURRENT_VERSION=latest + fi + + export DRIVERS_TOOLS="$(pwd)/.mod/drivers-evergreen-tools" + + export MONGO_ORCHESTRATION_HOME="$DRIVERS_TOOLS/.evergreen/orchestration" + export MONGODB_BINARIES="$DRIVERS_TOOLS/mongodb/bin" + export UPLOAD_BUCKET="${project}" + export PROJECT_DIRECTORY="$(pwd)" + + cat < expansion.yml + CURRENT_VERSION: "$CURRENT_VERSION" + DRIVERS_TOOLS: "$DRIVERS_TOOLS" + MONGO_ORCHESTRATION_HOME: "$MONGO_ORCHESTRATION_HOME" + MONGODB_BINARIES: "$MONGODB_BINARIES" + UPLOAD_BUCKET: "$UPLOAD_BUCKET" + PROJECT_DIRECTORY: "$PROJECT_DIRECTORY" + PREPARE_SHELL: | + set -o errexit + set -o xtrace + export DRIVERS_TOOLS="$DRIVERS_TOOLS" + export MONGO_ORCHESTRATION_HOME="$MONGO_ORCHESTRATION_HOME" + export MONGODB_BINARIES="$MONGODB_BINARIES" + export UPLOAD_BUCKET="$UPLOAD_BUCKET" + export PROJECT_DIRECTORY="$PROJECT_DIRECTORY" + + export TMPDIR="$MONGO_ORCHESTRATION_HOME/db" + export PATH="$MONGODB_BINARIES:$PATH" + export PROJECT="${project}" + + export MONGODB_VERSION="${VERSION}" + export TOPOLOGY="${TOPOLOGY}" + export SINGLE_MONGOS="${SINGLE_MONGOS}" + export AUTH="${AUTH}" + export SSL="${SSL}" + export APP_TESTS="${APP_TESTS}" + export DOCKER_DISTRO="${DOCKER_DISTRO}" + export RVM_RUBY="${RVM_RUBY}" + export RAILS="${RAILS}" + export DRIVER="${DRIVER}" + export I18N="${I18N}" + export TEST_I18N_FALLBACKS="${TEST_I18N_FALLBACKS}" + export FLE="${FLE}" + EOT + # See what we've done + cat expansion.yml # Load the expansion file to make an evergreen variable with the current unique version - command: expansions.update @@ -266,7 +272,7 @@ functions: ${PREPARE_SHELL} env \ MONGODB_URI="${MONGODB_URI}" \ - TOPOLOGY=${TOPOLOGY} \ + TOPOLOGY="${TOPOLOGY}" \ RVM_RUBY="${RVM_RUBY}" \ RAILS="${RAILS}" \ DRIVER="${DRIVER}" \ @@ -307,10 +313,66 @@ post: #- func: "upload test results" - func: "upload test results to s3" +task_groups: + - name: testatlas_task_group + setup_group_can_fail_task: true + setup_group_timeout_secs: 1800 # 30 minutes + setup_group: + - func: fetch source + - func: create expansions + - command: shell.exec + params: + shell: "bash" + working_dir: "src" + script: | + ${PREPARE_SHELL} + + DRIVERS_ATLAS_PUBLIC_API_KEY="${DRIVERS_ATLAS_PUBLIC_API_KEY}" \ + DRIVERS_ATLAS_PRIVATE_API_KEY="${DRIVERS_ATLAS_PRIVATE_API_KEY}" \ + DRIVERS_ATLAS_GROUP_ID="${DRIVERS_ATLAS_GROUP_ID}" \ + DRIVERS_ATLAS_LAMBDA_USER="${DRIVERS_ATLAS_LAMBDA_USER}" \ + DRIVERS_ATLAS_LAMBDA_PASSWORD="${DRIVERS_ATLAS_LAMBDA_PASSWORD}" \ + LAMBDA_STACK_NAME="dbx-ruby-lambda" \ + MONGODB_VERSION="7.0" \ + task_id="${task_id}" \ + execution="${execution}" \ + $DRIVERS_TOOLS/.evergreen/atlas/setup-atlas-cluster.sh + - command: expansions.update + params: + file: src/atlas-expansion.yml + teardown_group: + - command: shell.exec + params: + shell: "bash" + working_dir: "src" + script: | + ${PREPARE_SHELL} + + DRIVERS_ATLAS_PUBLIC_API_KEY="${DRIVERS_ATLAS_PUBLIC_API_KEY}" \ + DRIVERS_ATLAS_PRIVATE_API_KEY="${DRIVERS_ATLAS_PRIVATE_API_KEY}" \ + DRIVERS_ATLAS_GROUP_ID="${DRIVERS_ATLAS_GROUP_ID}" \ + LAMBDA_STACK_NAME="dbx-ruby-lambda" \ + task_id="${task_id}" \ + execution="${execution}" \ + $DRIVERS_TOOLS/.evergreen/atlas/teardown-atlas-cluster.sh + tasks: + - test-full-atlas-task + tasks: - name: "test" commands: - func: "run tests" + - name: "test-full-atlas-task" + commands: + - command: shell.exec + type: test + params: + working_dir: "src" + shell: "bash" + script: | + ${PREPARE_SHELL} + MONGODB_URI="${MONGODB_URI}" \ + .evergreen/run-tests-atlas-full.sh axes: - id: "mongodb-version" display_name: MongoDB Version @@ -432,13 +494,16 @@ axes: - id: "os" display_name: OS values: + - id: actual-ubuntu-22.04 + display_name: "Ubuntu 22.04" + run_on: ubuntu2204-small - id: ubuntu-18.04 display_name: "Ubuntu 18.04" run_on: ubuntu2004-small variables: DOCKER_DISTRO: ubuntu1804 - id: ubuntu-22.04 - display_name: "Ubuntu 20.04" + display_name: "Ubuntu 22.04" run_on: ubuntu2004-small variables: DOCKER_DISTRO: ubuntu2204 @@ -682,7 +747,7 @@ buildvariants: rails: ['7.0'] os: ubuntu-22.04 fle: helper - display_name: "${rails}, ${driver}, ${mongodb-version}" + display_name: "${rails}, ${driver}, ${mongodb-version} (FLE ${fle})" tasks: - name: "test" @@ -809,3 +874,13 @@ buildvariants: display_name: "FLE: ${rails}, ${driver}, ${mongodb-version}" tasks: - name: "test" + +- matrix_name: atlas-full + matrix_spec: + ruby: ruby-3.2 + os: actual-ubuntu-22.04 + auth: auth + ssl: ssl + display_name: "Atlas (Full)" + tasks: + - name: testatlas_task_group diff --git a/.evergreen/config/axes.yml.erb b/.evergreen/config/axes.yml.erb index c5e8b8f87..2c1e5a44c 100644 --- a/.evergreen/config/axes.yml.erb +++ b/.evergreen/config/axes.yml.erb @@ -119,13 +119,16 @@ axes: - id: "os" display_name: OS values: + - id: actual-ubuntu-22.04 + display_name: "Ubuntu 22.04" + run_on: ubuntu2204-small - id: ubuntu-18.04 display_name: "Ubuntu 18.04" run_on: ubuntu2004-small variables: DOCKER_DISTRO: ubuntu1804 - id: ubuntu-22.04 - display_name: "Ubuntu 20.04" + display_name: "Ubuntu 22.04" run_on: ubuntu2004-small variables: DOCKER_DISTRO: ubuntu2204 diff --git a/.evergreen/config/commands.yml.erb b/.evergreen/config/commands.yml.erb index 6d16b1990..396ab4531 100644 --- a/.evergreen/config/commands.yml.erb +++ b/.evergreen/config/commands.yml.erb @@ -18,50 +18,56 @@ functions: params: working_dir: "src" script: | - # Get the current unique version of this checkout - if [ "${is_patch}" = "true" ]; then - CURRENT_VERSION=$(git describe)-patch-${version_id} - else - CURRENT_VERSION=latest - fi + # Get the current unique version of this checkout + if [ "${is_patch}" = "true" ]; then + CURRENT_VERSION=$(git describe)-patch-${version_id} + else + CURRENT_VERSION=latest + fi - export DRIVERS_TOOLS="$(pwd)/../drivers-tools" + export DRIVERS_TOOLS="$(pwd)/.mod/drivers-evergreen-tools" - export MONGO_ORCHESTRATION_HOME="$DRIVERS_TOOLS/.evergreen/orchestration" - export MONGODB_BINARIES="$DRIVERS_TOOLS/mongodb/bin" - export UPLOAD_BUCKET="${project}" - export PROJECT_DIRECTORY="$(pwd)" + export MONGO_ORCHESTRATION_HOME="$DRIVERS_TOOLS/.evergreen/orchestration" + export MONGODB_BINARIES="$DRIVERS_TOOLS/mongodb/bin" + export UPLOAD_BUCKET="${project}" + export PROJECT_DIRECTORY="$(pwd)" - cat < expansion.yml - CURRENT_VERSION: "$CURRENT_VERSION" - DRIVERS_TOOLS: "$DRIVERS_TOOLS" - MONGO_ORCHESTRATION_HOME: "$MONGO_ORCHESTRATION_HOME" - MONGODB_BINARIES: "$MONGODB_BINARIES" - UPLOAD_BUCKET: "$UPLOAD_BUCKET" - PROJECT_DIRECTORY: "$PROJECT_DIRECTORY" - PREPARE_SHELL: | - set -o errexit - set -o xtrace - export DRIVERS_TOOLS="$DRIVERS_TOOLS" - export MONGO_ORCHESTRATION_HOME="$MONGO_ORCHESTRATION_HOME" - export MONGODB_BINARIES="$MONGODB_BINARIES" - export UPLOAD_BUCKET="$UPLOAD_BUCKET" - export PROJECT_DIRECTORY="$PROJECT_DIRECTORY" + cat < expansion.yml + CURRENT_VERSION: "$CURRENT_VERSION" + DRIVERS_TOOLS: "$DRIVERS_TOOLS" + MONGO_ORCHESTRATION_HOME: "$MONGO_ORCHESTRATION_HOME" + MONGODB_BINARIES: "$MONGODB_BINARIES" + UPLOAD_BUCKET: "$UPLOAD_BUCKET" + PROJECT_DIRECTORY: "$PROJECT_DIRECTORY" + PREPARE_SHELL: | + set -o errexit + set -o xtrace + export DRIVERS_TOOLS="$DRIVERS_TOOLS" + export MONGO_ORCHESTRATION_HOME="$MONGO_ORCHESTRATION_HOME" + export MONGODB_BINARIES="$MONGODB_BINARIES" + export UPLOAD_BUCKET="$UPLOAD_BUCKET" + export PROJECT_DIRECTORY="$PROJECT_DIRECTORY" - export TMPDIR="$MONGO_ORCHESTRATION_HOME/db" - export PATH="$MONGODB_BINARIES:$PATH" - export PROJECT="${project}" + export TMPDIR="$MONGO_ORCHESTRATION_HOME/db" + export PATH="$MONGODB_BINARIES:$PATH" + export PROJECT="${project}" - export MONGODB_VERSION=${VERSION} - export TOPOLOGY=${TOPOLOGY} - export SINGLE_MONGOS=${SINGLE_MONGOS} - export AUTH=${AUTH} - export SSL=${SSL} - export APP_TESTS=${APP_TESTS} - export DOCKER_DISTRO=${DOCKER_DISTRO} - EOT - # See what we've done - cat expansion.yml + export MONGODB_VERSION="${VERSION}" + export TOPOLOGY="${TOPOLOGY}" + export SINGLE_MONGOS="${SINGLE_MONGOS}" + export AUTH="${AUTH}" + export SSL="${SSL}" + export APP_TESTS="${APP_TESTS}" + export DOCKER_DISTRO="${DOCKER_DISTRO}" + export RVM_RUBY="${RVM_RUBY}" + export RAILS="${RAILS}" + export DRIVER="${DRIVER}" + export I18N="${I18N}" + export TEST_I18N_FALLBACKS="${TEST_I18N_FALLBACKS}" + export FLE="${FLE}" + EOT + # See what we've done + cat expansion.yml # Load the expansion file to make an evergreen variable with the current unique version - command: expansions.update @@ -240,7 +246,7 @@ functions: ${PREPARE_SHELL} env \ MONGODB_URI="${MONGODB_URI}" \ - TOPOLOGY=${TOPOLOGY} \ + TOPOLOGY="${TOPOLOGY}" \ RVM_RUBY="${RVM_RUBY}" \ RAILS="${RAILS}" \ DRIVER="${DRIVER}" \ @@ -281,7 +287,63 @@ post: #- func: "upload test results" - func: "upload test results to s3" +task_groups: + - name: testatlas_task_group + setup_group_can_fail_task: true + setup_group_timeout_secs: 1800 # 30 minutes + setup_group: + - func: fetch source + - func: create expansions + - command: shell.exec + params: + shell: "bash" + working_dir: "src" + script: | + ${PREPARE_SHELL} + + DRIVERS_ATLAS_PUBLIC_API_KEY="${DRIVERS_ATLAS_PUBLIC_API_KEY}" \ + DRIVERS_ATLAS_PRIVATE_API_KEY="${DRIVERS_ATLAS_PRIVATE_API_KEY}" \ + DRIVERS_ATLAS_GROUP_ID="${DRIVERS_ATLAS_GROUP_ID}" \ + DRIVERS_ATLAS_LAMBDA_USER="${DRIVERS_ATLAS_LAMBDA_USER}" \ + DRIVERS_ATLAS_LAMBDA_PASSWORD="${DRIVERS_ATLAS_LAMBDA_PASSWORD}" \ + LAMBDA_STACK_NAME="dbx-ruby-lambda" \ + MONGODB_VERSION="7.0" \ + task_id="${task_id}" \ + execution="${execution}" \ + $DRIVERS_TOOLS/.evergreen/atlas/setup-atlas-cluster.sh + - command: expansions.update + params: + file: src/atlas-expansion.yml + teardown_group: + - command: shell.exec + params: + shell: "bash" + working_dir: "src" + script: | + ${PREPARE_SHELL} + + DRIVERS_ATLAS_PUBLIC_API_KEY="${DRIVERS_ATLAS_PUBLIC_API_KEY}" \ + DRIVERS_ATLAS_PRIVATE_API_KEY="${DRIVERS_ATLAS_PRIVATE_API_KEY}" \ + DRIVERS_ATLAS_GROUP_ID="${DRIVERS_ATLAS_GROUP_ID}" \ + LAMBDA_STACK_NAME="dbx-ruby-lambda" \ + task_id="${task_id}" \ + execution="${execution}" \ + $DRIVERS_TOOLS/.evergreen/atlas/teardown-atlas-cluster.sh + tasks: + - test-full-atlas-task + tasks: - name: "test" commands: - func: "run tests" + - name: "test-full-atlas-task" + commands: + - command: shell.exec + type: test + params: + working_dir: "src" + shell: "bash" + script: | + ${PREPARE_SHELL} + MONGODB_URI="${MONGODB_URI}" \ + .evergreen/run-tests-atlas-full.sh diff --git a/.evergreen/config/variants.yml.erb b/.evergreen/config/variants.yml.erb index b0d633844..9348e6b05 100644 --- a/.evergreen/config/variants.yml.erb +++ b/.evergreen/config/variants.yml.erb @@ -245,3 +245,13 @@ buildvariants: display_name: "FLE: ${rails}, ${driver}, ${mongodb-version}" tasks: - name: "test" + +- matrix_name: atlas-full + matrix_spec: + ruby: ruby-3.2 + os: actual-ubuntu-22.04 + auth: auth + ssl: ssl + display_name: "Atlas (Full)" + tasks: + - name: testatlas_task_group diff --git a/.evergreen/run-tests-atlas-full.sh b/.evergreen/run-tests-atlas-full.sh new file mode 100755 index 000000000..f3114b816 --- /dev/null +++ b/.evergreen/run-tests-atlas-full.sh @@ -0,0 +1,24 @@ +#!/bin/bash + +set -ex + +. `dirname "$0"`/../spec/shared/shlib/distro.sh +. `dirname "$0"`/../spec/shared/shlib/set_env.sh +. `dirname "$0"`/functions.sh + +set_env_vars +set_env_python +set_env_ruby + +export BUNDLE_GEMFILE=gemfiles/driver_master.gemfile +bundle install + +ATLAS_URI=$MONGODB_URI \ + EXAMPLE_TIMEOUT=600 \ + bundle exec rspec -fd spec/mongoid/search_indexable_spec.rb + +test_status=$? + +kill_jruby + +exit ${test_status} diff --git a/.gitmodules b/.gitmodules index 805feb77d..05f15e6b9 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,3 +4,6 @@ [submodule "spec/shared"] path = spec/shared url = https://github.com/mongodb-labs/mongo-ruby-spec-shared +[submodule ".mod/drivers-evergreen-tools"] + path = .mod/drivers-evergreen-tools + url = https://github.com/mongodb-labs/drivers-evergreen-tools diff --git a/.mod/drivers-evergreen-tools b/.mod/drivers-evergreen-tools new file mode 160000 index 000000000..1f018c7a2 --- /dev/null +++ b/.mod/drivers-evergreen-tools @@ -0,0 +1 @@ +Subproject commit 1f018c7a248c4fcda6cb7a77043fd673755e0986 diff --git a/docs/reference/indexes.txt b/docs/reference/indexes.txt index 6602dd0ff..7a5d11a47 100644 --- a/docs/reference/indexes.txt +++ b/docs/reference/indexes.txt @@ -131,6 +131,28 @@ The ``background`` option has `no effect as of MongoDB 4.2 `_. +Specifying Search Indexes on MongoDB Atlas +========================================== + +If your application is connected to MongoDB Atlas, you can declare and manage +search indexes on your models. (This feature is only available on MongoDB +Atlas.) + +To declare a search index, use the ``search_index`` macro in your model: + +.. code-block:: ruby + + class Message + include Mongoid::Document + + search_index { ... } + search_index :named_index, { ... } + end + +Search indexes may be given an explicit name; this is necessary if you have +more than one search index on a model. + + Index Management Rake Tasks =========================== @@ -162,6 +184,45 @@ in Rails console: # Remove indexes for Model Model.remove_indexes +Managing Search Indexes on MongoDB Atlas +---------------------------------------- + +If you have defined search indexes on your model, there are rake tasks available +for creating and removing those search indexes: + +.. code-block:: bash + + $ rake db:mongoid:create_search_indexes + $ rake db:mongoid:remove_search_indexes + +By default, creating search indexes will wait for the indexes to be created, +which can take quite some time. If you want to simply let the database create +the indexes in the background, you can set the ``WAIT_FOR_SEARCH_INDEXES`` +environment variable to 0, like this: + +.. code-block:: bash + + $ rake WAIT_FOR_SEARCH_INDEXES=0 db:mongoid:create_search_indexes + +Note that the task for removing search indexes will remove all search indexes +from all models, and should be used with caution. + +You can also add and remove search indexes for a single model by invoking the +following in a Rails console: + +.. code-block:: ruby + + # Create all defined search indexes on the model; this will return + # immediately and the indexes will be created in the background. + Model.create_search_indexes + + # Remove all search indexes from the model + Model.remove_search_indexes + + # Enumerate all search indexes on the model + Model.search_indexes.each { |index| ... } + + Telling Mongoid Where to Look For Models ---------------------------------------- diff --git a/docs/release-notes/mongoid-9.0.txt b/docs/release-notes/mongoid-9.0.txt index b47977010..1c6642e60 100644 --- a/docs/release-notes/mongoid-9.0.txt +++ b/docs/release-notes/mongoid-9.0.txt @@ -338,6 +338,53 @@ Mongoid to allow literal BSON::Decimal128 fields: BSON 5 and later. BSON 4 and earlier ignore the setting entirely. +Search Index Management with MongoDB Atlas +------------------------------------------ + +When connected to MongoDB Atlas, Mongoid now supports creating and removing +search indexes. You may do so programmatically, via the Mongoid::SearchIndexable +API: + +.. code-block:: ruby + + class SearchablePerson + include Mongoid::Document + + search_index { ... } # define the search index here + end + + # create the declared search indexes; this returns immediately, but the + # search indexes may take several minutes before they are available. + SearchablePerson.create_search_indexes + + # query the available search indexes + SearchablePerson.search_indexes.each do |index| + # ... + end + + # remove all search indexes from the model's collection + SearchablePerson.remove_search_indexes + +If you are not connected to MongoDB Atlas, the search index definitions are +ignored. Trying to create, enumerate, or remove search indexes will result in +an error. + +There are also rake tasks available, for convenience: + +.. code-block:: bash + + # create search indexes for all models; waits for indexes to be created + # and shows progress on the terminal. + $ rake mongoid:db:create_search_indexes + + # as above, but returns immediately and lets the indexes be created in the + # background + $ rake WAIT_FOR_SEARCH_INDEXES=0 mongoid:db:create_search_indexes + + # removes search indexes from all models + $ rake mongoid:db:remove_search_indexes + + Bug Fixes and Improvements -------------------------- diff --git a/lib/mongoid/composable.rb b/lib/mongoid/composable.rb index 4afb69c24..d935e88ca 100644 --- a/lib/mongoid/composable.rb +++ b/lib/mongoid/composable.rb @@ -12,6 +12,7 @@ require "mongoid/matchable" require "mongoid/persistable" require "mongoid/reloadable" +require 'mongoid/search_indexable' require "mongoid/selectable" require "mongoid/scopable" require "mongoid/serializable" @@ -50,6 +51,7 @@ module Composable include Association include Reloadable include Scopable + include SearchIndexable include Selectable include Serializable include Shardable diff --git a/lib/mongoid/railties/database.rake b/lib/mongoid/railties/database.rake index d839602c2..5847b265a 100644 --- a/lib/mongoid/railties/database.rake +++ b/lib/mongoid/railties/database.rake @@ -71,6 +71,11 @@ namespace :db do task :create_indexes => "mongoid:create_indexes" end + unless Rake::Task.task_defined?("db:create_search_indexes") + desc "Create search indexes specified in Mongoid models" + task :create_search_indexes => "mongoid:create_search_indexes" + end + unless Rake::Task.task_defined?("db:remove_indexes") desc "Remove indexes specified in Mongoid models" task :remove_indexes => "mongoid:remove_indexes" diff --git a/lib/mongoid/search_indexable.rb b/lib/mongoid/search_indexable.rb new file mode 100644 index 000000000..134253922 --- /dev/null +++ b/lib/mongoid/search_indexable.rb @@ -0,0 +1,167 @@ +# frozen_string_literal: true + +module Mongoid + # Encapsulates behavior around managing search indexes. This feature + # is only supported when connected to an Atlas cluster. + module SearchIndexable + extend ActiveSupport::Concern + + # Represents the status of the indexes returned by a search_indexes + # call. + # + # @api private + class Status + # @return [ Array ] the raw index documents + attr_reader :indexes + + # Create a new Status object. + # + # @param [ Array ] indexes the raw index documents + def initialize(indexes) + @indexes = indexes + end + + # Returns the subset of indexes that have status == 'READY' + # + # @return [ Array ] index documents for "ready" indices + def ready + indexes.select { |i| i['status'] == 'READY' } + end + + # Returns the subset of indexes that have status == 'PENDING' + # + # @return [ Array ] index documents for "pending" indices + def pending + indexes.select { |i| i['status'] == 'PENDING' } + end + + # Returns the subset of indexes that are marked 'queryable' + # + # @return [ Array ] index documents for 'queryable' indices + def queryable + indexes.select { |i| i['queryable'] } + end + + # Returns true if all the given indexes are 'ready' and 'queryable'. + # + # @return [ true | false ] ready status of all indexes + def ready? + indexes.all? { |i| i['status'] == 'READY' && i['queryable'] } + end + end + + included do + cattr_accessor :search_index_specs + self.search_index_specs = [] + end + + # Implementations for the feature's class-level methods. + module ClassMethods + # Request the creation of all registered search indices. Note + # that the search indexes are created asynchronously, and may take + # several minutes to be fully available. + # + # @return [ Array ] The names of the search indexes. + def create_search_indexes + return if search_index_specs.empty? + + collection.search_indexes.create_many(search_index_specs) + end + + # Waits for the named search indexes to be created. + # + # @param [ Array ] names the list of index names to wait for + # @param [ Integer ] interval the number of seconds to wait before + # polling again (only used when a progress callback is given). + # + # @yield [ SearchIndexable::Status ] the status object + def wait_for_search_indexes(names, interval: 5) + loop do + status = Status.new(get_indexes(names)) + yield status if block_given? + break if status.ready? + + sleep interval + end + end + + # A convenience method for querying the search indexes available on the + # current model's collection. + # + # @param [ Hash ] options the options to pass through to the search + # index query. + # + # @option options [ String ] :id The id of the specific index to query (optional) + # @option options [ String ] :name The name of the specific index to query (optional) + # @option options [ Hash ] :aggregate The options hash to pass to the + # aggregate command (optional) + def search_indexes(options = {}) + collection.search_indexes(options) + end + + # Removes the search index specified by the given name or id. Either + # name OR id must be given, but not both. + # + # @param [ String | nil ] name the name of the index to remove + # @param [ String | nil ] id the id of the index to remove + def remove_search_index(name: nil, id: nil) + logger.info( + "MONGOID: Removing search index '#{name || id}' " \ + "on collection '#{collection.name}'." + ) + + collection.search_indexes.drop_one(name: name, id: id) + end + + # Request the removal of all registered search indexes. Note + # that the search indexes are removed asynchronously, and may take + # several minutes to be fully deleted. + # + # @note It would be nice if this could remove ONLY the search indexes + # that have been declared on the model, but because the model may not + # name the index, we can't guarantee that we'll know the name or id of + # the corresponding indexes. It is not unreasonable to assume, though, + # that the intention is for the model to declare, one-to-one, all + # desired search indexes, so removing all search indexes ought to suffice. + # If a specific index or set of indexes needs to be removed instead, + # consider using search_indexes.each with remove_search_index. + def remove_search_indexes + search_indexes.each do |spec| + remove_search_index id: spec['id'] + end + end + + # Adds an index definition for the provided single or compound keys. + # + # @example Create a basic index. + # class Person + # include Mongoid::Document + # field :name, type: String + # search_index({ ... }) + # search_index :name_of_index, { ... } + # end + # + # @param [ Symbol | String | Hash ] name_or_defn Either the name of the index to + # define, or the index definition. + # @param [ Hash ] defn The search index definition. + def search_index(name_or_defn, defn = nil) + name = name_or_defn + name, defn = nil, name if name.is_a?(Hash) + + spec = { definition: defn }.tap { |s| s[:name] = name.to_s if name } + search_index_specs.push(spec) + end + + private + + # Retrieves the index records for the indexes with the given names. + # + # @param [ Array ] names the index names to query + # + # @return [ Array ] the raw index documents + def get_indexes(names) + collection.search_indexes.select { |i| names.include?(i['name']) } + end + end + end +end diff --git a/lib/mongoid/tasks/database.rake b/lib/mongoid/tasks/database.rake index 36a01de77..76786cfab 100644 --- a/lib/mongoid/tasks/database.rake +++ b/lib/mongoid/tasks/database.rake @@ -17,6 +17,12 @@ namespace :db do ::Mongoid::Tasks::Database.create_indexes end + desc "Create search indexes specified in Mongoid models" + task :create_search_indexes => [:environment, :load_models] do + wait = Mongoid::Utils.truthy_string?(ENV['WAIT_FOR_SEARCH_INDEXES'] || '1') + ::Mongoid::Tasks::Database.create_search_indexes(wait: wait) + end + desc "Remove indexes that exist in the database but are not specified in Mongoid models" task :remove_undefined_indexes => [:environment, :load_models] do ::Mongoid::Tasks::Database.remove_undefined_indexes @@ -27,6 +33,11 @@ namespace :db do ::Mongoid::Tasks::Database.remove_indexes end + desc "Remove search indexes specified in Mongoid models" + task :remove_search_indexes => [:environment, :load_models] do + ::Mongoid::Tasks::Database.remove_search_indexes + end + desc "Shard collections with shard keys specified in Mongoid models" task :shard_collections => [:environment, :load_models] do ::Mongoid::Tasks::Database.shard_collections diff --git a/lib/mongoid/tasks/database.rb b/lib/mongoid/tasks/database.rb index a779b7cfb..aa3edbcc7 100644 --- a/lib/mongoid/tasks/database.rb +++ b/lib/mongoid/tasks/database.rb @@ -56,6 +56,26 @@ def create_indexes(models = ::Mongoid.models) end.compact end + # Submit requests for the search indexes to be created. This will happen + # asynchronously. If "wait" is true, the method will block while it + # waits for the indexes to be created. + # + # @param [ Array ] models the models to build search + # indexes for. + # @param [ true | false ] wait whether to wait for the indexes to be + # built. + def create_search_indexes(models = ::Mongoid.models, wait: true) + searchable = models.select { |m| m.search_index_specs.any? } + + # queue up the search index creation requests + index_names_by_model = searchable.each_with_object({}) do |model, obj| + logger.info("MONGOID: Creating search indexes on #{model}...") + obj[model] = model.create_search_indexes + end + + wait_for_search_indexes(index_names_by_model) if wait + end + # Return the list of indexes by model that exist in the database but aren't # specified on the models. # @@ -128,6 +148,17 @@ def remove_indexes(models = ::Mongoid.models) end.compact end + # Remove all search indexes from the given models. + # + # @params [ Array ] models the models to remove + # search indexes from. + def remove_search_indexes(models = ::Mongoid.models) + models.each do |model| + next if model.embedded? + model.remove_search_indexes + end + end + # Shard collections for models that declare shard keys. # # Returns the model classes that have had their collections sharded, @@ -216,6 +247,31 @@ def shard_collections(models = ::Mongoid.models) def logger Mongoid.logger end + + # Waits for the search indexes to be built on the given models. + # + # @param [ Hash> ] models a mapping of + # index names for each model + def wait_for_search_indexes(models) + logger.info('MONGOID: Waiting for search indexes to be created') + logger.info('MONGOID: Press ctrl-c to skip the wait and let the indexes be created in the background') + + models.each do |model, names| + model.wait_for_search_indexes(names) do |status| + if status.ready? + puts + logger.info("MONGOID: Search indexes on #{model} are READY") + else + print '.' + $stdout.flush + end + end + end + rescue Interrupt + # ignore ctrl-C here; we assume it is meant only to skip + # the wait, and that subsequent tasks ought to continue. + logger.info('MONGOID: Skipping the wait for search indexes; they will be created in the background') + end end end end diff --git a/lib/mongoid/utils.rb b/lib/mongoid/utils.rb index b3ac76141..20f8dc239 100644 --- a/lib/mongoid/utils.rb +++ b/lib/mongoid/utils.rb @@ -37,5 +37,16 @@ def placeholder?(value) def monotonic_time Process.clock_gettime(Process::CLOCK_MONOTONIC) end + + # Returns true if the string is any of the following values: "1", + # "yes", "true", "on". Anything else is assumed to be false. Case is + # ignored, as are leading or trailing spaces. + # + # @param [ String ] string the string value to consider + # + # @return [ true | false ] + def truthy_string?(string) + %w[ 1 yes true on ].include?(string.strip.downcase) + end end end diff --git a/spec/lite_spec_helper.rb b/spec/lite_spec_helper.rb index 19041be61..d3868211e 100644 --- a/spec/lite_spec_helper.rb +++ b/spec/lite_spec_helper.rb @@ -51,6 +51,28 @@ TimeoutInterrupt = Timeout end +STANDARD_TIMEOUTS = { + app: 500, # App tests under JRuby take a REALLY long time (over 5 minutes per test). + default: 30, +}.freeze + +def timeout_type + if ENV['EXAMPLE_TIMEOUT'].to_i > 0 + :custom + elsif SpecConfig.instance.app_tests? + :app + else + :default + end +end + +def example_timeout_seconds + STANDARD_TIMEOUTS.fetch( + timeout_type, + (ENV['EXAMPLE_TIMEOUT'] || STANDARD_TIMEOUTS[:default]).to_i + ) +end + RSpec.configure do |config| config.expect_with(:rspec) do |c| c.syntax = [:should, :expect] @@ -61,22 +83,25 @@ end if SpecConfig.instance.ci? && !%w(1 true yes).include?(ENV['INTERACTIVE']&.downcase) - timeout = if SpecConfig.instance.app_tests? - # App tests under JRuby take a REALLY long time (over 5 minutes per test). - 500 - else - # Allow a max of 30 seconds per test. - # Tests should take under 10 seconds ideally but it seems - # we have some that run for more than 10 seconds in CI. - 30 - end config.around(:each) do |example| - TimeoutInterrupt.timeout(timeout) do + TimeoutInterrupt.timeout(example_timeout_seconds) do example.run end end end + def local_env(env = nil, &block) + around do |example| + env ||= block.call + saved_env = ENV.to_h + ENV.update(env) + + example.run + ensure + ENV.replace(saved_env) if saved_env + end + end + config.extend(Mrss::LiteConstraints) end diff --git a/spec/mongoid/search_indexable_spec.rb b/spec/mongoid/search_indexable_spec.rb new file mode 100644 index 000000000..22115623b --- /dev/null +++ b/spec/mongoid/search_indexable_spec.rb @@ -0,0 +1,147 @@ +# frozen_string_literal: true + +require 'spec_helper' + +class SearchIndexHelper + attr_reader :model + + def initialize(model) + @model = model + model.collection.drop + model.collection.create + end + + def collection + model.collection + end + + # Wait for all of the indexes with the given names to be ready; then return + # the list of index definitions corresponding to those names. + def wait_for(*names, &condition) + names.flatten! + + timeboxed_wait do + result = collection.search_indexes + return filter_results(result, names) if names.all? { |name| ready?(result, name, &condition) } + end + end + + # Wait until all of the indexes with the given names are absent from the + # search index list. + def wait_for_absense_of(*names) + names.flatten.each do |name| + timeboxed_wait do + break if collection.search_indexes(name: name).empty? + end + end + end + + private + + def timeboxed_wait(step: 5, max: 300) + start = Mongo::Utils.monotonic_time + + loop do + yield + + sleep step + raise Timeout::Error, 'wait took too long' if Mongo::Utils.monotonic_time - start > max + end + end + + # Returns true if the list of search indexes includes one with the given name, + # which is ready to be queried. + def ready?(list, name, &condition) + condition ||= ->(index) { index['queryable'] } + list.any? { |index| index['name'] == name && condition[index] } + end + + def filter_results(result, names) + result.select { |index| names.include?(index['name']) } + end +end + +# rubocop:disable RSpec/MultipleMemoizedHelpers +describe Mongoid::SearchIndexable do + before do + skip "#{described_class} requires at Atlas environment (set ATLAS_URI)" if ENV['ATLAS_URI'].nil? + end + + let(:model) do + Class.new do + include Mongoid::Document + store_in collection: BSON::ObjectId.new.to_s + + search_index mappings: { dynamic: false } + search_index :with_dynamic_mappings, mappings: { dynamic: true } + end + end + + let(:helper) { SearchIndexHelper.new(model) } + + describe '.search_index_specs' do + context 'when no search indexes have been defined' do + it 'has no search index specs' do + expect(Person.search_index_specs).to be_empty + end + end + + context 'when search indexes have been defined' do + it 'has search index specs' do + expect(model.search_index_specs).to be == [ + { definition: { mappings: { dynamic: false } } }, + { name: 'with_dynamic_mappings', definition: { mappings: { dynamic: true } } } + ] + end + end + end + + context 'when needing to first create search indexes' do + let(:requested_definitions) { model.search_index_specs.map { |spec| spec[:definition].with_indifferent_access } } + let(:index_names) { model.create_search_indexes } + let(:actual_indexes) { helper.wait_for(*index_names) } + let(:actual_definitions) { actual_indexes.map { |i| i['latestDefinition'] } } + + describe '.create_search_indexes' do + it 'creates the indexes' do + expect(actual_definitions).to be == requested_definitions + end + end + + describe '.search_indexes' do + before { actual_indexes } # wait for the indices to be created + + let(:queried_definitions) { model.search_indexes.map { |i| i['latestDefinition'] } } + + it 'queries the available search indexes' do + expect(queried_definitions).to be == requested_definitions + end + end + + describe '.remove_search_index' do + let(:target_index) { actual_indexes.first } + + before do + model.remove_search_index id: target_index['id'] + helper.wait_for_absense_of target_index['name'] + end + + it 'removes the requested index' do + expect(model.search_indexes(id: target_index['id'])).to be_empty + end + end + + describe '.remove_search_indexes' do + before do + actual_indexes # wait for the indexes to be created + model.remove_search_indexes + helper.wait_for_absense_of(actual_indexes.map { |i| i['name'] }) + end + + it 'removes the indexes' do + expect(model.search_indexes).to be_empty + end + end + end +end +# rubocop:enable RSpec/MultipleMemoizedHelpers diff --git a/spec/mongoid/tasks/database_rake_spec.rb b/spec/mongoid/tasks/database_rake_spec.rb index 9340b9d48..c61adba2c 100644 --- a/spec/mongoid/tasks/database_rake_spec.rb +++ b/spec/mongoid/tasks/database_rake_spec.rb @@ -31,6 +31,34 @@ end end + shared_examples_for 'create_search_indexes' do + [ nil, *%w( 1 true yes on ) ].each do |truthy| + context "when WAIT_FOR_SEARCH_INDEXES is #{truthy.inspect}" do + local_env 'WAIT_FOR_SEARCH_INDEXES' => truthy + + it 'receives create_search_indexes with wait: true' do + expect(Mongoid::Tasks::Database) + .to receive(:create_search_indexes) + .with(wait: true) + task.invoke + end + end + end + + %w( 0 false no off bogus ).each do |falsey| + context "when WAIT_FOR_SEARCH_INDEXES is #{falsey.inspect}" do + local_env 'WAIT_FOR_SEARCH_INDEXES' => falsey + + it 'receives create_search_indexes with wait: false' do + expect(Mongoid::Tasks::Database) + .to receive(:create_search_indexes) + .with(wait: false) + task.invoke + end + end + end + end + shared_examples_for "create_collections" do it "receives create_collections" do @@ -203,6 +231,26 @@ end end +describe 'db:mongoid:create_search_indexes' do + include_context 'rake task' + + it_behaves_like 'create_search_indexes' + + it 'calls load_models' do + expect(task.prerequisites).to include('load_models') + end + + it 'calls environment' do + expect(task.prerequisites).to include('environment') + end + + context 'when using rails task' do + include_context 'rails rake task' + + it_behaves_like 'create_search_indexes' + end +end + describe "db:mongoid:create_collections" do include_context "rake task" @@ -287,6 +335,28 @@ end end +describe 'db:mongoid:remove_search_indexes' do + include_context 'rake task' + + it 'receives remove_search_indexes' do + expect(Mongoid::Tasks::Database).to receive(:remove_search_indexes) + task.invoke + end + + it 'calls environment' do + expect(task.prerequisites).to include('environment') + end + + context 'when using rails task' do + include_context 'rails rake task' + + it 'receives remove_search_indexes' do + expect(Mongoid::Tasks::Database).to receive(:remove_search_indexes) + task.invoke + end + end +end + describe "db:mongoid:drop" do include_context "rake task" diff --git a/spec/mongoid/tasks/database_spec.rb b/spec/mongoid/tasks/database_spec.rb index d5ef01960..91a0ecc9a 100644 --- a/spec/mongoid/tasks/database_spec.rb +++ b/spec/mongoid/tasks/database_spec.rb @@ -3,7 +3,7 @@ require "spec_helper" -describe "Mongoid::Tasks::Database" do +describe Mongoid::Tasks::Database do before(:all) do module DatabaseSpec @@ -213,6 +213,64 @@ class Note end end + describe '.create_search_indexes' do + let(:searchable_model) do + Class.new do + include Mongoid::Document + store_in collection: BSON::ObjectId.new.to_s + + search_index mappings: { dynamic: true } + end + end + + let(:index_names) { %w[ name1 name2 ] } + let(:searchable_model_spy) do + class_spy(searchable_model, + create_search_indexes: index_names, + search_index_specs: [ { mappings: { dynamic: true } } ]) + end + + context 'when wait is true' do + before do + allow(described_class).to receive(:wait_for_search_indexes) + described_class.create_search_indexes([ searchable_model_spy ], wait: true) + end + + it 'invokes both create_search_indexes and wait_for_search_indexes' do + expect(searchable_model_spy).to have_received(:create_search_indexes) + expect(described_class).to have_received(:wait_for_search_indexes).with(searchable_model_spy => index_names) + end + end + + context 'when wait is false' do + before do + allow(described_class).to receive(:wait_for_search_indexes) + described_class.create_search_indexes([ searchable_model_spy ], wait: false) + end + + it 'invokes only create_search_indexes' do + expect(searchable_model_spy).to have_received(:create_search_indexes) + expect(described_class).not_to have_received(:wait_for_search_indexes) + end + end + end + + describe '.remove_search_indexes' do + before do + models.each do |model| + allow(model).to receive(:remove_search_indexes) unless model.embedded? + end + + described_class.remove_search_indexes(models) + end + + it 'calls remove_search_indexes on all non-embedded models' do + models.each do |model| + expect(model).to have_received(:remove_search_indexes) unless model.embedded? + end + end + end + describe ".undefined_indexes" do before(:each) do diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index ba0f61ac4..9da2629fb 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -51,10 +51,13 @@ def database_id_alt require 'support/constraints' require 'support/crypt' +use_ssl = %w[ ssl 1 true ].include?(ENV['SSL']) +ssl_options = { ssl: use_ssl }.freeze + # Give MongoDB servers time to start up in CI environments if SpecConfig.instance.ci? starting = true - client = Mongo::Client.new(SpecConfig.instance.addresses) + client = Mongo::Client.new(SpecConfig.instance.addresses, ssl_options) while starting begin client.command(ping: 1) @@ -71,15 +74,15 @@ def database_id_alt default: { database: database_id, hosts: SpecConfig.instance.addresses, - options: { + options: ssl_options.merge( server_selection_timeout: 3.42, wait_queue_timeout: 1, max_pool_size: 5, heartbeat_frequency: 180, - user: MONGOID_ROOT_USER.name, - password: MONGOID_ROOT_USER.password, - auth_source: Mongo::Database::ADMIN, - } + user: SpecConfig.instance.uri.client_options[:user] || MONGOID_ROOT_USER.name, + password: SpecConfig.instance.uri.client_options[:password] || MONGOID_ROOT_USER.password, + auth_source: Mongo::Database::ADMIN + ) } }, options: { @@ -121,17 +124,19 @@ class Query require "i18n/backend/fallbacks" end -# The user must be created before any of the tests are loaded, until -# https://jira.mongodb.org/browse/MONGOID-4827 is implemented. -client = Mongo::Client.new(SpecConfig.instance.addresses, server_selection_timeout: 3.03) -begin - # Create the root user administrator as the first user to be added to the - # database. This user will need to be authenticated in order to add any - # more users to any other databases. - client.database.users.create(MONGOID_ROOT_USER) -rescue Mongo::Error::OperationFailure => e -ensure - client.close +unless SpecConfig.instance.atlas? + # The user must be created before any of the tests are loaded, until + # https://jira.mongodb.org/browse/MONGOID-4827 is implemented. + client = Mongo::Client.new(SpecConfig.instance.addresses, server_selection_timeout: 3.03) + begin + # Create the root user administrator as the first user to be added to the + # database. This user will need to be authenticated in order to add any + # more users to any other databases. + client.database.users.create(MONGOID_ROOT_USER) + rescue Mongo::Error::OperationFailure => e + ensure + client.close + end end RSpec.configure do |config| diff --git a/spec/support/spec_config.rb b/spec/support/spec_config.rb index 12ebcfc66..c4000f0d0 100644 --- a/spec/support/spec_config.rb +++ b/spec/support/spec_config.rb @@ -17,8 +17,8 @@ def initialize STDERR.puts "Please consider providing the correct uri via MONGODB_URI environment variable." @uri_str = DEFAULT_MONGODB_URI end - - @uri = Mongo::URI.new(@uri_str) + + @uri = Mongo::URI.get(@uri_str) end attr_reader :uri_str @@ -56,6 +56,10 @@ def ci? !!ENV['CI'] end + def atlas? + !!ENV['ATLAS_URI'] + end + def rails_version v = ENV['RAILS'] if v == '' From 8c79f57e40d312ccef91d19e5f358c04a53e7b7c Mon Sep 17 00:00:00 2001 From: Dmitry Rybakov Date: Fri, 29 Sep 2023 16:12:11 +0200 Subject: [PATCH 12/52] MONGOID-5686 Fix array operations in update_all (#5726) --- lib/mongoid/extensions/hash.rb | 20 +++++++++++- spec/mongoid/contextual/mongo_spec.rb | 47 +++++++++++++++++++++++---- 2 files changed, 60 insertions(+), 7 deletions(-) diff --git a/lib/mongoid/extensions/hash.rb b/lib/mongoid/extensions/hash.rb index 00104c38a..f1d1a27e2 100644 --- a/lib/mongoid/extensions/hash.rb +++ b/lib/mongoid/extensions/hash.rb @@ -46,7 +46,7 @@ def __consolidate__(klass) real_key = klass.database_field_name(key2) value.delete(key2) if real_key != key2 - value[real_key] = (key == "$rename") ? value2.to_s : mongoize_for(key, klass, real_key, value2) + value[real_key] = value_for(key, klass, real_key, value2) end consolidated[key] ||= {} consolidated[key].update(value) @@ -166,6 +166,24 @@ def to_criteria private + # Get the value for the provided operator, klass, key and value. + # + # This is necessary for special cases like $rename, $addToSet and $push. + # + # @param [ String ] operator The operator. + # @param [ Class ] klass The model class. + # @param [ String | Symbol ] key The field key. + # @param [ Object ] value The original value. + # + # @return [ Object ] Value prepared for the provided operator. + def value_for(operator, klass, key, value) + case operator + when "$rename" then value.to_s + when "$addToSet", "$push" then value.mongoize + else mongoize_for(operator, klass, operator, value) + end + end + # Mongoize for the klass, key and value. # # @api private diff --git a/spec/mongoid/contextual/mongo_spec.rb b/spec/mongoid/contextual/mongo_spec.rb index fd5583993..1e38025e3 100644 --- a/spec/mongoid/contextual/mongo_spec.rb +++ b/spec/mongoid/contextual/mongo_spec.rb @@ -3608,16 +3608,51 @@ context "when the attributes are in the correct type" do - before do - context.update_all("$set" => { name: "Smiths" }) + context "when operation is $set" do + + before do + context.update_all("$set" => { name: "Smiths" }) + end + + it "updates the first matching document" do + expect(depeche_mode.reload.name).to eq("Smiths") + end + + it "updates the last matching document" do + expect(new_order.reload.name).to eq("Smiths") + end end - it "updates the first matching document" do - expect(depeche_mode.reload.name).to eq("Smiths") + context "when operation is $push" do + + before do + depeche_mode.update_attribute(:genres, ["electronic"]) + new_order.update_attribute(:genres, ["electronic"]) + context.update_all("$push" => { genres: "pop" }) + end + + it "updates the first matching document" do + expect(depeche_mode.reload.genres).to eq(["electronic", "pop"]) + end + + it "updates the last matching document" do + expect(new_order.reload.genres).to eq(["electronic", "pop"]) + end end - it "updates the last matching document" do - expect(new_order.reload.name).to eq("Smiths") + context "when operation is $addToSet" do + + before do + context.update_all("$addToSet" => { genres: "electronic" }) + end + + it "updates the first matching document" do + expect(depeche_mode.reload.genres).to eq(["electronic"]) + end + + it "updates the last matching document" do + expect(new_order.reload.genres).to eq(["electronic"]) + end end end From 355ecd751246a03bb9dc1c35e2775040d181f85d Mon Sep 17 00:00:00 2001 From: Dmitry Rybakov Date: Mon, 9 Oct 2023 12:36:40 +0200 Subject: [PATCH 13/52] MONGOID-5689 Add Rails 7.1 support (#5728) * MONGOID-5689 Add Rails 7.1 support * Fix 7.1 incompatibilities * Update docs --- .evergreen/config.yml | 8 ++++++-- .evergreen/config/axes.yml.erb | 4 ++++ .evergreen/config/variants.yml.erb | 4 ++-- docs/reference/compatibility.txt | 13 +++++++++++++ lib/mongoid/deprecable.rb | 3 ++- lib/mongoid/deprecation.rb | 6 +++--- lib/mongoid/interceptable.rb | 8 +++++++- mongoid.gemspec | 2 +- 8 files changed, 38 insertions(+), 10 deletions(-) diff --git a/.evergreen/config.yml b/.evergreen/config.yml index 70e4ae92e..a505b0755 100644 --- a/.evergreen/config.yml +++ b/.evergreen/config.yml @@ -589,6 +589,10 @@ axes: display_name: "Rails 7.0" variables: RAILS: "7.0" + - id: "7.1" + display_name: "Rails 7.1" + variables: + RAILS: "7.1" - id: "i18n" display_name: I18n version @@ -744,7 +748,7 @@ buildvariants: driver: ["current"] mongodb-version: "6.0" topology: "standalone" - rails: ['7.0'] + rails: ['7.0', '7.1'] os: ubuntu-22.04 fle: helper display_name: "${rails}, ${driver}, ${mongodb-version} (FLE ${fle})" @@ -830,7 +834,7 @@ buildvariants: mongodb-version: '6.0' topology: standalone app-tests: yes - rails: ['6.0', '6.1', '7.0'] + rails: ['6.0', '6.1', '7.0', '7.1'] os: rhel80 display_name: "app tests ${driver}, ${ruby}, ${rails}" tasks: diff --git a/.evergreen/config/axes.yml.erb b/.evergreen/config/axes.yml.erb index 2c1e5a44c..e7a5a2aba 100644 --- a/.evergreen/config/axes.yml.erb +++ b/.evergreen/config/axes.yml.erb @@ -214,6 +214,10 @@ axes: display_name: "Rails 7.0" variables: RAILS: "7.0" + - id: "7.1" + display_name: "Rails 7.1" + variables: + RAILS: "7.1" - id: "i18n" display_name: I18n version diff --git a/.evergreen/config/variants.yml.erb b/.evergreen/config/variants.yml.erb index 9348e6b05..78cfa7a66 100644 --- a/.evergreen/config/variants.yml.erb +++ b/.evergreen/config/variants.yml.erb @@ -115,7 +115,7 @@ buildvariants: driver: ["current"] mongodb-version: "6.0" topology: "standalone" - rails: ['7.0'] + rails: ['7.0', '7.1'] os: ubuntu-22.04 fle: helper display_name: "${rails}, ${driver}, ${mongodb-version} (FLE ${fle})" @@ -201,7 +201,7 @@ buildvariants: mongodb-version: '6.0' topology: standalone app-tests: yes - rails: ['6.0', '6.1', '7.0'] + rails: ['6.0', '6.1', '7.0', '7.1'] os: rhel80 display_name: "app tests ${driver}, ${ruby}, ${rails}" tasks: diff --git a/docs/reference/compatibility.txt b/docs/reference/compatibility.txt index 85b4bbf4b..4c7751a67 100644 --- a/docs/reference/compatibility.txt +++ b/docs/reference/compatibility.txt @@ -399,6 +399,7 @@ are supported by Mongoid. :class: compatibility-large no-padding * - Mongoid + - Rails 7.1 - Rails 7.0 - Rails 6.1 - Rails 6.0 @@ -406,6 +407,7 @@ are supported by Mongoid. - Rails 5.1 * - 8.1 + - |checkmark| [#rails-7.1]_ - |checkmark| - |checkmark| - |checkmark| @@ -413,6 +415,7 @@ are supported by Mongoid. - * - 8.0 + - |checkmark| [#rails-7.1]_ - |checkmark| - |checkmark| - |checkmark| @@ -420,6 +423,7 @@ are supported by Mongoid. - * - 7.5 + - - |checkmark| - |checkmark| - |checkmark| @@ -427,6 +431,7 @@ are supported by Mongoid. - D * - 7.4 + - - |checkmark| - |checkmark| - |checkmark| @@ -434,6 +439,7 @@ are supported by Mongoid. - |checkmark| [#rails-5-ruby-3.0]_ * - 7.3 + - - |checkmark| [#rails-7-Mongoid-7.3]_ - |checkmark| - |checkmark| @@ -441,6 +447,7 @@ are supported by Mongoid. - |checkmark| [#rails-5-ruby-3.0]_ * - 7.2 + - - - |checkmark| [#rails-6.1]_ - |checkmark| @@ -448,6 +455,7 @@ are supported by Mongoid. - |checkmark| [#rails-5-ruby-3.0]_ * - 7.1 + - - - |checkmark| [#rails-6.1]_ - |checkmark| @@ -455,6 +463,7 @@ are supported by Mongoid. - |checkmark| * - 7.0 + - - - |checkmark| [#rails-6.1]_ - |checkmark| [#rails-6]_ @@ -465,6 +474,7 @@ are supported by Mongoid. - - - + - - |checkmark| - |checkmark| @@ -477,4 +487,7 @@ are supported by Mongoid. .. [#rails-7-Mongoid-7.3] Rails 7.x requires Mongoid 7.3.4 or later. +.. [#rails-7.1] Rails 7.1 requires Mongoid 8.0.7 or 8.1.3 in the respective + 8.0 and 8.1 stable branches. + .. include:: /includes/unicode-checkmark.rst diff --git a/lib/mongoid/deprecable.rb b/lib/mongoid/deprecable.rb index 327bd72b7..36ae936d0 100644 --- a/lib/mongoid/deprecable.rb +++ b/lib/mongoid/deprecable.rb @@ -28,7 +28,8 @@ module Deprecable # @param [ [ Symbol | Hash ]... ] *method_descriptors # The methods to deprecate, with optional replacement instructions. def deprecate(target_module, *method_descriptors) - Mongoid::Deprecation.deprecate_methods(target_module, *method_descriptors) + @_deprecator ||= Mongoid::Deprecation.new + @_deprecator.deprecate_methods(target_module, *method_descriptors) end end end diff --git a/lib/mongoid/deprecation.rb b/lib/mongoid/deprecation.rb index 4fb299418..faab29a25 100644 --- a/lib/mongoid/deprecation.rb +++ b/lib/mongoid/deprecation.rb @@ -19,10 +19,10 @@ def initialize # # @return [ Array ] The deprecation behavior. def behavior - @behavior ||= Array(->(message, callstack, _deprecation_horizon, _gem_name) { + @behavior ||= Array(->(*args) { logger = Mongoid.logger - logger.warn(message) - logger.debug(callstack.join("\n ")) if debug + logger.warn(args[0]) + logger.debug(args[1].join("\n ")) if debug }) end end diff --git a/lib/mongoid/interceptable.rb b/lib/mongoid/interceptable.rb index 155236eae..e2148637a 100644 --- a/lib/mongoid/interceptable.rb +++ b/lib/mongoid/interceptable.rb @@ -314,7 +314,13 @@ def run_targeted_callbacks(place, kind) end self.class.send :define_method, name do env = ActiveSupport::Callbacks::Filters::Environment.new(self, false, nil) - sequence = chain.compile + sequence = if chain.method(:compile).arity == 0 + # ActiveSupport < 7.1 + chain.compile + else + # ActiveSupport >= 7.1 + chain.compile(nil) + end sequence.invoke_before(env) env.value = !env.halted sequence.invoke_after(env) diff --git a/mongoid.gemspec b/mongoid.gemspec index 4444859fb..0a12d74ec 100644 --- a/mongoid.gemspec +++ b/mongoid.gemspec @@ -38,7 +38,7 @@ Gem::Specification.new do |s| # Ruby 3.0 requires ActiveModel 6.0 or higher. # activemodel 7.0.0 cannot be used due to Class#descendants issue # See: https://github.com/rails/rails/pull/43951 - s.add_dependency("activemodel", ['>=5.1', '<7.1', '!= 7.0.0']) + s.add_dependency("activemodel", ['>=5.1', '<7.2', '!= 7.0.0']) s.add_dependency("mongo", ['>=2.18.0', '<3.0.0']) s.add_dependency("concurrent-ruby", ['>= 1.0.5', '< 2.0']) From 512e1659f7803dd208efe5bb2fc80a50003712e3 Mon Sep 17 00:00:00 2001 From: Dmitry Rybakov Date: Tue, 17 Oct 2023 09:56:53 +0200 Subject: [PATCH 14/52] MONGOID-5658 Fix callbacks for embedded (#5727) * 5658 * 5658 * 5658 * Document the config option * Add warning * Add docstrings * Update for ActiveSupport 7.1 * Add test case * Add release notes * Update specs --- docs/release-notes/mongoid-9.0.txt | 17 + lib/mongoid/config.rb | 10 + lib/mongoid/interceptable.rb | 113 ++++++- spec/integration/callbacks_spec.rb | 20 ++ spec/mongoid/interceptable_spec.rb | 517 ++++++++++++++++++++--------- 5 files changed, 516 insertions(+), 161 deletions(-) diff --git a/docs/release-notes/mongoid-9.0.txt b/docs/release-notes/mongoid-9.0.txt index 1c6642e60..f6974ac5d 100644 --- a/docs/release-notes/mongoid-9.0.txt +++ b/docs/release-notes/mongoid-9.0.txt @@ -18,6 +18,23 @@ please consult GitHub releases for detailed release notes and JIRA for the complete list of issues fixed in each release, including bug fixes. +``around_*`` callbacks for embedded documents are now ignored +------------------------------------------------------------- + +Mongoid 8.x and older allows user to define ``around_*`` callbacks for embedded +documents. Starting from 9.0 these callbacks are ignored and will not be executed. +A warning will be printed to the console if such callbacks are defined. + +If you want to restore the old behavior, you can set +``Mongoid.around_embedded_document_callbacks`` to true in your application. + +.. note:: + Enabling ``around_*`` callbacks for embedded documents is not recommended + as it may cause ``SystemStackError`` exceptions when a document has many + embedded documents. See `MONGOID-5658 `_ + for more details. + + ``for_js`` method is deprecated ------------------------------- diff --git a/lib/mongoid/config.rb b/lib/mongoid/config.rb index efc78689e..52eb3a7e9 100644 --- a/lib/mongoid/config.rb +++ b/lib/mongoid/config.rb @@ -137,6 +137,16 @@ module Config # See https://jira.mongodb.org/browse/MONGOID-5542 option :prevent_multiple_calls_of_embedded_callbacks, default: true + # When this flag is false, callbacks for embedded documents will not be + # called. This is the default in 9.0. + # + # Setting this flag to true restores the pre-9.0 behavior, where callbacks + # for embedded documents are called. This may lead to stack overflow errors + # if there are more than cicrca 1000 embedded documents in the root + # document's dependencies graph. + # See https://jira.mongodb.org/browse/MONGOID-5658 for more details. + option :around_callbacks_for_embeds, default: false + # Returns the Config singleton, for use in the configure DSL. # # @return [ self ] The Config singleton. diff --git a/lib/mongoid/interceptable.rb b/lib/mongoid/interceptable.rb index e2148637a..9b5f205f8 100644 --- a/lib/mongoid/interceptable.rb +++ b/lib/mongoid/interceptable.rb @@ -149,6 +149,28 @@ def run_callbacks(kind, with_children: true, skip_if: nil, &block) # # @api private def _mongoid_run_child_callbacks(kind, children: nil, &block) + if Mongoid::Config.around_callbacks_for_embeds + _mongoid_run_child_callbacks_with_around(kind, children: children, &block) + else + _mongoid_run_child_callbacks_without_around(kind, children: children, &block) + end + end + + # Execute the callbacks of given kind for embedded documents including + # around callbacks. + # + # @note This method is prone to stack overflow errors if the document + # has a large number of embedded documents. It is recommended to avoid + # using around callbacks for embedded documents until a proper solution + # is implemented. + # + # @param [ Symbol ] kind The type of callback to execute. + # @param [ Array ] children Children to execute callbacks on. If + # nil, callbacks will be executed on all cascadable children of + # the document. + # + # @api private + def _mongoid_run_child_callbacks_with_around(kind, children: nil, &block) child, *tail = (children || cascadable_children(kind)) with_children = !Mongoid::Config.prevent_multiple_calls_of_embedded_callbacks if child.nil? @@ -157,11 +179,73 @@ def _mongoid_run_child_callbacks(kind, children: nil, &block) child.run_callbacks(child_callback_type(kind, child), with_children: with_children, &block) else child.run_callbacks(child_callback_type(kind, child), with_children: with_children) do - _mongoid_run_child_callbacks(kind, children: tail, &block) + _mongoid_run_child_callbacks_with_around(kind, children: tail, &block) end end end + # Execute the callbacks of given kind for embedded documents without + # around callbacks. + # + # @param [ Symbol ] kind The type of callback to execute. + # @param [ Array ] children Children to execute callbacks on. If + # nil, callbacks will be executed on all cascadable children of + # the document. + # + # @api private + def _mongoid_run_child_callbacks_without_around(kind, children: nil, &block) + children = (children || cascadable_children(kind)) + callback_list = _mongoid_run_child_before_callbacks(kind, children: children) + return false if callback_list == false + value = block&.call + callback_list.each do |_next_sequence, env| + env.value &&= value + end + return false if _mongoid_run_child_after_callbacks(callback_list: callback_list) == false + + value + end + + # Execute the before callbacks of given kind for embedded documents. + # + # @param [ Symbol ] kind The type of callback to execute. + # @param [ Array ] children Children to execute callbacks on. + # @param [ Array ] callback_list List of + # pairs of callback sequence and environment. This list will be later used + # to execute after callbacks in reverse order. + # + # @api private + def _mongoid_run_child_before_callbacks(kind, children: [], callback_list: []) + children.each do |child| + chain = child.__callbacks[child_callback_type(kind, child)] + env = ActiveSupport::Callbacks::Filters::Environment.new(child, false, nil) + next_sequence = compile_callbacks(chain) + unless next_sequence.final? + Mongoid.logger.warn("Around callbacks are disabled for embedded documents. Skipping around callbacks for #{child.class.name}.") + Mongoid.logger.warn("To enable around callbacks for embedded documents, set Mongoid::Config.around_callbacks_for_embeds to true.") + end + next_sequence.invoke_before(env) + return false if env.halted + env.value = !env.halted + callback_list << [next_sequence, env] + if (grandchildren = child.send(:cascadable_children, kind)) + _mongoid_run_child_before_callbacks(kind, children: grandchildren, callback_list: callback_list) + end + end + callback_list + end + + # Execute the after callbacks. + # + # @param [ Array ] callback_list List of + # pairs of callback sequence and environment. + def _mongoid_run_child_after_callbacks(callback_list: []) + callback_list.reverse_each do |next_sequence, env| + next_sequence.invoke_after(env) + return false if env.halted + end + end + # Returns the stored callbacks to be executed later. # # @return [ Array ] Method symbols of the stored pending callbacks. @@ -314,13 +398,7 @@ def run_targeted_callbacks(place, kind) end self.class.send :define_method, name do env = ActiveSupport::Callbacks::Filters::Environment.new(self, false, nil) - sequence = if chain.method(:compile).arity == 0 - # ActiveSupport < 7.1 - chain.compile - else - # ActiveSupport >= 7.1 - chain.compile(nil) - end + sequence = compile_callbacks(chain) sequence.invoke_before(env) env.value = !env.halted sequence.invoke_after(env) @@ -330,5 +408,24 @@ def run_targeted_callbacks(place, kind) end send(name) end + + # Compile the callback chain. + # + # This method hides the differences between ActiveSupport implementations + # before and after 7.1. + # + # @param [ ActiveSupport::Callbacks::CallbackChain ] chain The callback chain. + # @param [ Symbol | nil ] type The type of callback chain to compile. + # + # @return [ ActiveSupport::Callbacks::CallbackSequence ] The compiled callback sequence. + def compile_callbacks(chain, type = nil) + if chain.method(:compile).arity == 0 + # ActiveSupport < 7.1 + chain.compile + else + # ActiveSupport >= 7.1 + chain.compile(type) + end + end end end diff --git a/spec/integration/callbacks_spec.rb b/spec/integration/callbacks_spec.rb index 206d40868..c580258d4 100644 --- a/spec/integration/callbacks_spec.rb +++ b/spec/integration/callbacks_spec.rb @@ -558,6 +558,7 @@ def will_save_change_to_attribute_values_before context 'nested embedded documents' do config_override :prevent_multiple_calls_of_embedded_callbacks, true + config_override :around_callbacks_for_embeds, true let(:logger) { Array.new } @@ -582,4 +583,23 @@ def will_save_change_to_attribute_values_before expect(logger).to eq(%i[embedded_twice embedded_once root]) end end + + context 'cascade callbacks' do + ruby_version_gte '3.0' + + let(:book) do + Book.new + end + + before do + 1500.times do + book.pages.build + end + end + + # https://jira.mongodb.org/browse/MONGOID-5658 + it 'does not raise SystemStackError' do + expect { book.save! }.not_to raise_error(SystemStackError) + end + end end diff --git a/spec/mongoid/interceptable_spec.rb b/spec/mongoid/interceptable_spec.rb index 3e4ba18eb..4549ca675 100644 --- a/spec/mongoid/interceptable_spec.rb +++ b/spec/mongoid/interceptable_spec.rb @@ -583,6 +583,7 @@ class TestClass context "when saving the root" do context 'with prevent_multiple_calls_of_embedded_callbacks enabled' do config_override :prevent_multiple_calls_of_embedded_callbacks, true + config_override :around_callbacks_for_embeds, true it "executes the callbacks only once for each document" do expect(note).to receive(:update_saved).once @@ -592,6 +593,7 @@ class TestClass context 'with prevent_multiple_calls_of_embedded_callbacks disabled' do config_override :prevent_multiple_calls_of_embedded_callbacks, false + config_override :around_callbacks_for_embeds, true it "executes the callbacks once for each ember" do expect(note).to receive(:update_saved).twice @@ -1784,40 +1786,80 @@ class TestClass end end - let(:expected) do - [ - [InterceptableSpec::CbCascadedChild, :before_validation], - [InterceptableSpec::CbCascadedChild, :after_validation], - [InterceptableSpec::CbParent, :before_validation], - [InterceptableSpec::CbCascadedChild, :before_validation], - [InterceptableSpec::CbCascadedChild, :after_validation], + context 'with around callbacks' do + config_override :around_callbacks_for_embeds, true - [InterceptableSpec::CbParent, :after_validation], - [InterceptableSpec::CbParent, :before_save], - [InterceptableSpec::CbParent, :around_save_open], - [InterceptableSpec::CbParent, :before_create], - [InterceptableSpec::CbParent, :around_create_open], + let(:expected) do + [ + [InterceptableSpec::CbCascadedChild, :before_validation], + [InterceptableSpec::CbCascadedChild, :after_validation], + [InterceptableSpec::CbParent, :before_validation], + [InterceptableSpec::CbCascadedChild, :before_validation], + [InterceptableSpec::CbCascadedChild, :after_validation], + + [InterceptableSpec::CbParent, :after_validation], + [InterceptableSpec::CbParent, :before_save], + [InterceptableSpec::CbParent, :around_save_open], + [InterceptableSpec::CbParent, :before_create], + [InterceptableSpec::CbParent, :around_create_open], + + [InterceptableSpec::CbCascadedChild, :before_save], + [InterceptableSpec::CbCascadedChild, :around_save_open], + [InterceptableSpec::CbCascadedChild, :before_create], + [InterceptableSpec::CbCascadedChild, :around_create_open], + + [InterceptableSpec::CbCascadedChild, :around_create_close], + [InterceptableSpec::CbCascadedChild, :after_create], + [InterceptableSpec::CbCascadedChild, :around_save_close], + [InterceptableSpec::CbCascadedChild, :after_save], + + [InterceptableSpec::CbParent, :around_create_close], + [InterceptableSpec::CbParent, :after_create], + [InterceptableSpec::CbParent, :around_save_close], + [InterceptableSpec::CbParent, :after_save] + ] + end - [InterceptableSpec::CbCascadedChild, :before_save], - [InterceptableSpec::CbCascadedChild, :around_save_open], - [InterceptableSpec::CbCascadedChild, :before_create], - [InterceptableSpec::CbCascadedChild, :around_create_open], + it 'calls callbacks in the right order' do + parent.save! + expect(registry.calls).to eq expected + end + end - [InterceptableSpec::CbCascadedChild, :around_create_close], - [InterceptableSpec::CbCascadedChild, :after_create], - [InterceptableSpec::CbCascadedChild, :around_save_close], - [InterceptableSpec::CbCascadedChild, :after_save], + context 'without around callbacks' do + config_override :around_callbacks_for_embeds, false - [InterceptableSpec::CbParent, :around_create_close], - [InterceptableSpec::CbParent, :after_create], - [InterceptableSpec::CbParent, :around_save_close], - [InterceptableSpec::CbParent, :after_save] - ] - end + let(:expected) do + [ + [InterceptableSpec::CbCascadedChild, :before_validation], + [InterceptableSpec::CbCascadedChild, :after_validation], + [InterceptableSpec::CbParent, :before_validation], + [InterceptableSpec::CbCascadedChild, :before_validation], + [InterceptableSpec::CbCascadedChild, :after_validation], + + [InterceptableSpec::CbParent, :after_validation], + [InterceptableSpec::CbParent, :before_save], + [InterceptableSpec::CbParent, :around_save_open], + [InterceptableSpec::CbParent, :before_create], + [InterceptableSpec::CbParent, :around_create_open], + + [InterceptableSpec::CbCascadedChild, :before_save], + [InterceptableSpec::CbCascadedChild, :before_create], + + [InterceptableSpec::CbCascadedChild, :after_create], + [InterceptableSpec::CbCascadedChild, :after_save], + + [InterceptableSpec::CbParent, :around_create_close], + [InterceptableSpec::CbParent, :after_create], + [InterceptableSpec::CbParent, :around_save_close], + [InterceptableSpec::CbParent, :after_save] + ] + end - it 'calls callbacks in the right order' do - parent.save! - expect(registry.calls).to eq expected + it 'calls callbacks in the right order' do + parent.save! + expect(registry.calls).to eq expected + end end end @@ -1880,89 +1922,180 @@ class TestClass end context "create" do - let(:expected) do - [ - [InterceptableSpec::CbEmbedsOneChild, :before_validation], - [InterceptableSpec::CbEmbedsOneChild, :after_validation], - [InterceptableSpec::CbEmbedsOneParent, :before_validation], - [InterceptableSpec::CbEmbedsOneChild, :before_validation], - [InterceptableSpec::CbEmbedsOneChild, :after_validation], - [InterceptableSpec::CbEmbedsOneParent, :after_validation], - - [InterceptableSpec::CbEmbedsOneParent, :before_save], - [InterceptableSpec::CbEmbedsOneParent, :around_save_open], - [InterceptableSpec::CbEmbedsOneParent, :before_create], - [InterceptableSpec::CbEmbedsOneParent, :around_create_open], - - [InterceptableSpec::CbEmbedsOneChild, :before_save], - [InterceptableSpec::CbEmbedsOneChild, :around_save_open], - [InterceptableSpec::CbEmbedsOneChild, :before_create], - [InterceptableSpec::CbEmbedsOneChild, :around_create_open], - - [InterceptableSpec::CbEmbedsOneParent, :insert_into_database], - - [InterceptableSpec::CbEmbedsOneChild, :around_create_close], - [InterceptableSpec::CbEmbedsOneChild, :after_create], - [InterceptableSpec::CbEmbedsOneChild, :around_save_close], - [InterceptableSpec::CbEmbedsOneChild, :after_save], - - [InterceptableSpec::CbEmbedsOneParent, :around_create_close], - [InterceptableSpec::CbEmbedsOneParent, :after_create], - [InterceptableSpec::CbEmbedsOneParent, :around_save_close], - [InterceptableSpec::CbEmbedsOneParent, :after_save] - ] + context "with around callbacks" do + config_override :around_callbacks_for_embeds, true + + let(:expected) do + [ + [InterceptableSpec::CbEmbedsOneChild, :before_validation], + [InterceptableSpec::CbEmbedsOneChild, :after_validation], + [InterceptableSpec::CbEmbedsOneParent, :before_validation], + [InterceptableSpec::CbEmbedsOneChild, :before_validation], + [InterceptableSpec::CbEmbedsOneChild, :after_validation], + [InterceptableSpec::CbEmbedsOneParent, :after_validation], + + [InterceptableSpec::CbEmbedsOneParent, :before_save], + [InterceptableSpec::CbEmbedsOneParent, :around_save_open], + [InterceptableSpec::CbEmbedsOneParent, :before_create], + [InterceptableSpec::CbEmbedsOneParent, :around_create_open], + + [InterceptableSpec::CbEmbedsOneChild, :before_save], + [InterceptableSpec::CbEmbedsOneChild, :around_save_open], + [InterceptableSpec::CbEmbedsOneChild, :before_create], + [InterceptableSpec::CbEmbedsOneChild, :around_create_open], + + [InterceptableSpec::CbEmbedsOneParent, :insert_into_database], + + [InterceptableSpec::CbEmbedsOneChild, :around_create_close], + [InterceptableSpec::CbEmbedsOneChild, :after_create], + [InterceptableSpec::CbEmbedsOneChild, :around_save_close], + [InterceptableSpec::CbEmbedsOneChild, :after_save], + + [InterceptableSpec::CbEmbedsOneParent, :around_create_close], + [InterceptableSpec::CbEmbedsOneParent, :after_create], + [InterceptableSpec::CbEmbedsOneParent, :around_save_close], + [InterceptableSpec::CbEmbedsOneParent, :after_save] + ] + end + + it 'calls callbacks in the right order' do + parent.save! + expect(registry.calls).to eq expected + end end - it 'calls callbacks in the right order' do - parent.save! - expect(registry.calls).to eq expected + context "without around callbacks" do + config_override :around_callbacks_for_embeds, false + + let(:expected) do + [ + [InterceptableSpec::CbEmbedsOneChild, :before_validation], + [InterceptableSpec::CbEmbedsOneChild, :after_validation], + [InterceptableSpec::CbEmbedsOneParent, :before_validation], + [InterceptableSpec::CbEmbedsOneChild, :before_validation], + [InterceptableSpec::CbEmbedsOneChild, :after_validation], + [InterceptableSpec::CbEmbedsOneParent, :after_validation], + + [InterceptableSpec::CbEmbedsOneParent, :before_save], + [InterceptableSpec::CbEmbedsOneParent, :around_save_open], + [InterceptableSpec::CbEmbedsOneParent, :before_create], + [InterceptableSpec::CbEmbedsOneParent, :around_create_open], + + [InterceptableSpec::CbEmbedsOneChild, :before_save], + [InterceptableSpec::CbEmbedsOneChild, :before_create], + + [InterceptableSpec::CbEmbedsOneParent, :insert_into_database], + + [InterceptableSpec::CbEmbedsOneChild, :after_create], + [InterceptableSpec::CbEmbedsOneChild, :after_save], + + [InterceptableSpec::CbEmbedsOneParent, :around_create_close], + [InterceptableSpec::CbEmbedsOneParent, :after_create], + [InterceptableSpec::CbEmbedsOneParent, :around_save_close], + [InterceptableSpec::CbEmbedsOneParent, :after_save] + ] + end + + it 'calls callbacks in the right order' do + parent.save! + expect(registry.calls).to eq expected + end end end context "update" do - let(:expected) do - [ - [InterceptableSpec::CbEmbedsOneChild, :before_validation], - [InterceptableSpec::CbEmbedsOneChild, :after_validation], - [InterceptableSpec::CbEmbedsOneParent, :before_validation], - [InterceptableSpec::CbEmbedsOneChild, :before_validation], - [InterceptableSpec::CbEmbedsOneChild, :after_validation], - [InterceptableSpec::CbEmbedsOneParent, :after_validation], - - [InterceptableSpec::CbEmbedsOneParent, :before_save], - [InterceptableSpec::CbEmbedsOneParent, :around_save_open], - [InterceptableSpec::CbEmbedsOneParent, :before_update], - [InterceptableSpec::CbEmbedsOneParent, :around_update_open], - - [InterceptableSpec::CbEmbedsOneChild, :before_save], - [InterceptableSpec::CbEmbedsOneChild, :around_save_open], - [InterceptableSpec::CbEmbedsOneChild, :before_update], - [InterceptableSpec::CbEmbedsOneChild, :around_update_open], - - [InterceptableSpec::CbEmbedsOneChild, :around_update_close], - [InterceptableSpec::CbEmbedsOneChild, :after_update], - [InterceptableSpec::CbEmbedsOneChild, :around_save_close], - [InterceptableSpec::CbEmbedsOneChild, :after_save], - - [InterceptableSpec::CbEmbedsOneParent, :around_update_close], - [InterceptableSpec::CbEmbedsOneParent, :after_update], - [InterceptableSpec::CbEmbedsOneParent, :around_save_close], - [InterceptableSpec::CbEmbedsOneParent, :after_save] - ] + context "with around callbacks" do + config_override :around_callbacks_for_embeds, true + + let(:expected) do + [ + [InterceptableSpec::CbEmbedsOneChild, :before_validation], + [InterceptableSpec::CbEmbedsOneChild, :after_validation], + [InterceptableSpec::CbEmbedsOneParent, :before_validation], + [InterceptableSpec::CbEmbedsOneChild, :before_validation], + [InterceptableSpec::CbEmbedsOneChild, :after_validation], + [InterceptableSpec::CbEmbedsOneParent, :after_validation], + + [InterceptableSpec::CbEmbedsOneParent, :before_save], + [InterceptableSpec::CbEmbedsOneParent, :around_save_open], + [InterceptableSpec::CbEmbedsOneParent, :before_update], + [InterceptableSpec::CbEmbedsOneParent, :around_update_open], + + [InterceptableSpec::CbEmbedsOneChild, :before_save], + [InterceptableSpec::CbEmbedsOneChild, :around_save_open], + [InterceptableSpec::CbEmbedsOneChild, :before_update], + [InterceptableSpec::CbEmbedsOneChild, :around_update_open], + + [InterceptableSpec::CbEmbedsOneChild, :around_update_close], + [InterceptableSpec::CbEmbedsOneChild, :after_update], + [InterceptableSpec::CbEmbedsOneChild, :around_save_close], + [InterceptableSpec::CbEmbedsOneChild, :after_save], + + [InterceptableSpec::CbEmbedsOneParent, :around_update_close], + [InterceptableSpec::CbEmbedsOneParent, :after_update], + [InterceptableSpec::CbEmbedsOneParent, :around_save_close], + [InterceptableSpec::CbEmbedsOneParent, :after_save] + ] + end + + it 'calls callbacks in the right order' do + parent.callback_registry = nil + parent.child.callback_registry = nil + parent.save! + + parent.callback_registry = registry + parent.child.callback_registry = registry + parent.name = "name" + parent.child.age = 10 + + parent.save! + expect(registry.calls).to eq expected + end end - it 'calls callbacks in the right order' do - parent.callback_registry = nil - parent.child.callback_registry = nil - parent.save! + context "without around callbacks" do + config_override :around_callbacks_for_embeds, false - parent.callback_registry = registry - parent.child.callback_registry = registry - parent.name = "name" - parent.child.age = 10 + let(:expected) do + [ + [InterceptableSpec::CbEmbedsOneChild, :before_validation], + [InterceptableSpec::CbEmbedsOneChild, :after_validation], + [InterceptableSpec::CbEmbedsOneParent, :before_validation], + [InterceptableSpec::CbEmbedsOneChild, :before_validation], + [InterceptableSpec::CbEmbedsOneChild, :after_validation], + [InterceptableSpec::CbEmbedsOneParent, :after_validation], - parent.save! - expect(registry.calls).to eq expected + [InterceptableSpec::CbEmbedsOneParent, :before_save], + [InterceptableSpec::CbEmbedsOneParent, :around_save_open], + [InterceptableSpec::CbEmbedsOneParent, :before_update], + [InterceptableSpec::CbEmbedsOneParent, :around_update_open], + + [InterceptableSpec::CbEmbedsOneChild, :before_save], + [InterceptableSpec::CbEmbedsOneChild, :before_update], + + [InterceptableSpec::CbEmbedsOneChild, :after_update], + [InterceptableSpec::CbEmbedsOneChild, :after_save], + + [InterceptableSpec::CbEmbedsOneParent, :around_update_close], + [InterceptableSpec::CbEmbedsOneParent, :after_update], + [InterceptableSpec::CbEmbedsOneParent, :around_save_close], + [InterceptableSpec::CbEmbedsOneParent, :after_save] + ] + end + + it 'calls callbacks in the right order' do + parent.callback_registry = nil + parent.child.callback_registry = nil + parent.save! + + parent.callback_registry = registry + parent.child.callback_registry = registry + parent.name = "name" + parent.child.age = 10 + + parent.save! + expect(registry.calls).to eq expected + end end end end @@ -2042,59 +2175,114 @@ class TestClass end end - let(:expected) do - [ - [InterceptableSpec::CbEmbedsManyChild, :before_validation], - [InterceptableSpec::CbEmbedsManyChild, :after_validation], - [InterceptableSpec::CbEmbedsManyChild, :before_validation], - [InterceptableSpec::CbEmbedsManyChild, :after_validation], - [InterceptableSpec::CbEmbedsManyParent, :before_validation], - [InterceptableSpec::CbEmbedsManyChild, :before_validation], - [InterceptableSpec::CbEmbedsManyChild, :after_validation], - [InterceptableSpec::CbEmbedsManyChild, :before_validation], - [InterceptableSpec::CbEmbedsManyChild, :after_validation], - [InterceptableSpec::CbEmbedsManyParent, :after_validation], - - [InterceptableSpec::CbEmbedsManyParent, :before_save], - [InterceptableSpec::CbEmbedsManyParent, :around_save_open], - [InterceptableSpec::CbEmbedsManyParent, :before_create], - [InterceptableSpec::CbEmbedsManyParent, :around_create_open], - - [InterceptableSpec::CbEmbedsManyChild, :before_save], - [InterceptableSpec::CbEmbedsManyChild, :around_save_open], - [InterceptableSpec::CbEmbedsManyChild, :before_save], - - [InterceptableSpec::CbEmbedsManyChild, :around_save_open], - [InterceptableSpec::CbEmbedsManyChild, :before_create], - [InterceptableSpec::CbEmbedsManyChild, :around_create_open], - - [InterceptableSpec::CbEmbedsManyChild, :before_create], - [InterceptableSpec::CbEmbedsManyChild, :around_create_open], - - [InterceptableSpec::CbEmbedsManyParent, :insert_into_database], - - [InterceptableSpec::CbEmbedsManyChild, :around_create_close], - [InterceptableSpec::CbEmbedsManyChild, :after_create], - - [InterceptableSpec::CbEmbedsManyChild, :around_create_close], - [InterceptableSpec::CbEmbedsManyChild, :after_create], - - [InterceptableSpec::CbEmbedsManyChild, :around_save_close], - [InterceptableSpec::CbEmbedsManyChild, :after_save], - - [InterceptableSpec::CbEmbedsManyChild, :around_save_close], - [InterceptableSpec::CbEmbedsManyChild, :after_save], - - [InterceptableSpec::CbEmbedsManyParent, :around_create_close], - [InterceptableSpec::CbEmbedsManyParent, :after_create], - [InterceptableSpec::CbEmbedsManyParent, :around_save_close], - [InterceptableSpec::CbEmbedsManyParent, :after_save] - ] + context "with around callbacks" do + config_override :around_callbacks_for_embeds, true + + let(:expected) do + [ + [InterceptableSpec::CbEmbedsManyChild, :before_validation], + [InterceptableSpec::CbEmbedsManyChild, :after_validation], + [InterceptableSpec::CbEmbedsManyChild, :before_validation], + [InterceptableSpec::CbEmbedsManyChild, :after_validation], + [InterceptableSpec::CbEmbedsManyParent, :before_validation], + [InterceptableSpec::CbEmbedsManyChild, :before_validation], + [InterceptableSpec::CbEmbedsManyChild, :after_validation], + [InterceptableSpec::CbEmbedsManyChild, :before_validation], + [InterceptableSpec::CbEmbedsManyChild, :after_validation], + [InterceptableSpec::CbEmbedsManyParent, :after_validation], + + [InterceptableSpec::CbEmbedsManyParent, :before_save], + [InterceptableSpec::CbEmbedsManyParent, :around_save_open], + [InterceptableSpec::CbEmbedsManyParent, :before_create], + [InterceptableSpec::CbEmbedsManyParent, :around_create_open], + + [InterceptableSpec::CbEmbedsManyChild, :before_save], + [InterceptableSpec::CbEmbedsManyChild, :around_save_open], + [InterceptableSpec::CbEmbedsManyChild, :before_save], + + [InterceptableSpec::CbEmbedsManyChild, :around_save_open], + [InterceptableSpec::CbEmbedsManyChild, :before_create], + [InterceptableSpec::CbEmbedsManyChild, :around_create_open], + + [InterceptableSpec::CbEmbedsManyChild, :before_create], + [InterceptableSpec::CbEmbedsManyChild, :around_create_open], + + [InterceptableSpec::CbEmbedsManyParent, :insert_into_database], + + [InterceptableSpec::CbEmbedsManyChild, :around_create_close], + [InterceptableSpec::CbEmbedsManyChild, :after_create], + + [InterceptableSpec::CbEmbedsManyChild, :around_create_close], + [InterceptableSpec::CbEmbedsManyChild, :after_create], + + [InterceptableSpec::CbEmbedsManyChild, :around_save_close], + [InterceptableSpec::CbEmbedsManyChild, :after_save], + + [InterceptableSpec::CbEmbedsManyChild, :around_save_close], + [InterceptableSpec::CbEmbedsManyChild, :after_save], + + [InterceptableSpec::CbEmbedsManyParent, :around_create_close], + [InterceptableSpec::CbEmbedsManyParent, :after_create], + [InterceptableSpec::CbEmbedsManyParent, :around_save_close], + [InterceptableSpec::CbEmbedsManyParent, :after_save] + ] + end + + it 'calls callbacks in the right order' do + parent.save! + expect(registry.calls).to eq expected + end end - it 'calls callbacks in the right order' do - parent.save! - expect(registry.calls).to eq expected + context "without around callbacks" do + config_override :around_callbacks_for_embeds, false + + let(:expected) do + [ + [InterceptableSpec::CbEmbedsManyChild, :before_validation], + [InterceptableSpec::CbEmbedsManyChild, :after_validation], + [InterceptableSpec::CbEmbedsManyChild, :before_validation], + [InterceptableSpec::CbEmbedsManyChild, :after_validation], + [InterceptableSpec::CbEmbedsManyParent, :before_validation], + [InterceptableSpec::CbEmbedsManyChild, :before_validation], + [InterceptableSpec::CbEmbedsManyChild, :after_validation], + [InterceptableSpec::CbEmbedsManyChild, :before_validation], + [InterceptableSpec::CbEmbedsManyChild, :after_validation], + [InterceptableSpec::CbEmbedsManyParent, :after_validation], + + [InterceptableSpec::CbEmbedsManyParent, :before_save], + [InterceptableSpec::CbEmbedsManyParent, :around_save_open], + [InterceptableSpec::CbEmbedsManyParent, :before_create], + [InterceptableSpec::CbEmbedsManyParent, :around_create_open], + + [InterceptableSpec::CbEmbedsManyChild, :before_save], + [InterceptableSpec::CbEmbedsManyChild, :before_save], + + [InterceptableSpec::CbEmbedsManyChild, :before_create], + + [InterceptableSpec::CbEmbedsManyChild, :before_create], + + [InterceptableSpec::CbEmbedsManyParent, :insert_into_database], + + [InterceptableSpec::CbEmbedsManyChild, :after_create], + + [InterceptableSpec::CbEmbedsManyChild, :after_create], + + [InterceptableSpec::CbEmbedsManyChild, :after_save], + + [InterceptableSpec::CbEmbedsManyChild, :after_save], + + [InterceptableSpec::CbEmbedsManyParent, :around_create_close], + [InterceptableSpec::CbEmbedsManyParent, :after_create], + [InterceptableSpec::CbEmbedsManyParent, :around_save_close], + [InterceptableSpec::CbEmbedsManyParent, :after_save] + ] + end + + it 'calls callbacks in the right order' do + parent.save! + expect(registry.calls).to eq expected + end end end end @@ -2383,4 +2571,27 @@ class TestClass end.to_not raise_error(Mongoid::Errors::AttributeNotLoaded) end end + + context "when around callbacks for embedded are disabled" do + config_override :around_callbacks_for_embeds, false + + context "when around callback is defined" do + let(:registry) { InterceptableSpec::CallbackRegistry.new } + + let(:parent) do + InterceptableSpec::CbEmbedsOneParent.new(registry).tap do |parent| + parent.child = InterceptableSpec::CbEmbedsOneChild.new(registry) + end + end + + before do + expect(Mongoid.logger).to receive(:warn).with(/Around callbacks are disabled for embedded documents/).twice.and_call_original + expect(Mongoid.logger).to receive(:warn).with(/To enable around callbacks for embedded documents/).twice.and_call_original + end + + it "logs a warning" do + parent.save! + end + end + end end From 78fb959cb40905e8f7aaa0305ea7eaf0468daeed Mon Sep 17 00:00:00 2001 From: Jamis Buck Date: Thu, 2 Nov 2023 09:59:42 -0600 Subject: [PATCH 15/52] MONGOID-5608 allow using `#exists?` with args on relations (#5736) * MONGOID-5608 allow using `#exists?` with args on relations * simplify * test the nil/false and _id matching paths, too --- .../association/embedded/embeds_many/proxy.rb | 21 ++++++++-- .../association/referenced/has_many/proxy.rb | 13 +++++- .../embedded/embeds_many/proxy_spec.rb | 35 ++++++++++++++++ .../referenced/has_many/proxy_spec.rb | 42 +++++++++++++++++++ 4 files changed, 106 insertions(+), 5 deletions(-) diff --git a/lib/mongoid/association/embedded/embeds_many/proxy.rb b/lib/mongoid/association/embedded/embeds_many/proxy.rb index 5c3d7b839..f7e5d810a 100644 --- a/lib/mongoid/association/embedded/embeds_many/proxy.rb +++ b/lib/mongoid/association/embedded/embeds_many/proxy.rb @@ -292,9 +292,24 @@ def destroy_all(conditions = {}) # @example Are there persisted documents? # person.posts.exists? # - # @return [ true | false ] True is persisted documents exist, false if not. - def exists? - _target.any?(&:persisted?) + # @param [ :none | nil | false | Hash | Object ] id_or_conditions + # When :none (the default), returns true if any persisted + # documents exist in the association. When nil or false, this + # will always return false. When a Hash is given, this queries + # the documents in the association for those that match the given + # conditions, and returns true if any match which have been + # persisted. Any other argument is interpreted as an id, and + # queries for the existence of persisted documents in the + # association with a matching _id. + # + # @return [ true | false ] True if persisted documents exist, false if not. + def exists?(id_or_conditions = :none) + case id_or_conditions + when :none then _target.any?(&:persisted?) + when nil, false then false + when Hash then where(id_or_conditions).any?(&:persisted?) + else where(_id: id_or_conditions).any?(&:persisted?) + end end # Finds a document in this association through several different diff --git a/lib/mongoid/association/referenced/has_many/proxy.rb b/lib/mongoid/association/referenced/has_many/proxy.rb index 6a652d424..ef0bd2bd0 100644 --- a/lib/mongoid/association/referenced/has_many/proxy.rb +++ b/lib/mongoid/association/referenced/has_many/proxy.rb @@ -217,9 +217,18 @@ def each(&block) # @example Are there persisted documents? # person.posts.exists? # + # @param [ :none | nil | false | Hash | Object ] id_or_conditions + # When :none (the default), returns true if any persisted + # documents exist in the association. When nil or false, this + # will always return false. When a Hash is given, this queries + # the documents in the association for those that match the given + # conditions, and returns true if any match. Any other argument is + # interpreted as an id, and queries for the existence of documents + # in the association with a matching _id. + # # @return [ true | false ] True is persisted documents exist, false if not. - def exists? - criteria.exists? + def exists?(id_or_conditions = :none) + criteria.exists?(id_or_conditions) end # Find the matching document on the association, either based on id or diff --git a/spec/mongoid/association/embedded/embeds_many/proxy_spec.rb b/spec/mongoid/association/embedded/embeds_many/proxy_spec.rb index ae501c84c..7ef43ae68 100644 --- a/spec/mongoid/association/embedded/embeds_many/proxy_spec.rb +++ b/spec/mongoid/association/embedded/embeds_many/proxy_spec.rb @@ -2311,9 +2311,37 @@ class TrackingIdValidationHistory person.addresses.create!(street: "Bond St") end + let(:address) { person.addresses.first } + it "returns true" do expect(person.addresses.exists?).to be true end + + context 'when given specifying conditions' do + context 'when the record exists in the association' do + it 'returns true by condition' do + expect(person.addresses.exists?(street: 'Bond St')).to be true + end + + it 'returns true by id' do + expect(person.addresses.exists?(address._id)).to be true + end + + it 'returns false when given false' do + expect(person.addresses.exists?(false)).to be false + end + + it 'returns false when given nil' do + expect(person.addresses.exists?(nil)).to be false + end + end + + context 'when the record does not exist in the association' do + it 'returns false' do + expect(person.addresses.exists?(street: 'Garfield Ave')).to be false + end + end + end end context "when no documents exist in the database" do @@ -2325,6 +2353,13 @@ class TrackingIdValidationHistory it "returns false" do expect(person.addresses.exists?).to be false end + + context 'when given specifying conditions' do + it 'returns false' do + expect(person.addresses.exists?(street: 'Hyde Park Dr')).to be false + expect(person.addresses.exists?(street: 'Garfield Ave')).to be false + end + end end end diff --git a/spec/mongoid/association/referenced/has_many/proxy_spec.rb b/spec/mongoid/association/referenced/has_many/proxy_spec.rb index 8e4e3629e..3f1f345a0 100644 --- a/spec/mongoid/association/referenced/has_many/proxy_spec.rb +++ b/spec/mongoid/association/referenced/has_many/proxy_spec.rb @@ -1924,6 +1924,42 @@ def with_transaction_via(model, &block) expect_query(1) { expect(person.posts.exists?).to be true } end end + + context 'when invoked with specifying conditions' do + let(:other_person) { Person.create! } + let(:post) { person.posts.first } + + before do + person.posts.create title: 'bumfuzzle' + other_person.posts.create title: 'bumbershoot' + end + + context 'when the conditions match an associated record' do + it 'detects its existence by condition' do + expect(person.posts.exists?(title: 'bumfuzzle')).to be true + expect(other_person.posts.exists?(title: 'bumbershoot')).to be true + end + + it 'detects its existence by id' do + expect(person.posts.exists?(post._id)).to be true + end + + it 'returns false when given false' do + expect(person.posts.exists?(false)).to be false + end + + it 'returns false when given nil' do + expect(person.posts.exists?(nil)).to be false + end + end + + context 'when the conditions match an unassociated record' do + it 'does not detect its existence' do + expect(person.posts.exists?(title: 'bumbershoot')).to be false + expect(other_person.posts.exists?(title: 'bumfuzzle')).to be false + end + end + end end context 'when documents exist in application but not in database' do @@ -1972,6 +2008,12 @@ def with_transaction_via(model, &block) expect_query(1) { expect(person.posts.exists?).to be false } end end + + context 'when invoked with specifying conditions' do + it 'returns false' do + expect(person.posts.exists?(title: 'hullaballoo')).to be false + end + end end end From ccb519ce82a52b9a58ffb867de4c20ba3545c812 Mon Sep 17 00:00:00 2001 From: Johnny Shields <27655+johnnyshields@users.noreply.github.com> Date: Tue, 7 Nov 2023 07:13:29 +0900 Subject: [PATCH 16/52] MONGOID-5663 [Monkey Patch Removal] Remove Object#__sortable__ (#5708) * Remove Object#__sortable__ monkey patch, which is only used in Contextual::Memory. Its been moved to private method with a minor amount of refactoring. Existing tests already cover all the functionality. * deprecate __sortable__ instead of removing it --------- Co-authored-by: Jamis Buck --- lib/mongoid/contextual/memory.rb | 37 +++++++++++++++------ lib/mongoid/extensions/false_class.rb | 6 ++-- lib/mongoid/extensions/object.rb | 2 ++ lib/mongoid/extensions/true_class.rb | 6 ++-- spec/mongoid/extensions/false_class_spec.rb | 7 ---- spec/mongoid/extensions/object_spec.rb | 7 ---- spec/mongoid/extensions/true_class_spec.rb | 7 ---- 7 files changed, 35 insertions(+), 37 deletions(-) diff --git a/lib/mongoid/contextual/memory.rb b/lib/mongoid/contextual/memory.rb index 30ceb30f0..6f9d2e808 100644 --- a/lib/mongoid/contextual/memory.rb +++ b/lib/mongoid/contextual/memory.rb @@ -627,7 +627,8 @@ def apply_sorting end end - # Compare two values, checking for nil. + # Compare two values, handling the cases when + # either value is nil. # # @api private # @@ -635,15 +636,15 @@ def apply_sorting # context.compare(a, b) # # @param [ Object ] a The first object. - # @param [ Object ] b The first object. + # @param [ Object ] b The second object. # # @return [ Integer ] The comparison value. def compare(a, b) - case - when a.nil? then b.nil? ? 0 : 1 - when b.nil? then -1 - else a <=> b - end + return 0 if a.nil? && b.nil? + return 1 if a.nil? + return -1 if b.nil? + + compare_operand(a) <=> compare_operand(b) end # Sort the documents in place. @@ -655,9 +656,8 @@ def compare(a, b) def in_place_sort(values) documents.sort! do |a, b| values.map do |field, direction| - a_value, b_value = a[field], b[field] - direction * compare(a_value.__sortable__, b_value.__sortable__) - end.find { |value| !value.zero? } || 0 + direction * compare(a[field], b[field]) + end.detect { |value| !value.zero? } || 0 end end @@ -683,6 +683,23 @@ def _session @criteria.send(:_session) end + # Get the operand value to be used in comparison. + # Adds capability to sort boolean values. + # + # @example Get the comparison operand. + # compare_operand(true) #=> 1 + # + # @param [ Object ] value The value to be used in comparison. + # + # @return [ Integer | Object ] The comparison operand. + def compare_operand(value) + case value + when TrueClass then 1 + when FalseClass then 0 + else value + end + end + # Retrieve the value for the current document at the given field path. # # For example, if I have the following models: diff --git a/lib/mongoid/extensions/false_class.rb b/lib/mongoid/extensions/false_class.rb index 4b3cfad25..d50e6a6b9 100644 --- a/lib/mongoid/extensions/false_class.rb +++ b/lib/mongoid/extensions/false_class.rb @@ -3,19 +3,19 @@ module Mongoid module Extensions - # Adds type-casting behavior to FalseClass. module FalseClass - # Get the value of the object as a mongo friendly sort value. # # @example Get the object as sort criteria. # object.__sortable__ # # @return [ Integer ] 0. + # @deprecated def __sortable__ 0 end + Mongoid.deprecate(self, :__sortable__) # Is the passed value a boolean? # @@ -35,4 +35,4 @@ def is_a?(other) end end -::FalseClass.__send__(:include, Mongoid::Extensions::FalseClass) +FalseClass.include Mongoid::Extensions::FalseClass diff --git a/lib/mongoid/extensions/object.rb b/lib/mongoid/extensions/object.rb index ec0cd89f9..69e06680d 100644 --- a/lib/mongoid/extensions/object.rb +++ b/lib/mongoid/extensions/object.rb @@ -60,9 +60,11 @@ def __setter__ # object.__sortable__ # # @return [ Object ] self. + # @deprecated def __sortable__ self end + Mongoid.deprecate(self, :__sortable__) # Conversion of an object to an $inc-able value. # diff --git a/lib/mongoid/extensions/true_class.rb b/lib/mongoid/extensions/true_class.rb index 147659f0b..779586c74 100644 --- a/lib/mongoid/extensions/true_class.rb +++ b/lib/mongoid/extensions/true_class.rb @@ -3,19 +3,19 @@ module Mongoid module Extensions - # Adds type-casting behavior to TrueClass module TrueClass - # Get the value of the object as a mongo friendly sort value. # # @example Get the object as sort criteria. # object.__sortable__ # # @return [ Integer ] 1. + # @deprecated def __sortable__ 1 end + Mongoid.deprecate(self, :__sortable__) # Is the passed value a boolean? # @@ -35,4 +35,4 @@ def is_a?(other) end end -::TrueClass.__send__(:include, Mongoid::Extensions::TrueClass) +TrueClass.include Mongoid::Extensions::TrueClass diff --git a/spec/mongoid/extensions/false_class_spec.rb b/spec/mongoid/extensions/false_class_spec.rb index b2dd763e3..645089854 100644 --- a/spec/mongoid/extensions/false_class_spec.rb +++ b/spec/mongoid/extensions/false_class_spec.rb @@ -5,13 +5,6 @@ describe Mongoid::Extensions::FalseClass do - describe "#__sortable__" do - - it "returns 0" do - expect(false.__sortable__).to eq(0) - end - end - describe "#is_a?" do context "when provided a Boolean" do diff --git a/spec/mongoid/extensions/object_spec.rb b/spec/mongoid/extensions/object_spec.rb index 80d313a3a..0e95c16ca 100644 --- a/spec/mongoid/extensions/object_spec.rb +++ b/spec/mongoid/extensions/object_spec.rb @@ -128,13 +128,6 @@ end end - describe "#__sortable__" do - - it "returns self" do - expect(object.__sortable__).to eq(object) - end - end - describe ".demongoize" do let(:object) do diff --git a/spec/mongoid/extensions/true_class_spec.rb b/spec/mongoid/extensions/true_class_spec.rb index 221aaf553..7aa561412 100644 --- a/spec/mongoid/extensions/true_class_spec.rb +++ b/spec/mongoid/extensions/true_class_spec.rb @@ -5,13 +5,6 @@ describe Mongoid::Extensions::TrueClass do - describe "#__sortable__" do - - it "returns 1" do - expect(true.__sortable__).to eq(1) - end - end - describe "#is_a?" do context "when provided a Boolean" do From 66bd54dadc9d35013f89d883216e47a220a55d82 Mon Sep 17 00:00:00 2001 From: Johnny Shields <27655+johnnyshields@users.noreply.github.com> Date: Tue, 7 Nov 2023 07:22:16 +0900 Subject: [PATCH 17/52] MONGOID-5664 [Monkey Patch Removal] Remove Object#__setter__ monkey-patch (#5707) * Remove Object#__setter__ monkey-patch. This is a trivial method so I've just inlined it. (It was already inlined in several other places.) * deprecate __setter__ instead of removing it --------- Co-authored-by: Jamis Buck --- lib/mongoid/association/relatable.rb | 4 ++-- lib/mongoid/extensions/nil_class.rb | 6 +++--- lib/mongoid/extensions/object.rb | 2 ++ 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/lib/mongoid/association/relatable.rb b/lib/mongoid/association/relatable.rb index fb7ca7ebe..fc485c4d9 100644 --- a/lib/mongoid/association/relatable.rb +++ b/lib/mongoid/association/relatable.rb @@ -81,7 +81,7 @@ def get_callbacks(callback_type) # # @return [ String ] The type setter method. def type_setter - @type_setter ||= type.__setter__ + @type_setter ||= "#{type}=" if type end # Whether trying to bind an object using this association should raise @@ -233,7 +233,7 @@ def path(document) # # @return [ String ] The name of the setter. def inverse_type_setter - @inverse_type_setter ||= inverse_type.__setter__ + @inverse_type_setter ||= "#{inverse_type}=" if inverse_type end # Get the name of the method to check if the foreign key has changed. diff --git a/lib/mongoid/extensions/nil_class.rb b/lib/mongoid/extensions/nil_class.rb index f8ca4e880..7cfc52a95 100644 --- a/lib/mongoid/extensions/nil_class.rb +++ b/lib/mongoid/extensions/nil_class.rb @@ -3,19 +3,19 @@ module Mongoid module Extensions - # Adds type-casting behavior to NilClass. module NilClass - # Try to form a setter from this object. # # @example Try to form a setter. # object.__setter__ # # @return [ nil ] Always nil. + # @deprecated def __setter__ self end + Mongoid.deprecate(self, :__setter__) # Get the name of a nil collection. # @@ -30,4 +30,4 @@ def collectionize end end -::NilClass.__send__(:include, Mongoid::Extensions::NilClass) +NilClass.include Mongoid::Extensions::NilClass diff --git a/lib/mongoid/extensions/object.rb b/lib/mongoid/extensions/object.rb index 69e06680d..c3c469e9d 100644 --- a/lib/mongoid/extensions/object.rb +++ b/lib/mongoid/extensions/object.rb @@ -50,9 +50,11 @@ def __mongoize_time__ # object.__setter__ # # @return [ String ] The object as a string plus =. + # @deprecated def __setter__ "#{self}=" end + Mongoid.deprecate(self, :__setter__) # Get the value of the object as a mongo friendly sort value. # From 20c656345d84e1d02da4614acf7279f9a4a7a6a1 Mon Sep 17 00:00:00 2001 From: Johnny Shields <27655+johnnyshields@users.noreply.github.com> Date: Tue, 7 Nov 2023 07:54:13 +0900 Subject: [PATCH 18/52] MONGOID-5669 [Monkey Patch Removal] Remove Array#multi_arged? (#5702) * Move Array#multi_arged? to Criteria::Findable.multiple_args? * Update findable_spec.rb * Clarify code docs * Add pending for Set spec * deprecate __multi_arged?__ instead of removing it --------- Co-authored-by: Jamis Buck --- lib/mongoid/criteria/findable.rb | 21 ++++++-- lib/mongoid/extensions/array.rb | 3 +- lib/mongoid/extensions/object.rb | 2 + spec/mongoid/extensions/array_spec.rb | 72 --------------------------- 4 files changed, 22 insertions(+), 76 deletions(-) diff --git a/lib/mongoid/criteria/findable.rb b/lib/mongoid/criteria/findable.rb index 09d557b44..2f116dc35 100644 --- a/lib/mongoid/criteria/findable.rb +++ b/lib/mongoid/criteria/findable.rb @@ -14,7 +14,8 @@ module Findable # criteria.execute_or_raise(id) # # @param [ Object ] ids The arguments passed. - # @param [ true | false ] multi Whether there arguments were a list. + # @param [ true | false ] multi Whether there arguments were a list, + # and therefore the return value should be an array. # # @raise [ Errors::DocumentNotFound ] If nothing returned. # @@ -42,7 +43,7 @@ def execute_or_raise(ids, multi) def find(*args) ids = args.__find_args__ raise_invalid if ids.any?(&:nil?) - for_ids(ids).execute_or_raise(ids, args.multi_arged?) + for_ids(ids).execute_or_raise(ids, multi_args?(args)) end # Adds a criterion to the +Criteria+ that specifies an id that must be matched. @@ -108,7 +109,7 @@ def from_database(ids) from_database_selector(ids).entries end - private def from_database_selector(ids) + def from_database_selector(ids) if ids.size > 1 any_in(_id: ids) else @@ -133,6 +134,20 @@ def mongoize_ids(ids) end end + # Indicates whether the given arguments array is a list of values. + # Used by the +find+ method to determine whether to return an array + # or single value. + # + # @example Are these arguments a list of values? + # multi_args?([ 1, 2, 3 ]) #=> true + # + # @param [ Array ] args The arguments. + # + # @return [ true | false ] Whether the arguments are a list. + def multi_args?(args) + args.size > 1 || !args.first.is_a?(Hash) && args.first.resizable? + end + # Convenience method of raising an invalid options error. # # @example Raise the error. diff --git a/lib/mongoid/extensions/array.rb b/lib/mongoid/extensions/array.rb index 812ac620d..7cf774c75 100644 --- a/lib/mongoid/extensions/array.rb +++ b/lib/mongoid/extensions/array.rb @@ -3,7 +3,6 @@ module Mongoid module Extensions - # Adds type-casting behavior to Array class. module Array @@ -80,9 +79,11 @@ def blank_criteria? # [ 1, 2, 3 ].multi_arged? # # @return [ true | false ] If the array is multi args. + # @deprecated def multi_arged? !first.is_a?(Hash) && first.resizable? || size > 1 end + Mongoid.deprecate(self, :multi_arged?) # Turn the object from the ruby type we deal with to a Mongo friendly # type. diff --git a/lib/mongoid/extensions/object.rb b/lib/mongoid/extensions/object.rb index c3c469e9d..98e480c91 100644 --- a/lib/mongoid/extensions/object.rb +++ b/lib/mongoid/extensions/object.rb @@ -140,9 +140,11 @@ def mongoize # object.multi_arged? # # @return [ false ] false. + # @deprecated def multi_arged? false end + Mongoid.deprecate(self, :multi_arged?) # Is the object a number? # diff --git a/spec/mongoid/extensions/array_spec.rb b/spec/mongoid/extensions/array_spec.rb index 18ab1ee2f..49c402986 100644 --- a/spec/mongoid/extensions/array_spec.rb +++ b/spec/mongoid/extensions/array_spec.rb @@ -533,78 +533,6 @@ end end - describe "#multi_arged?" do - - context "when there are multiple elements" do - - let(:array) do - [ 1, 2, 3 ] - end - - it "returns true" do - expect(array).to be_multi_arged - end - end - - context "when there is one element" do - - context "when the element is a non enumerable" do - - let(:array) do - [ 1 ] - end - - it "returns false" do - expect(array).to_not be_multi_arged - end - end - - context "when the element is resizable Hash instance" do - - let(:array) do - [{'key' => 'value'}] - end - - it "returns false" do - expect(array).to_not be_multi_arged - end - end - - context "when the element is array of resizable Hash instances" do - - let(:array) do - [[{'key1' => 'value2'},{'key1' => 'value2'}]] - end - - it "returns true" do - expect(array).to be_multi_arged - end - end - - context "when the element is an array" do - - let(:array) do - [[ 1 ]] - end - - it "returns true" do - expect(array).to be_multi_arged - end - end - - context "when the element is a range" do - - let(:array) do - [ 1..2 ] - end - - it "returns true" do - expect(array).to be_multi_arged - end - end - end - end - describe ".resizable?" do it "returns true" do From 67c8035298550892716c427e82c7ee732090da9c Mon Sep 17 00:00:00 2001 From: Johnny Shields <27655+johnnyshields@users.noreply.github.com> Date: Tue, 7 Nov 2023 08:41:13 +0900 Subject: [PATCH 19/52] MONGOID-5665 [Monkey Patch Removal] Remove Object#__find_args__ (#5706) * Remove Object#__find_args__. This primarily used in Criteria::Findable where it is now a private method. 2 points to note: - __find_args__ was mistakenly called in Atomic#unset method. Although technically __find_args__ does a similar thing as what #unset requires for its argument preparation, this is pure coincidence and its a kludge to use find_args here. So I've inlined the code and made a new private method. - __find_args__ for Range previously called to_a. This is actually a bug / DOS risk, because ranges of strings tend to explode when you use to_a. As an example, ('64e0'..'64ff').to_a.size => 9320, and it gets much worse for 24-char BSON ids! Interestingly however, MongoDB itself can handle ranges of ids gracefully, so I am now just passing them into the DB as-is and it magically works--I've added tests. * Fix specs * Add test to cover #find for nested arrays, which was pre-existing behavior. * deprecate __find_args__ instead of removing it * Set#resizable? has to be true for this to work --------- Co-authored-by: Jamis Buck --- lib/mongoid/contextual/atomic.rb | 34 +++-- lib/mongoid/criteria/findable.rb | 23 ++- lib/mongoid/extensions/array.rb | 2 + lib/mongoid/extensions/object.rb | 2 + lib/mongoid/extensions/range.rb | 10 +- lib/mongoid/extensions/set.rb | 12 +- spec/mongoid/criteria/findable_spec.rb | 190 +++++++++++++++++++++++++ spec/mongoid/extensions/object_spec.rb | 7 - spec/mongoid/extensions/range_spec.rb | 11 -- 9 files changed, 257 insertions(+), 34 deletions(-) diff --git a/lib/mongoid/contextual/atomic.rb b/lib/mongoid/contextual/atomic.rb index 1587c4aca..8ab491150 100644 --- a/lib/mongoid/contextual/atomic.rb +++ b/lib/mongoid/contextual/atomic.rb @@ -169,17 +169,14 @@ def set(sets) # @example Unset the field on the matches. # context.unset(:name) # - # @param [ [ String | Symbol | Array | Hash ]... ] *args - # The name(s) of the field(s) to unset. - # If a Hash is specified, its keys will be used irrespective of what - # each key's value is, even if the value is nil or false. + # @param [ [ String | Symbol | Array | Hash ]... ] *unsets + # The name(s) of the field(s) to unset. If a Hash is specified, + # its keys will be used irrespective of value, even if the value + # is nil or false. # # @return [ nil ] Nil. - def unset(*args) - fields = args.map { |a| a.is_a?(Hash) ? a.keys : a } - .__find_args__ - .map { |f| [database_field_name(f), true] } - view.update_many("$unset" => Hash[fields]) + def unset(*unsets) + view.update_many('$unset' => collect_unset_operations(unsets)) end # Performs an atomic $min update operation on the given field or fields. @@ -247,6 +244,25 @@ def collect_each_operations(ops) operations[database_field_name(field)] = { "$each" => Array.wrap(value).mongoize } end end + + # Builds the selector an atomic $unset operation from arguments. + # + # @example Prepare selector from array. + # context.collect_unset_operations([:name, :age]) + # #=> { "name" => true, "age" => true } + # + # @example Prepare selector from hash. + # context.collect_unset_operations({ name: 1 }, { age: 1 }) + # #=> { "name" => true, "age" => true } + # + # @param [ String | Symbol | Array | Hash ] ops + # The name(s) of the field(s) to unset. + # + # @return [ Hash ] The selector for the atomic $unset operation. + def collect_unset_operations(ops) + ops.map { |op| op.is_a?(Hash) ? op.keys : op }.flatten + .map { |field| [database_field_name(field), true] }.to_h + end end end end diff --git a/lib/mongoid/criteria/findable.rb b/lib/mongoid/criteria/findable.rb index 2f116dc35..527821a63 100644 --- a/lib/mongoid/criteria/findable.rb +++ b/lib/mongoid/criteria/findable.rb @@ -41,7 +41,7 @@ def execute_or_raise(ids, multi) # # @return [ Document | Array ] The matching document(s). def find(*args) - ids = args.__find_args__ + ids = prepare_ids_for_find(args) raise_invalid if ids.any?(&:nil?) for_ids(ids).execute_or_raise(ids, multi_args?(args)) end @@ -134,6 +134,27 @@ def mongoize_ids(ids) end end + # Convert args to the +#find+ method into a flat array of ids. + # + # @example Get the ids. + # prepare_ids_for_find([ 1, [ 2, 3 ] ]) + # + # @param [ Array ] args The arguments. + # + # @return [ Array ] The array of ids. + def prepare_ids_for_find(args) + args.flat_map do |arg| + case arg + when Array, Set + prepare_ids_for_find(arg) + when Range + arg.begin&.numeric? && arg.end&.numeric? ? arg.to_a : arg + else + arg + end + end.uniq(&:to_s) + end + # Indicates whether the given arguments array is a list of values. # Used by the +find+ method to determine whether to return an array # or single value. diff --git a/lib/mongoid/extensions/array.rb b/lib/mongoid/extensions/array.rb index 7cf774c75..072a1d42b 100644 --- a/lib/mongoid/extensions/array.rb +++ b/lib/mongoid/extensions/array.rb @@ -23,9 +23,11 @@ def __evolve_object_id__ # [ 1, 2, 3 ].__find_args__ # # @return [ Array ] The array of args. + # @deprecated def __find_args__ flat_map{ |a| a.__find_args__ }.uniq{ |a| a.to_s } end + Mongoid.deprecate(self, :__find_args__) # Mongoize the array into an array of object ids. # diff --git a/lib/mongoid/extensions/object.rb b/lib/mongoid/extensions/object.rb index 98e480c91..c14661bbc 100644 --- a/lib/mongoid/extensions/object.rb +++ b/lib/mongoid/extensions/object.rb @@ -24,9 +24,11 @@ def __evolve_object_id__ # object.__find_args__ # # @return [ Object ] self. + # @deprecated def __find_args__ self end + Mongoid.deprecate(self, :__find_args__) # Mongoize a plain object into a time. # diff --git a/lib/mongoid/extensions/range.rb b/lib/mongoid/extensions/range.rb index a804dc2da..e5dc8e89c 100644 --- a/lib/mongoid/extensions/range.rb +++ b/lib/mongoid/extensions/range.rb @@ -3,9 +3,11 @@ module Mongoid module Extensions - # Adds type-casting behavior to Range class. module Range + def self.included(base) + base.extend(ClassMethods) + end # Get the range as arguments for a find. # @@ -13,9 +15,11 @@ module Range # range.__find_args__ # # @return [ Array ] The range as an array. + # @deprecated def __find_args__ to_a end + Mongoid.deprecate(self, :__find_args__) # Turn the object from the ruby type we deal with to a Mongo friendly # type. @@ -39,7 +43,6 @@ def resizable? end module ClassMethods - # Convert the object from its mongo friendly ruby type to this type. # # @example Demongoize the object. @@ -107,5 +110,4 @@ def __mongoize_range__(object) end end -::Range.__send__(:include, Mongoid::Extensions::Range) -::Range.extend(Mongoid::Extensions::Range::ClassMethods) +Range.include(Mongoid::Extensions::Range) diff --git a/lib/mongoid/extensions/set.rb b/lib/mongoid/extensions/set.rb index 21a887a5f..574723f84 100644 --- a/lib/mongoid/extensions/set.rb +++ b/lib/mongoid/extensions/set.rb @@ -6,7 +6,6 @@ module Extensions # Adds type-casting behavior to Set class. module Set - # Turn the object from the ruby type we deal with to a Mongo friendly # type. # @@ -18,8 +17,17 @@ def mongoize ::Set.mongoize(self) end - module ClassMethods + # Returns whether the object's size can be changed. + # + # @example Is the object resizable? + # object.resizable? + # + # @return [ true ] true. + def resizable? + true + end + module ClassMethods # Convert the object from its mongo friendly ruby type to this type. # # @example Demongoize the object. diff --git a/spec/mongoid/criteria/findable_spec.rb b/spec/mongoid/criteria/findable_spec.rb index 9715fe1ae..1468fc5f8 100644 --- a/spec/mongoid/criteria/findable_spec.rb +++ b/spec/mongoid/criteria/findable_spec.rb @@ -260,6 +260,196 @@ end end + context "when providing nested arrays of ids" do + + let!(:band_two) do + Band.create!(name: "Tool") + end + + context "when all ids match" do + + let(:found) do + Band.find([ [ band.id ], [ [ band_two.id ] ] ]) + end + + it "contains the first match" do + expect(found).to include(band) + end + + it "contains the second match" do + expect(found).to include(band_two) + end + + context "when ids are duplicates" do + + let(:found) do + Band.find([ [ band.id ], [ [ band.id ] ] ]) + end + + it "contains only the first match" do + expect(found).to eq([band]) + end + end + end + + context "when any id does not match" do + + context "when raising a not found error" do + config_override :raise_not_found_error, true + + let(:found) do + Band.find([ [ band.id ], [ [ BSON::ObjectId.new ] ] ]) + end + + it "raises an error" do + expect { + found + }.to raise_error(Mongoid::Errors::DocumentNotFound, /Document\(s\) not found for class Band with id\(s\)/) + end + end + + context "when raising no error" do + config_override :raise_not_found_error, false + + let(:found) do + Band.find([ [ band.id ], [ [ BSON::ObjectId.new ] ] ]) + end + + it "returns only the matching documents" do + expect(found).to eq([ band ]) + end + end + end + end + + context "when providing a Set of ids" do + + let!(:band_two) do + Band.create!(name: "Tool") + end + + context "when all ids match" do + let(:found) do + Band.find(Set[ band.id, band_two.id ]) + end + + it "contains the first match" do + expect(found).to include(band) + end + + it "contains the second match" do + expect(found).to include(band_two) + end + end + + context "when any id does not match" do + + context "when raising a not found error" do + config_override :raise_not_found_error, true + + let(:found) do + Band.find(Set[ band.id, BSON::ObjectId.new ]) + end + + it "raises an error" do + expect { + found + }.to raise_error(Mongoid::Errors::DocumentNotFound, /Document\(s\) not found for class Band with id\(s\)/) + end + end + + context "when raising no error" do + config_override :raise_not_found_error, false + + let(:found) do + Band.find(Set[ band.id, BSON::ObjectId.new ]) + end + + it "returns only the matching documents" do + expect(found).to eq([ band ]) + end + end + end + end + + context "when providing a Range of ids" do + + let!(:band_two) do + Band.create!(name: "Tool") + end + + context "when all ids match" do + let(:found) do + Band.find(band.id.to_s..band_two.id.to_s) + end + + it "contains the first match" do + expect(found).to include(band) + end + + it "contains the second match" do + expect(found).to include(band_two) + end + + + context "when any id does not match" do + + context "when raising a not found error" do + config_override :raise_not_found_error, true + + let(:found) do + Band.find(band_two.id.to_s..BSON::ObjectId.new) + end + + it "does not raise error and returns only the matching documents" do + expect(found).to eq([ band_two ]) + end + end + + context "when raising no error" do + config_override :raise_not_found_error, false + + let(:found) do + Band.find(band_two.id.to_s..BSON::ObjectId.new) + end + + it "returns only the matching documents" do + expect(found).to eq([ band_two ]) + end + end + end + end + + context "when all ids do not match" do + + context "when raising a not found error" do + config_override :raise_not_found_error, true + + let(:found) do + Band.find(BSON::ObjectId.new..BSON::ObjectId.new) + end + + it "raises an error" do + expect { + found + }.to raise_error(Mongoid::Errors::DocumentNotFound, /Document\(s\) not found for class Band with id\(s\)/) + end + end + + context "when raising no error" do + config_override :raise_not_found_error, false + + let(:found) do + Band.find(BSON::ObjectId.new..BSON::ObjectId.new) + end + + it "returns only the matching documents" do + expect(found).to eq([]) + end + end + end + end + context "when providing a single id as extended json" do context "when the id matches" do diff --git a/spec/mongoid/extensions/object_spec.rb b/spec/mongoid/extensions/object_spec.rb index 0e95c16ca..4b2e294de 100644 --- a/spec/mongoid/extensions/object_spec.rb +++ b/spec/mongoid/extensions/object_spec.rb @@ -16,13 +16,6 @@ end end - describe "#__find_args__" do - - it "returns self" do - expect(object.__find_args__).to eq(object) - end - end - describe "#__mongoize_object_id__" do it "returns self" do diff --git a/spec/mongoid/extensions/range_spec.rb b/spec/mongoid/extensions/range_spec.rb index 572f076fb..58ea85c9f 100644 --- a/spec/mongoid/extensions/range_spec.rb +++ b/spec/mongoid/extensions/range_spec.rb @@ -5,17 +5,6 @@ describe Mongoid::Extensions::Range do - describe "#__find_args__" do - - let(:range) do - 1..3 - end - - it "returns the range as an array" do - expect(range.__find_args__).to eq([ 1, 2, 3 ]) - end - end - describe ".demongoize" do subject { Range.demongoize(hash) } From 5dff6d639b7e710f753c40299e1c3cf5484a4d85 Mon Sep 17 00:00:00 2001 From: Jamis Buck Date: Tue, 7 Nov 2023 10:06:08 -0700 Subject: [PATCH 20/52] MONGOID-5654: refactor and deprecate Hash#__consolidate__ (#5740) * refactor and deprecate Hash#__consolidate__ * fix linter complaints * another minor refactoring for optimization * remove pre-refactoring docs * fix failing specs --- lib/mongoid/atomic_update_preparer.rb | 88 +++++++++++++++++++++ lib/mongoid/contextual/mongo.rb | 5 +- lib/mongoid/extensions/hash.rb | 69 ++-------------- spec/mongoid/atomic_update_preparer_spec.rb | 83 +++++++++++++++++++ spec/mongoid/extensions/hash_spec.rb | 57 ------------- 5 files changed, 181 insertions(+), 121 deletions(-) create mode 100644 lib/mongoid/atomic_update_preparer.rb create mode 100644 spec/mongoid/atomic_update_preparer_spec.rb diff --git a/lib/mongoid/atomic_update_preparer.rb b/lib/mongoid/atomic_update_preparer.rb new file mode 100644 index 000000000..c4655eefd --- /dev/null +++ b/lib/mongoid/atomic_update_preparer.rb @@ -0,0 +1,88 @@ +# frozen_string_literal: true + +module Mongoid + # A singleton class to assist with preparing attributes for atomic + # updates. + # + # Once the deprecated Hash#__consolidate__ method is removed entirely, + # these methods may be moved into Mongoid::Contextual::Mongo as private + # methods. + # + # @api private + class AtomicUpdatePreparer + class << self + # Convert the key/values in the attributes into a hash of atomic updates. + # Non-operator keys are assumed to use $set operation. + # + # @param [ Class ] klass The model class. + # @param [ Hash ] attributes The attributes to convert. + # + # @return [ Hash ] The prepared atomic updates. + def prepare(attributes, klass) + attributes.each_pair.with_object({}) do |(key, value), atomic_updates| + key = klass.database_field_name(key.to_s) + + if key.to_s.start_with?('$') + (atomic_updates[key] ||= {}).update(prepare_operation(klass, key, value)) + else + (atomic_updates['$set'] ||= {})[key] = mongoize_for(key, klass, key, value) + end + end + end + + private + + # Treats the key as if it were a MongoDB operator and prepares + # the value accordingly. + # + # @param [ Class ] klass the model class + # @param [ String | Symbol ] key the operator + # @param [ Hash ] value the operand + # + # @return [ Hash ] the prepared value. + def prepare_operation(klass, key, value) + value.each_with_object({}) do |(key2, value2), hash| + key2 = klass.database_field_name(key2) + hash[key2] = value_for(key, klass, value2) + end + end + + # Get the value for the provided operator, klass, key and value. + # + # This is necessary for special cases like $rename, $addToSet and $push. + # + # @param [ String ] operator The operator. + # @param [ Class ] klass The model class. + # @param [ Object ] value The original value. + # + # @return [ Object ] Value prepared for the provided operator. + def value_for(operator, klass, value) + case operator + when '$rename' then value.to_s + when '$addToSet', '$push' then value.mongoize + else mongoize_for(operator, klass, operator, value) + end + end + + # Mongoize for the klass, key and value. + # + # @param [ String ] operator The operator. + # @param [ Class ] klass The model class. + # @param [ String | Symbol ] key The field key. + # @param [ Object ] value The value to mongoize. + # + # @return [ Object ] The mongoized value. + def mongoize_for(operator, klass, key, value) + field = klass.fields[key.to_s] + return value unless field + + mongoized = field.mongoize(value) + if Mongoid::Persistable::LIST_OPERATIONS.include?(operator) && field.resizable? && !value.is_a?(Array) + return mongoized.first + end + + mongoized + end + end + end +end diff --git a/lib/mongoid/contextual/mongo.rb b/lib/mongoid/contextual/mongo.rb index 916d8a2a7..3fceba9df 100644 --- a/lib/mongoid/contextual/mongo.rb +++ b/lib/mongoid/contextual/mongo.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true # rubocop:todo all +require 'mongoid/atomic_update_preparer' require "mongoid/contextual/mongo/documents_loader" require "mongoid/contextual/atomic" require "mongoid/contextual/aggregable/mongo" @@ -817,8 +818,8 @@ def load_async # @return [ true | false ] If the update succeeded. def update_documents(attributes, method = :update_one, opts = {}) return false unless attributes - attributes = Hash[attributes.map { |k, v| [klass.database_field_name(k.to_s), v] }] - view.send(method, attributes.__consolidate__(klass), opts) + + view.send(method, AtomicUpdatePreparer.prepare(attributes, klass), opts) end # Apply the field limitations. diff --git a/lib/mongoid/extensions/hash.rb b/lib/mongoid/extensions/hash.rb index f1d1a27e2..1cf19d539 100644 --- a/lib/mongoid/extensions/hash.rb +++ b/lib/mongoid/extensions/hash.rb @@ -32,31 +32,20 @@ def __mongoize_object_id__ end # Consolidate the key/values in the hash under an atomic $set. + # DEPRECATED. This was never intended to be a public API and + # the functionality will no longer be exposed once this method + # is eventually removed. # # @example Consolidate the hash. # { name: "Placebo" }.__consolidate__ # # @return [ Hash ] A new consolidated hash. + # + # @deprecated def __consolidate__(klass) - consolidated = {} - each_pair do |key, value| - if key =~ /\$/ - value.keys.each do |key2| - value2 = value[key2] - real_key = klass.database_field_name(key2) - - value.delete(key2) if real_key != key2 - value[real_key] = value_for(key, klass, real_key, value2) - end - consolidated[key] ||= {} - consolidated[key].update(value) - else - consolidated["$set"] ||= {} - consolidated["$set"].update(key => mongoize_for(key, klass, key, value)) - end - end - consolidated + Mongoid::AtomicUpdatePreparer.prepare(self, klass) end + Mongoid.deprecate(self, :__consolidate__) # Checks whether conditions given in this hash are known to be # unsatisfiable, i.e., querying with this hash will always return no @@ -166,50 +155,6 @@ def to_criteria private - # Get the value for the provided operator, klass, key and value. - # - # This is necessary for special cases like $rename, $addToSet and $push. - # - # @param [ String ] operator The operator. - # @param [ Class ] klass The model class. - # @param [ String | Symbol ] key The field key. - # @param [ Object ] value The original value. - # - # @return [ Object ] Value prepared for the provided operator. - def value_for(operator, klass, key, value) - case operator - when "$rename" then value.to_s - when "$addToSet", "$push" then value.mongoize - else mongoize_for(operator, klass, operator, value) - end - end - - # Mongoize for the klass, key and value. - # - # @api private - # - # @example Mongoize for the klass, field and value. - # {}.mongoize_for("$push", Band, "name", "test") - # - # @param [ String ] operator The operator. - # @param [ Class ] klass The model class. - # @param [ String | Symbol ] key The field key. - # @param [ Object ] value The value to mongoize. - # - # @return [ Object ] The mongoized value. - def mongoize_for(operator, klass, key, value) - field = klass.fields[key.to_s] - if field - val = field.mongoize(value) - if Mongoid::Persistable::LIST_OPERATIONS.include?(operator) && field.resizable? - val = val.first if !value.is_a?(Array) - end - val - else - value - end - end - module ClassMethods # Turn the object from the ruby type we deal with to a Mongo friendly diff --git a/spec/mongoid/atomic_update_preparer_spec.rb b/spec/mongoid/atomic_update_preparer_spec.rb new file mode 100644 index 000000000..6c39ac3aa --- /dev/null +++ b/spec/mongoid/atomic_update_preparer_spec.rb @@ -0,0 +1,83 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Mongoid::AtomicUpdatePreparer do + describe '#prepare' do + let(:prepared) { described_class.prepare(hash, Band) } + + context 'when the hash already contains $set' do + context 'when the $set is first' do + let(:hash) do + { '$set' => { name: 'Tool' }, likes: 10, '$inc' => { plays: 1 } } + end + + it 'moves the non hash values under the provided key' do + expect(prepared).to eq( + '$set' => { 'name' => 'Tool', 'likes' => 10 }, + '$inc' => { 'plays' => 1 } + ) + end + end + + context 'when the $set is not first' do + let(:hash) do + { likes: 10, '$inc' => { plays: 1 }, '$set' => { name: 'Tool' } } + end + + it 'moves the non hash values under the provided key' do + expect(prepared).to eq( + '$set' => { 'likes' => 10, 'name' => 'Tool' }, + '$inc' => { 'plays' => 1 } + ) + end + end + end + + context 'when the hash does not contain $set' do + let(:hash) do + { likes: 10, '$inc' => { plays: 1 }, name: 'Tool' } + end + + it 'moves the non hash values under the provided key' do + expect(prepared).to eq( + '$set' => { 'likes' => 10, 'name' => 'Tool' }, + '$inc' => { 'plays' => 1 } + ) + end + end + + context 'when the hash contains $rename' do + let(:hash) { { likes: 10, '$rename' => { old: 'new' } } } + + it 'preserves the $rename operator' do + expect(prepared).to eq( + '$set' => { 'likes' => 10 }, + '$rename' => { 'old' => 'new' } + ) + end + end + + context 'when the hash contains $addToSet' do + let(:hash) { { likes: 10, '$addToSet' => { list: 'new' } } } + + it 'preserves the $addToSet operator' do + expect(prepared).to eq( + '$set' => { 'likes' => 10 }, + '$addToSet' => { 'list' => 'new' } + ) + end + end + + context 'when the hash contains $push' do + let(:hash) { { likes: 10, '$push' => { list: 14 } } } + + it 'preserves the $push operator' do + expect(prepared).to eq( + '$set' => { 'likes' => 10 }, + '$push' => { 'list' => 14 } + ) + end + end + end +end diff --git a/spec/mongoid/extensions/hash_spec.rb b/spec/mongoid/extensions/hash_spec.rb index cc4135a74..bcc1a58d6 100644 --- a/spec/mongoid/extensions/hash_spec.rb +++ b/spec/mongoid/extensions/hash_spec.rb @@ -163,63 +163,6 @@ end end - describe "#__consolidate__" do - - context "when the hash already contains the key" do - - context "when the $set is first" do - - let(:hash) do - { "$set" => { name: "Tool" }, likes: 10, "$inc" => { plays: 1 }} - end - - let(:consolidated) do - hash.__consolidate__(Band) - end - - it "moves the non hash values under the provided key" do - expect(consolidated).to eq({ - "$set" => { 'name' => "Tool", likes: 10 }, "$inc" => { 'plays' => 1 } - }) - end - end - - context "when the $set is not first" do - - let(:hash) do - { likes: 10, "$inc" => { plays: 1 }, "$set" => { name: "Tool" }} - end - - let(:consolidated) do - hash.__consolidate__(Band) - end - - it "moves the non hash values under the provided key" do - expect(consolidated).to eq({ - "$set" => { likes: 10, 'name' => "Tool" }, "$inc" => { 'plays' => 1 } - }) - end - end - end - - context "when the hash does not contain the key" do - - let(:hash) do - { likes: 10, "$inc" => { plays: 1 }, name: "Tool"} - end - - let(:consolidated) do - hash.__consolidate__(Band) - end - - it "moves the non hash values under the provided key" do - expect(consolidated).to eq({ - "$set" => { likes: 10, name: "Tool" }, "$inc" => { 'plays' => 1 } - }) - end - end - end - describe ".demongoize" do let(:hash) do From f99e917c7e1fbba0f66f66836b89577e33ee40d0 Mon Sep 17 00:00:00 2001 From: Jamis Buck Date: Tue, 7 Nov 2023 11:05:33 -0700 Subject: [PATCH 21/52] MONGOID-5662 Deprecate Object#__to_inc__ and BigDecimal#__to_inc__ (#5741) --- lib/mongoid/extensions/big_decimal.rb | 15 +++++++++++---- lib/mongoid/extensions/object.rb | 13 +++++++------ lib/mongoid/persistable/incrementable.rb | 2 +- lib/mongoid/persistable/multipliable.rb | 2 +- 4 files changed, 20 insertions(+), 12 deletions(-) diff --git a/lib/mongoid/extensions/big_decimal.rb b/lib/mongoid/extensions/big_decimal.rb index 72641890c..cce430f2b 100644 --- a/lib/mongoid/extensions/big_decimal.rb +++ b/lib/mongoid/extensions/big_decimal.rb @@ -3,9 +3,16 @@ module Mongoid module Extensions - # Adds type-casting behavior to BigDecimal class. module BigDecimal + # Behavior to be invoked when the module is included. + # + # @param [ Module ] base the class or module doing the including + # + # @api private + def self.included(base) + base.extend(ClassMethods) + end # Convert the big decimal to an $inc-able value. # @@ -13,9 +20,11 @@ module BigDecimal # bd.__to_inc__ # # @return [ Float ] The big decimal as a float. + # @deprecated def __to_inc__ to_f end + Mongoid.deprecate(self, :__to_inc__) # Turn the object from the ruby type we deal with to a Mongo friendly # type. @@ -39,7 +48,6 @@ def numeric? end module ClassMethods - # Convert the object from its mongo friendly ruby type to this type. # # @param [ Object ] object The object to demongoize. @@ -89,5 +97,4 @@ def mongoize(object) end end -::BigDecimal.__send__(:include, Mongoid::Extensions::BigDecimal) -::BigDecimal.extend(Mongoid::Extensions::BigDecimal::ClassMethods) +BigDecimal.include Mongoid::Extensions::BigDecimal diff --git a/lib/mongoid/extensions/object.rb b/lib/mongoid/extensions/object.rb index c14661bbc..089269bc7 100644 --- a/lib/mongoid/extensions/object.rb +++ b/lib/mongoid/extensions/object.rb @@ -3,9 +3,11 @@ module Mongoid module Extensions - # Adds type-casting behavior to Object class. module Object + def self.included(base) + base.extend(ClassMethods) + end # Evolve a plain object into an object id. # @@ -76,9 +78,11 @@ def __sortable__ # 1.__to_inc__ # # @return [ Object ] The object. + # @deprecated def __to_inc__ self end + Mongoid.deprecate(self, :__to_inc__) # Checks whether conditions given in this object are known to be # unsatisfiable, i.e., querying with this object will always return no @@ -93,6 +97,7 @@ def __to_inc__ def blank_criteria? false end + Mongoid.deprecate(self, :blank_criteria?) # Do or do not, there is no try. -- Yoda. # @@ -210,7 +215,6 @@ def you_must(name, *args) end module ClassMethods - # Convert the provided object to a foreign key, given the metadata key # contstraint. # @@ -255,7 +259,4 @@ def mongoize(object) end end -::Object.__send__(:include, Mongoid::Extensions::Object) -::Object.extend(Mongoid::Extensions::Object::ClassMethods) - -::Mongoid.deprecate(Object, :blank_criteria) +Object.include Mongoid::Extensions::Object diff --git a/lib/mongoid/persistable/incrementable.rb b/lib/mongoid/persistable/incrementable.rb index 566b3c113..1fd5b9a45 100644 --- a/lib/mongoid/persistable/incrementable.rb +++ b/lib/mongoid/persistable/incrementable.rb @@ -21,7 +21,7 @@ module Incrementable def inc(increments) prepare_atomic_operation do |ops| process_atomic_operations(increments) do |field, value| - increment = value.__to_inc__ + increment = value.is_a?(BigDecimal) ? value.to_f : value current = attributes[field] new_value = (current || 0) + increment process_attribute field, new_value if executing_atomically? diff --git a/lib/mongoid/persistable/multipliable.rb b/lib/mongoid/persistable/multipliable.rb index 4569d01da..787d94059 100644 --- a/lib/mongoid/persistable/multipliable.rb +++ b/lib/mongoid/persistable/multipliable.rb @@ -21,7 +21,7 @@ module Multipliable def mul(factors) prepare_atomic_operation do |ops| process_atomic_operations(factors) do |field, value| - factor = value.__to_inc__ + factor = value.is_a?(BigDecimal) ? value.to_f : value current = attributes[field] new_value = (current || 0) * factor process_attribute field, new_value if executing_atomically? From c30458910b970a0b89c42f61ab032ac76b9a69e3 Mon Sep 17 00:00:00 2001 From: Johnny Shields <27655+johnnyshields@users.noreply.github.com> Date: Wed, 8 Nov 2023 03:07:55 +0900 Subject: [PATCH 22/52] MONGOID-5667 [Monkey Patch Removal] Remove String/Integer#unconvertable_to_bson? (#5704) * Remove String#unconvertable_to_bson? and Integer#unconvertable_to_bson? * Remove one more usage * MONGOID-5667 deprecate instead of remove --------- Co-authored-by: Jamis Buck --- lib/mongoid/extensions/integer.rb | 2 ++ lib/mongoid/extensions/string.rb | 5 +++++ 2 files changed, 7 insertions(+) diff --git a/lib/mongoid/extensions/integer.rb b/lib/mongoid/extensions/integer.rb index 307625627..d8fbf4a64 100644 --- a/lib/mongoid/extensions/integer.rb +++ b/lib/mongoid/extensions/integer.rb @@ -33,9 +33,11 @@ def numeric? # object.unconvertable_to_bson? # # @return [ true ] If the object is unconvertable. + # @deprecated def unconvertable_to_bson? true end + Mongoid.deprecate(self, :unconvertable_to_bson?) module ClassMethods diff --git a/lib/mongoid/extensions/string.rb b/lib/mongoid/extensions/string.rb index 38858afe5..5e31d84ef 100644 --- a/lib/mongoid/extensions/string.rb +++ b/lib/mongoid/extensions/string.rb @@ -8,7 +8,9 @@ module Extensions module String # @attribute [rw] unconvertable_to_bson If the document is unconvertable. + # @deprecated attr_accessor :unconvertable_to_bson + Mongoid.deprecate(self, :unconvertable_to_bson, :unconvertable_to_bson=) # Evolve the string into an object id if possible. # @@ -126,15 +128,18 @@ def before_type_cast? ends_with?("_before_type_cast") end + # Is the object not to be converted to bson on criteria creation? # # @example Is the object unconvertable? # object.unconvertable_to_bson? # # @return [ true | false ] If the object is unconvertable. + # @deprecated def unconvertable_to_bson? @unconvertable_to_bson ||= false end + Mongoid.deprecate(self, :unconvertable_to_bson?) private From fc98ff0f31f3f6c5a62ae9268f81ec0db2f656ff Mon Sep 17 00:00:00 2001 From: Johnny Shields <27655+johnnyshields@users.noreply.github.com> Date: Wed, 8 Nov 2023 03:09:01 +0900 Subject: [PATCH 23/52] MONGOID-5668 [Monkey Patch Removal] Remove String/Symbol#mongoid_id? (#5703) * Remove String#mongoid_id? and Symbol#mongoid_id? * deprecate instead of remove --------- Co-authored-by: Jamis Buck --- lib/mongoid/extensions/string.rb | 2 ++ lib/mongoid/extensions/symbol.rb | 2 ++ spec/mongoid/extensions/string_spec.rb | 31 -------------------------- spec/mongoid/extensions/symbol_spec.rb | 31 -------------------------- 4 files changed, 4 insertions(+), 62 deletions(-) diff --git a/lib/mongoid/extensions/string.rb b/lib/mongoid/extensions/string.rb index 5e31d84ef..df210f945 100644 --- a/lib/mongoid/extensions/string.rb +++ b/lib/mongoid/extensions/string.rb @@ -71,9 +71,11 @@ def collectionize # "_id".mongoid_id? # # @return [ true | false ] If the string is id or _id. + # @deprecated def mongoid_id? self =~ /\A(|_)id\z/ end + Mongoid.deprecate(self, :mongoid_id?) # Is the string a number? The literals "NaN", "Infinity", and "-Infinity" # are counted as numbers. diff --git a/lib/mongoid/extensions/symbol.rb b/lib/mongoid/extensions/symbol.rb index 65c9b519a..c37536e96 100644 --- a/lib/mongoid/extensions/symbol.rb +++ b/lib/mongoid/extensions/symbol.rb @@ -13,9 +13,11 @@ module Symbol # :_id.mongoid_id? # # @return [ true | false ] If the symbol is :id or :_id. + # @deprecated def mongoid_id? to_s.mongoid_id? end + Mongoid.deprecate(self, :mongoid_id?) module ClassMethods diff --git a/spec/mongoid/extensions/string_spec.rb b/spec/mongoid/extensions/string_spec.rb index adc9cd49b..e7d27c7aa 100644 --- a/spec/mongoid/extensions/string_spec.rb +++ b/spec/mongoid/extensions/string_spec.rb @@ -172,37 +172,6 @@ class Patient end end - describe "#mongoid_id?" do - - context "when the string is id" do - - it "returns true" do - expect("id").to be_mongoid_id - end - end - - context "when the string is _id" do - - it "returns true" do - expect("_id").to be_mongoid_id - end - end - - context "when the string contains id" do - - it "returns false" do - expect("identity").to_not be_mongoid_id - end - end - - context "when the string contains _id" do - - it "returns false" do - expect("something_id").to_not be_mongoid_id - end - end - end - [ :mongoize, :demongoize ].each do |method| describe ".#{method}" do diff --git a/spec/mongoid/extensions/symbol_spec.rb b/spec/mongoid/extensions/symbol_spec.rb index 2bd4d7b6a..6ec6bd0e7 100644 --- a/spec/mongoid/extensions/symbol_spec.rb +++ b/spec/mongoid/extensions/symbol_spec.rb @@ -5,37 +5,6 @@ describe Mongoid::Extensions::Symbol do - describe "#mongoid_id?" do - - context "when the string is id" do - - it "returns true" do - expect(:id).to be_mongoid_id - end - end - - context "when the string is _id" do - - it "returns true" do - expect(:_id).to be_mongoid_id - end - end - - context "when the string contains id" do - - it "returns false" do - expect(:identity).to_not be_mongoid_id - end - end - - context "when the string contains _id" do - - it "returns false" do - expect(:something_id).to_not be_mongoid_id - end - end - end - [ :mongoize, :demongoize ].each do |method| describe ".mongoize" do From 73781415b5dc5d2c2c28221c0898bf7c517b445d Mon Sep 17 00:00:00 2001 From: Johnny Shields <27655+johnnyshields@users.noreply.github.com> Date: Wed, 8 Nov 2023 07:22:34 +0900 Subject: [PATCH 24/52] MONGOID-5670 [Monkey Patch Removal] Remove Hash#delete_id and Hash#extract_id (#5701) * Move Hash#delete_id and Hash#extract_id to Nested::NestedBuildable * Fix failing spec * deprecate, don't remove --------- Co-authored-by: Jamis Buck --- lib/mongoid/association/nested/many.rb | 5 ++-- .../association/nested/nested_buildable.rb | 27 +++++++++++++++++++ lib/mongoid/association/nested/one.rb | 2 +- lib/mongoid/criteria.rb | 4 +-- lib/mongoid/extensions/hash.rb | 4 +++ 5 files changed, 37 insertions(+), 5 deletions(-) diff --git a/lib/mongoid/association/nested/many.rb b/lib/mongoid/association/nested/many.rb index 636bc8014..a078a5c64 100644 --- a/lib/mongoid/association/nested/many.rb +++ b/lib/mongoid/association/nested/many.rb @@ -101,7 +101,8 @@ def over_limit?(attributes) # @param [ Hash ] attrs The single document attributes to process. def process_attributes(parent, attrs) return if reject?(parent, attrs) - if id = attrs.extract_id + + if (id = extract_id(attrs)) update_nested_relation(parent, id, attrs) else existing.push(Factory.build(@class_name, attrs)) unless destroyable?(attrs) @@ -153,7 +154,7 @@ def destroy_document(relation, doc) # @param [ Document ] doc The document to update. # @param [ Hash ] attrs The attributes. def update_document(doc, attrs) - attrs.delete_id + delete_id(attrs) if association.embedded? doc.assign_attributes(attrs) else diff --git a/lib/mongoid/association/nested/nested_buildable.rb b/lib/mongoid/association/nested/nested_buildable.rb index 55254450b..81d6b292b 100644 --- a/lib/mongoid/association/nested/nested_buildable.rb +++ b/lib/mongoid/association/nested/nested_buildable.rb @@ -65,6 +65,33 @@ def update_only? def convert_id(klass, id) klass.using_object_ids? ? BSON::ObjectId.mongoize(id) : id end + + private + + # Get the id attribute from the given hash, whether it's + # prefixed with an underscore or is a symbol. + # + # @example Get the id. + # extract_id({ _id: 1 }) + # + # @param [ Hash ] hash The hash from which to extract. + # + # @return [ Object ] The value of the id. + def extract_id(hash) + hash['_id'] || hash[:_id] || hash['id'] || hash[:id] + end + + # Deletes the id key from the given hash. + # + # @example Delete an id value. + # delete_id({ "_id" => 1 }) + # + # @param [ Hash ] hash The hash from which to delete. + # + # @return [ Object ] The deleted value, or nil. + def delete_id(hash) + hash.delete('_id') || hash.delete(:_id) || hash.delete('id') || hash.delete(:id) + end end end end diff --git a/lib/mongoid/association/nested/one.rb b/lib/mongoid/association/nested/one.rb index 126e6f1de..5fbc9397d 100644 --- a/lib/mongoid/association/nested/one.rb +++ b/lib/mongoid/association/nested/one.rb @@ -29,7 +29,7 @@ def build(parent) return if reject?(parent, attributes) @existing = parent.send(association.name) if update? - attributes.delete_id + delete_id(attributes) existing.assign_attributes(attributes) elsif replace? parent.send(association.setter, Factory.build(@class_name, attributes)) diff --git a/lib/mongoid/criteria.rb b/lib/mongoid/criteria.rb index ad365b5a8..a7013e571 100644 --- a/lib/mongoid/criteria.rb +++ b/lib/mongoid/criteria.rb @@ -162,7 +162,7 @@ def embedded? # # @return [ Object ] The id. def extract_id - selector.extract_id + selector['_id'] || selector[:_id] || selector['id'] || selector[:id] end # Adds a criterion to the +Criteria+ that specifies additional options @@ -225,7 +225,7 @@ def initialize(klass) # may be desired. # # @example Merge the criteria with another criteria. - # criteri.merge(other_criteria) + # criteria.merge(other_criteria) # # @example Merge the criteria with a hash. The hash must contain a klass # key and the key/value pairs correspond to method names/args. diff --git a/lib/mongoid/extensions/hash.rb b/lib/mongoid/extensions/hash.rb index 1cf19d539..0bb04eaa2 100644 --- a/lib/mongoid/extensions/hash.rb +++ b/lib/mongoid/extensions/hash.rb @@ -101,9 +101,11 @@ def _mongoid_unsatisfiable_criteria? # {}.delete_id # # @return [ Object ] The deleted value, or nil. + # @deprecated def delete_id delete("_id") || delete(:_id) || delete("id") || delete(:id) end + Mongoid.deprecate(self, :delete_id) # Get the id attribute from this hash, whether it's prefixed with an # underscore or is a symbol. @@ -112,9 +114,11 @@ def delete_id # { :_id => 1 }.extract_id # # @return [ Object ] The value of the id. + # @deprecated def extract_id self["_id"] || self[:_id] || self["id"] || self[:id] end + Mongoid.deprecate(self, :extract_id) # Turn the object from the ruby type we deal with to a Mongo friendly # type. From fb1d2805ccb0f6f2c44d9d570ff5ed484606c378 Mon Sep 17 00:00:00 2001 From: Johnny Shields <27655+johnnyshields@users.noreply.github.com> Date: Wed, 8 Nov 2023 07:58:45 +0900 Subject: [PATCH 25/52] MONGOID-5671 [Monkey Patch Removal] Remove Object#blank_criteria? and Hash#__mongoid_unsatisfiable_criteria? (#5700) * - Remove ``Object#blank_criteria?`` method entirely (was previously deprecated and not used in code.) - Remove ``Hash#_mongoid_unsatisfiable_criteria?`` method is removed (was previously marked @api private) and move it to a private method of Referenced::HasMany::Enumerable. * Update enumerable.rb * removing the specs for unsatisifiable_criteria this tests an implementation detail and not a behavior, which is fragile. I'm not even sure this is an implementation detail we want, and testing it specifically pours metaphorical concrete around it, making it that much harder to remove later. --------- Co-authored-by: Jamis Buck --- docs/release-notes/mongoid-9.0.txt | 1 + .../referenced/has_many/enumerable.rb | 38 ++++++- lib/mongoid/extensions/array.rb | 22 ---- lib/mongoid/extensions/hash.rb | 50 --------- lib/mongoid/extensions/object.rb | 15 --- spec/mongoid/extensions/array_spec.rb | 28 ----- spec/mongoid/extensions/hash_spec.rb | 103 ------------------ spec/mongoid/extensions/object_spec.rb | 7 -- 8 files changed, 38 insertions(+), 226 deletions(-) diff --git a/docs/release-notes/mongoid-9.0.txt b/docs/release-notes/mongoid-9.0.txt index f6974ac5d..620f2a69a 100644 --- a/docs/release-notes/mongoid-9.0.txt +++ b/docs/release-notes/mongoid-9.0.txt @@ -74,6 +74,7 @@ Deprecated functionality removed The method ``Mongoid::QueryCache#clear_cache`` should be replaced with ``Mongo::QueryCache#clear``. All other methods and submodules are identically named. Refer to the `driver query cache documentation `_ for more details. +- ``Object#blank_criteria?`` method is removed (was previously deprecated.) ``touch`` method now clears changed state diff --git a/lib/mongoid/association/referenced/has_many/enumerable.rb b/lib/mongoid/association/referenced/has_many/enumerable.rb index 131a09568..b388e404b 100644 --- a/lib/mongoid/association/referenced/has_many/enumerable.rb +++ b/lib/mongoid/association/referenced/has_many/enumerable.rb @@ -478,12 +478,48 @@ def set_base(document) end def unloaded_documents - if _unloaded.selector._mongoid_unsatisfiable_criteria? + if unsatisfiable_criteria?(_unloaded.selector) [] else _unloaded end end + + # Checks whether conditions in the given hash are known to be + # unsatisfiable, i.e. querying with this hash will always return no + # documents. + # + # This method only handles condition shapes that Mongoid itself uses when + # it builds association queries. Return value true indicates the condition + # always produces an empty document set. Note however that return value false + # is not a guarantee that the condition won't produce an empty document set. + # + # @example Unsatisfiable conditions + # unsatisfiable_criteria?({'_id' => {'$in' => []}}) + # # => true + # + # @example Conditions which may be satisfiable + # unsatisfiable_criteria?({'_id' => '123'}) + # # => false + # + # @example Conditions which are unsatisfiable that this method does not handle + # unsatisfiable_criteria?({'foo' => {'$in' => []}}) + # # => false + # + # @param [ Hash ] selector The conditions to check. + # + # @return [ true | false ] Whether hash contains known unsatisfiable + # conditions. + def unsatisfiable_criteria?(selector) + unsatisfiable_criteria = { '_id' => { '$in' => [] } } + return true if selector == unsatisfiable_criteria + return false unless selector.length == 1 && selector.keys == %w[$and] + + value = selector.values.first + value.is_a?(Array) && value.any? do |sub_value| + sub_value.is_a?(Hash) && unsatisfiable_criteria?(sub_value) + end + end end end end diff --git a/lib/mongoid/extensions/array.rb b/lib/mongoid/extensions/array.rb index 072a1d42b..5188ca557 100644 --- a/lib/mongoid/extensions/array.rb +++ b/lib/mongoid/extensions/array.rb @@ -55,26 +55,6 @@ def __mongoize_time__ ::Time.configured.local(*self) end - # Checks whether conditions given in this array are known to be - # unsatisfiable, i.e., querying with this array will always return no - # documents. - # - # This method used to assume that the array is the list of criteria - # to be used with an $and operator. This assumption is no longer made; - # therefore, since the interpretation of conditions in the array differs - # between $and, $or and $nor operators, this method now always returns - # false. - # - # This method is deprecated. Mongoid now uses - # +_mongoid_unsatisfiable_criteria?+ internally; this method is retained - # for backwards compatibility only. It always returns false. - # - # @return [ false ] Always false. - # @deprecated - def blank_criteria? - false - end - # Is the array a set of multiple arguments in a method? # # @example Is this multi args? @@ -175,5 +155,3 @@ def resizable? ::Array.__send__(:include, Mongoid::Extensions::Array) ::Array.extend(Mongoid::Extensions::Array::ClassMethods) - -::Mongoid.deprecate(Array, :blank_criteria) diff --git a/lib/mongoid/extensions/hash.rb b/lib/mongoid/extensions/hash.rb index 0bb04eaa2..67490aa5f 100644 --- a/lib/mongoid/extensions/hash.rb +++ b/lib/mongoid/extensions/hash.rb @@ -47,54 +47,6 @@ def __consolidate__(klass) end Mongoid.deprecate(self, :__consolidate__) - # Checks whether conditions given in this hash are known to be - # unsatisfiable, i.e., querying with this hash will always return no - # documents. - # - # This method only handles condition shapes that Mongoid itself uses when - # it builds association queries. It does not guarantee that a false - # return value means the condition can produce a non-empty document set - - # only that if the return value is true, the condition always produces - # an empty document set. - # - # @example Unsatisfiable conditions - # {'_id' => {'$in' => []}}._mongoid_unsatisfiable_criteria? - # # => true - # - # @example Conditions which could be satisfiable - # {'_id' => '123'}._mongoid_unsatisfiable_criteria? - # # => false - # - # @example Conditions which are unsatisfiable that this method does not handle - # {'foo' => {'$in' => []}}._mongoid_unsatisfiable_criteria? - # # => false - # - # @return [ true | false ] Whether hash contains known unsatisfiable - # conditions. - # @api private - def _mongoid_unsatisfiable_criteria? - unsatisfiable_criteria = { "_id" => { "$in" => [] }} - return true if self == unsatisfiable_criteria - return false unless length == 1 && keys == %w($and) - value = values.first - value.is_a?(Array) && value.any? do |sub_v| - sub_v.is_a?(Hash) && sub_v._mongoid_unsatisfiable_criteria? - end - end - - # Checks whether conditions given in this hash are known to be - # unsatisfiable, i.e., querying with this hash will always return no - # documents. - # - # This method is deprecated. Mongoid now uses - # +_mongoid_unsatisfiable_criteria?+ internally; this method is retained - # for backwards compatibility only. - # - # @return [ true | false ] Whether hash contains known unsatisfiable - # conditions. - # @deprecated - alias :blank_criteria? :_mongoid_unsatisfiable_criteria? - # Deletes an id value from the hash. # # @example Delete an id value. @@ -196,5 +148,3 @@ def resizable? ::Hash.__send__(:include, Mongoid::Extensions::Hash) ::Hash.extend(Mongoid::Extensions::Hash::ClassMethods) - -::Mongoid.deprecate(Hash, :blank_criteria) diff --git a/lib/mongoid/extensions/object.rb b/lib/mongoid/extensions/object.rb index 089269bc7..52fa4b4b3 100644 --- a/lib/mongoid/extensions/object.rb +++ b/lib/mongoid/extensions/object.rb @@ -84,21 +84,6 @@ def __to_inc__ end Mongoid.deprecate(self, :__to_inc__) - # Checks whether conditions given in this object are known to be - # unsatisfiable, i.e., querying with this object will always return no - # documents. - # - # This method is deprecated. Mongoid now uses - # +_mongoid_unsatisfiable_criteria?+ internally; this method is retained - # for backwards compatibility only. It always returns false. - # - # @return [ false ] Always false. - # @deprecated - def blank_criteria? - false - end - Mongoid.deprecate(self, :blank_criteria?) - # Do or do not, there is no try. -- Yoda. # # @example Do or do not. diff --git a/spec/mongoid/extensions/array_spec.rb b/spec/mongoid/extensions/array_spec.rb index 49c402986..e9fa5b2fd 100644 --- a/spec/mongoid/extensions/array_spec.rb +++ b/spec/mongoid/extensions/array_spec.rb @@ -378,34 +378,6 @@ end end - describe "#blank_criteria?" do - - context "when the array has an empty _id criteria" do - - context "when only the id criteria is in the array" do - - let(:array) do - [{ "_id" => { "$in" => [] }}] - end - - it "is false" do - expect(array.blank_criteria?).to be false - end - end - - context "when the id criteria is in the array with others" do - - let(:array) do - [{ "_id" => "test" }, { "_id" => { "$in" => [] }}] - end - - it "is false" do - expect(array.blank_criteria?).to be false - end - end - end - end - describe "#delete_one" do context "when the object doesn't exist" do diff --git a/spec/mongoid/extensions/hash_spec.rb b/spec/mongoid/extensions/hash_spec.rb index bcc1a58d6..5d797a14d 100644 --- a/spec/mongoid/extensions/hash_spec.rb +++ b/spec/mongoid/extensions/hash_spec.rb @@ -294,107 +294,4 @@ expect(Hash).to be_resizable end end - - shared_examples_for 'unsatisfiable criteria method' do - - context "when the hash has only an empty _id criteria" do - - let(:hash) do - { "_id" => { "$in" => [] }} - end - - it "is true" do - expect(hash.send(meth)).to be true - end - end - - context "when the hash has an empty _id criteria and another criteria" do - - let(:hash) do - { "_id" => { "$in" => [] }, 'foo' => 'bar'} - end - - it "is false" do - expect(hash.send(meth)).to be false - end - end - - context "when the hash has an empty _id criteria via $and" do - - let(:hash) do - {'$and' => [{ "_id" => { "$in" => [] }}]} - end - - it "is true" do - expect(hash.send(meth)).to be true - end - end - - context "when the hash has an empty _id criteria via $and and another criteria at top level" do - - let(:hash) do - {'$and' => [{ "_id" => { "$in" => [] }}], 'foo' => 'bar'} - end - - it "is false" do - expect(hash.send(meth)).to be false - end - end - - context "when the hash has an empty _id criteria via $and and another criteria in $and" do - - let(:hash) do - {'$and' => [{ "_id" => { "$in" => [] }}, {'foo' => 'bar'}]} - end - - it "is true" do - expect(hash.send(meth)).to be true - end - end - - context "when the hash has an empty _id criteria via $and and another criteria in $and value" do - - let(:hash) do - {'$and' => [{ "_id" => { "$in" => [] }, 'foo' => 'bar'}]} - end - - it "is false" do - expect(hash.send(meth)).to be false - end - end - - context "when the hash has an empty _id criteria via $or" do - - let(:hash) do - {'$or' => [{ "_id" => { "$in" => [] }}]} - end - - it "is false" do - expect(hash.send(meth)).to be false - end - end - - context "when the hash has an empty _id criteria via $nor" do - - let(:hash) do - {'$nor' => [{ "_id" => { "$in" => [] }}]} - end - - it "is false" do - expect(hash.send(meth)).to be false - end - end - end - - describe "#blank_criteria?" do - let(:meth) { :blank_criteria? } - - it_behaves_like 'unsatisfiable criteria method' - end - - describe "#_mongoid_unsatisfiable_criteria?" do - let(:meth) { :_mongoid_unsatisfiable_criteria? } - - it_behaves_like 'unsatisfiable criteria method' - end end diff --git a/spec/mongoid/extensions/object_spec.rb b/spec/mongoid/extensions/object_spec.rb index 4b2e294de..73c01fb68 100644 --- a/spec/mongoid/extensions/object_spec.rb +++ b/spec/mongoid/extensions/object_spec.rb @@ -265,11 +265,4 @@ expect(object.numeric?).to eq(false) end end - - describe "#blank_criteria?" do - - it "is false" do - expect(object.blank_criteria?).to be false - end - end end From f12e44235b165239f9840eedd58b90e52e660d18 Mon Sep 17 00:00:00 2001 From: Johnny Shields <27655+johnnyshields@users.noreply.github.com> Date: Thu, 9 Nov 2023 01:03:44 +0900 Subject: [PATCH 26/52] MONGOID-5673 [Monkey Patch Removal] Remove Object#do_or_do_not and Object#you_must (#5713) * Remove do_or_do_not and you_must * Remove more do_or_do_not * reduce indent * Remove test * deprecate instead of remove --------- Co-authored-by: Jamis Buck --- lib/mongoid/association/bindable.rb | 32 ++++++++--- .../embedded/embedded_in/binding.rb | 8 +-- .../embedded/embeds_many/binding.rb | 4 +- .../embedded/embeds_one/binding.rb | 4 +- .../has_and_belongs_to_many/binding.rb | 6 +- lib/mongoid/cacheable.rb | 2 +- lib/mongoid/extensions/object.rb | 5 ++ lib/mongoid/validatable.rb | 2 +- spec/mongoid/extensions/object_spec.rb | 57 ------------------- 9 files changed, 43 insertions(+), 77 deletions(-) diff --git a/lib/mongoid/association/bindable.rb b/lib/mongoid/association/bindable.rb index 5a3814829..e1ec116a5 100644 --- a/lib/mongoid/association/bindable.rb +++ b/lib/mongoid/association/bindable.rb @@ -120,7 +120,7 @@ def remove_associated_in_to(doc, inverse) # @param [ Object ] id The id of the bound document. def bind_foreign_key(keyed, id) unless keyed.frozen? - keyed.you_must(_association.foreign_key_setter, id) + try_method(keyed, _association.foreign_key_setter, id) end end @@ -135,8 +135,8 @@ def bind_foreign_key(keyed, id) # @param [ Document ] typed The document that stores the type field. # @param [ String ] name The name of the model. def bind_polymorphic_type(typed, name) - if _association.type - typed.you_must(_association.type_setter, name) + if _association.type && !typed.frozen? + try_method(typed, _association.type_setter, name) end end @@ -151,8 +151,8 @@ def bind_polymorphic_type(typed, name) # @param [ Document ] typed The document that stores the type field. # @param [ String ] name The name of the model. def bind_polymorphic_inverse_type(typed, name) - if _association.inverse_type - typed.you_must(_association.inverse_type_setter, name) + if _association.inverse_type && !typed.frozen? + try_method(typed, _association.inverse_type_setter, name) end end @@ -167,8 +167,8 @@ def bind_polymorphic_inverse_type(typed, name) # @param [ Document ] doc The base document. # @param [ Document ] inverse The inverse document. def bind_inverse(doc, inverse) - if doc.respond_to?(_association.inverse_setter) - doc.you_must(_association.inverse_setter, inverse) + if doc.respond_to?(_association.inverse_setter) && !doc.frozen? + try_method(doc, _association.inverse_setter, inverse) end end @@ -223,6 +223,24 @@ def unbind_from_relational_parent(doc) bind_polymorphic_type(doc, nil) bind_inverse(doc, nil) end + + # Convenience method to perform +#try+ but return + # nil if the method argument is nil. + # + # @example Call method if it exists. + # object.try_method(:use, "The Force") + # + # @example Return nil if method argument is nil. + # object.try_method(nil, "The Force") #=> nil + # + # @param [ String | Symbol ] method_name The method name. + # @param [ Object... ] *args The arguments. + # + # @return [ Object | nil ] The result of the try or nil if the + # method does not exist. + def try_method(object, method_name, *args) + object.try(method_name, *args) if method_name + end end end end diff --git a/lib/mongoid/association/embedded/embedded_in/binding.rb b/lib/mongoid/association/embedded/embedded_in/binding.rb index a799bf2e6..ea55921ab 100644 --- a/lib/mongoid/association/embedded/embedded_in/binding.rb +++ b/lib/mongoid/association/embedded/embedded_in/binding.rb @@ -25,10 +25,10 @@ def bind_one _base._association = _association.inverse_association(_target) unless _base._association _base.parentize(_target) if _base.embedded_many? - _target.do_or_do_not(_association.inverse(_target)).push(_base) + _target.send(_association.inverse(_target)).push(_base) else remove_associated(_target) - _target.do_or_do_not(_association.inverse_setter(_target), _base) + try_method(_target, _association.inverse_setter(_target), _base) end end end @@ -42,9 +42,9 @@ def bind_one def unbind_one binding do if _base.embedded_many? - _target.do_or_do_not(_association.inverse(_target)).delete(_base) + _target.send(_association.inverse(_target)).delete(_base) else - _target.do_or_do_not(_association.inverse_setter(_target), nil) + try_method(_target, _association.inverse_setter(_target), nil) end end end diff --git a/lib/mongoid/association/embedded/embeds_many/binding.rb b/lib/mongoid/association/embedded/embeds_many/binding.rb index c178486a8..e2d7f41a7 100644 --- a/lib/mongoid/association/embedded/embeds_many/binding.rb +++ b/lib/mongoid/association/embedded/embeds_many/binding.rb @@ -21,7 +21,7 @@ def bind_one(doc) doc.parentize(_base) binding do remove_associated(doc) - doc.do_or_do_not(_association.inverse_setter(_target), _base) + try_method(doc, _association.inverse_setter(_target), _base) end end @@ -33,7 +33,7 @@ def bind_one(doc) # @param [ Document ] doc The single document to unbind. def unbind_one(doc) binding do - doc.do_or_do_not(_association.inverse_setter(_target), nil) + try_method(doc, _association.inverse_setter(_target), nil) end end end diff --git a/lib/mongoid/association/embedded/embeds_one/binding.rb b/lib/mongoid/association/embedded/embeds_one/binding.rb index 9b719721a..6c6c356fe 100644 --- a/lib/mongoid/association/embedded/embeds_one/binding.rb +++ b/lib/mongoid/association/embedded/embeds_one/binding.rb @@ -22,7 +22,7 @@ class Binding def bind_one _target.parentize(_base) binding do - _target.do_or_do_not(_association.inverse_setter(_target), _base) + try_method(_target, _association.inverse_setter(_target), _base) end end @@ -34,7 +34,7 @@ def bind_one # person.name = nil def unbind_one binding do - _target.do_or_do_not(_association.inverse_setter(_target), nil) + try_method(_target, _association.inverse_setter(_target), nil) end end end diff --git a/lib/mongoid/association/referenced/has_and_belongs_to_many/binding.rb b/lib/mongoid/association/referenced/has_and_belongs_to_many/binding.rb index 5b56a8670..c67f42154 100644 --- a/lib/mongoid/association/referenced/has_and_belongs_to_many/binding.rb +++ b/lib/mongoid/association/referenced/has_and_belongs_to_many/binding.rb @@ -19,11 +19,11 @@ class Binding # @param [ Document ] doc The single document to bind. def bind_one(doc) binding do - inverse_keys = doc.you_must(_association.inverse_foreign_key) + inverse_keys = try_method(doc, _association.inverse_foreign_key) unless doc.frozen? if inverse_keys record_id = inverse_record_id(doc) unless inverse_keys.include?(record_id) - doc.you_must(_association.inverse_foreign_key_setter, inverse_keys.push(record_id)) + try_method(doc, _association.inverse_foreign_key_setter, inverse_keys.push(record_id)) end doc.reset_relation_criteria(_association.inverse) end @@ -39,7 +39,7 @@ def bind_one(doc) def unbind_one(doc) binding do _base.send(_association.foreign_key).delete_one(record_id(doc)) - inverse_keys = doc.you_must(_association.inverse_foreign_key) + inverse_keys = try_method(doc, _association.inverse_foreign_key) unless doc.frozen? if inverse_keys inverse_keys.delete_one(inverse_record_id(doc)) doc.reset_relation_criteria(_association.inverse) diff --git a/lib/mongoid/cacheable.rb b/lib/mongoid/cacheable.rb index 71bba22a2..dae0e4731 100644 --- a/lib/mongoid/cacheable.rb +++ b/lib/mongoid/cacheable.rb @@ -27,7 +27,7 @@ module Cacheable # @return [ String ] the string with or without updated_at def cache_key return "#{model_key}/new" if new_record? - return "#{model_key}/#{_id}-#{updated_at.utc.to_formatted_s(cache_timestamp_format)}" if do_or_do_not(:updated_at) + return "#{model_key}/#{_id}-#{updated_at.utc.to_formatted_s(cache_timestamp_format)}" if try(:updated_at) "#{model_key}/#{_id}" end end diff --git a/lib/mongoid/extensions/object.rb b/lib/mongoid/extensions/object.rb index 52fa4b4b3..495f7b2c4 100644 --- a/lib/mongoid/extensions/object.rb +++ b/lib/mongoid/extensions/object.rb @@ -84,6 +84,7 @@ def __to_inc__ end Mongoid.deprecate(self, :__to_inc__) + # Do or do not, there is no try. -- Yoda. # # @example Do or do not. @@ -94,9 +95,11 @@ def __to_inc__ # # @return [ Object | nil ] The result of the method call or nil if the # method does not exist. + # @deprecated def do_or_do_not(name, *args) send(name, *args) if name && respond_to?(name) end + Mongoid.deprecate(self, :do_or_do_not) # Get the value for an instance variable or false if it doesn't exist. # @@ -195,9 +198,11 @@ def substitutable # # @return [ Object | nil ] The result of the method call or nil if the # method does not exist. Nil if the object is frozen. + # @deprecated def you_must(name, *args) frozen? ? nil : do_or_do_not(name, *args) end + Mongoid.deprecate(self, :you_must) module ClassMethods # Convert the provided object to a foreign key, given the metadata key diff --git a/lib/mongoid/validatable.rb b/lib/mongoid/validatable.rb index 34183e348..e43fe432f 100644 --- a/lib/mongoid/validatable.rb +++ b/lib/mongoid/validatable.rb @@ -68,7 +68,7 @@ def read_attribute_for_validation(attr) begin_validate relation = without_autobuild { send(attr) } exit_validate - relation.do_or_do_not(:in_memory) || relation + relation.try(:in_memory) || relation elsif fields[attribute].try(:localized?) attributes[attribute] else diff --git a/spec/mongoid/extensions/object_spec.rb b/spec/mongoid/extensions/object_spec.rb index 73c01fb68..2a71c1931 100644 --- a/spec/mongoid/extensions/object_spec.rb +++ b/spec/mongoid/extensions/object_spec.rb @@ -132,45 +132,6 @@ end end - describe "#do_or_do_not" do - - context "when the object is nil" do - - let(:result) do - nil.do_or_do_not(:not_a_method, "The force is strong with you") - end - - it "returns nil" do - expect(result).to be_nil - end - end - - context "when the object is not nil" do - - context "when the object responds to the method" do - - let(:result) do - [ "Yoda", "Luke" ].do_or_do_not(:join, ",") - end - - it "returns the result of the method" do - expect(result).to eq("Yoda,Luke") - end - end - - context "when the object does not respond to the method" do - - let(:result) do - "Yoda".do_or_do_not(:use, "The Force", 1000) - end - - it "returns the result of the method" do - expect(result).to be_nil - end - end - end - end - describe ".mongoize" do let(:object) do @@ -200,24 +161,6 @@ end end - describe "#you_must" do - - context "when the object is frozen" do - - let(:person) do - Person.new.tap { |peep| peep.freeze } - end - - let(:result) do - person.you_must(:aliases=, []) - end - - it "returns nil" do - expect(result).to be_nil - end - end - end - describe "#remove_ivar" do context "when the instance variable is defined" do From 3e2dd93bbc6a3fb6391e5557fc21b6d75bb2c7a6 Mon Sep 17 00:00:00 2001 From: Johnny Shields <27655+johnnyshields@users.noreply.github.com> Date: Thu, 9 Nov 2023 02:01:05 +0900 Subject: [PATCH 27/52] MONGOID-5674 [Monkey Patch Removal] Remove Object#regexp? (#5714) * Remove Object#regexp? * deprecate rather than remove --------- Co-authored-by: Jamis Buck --- .../criteria/queryable/extensions/object.rb | 2 + .../criteria/queryable/extensions/regexp.rb | 4 + .../criteria/queryable/extensions/string.rb | 15 ++- .../queryable/extensions/regexp_raw_spec.rb | 11 --- .../queryable/extensions/regexp_spec.rb | 11 --- .../queryable/extensions/string_spec.rb | 93 +++++++++---------- 6 files changed, 65 insertions(+), 71 deletions(-) diff --git a/lib/mongoid/criteria/queryable/extensions/object.rb b/lib/mongoid/criteria/queryable/extensions/object.rb index aaf889006..32605781d 100644 --- a/lib/mongoid/criteria/queryable/extensions/object.rb +++ b/lib/mongoid/criteria/queryable/extensions/object.rb @@ -128,9 +128,11 @@ def __expand_complex__ # obj.regexp? # # @return [ false ] Always false. + # @deprecated def regexp? false end + Mongoid.deprecate(self, :regexp?) module ClassMethods diff --git a/lib/mongoid/criteria/queryable/extensions/regexp.rb b/lib/mongoid/criteria/queryable/extensions/regexp.rb index 31cdafb95..514c564b2 100644 --- a/lib/mongoid/criteria/queryable/extensions/regexp.rb +++ b/lib/mongoid/criteria/queryable/extensions/regexp.rb @@ -15,7 +15,9 @@ module Regexp # /\A[123]/.regexp? # # @return [ true ] Always true. + # @deprecated def regexp?; true; end + Mongoid.deprecate(self, :regexp?) module ClassMethods @@ -43,7 +45,9 @@ module Raw_ # bson_raw_regexp.regexp? # # @return [ true ] Always true. + # @deprecated def regexp?; true; end + Mongoid.deprecate(self, :regexp?) module ClassMethods diff --git a/lib/mongoid/criteria/queryable/extensions/string.rb b/lib/mongoid/criteria/queryable/extensions/string.rb index 4cc513334..1bbd83d0a 100644 --- a/lib/mongoid/criteria/queryable/extensions/string.rb +++ b/lib/mongoid/criteria/queryable/extensions/string.rb @@ -82,7 +82,7 @@ module ClassMethods # @return [ Hash ] The selection. def __expr_part__(key, value, negating = false) if negating - { key => { "$#{value.regexp? ? "not" : "ne"}" => value }} + { key => { "$#{__regexp?(value) ? "not" : "ne"}" => value }} else { key => value } end @@ -99,9 +99,20 @@ def __expr_part__(key, value, negating = false) # @return [ String ] The value as a string. def evolve(object) __evolve__(object) do |obj| - obj.regexp? ? obj : obj.to_s + __regexp?(obj) ? obj : obj.to_s end end + + private + + # Returns whether the object is Regexp-like. + # + # @param [ Object ] object The object to evaluate. + # + # @return [ Boolean ] Whether the object is Regexp-like. + def __regexp?(object) + object.is_a?(Regexp) || object.is_a?(BSON::Regexp::Raw) + end end end end diff --git a/spec/mongoid/criteria/queryable/extensions/regexp_raw_spec.rb b/spec/mongoid/criteria/queryable/extensions/regexp_raw_spec.rb index 11be3f670..1f8807bf1 100644 --- a/spec/mongoid/criteria/queryable/extensions/regexp_raw_spec.rb +++ b/spec/mongoid/criteria/queryable/extensions/regexp_raw_spec.rb @@ -78,15 +78,4 @@ end end end - - describe "#regexp?" do - - let(:regexp) do - BSON::Regexp::Raw.new('^[123]') - end - - it "returns true" do - expect(regexp).to be_regexp - end - end end diff --git a/spec/mongoid/criteria/queryable/extensions/regexp_spec.rb b/spec/mongoid/criteria/queryable/extensions/regexp_spec.rb index 644d77beb..9a0639f0b 100644 --- a/spec/mongoid/criteria/queryable/extensions/regexp_spec.rb +++ b/spec/mongoid/criteria/queryable/extensions/regexp_spec.rb @@ -79,15 +79,4 @@ end end end - - describe "#regexp?" do - - let(:regexp) do - /\A[123]/ - end - - it "returns true" do - expect(regexp).to be_regexp - end - end end diff --git a/spec/mongoid/criteria/queryable/extensions/string_spec.rb b/spec/mongoid/criteria/queryable/extensions/string_spec.rb index 2eb5a722a..c99c3a420 100644 --- a/spec/mongoid/criteria/queryable/extensions/string_spec.rb +++ b/spec/mongoid/criteria/queryable/extensions/string_spec.rb @@ -181,84 +181,83 @@ end end - describe "#__expr_part__" do + describe '#__expr_part__' do + subject(:specified) { 'field'.__expr_part__(value) } + let(:value) { 10 } - let(:specified) do - "field".__expr_part__(10) + it 'returns the expression with the value' do + expect(specified).to eq({ 'field' => 10 }) end - it "returns the string with the value" do - expect(specified).to eq({ "field" => 10 }) - end - - context "with a regexp" do + context 'with a Regexp' do + let(:value) { /test/ } - let(:specified) do - "field".__expr_part__(/test/) + it 'returns the expression with the value' do + expect(specified).to eq({ 'field' => /test/ }) end + end - it "returns the symbol with the value" do - expect(specified).to eq({ "field" => /test/ }) - end + context 'with a BSON::Regexp::Raw' do + let(:value) { BSON::Regexp::Raw.new('^[123]') } + it 'returns the expression with the value' do + expect(specified).to eq({ 'field' => BSON::Regexp::Raw.new('^[123]') }) + end end - context "when negated" do + context 'when negated' do + subject(:specified) { 'field'.__expr_part__(value, true) } - context "with a regexp" do + context 'with a Regexp' do + let(:value) { /test/ } - let(:specified) do - "field".__expr_part__(/test/, true) + it 'returns the expression with the value negated' do + expect(specified).to eq({ 'field' => { '$not' => /test/ } }) end - - it "returns the string with the value negated" do - expect(specified).to eq({ "field" => { "$not" => /test/ } }) - end - end - context "with anything else" do + context 'with a BSON::Regexp::Raw' do + let(:value) { BSON::Regexp::Raw.new('^[123]') } - let(:specified) do - "field".__expr_part__('test', true) + it 'returns the expression with the value' do + expect(specified).to eq({ 'field' => { '$not' => BSON::Regexp::Raw.new('^[123]') } }) end + end - it "returns the string with the value negated" do - expect(specified).to eq({ "field" => { "$ne" => "test" }}) + context 'with anything else' do + let(:value) { 'test' } + + it 'returns the expression with the value negated' do + expect(specified).to eq({ 'field' => { '$ne' => 'test' }}) end end end end - describe ".evolve" do + describe '.evolve' do + subject(:evolved) { described_class.evolve(object) } - context "when provided a regex" do + context 'when provided a Regexp' do + let(:object) { /\A[123]/.freeze } - let(:regex) do - /\A[123]/.freeze - end - - let(:evolved) do - described_class.evolve(regex) - end - - it "returns the regex" do - expect(evolved).to eq(regex) + it 'returns the regexp' do + expect(evolved).to eq(/\A[123]/) end end - context "when provided an object" do + context 'when provided a BSON::Regexp::Raw' do + let(:object) { BSON::Regexp::Raw.new('^[123]') } - let(:object) do - 1234 + it 'returns the BSON::Regexp::Raw' do + expect(evolved).to eq(BSON::Regexp::Raw.new('^[123]')) end + end - let(:evolved) do - described_class.evolve(object) - end + context 'when provided an object' do + let(:object) { 1234 } - it "returns the object as a string" do - expect(evolved).to eq("1234") + it 'returns the object as a string' do + expect(evolved).to eq('1234') end end end From f4b29cecda47dad38b0e963eafe5b4dd22431ed6 Mon Sep 17 00:00:00 2001 From: Johnny Shields <27655+johnnyshields@users.noreply.github.com> Date: Thu, 9 Nov 2023 03:00:32 +0900 Subject: [PATCH 28/52] MONGOID-5676 [Monkey Patch Removal] Remove Time#configured (#5716) * Removes Time#configured monkey patch. * Set UTC timezone by default * More spec cleanup * Re-add error check * add release notes for removal of `Time.configured` --------- Co-authored-by: Jamis Buck --- docs/release-notes/mongoid-9.0.txt | 40 +++++++++++++++++++ .../criteria/queryable/extensions/date.rb | 2 +- lib/mongoid/extensions/array.rb | 2 +- lib/mongoid/extensions/date.rb | 2 +- lib/mongoid/extensions/float.rb | 2 +- lib/mongoid/extensions/integer.rb | 2 +- lib/mongoid/extensions/string.rb | 18 ++++----- lib/mongoid/extensions/time.rb | 13 ------ lib/mongoid/timestamps/created.rb | 6 +-- lib/mongoid/timestamps/updated.rb | 2 +- lib/mongoid/touchable.rb | 2 +- spec/integration/criteria/raw_value_spec.rb | 1 - spec/mongoid/findable_spec.rb | 2 +- spec/spec_helper.rb | 3 +- spec/support/macros.rb | 7 +--- spec/support/shared/time.rb | 8 +--- 16 files changed, 64 insertions(+), 48 deletions(-) diff --git a/docs/release-notes/mongoid-9.0.txt b/docs/release-notes/mongoid-9.0.txt index 620f2a69a..85e496eb8 100644 --- a/docs/release-notes/mongoid-9.0.txt +++ b/docs/release-notes/mongoid-9.0.txt @@ -403,6 +403,46 @@ There are also rake tasks available, for convenience: $ rake mongoid:db:remove_search_indexes +``Time.configured`` has been removed +------------------------------------ + +``Time.configured`` returned either the time object wrapping the configured +time zone, or the standard Ruby ``Time`` class. This allowed you to query +a time value even if no time zone had been configured. + +Mongoid now requires that you set a time zone if you intend to do +anything with time values (including using timestamps in your documents). +Any uses of ``Time.configured`` must be replaced with ``Time.zone``. + +... code-block:: ruby + + # before: + puts Time.configured.now + + # after: + puts Time.zone.now + + # or, better for finding the current Time specifically: + puts Time.current + +If you do not set a time zone, you will see errors in your code related +to ``nil`` values. If you are using Rails, the default time zone is already +set to UTC. If you are not using Rails, you may set a time zone at the start +of your program like this: + +... code-block:: ruby + + Time.zone = 'UTC' + +This will set the time zone to UTC. You can see all available time zone names +by running the following command: + +... code-block:: bash + +$ ruby -ractive_support/values/time_zone \ + -e 'puts ActiveSupport::TimeZone::MAPPING.keys' + + Bug Fixes and Improvements -------------------------- diff --git a/lib/mongoid/criteria/queryable/extensions/date.rb b/lib/mongoid/criteria/queryable/extensions/date.rb index 55b95bfa4..d4bb4837d 100644 --- a/lib/mongoid/criteria/queryable/extensions/date.rb +++ b/lib/mongoid/criteria/queryable/extensions/date.rb @@ -26,7 +26,7 @@ def __evolve_date__ # # @return [ Time | ActiveSupport::TimeWithZone ] The date as a local time. def __evolve_time__ - ::Time.configured.local(year, month, day) + ::Time.zone.local(year, month, day) end module ClassMethods diff --git a/lib/mongoid/extensions/array.rb b/lib/mongoid/extensions/array.rb index 5188ca557..6fba2f474 100644 --- a/lib/mongoid/extensions/array.rb +++ b/lib/mongoid/extensions/array.rb @@ -52,7 +52,7 @@ def __mongoize_object_id__ # configured default time zone corresponding to date/time components # in this array. def __mongoize_time__ - ::Time.configured.local(*self) + ::Time.zone.local(*self) end # Is the array a set of multiple arguments in a method? diff --git a/lib/mongoid/extensions/date.rb b/lib/mongoid/extensions/date.rb index 4a0bfa605..d28e181d1 100644 --- a/lib/mongoid/extensions/date.rb +++ b/lib/mongoid/extensions/date.rb @@ -17,7 +17,7 @@ module Date # configured default time zone corresponding to local midnight of # this date. def __mongoize_time__ - ::Time.configured.local(year, month, day) + ::Time.zone.local(year, month, day) end # Turn the object from the ruby type we deal with to a Mongo friendly diff --git a/lib/mongoid/extensions/float.rb b/lib/mongoid/extensions/float.rb index bedd71b4d..52f45a0f4 100644 --- a/lib/mongoid/extensions/float.rb +++ b/lib/mongoid/extensions/float.rb @@ -14,7 +14,7 @@ module Float # # @return [ Time | ActiveSupport::TimeWithZone ] The time. def __mongoize_time__ - ::Time.configured.at(self) + ::Time.zone.at(self) end # Is the float a number? diff --git a/lib/mongoid/extensions/integer.rb b/lib/mongoid/extensions/integer.rb index d8fbf4a64..ec35ee97b 100644 --- a/lib/mongoid/extensions/integer.rb +++ b/lib/mongoid/extensions/integer.rb @@ -14,7 +14,7 @@ module Integer # # @return [ Time | ActiveSupport::TimeWithZone ] The time. def __mongoize_time__ - ::Time.configured.at(self) + ::Time.zone.at(self) end # Is the integer a number? diff --git a/lib/mongoid/extensions/string.rb b/lib/mongoid/extensions/string.rb index df210f945..b541b3adc 100644 --- a/lib/mongoid/extensions/string.rb +++ b/lib/mongoid/extensions/string.rb @@ -40,19 +40,17 @@ def __mongoize_object_id__ # "2012-01-01".__mongoize_time__ # # => 2012-01-01 00:00:00 -0500 # + # @raise [ ArgumentError ] The string is not a valid time string. + # # @return [ Time | ActiveSupport::TimeWithZone ] Local time in the # configured default time zone corresponding to this string. def __mongoize_time__ - # This extra parse from Time is because ActiveSupport::TimeZone - # either returns nil or Time.now if the string is empty or invalid, - # which is a regression from pre-3.0 and also does not agree with - # the core Time API. - parsed = ::Time.parse(self) - if ::Time == ::Time.configured - parsed - else - ::Time.configured.parse(self) - end + # This extra Time.parse is required to raise an error if the string + # is not a valid time string. ActiveSupport::TimeZone does not + # perform this check. + ::Time.parse(self) + + ::Time.zone.parse(self) end # Convert the string to a collection friendly name. diff --git a/lib/mongoid/extensions/time.rb b/lib/mongoid/extensions/time.rb index 3f3e897cc..a2689d664 100644 --- a/lib/mongoid/extensions/time.rb +++ b/lib/mongoid/extensions/time.rb @@ -30,19 +30,6 @@ def mongoize module ClassMethods - # Get the configured time to use when converting - either the time zone - # or the time. - # - # @example Get the configured time. - # ::Time.configured - # - # @return [ Time ] The configured time. - # - # @deprecated - def configured - ::Time.zone || ::Time - end - # Convert the object from its mongo friendly ruby type to this type. # # @example Demongoize the object. diff --git a/lib/mongoid/timestamps/created.rb b/lib/mongoid/timestamps/created.rb index 42dc003f7..28564148d 100644 --- a/lib/mongoid/timestamps/created.rb +++ b/lib/mongoid/timestamps/created.rb @@ -24,9 +24,9 @@ module Created # person.set_created_at def set_created_at if !timeless? && !created_at - time = Time.configured.now - self.updated_at = time if is_a?(Updated) && !updated_at_changed? - self.created_at = time + now = Time.current + self.updated_at = now if is_a?(Updated) && !updated_at_changed? + self.created_at = now end clear_timeless_option end diff --git a/lib/mongoid/timestamps/updated.rb b/lib/mongoid/timestamps/updated.rb index 0e1951303..7ad78dc6d 100644 --- a/lib/mongoid/timestamps/updated.rb +++ b/lib/mongoid/timestamps/updated.rb @@ -25,7 +25,7 @@ module Updated # person.set_updated_at def set_updated_at if able_to_set_updated_at? - self.updated_at = Time.configured.now unless updated_at_changed? + self.updated_at = Time.current unless updated_at_changed? end clear_timeless_option diff --git a/lib/mongoid/touchable.rb b/lib/mongoid/touchable.rb index a7aeb7a87..ded0a8f88 100644 --- a/lib/mongoid/touchable.rb +++ b/lib/mongoid/touchable.rb @@ -52,7 +52,7 @@ def touch(field = nil) return false if _root.new_record? begin - touches = _gather_touch_updates(Time.configured.now, field) + touches = _gather_touch_updates(Time.current, field) _root.send(:persist_atomic_operations, '$set' => touches) if touches.present? _run_touch_callbacks_from_root ensure diff --git a/spec/integration/criteria/raw_value_spec.rb b/spec/integration/criteria/raw_value_spec.rb index f72e05514..1b1397514 100644 --- a/spec/integration/criteria/raw_value_spec.rb +++ b/spec/integration/criteria/raw_value_spec.rb @@ -4,7 +4,6 @@ require 'spec_helper' describe 'Queries with Mongoid::RawValue criteria' do - before { Time.zone = 'UTC'} let(:now_utc) { Time.utc(2020, 1, 1, 16, 0, 0, 0) } let(:today) { Date.new(2020, 1, 1) } diff --git a/spec/mongoid/findable_spec.rb b/spec/mongoid/findable_spec.rb index 6189f3f3a..bbe576902 100644 --- a/spec/mongoid/findable_spec.rb +++ b/spec/mongoid/findable_spec.rb @@ -904,7 +904,7 @@ time_zone_override "Asia/Kolkata" let!(:time) do - Time.zone.now.tap do |t| + Time.current.tap do |t| User.create!(last_login: t, name: 'Tom') end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 9da2629fb..4a3ba31cc 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -117,8 +117,9 @@ class Query inflect.singular("address_components", "address_component") end -I18n.config.enforce_available_locales = false +Time.zone = 'UTC' +I18n.config.enforce_available_locales = false if %w(yes true 1).include?((ENV['TEST_I18N_FALLBACKS'] || '').downcase) require "i18n/backend/fallbacks" diff --git a/spec/support/macros.rb b/spec/support/macros.rb index 294fca7eb..8751cdf08 100644 --- a/spec/support/macros.rb +++ b/spec/support/macros.rb @@ -103,12 +103,9 @@ def persistence_context_override(component, value) end end - def time_zone_override(tz) + def time_zone_override(time_zone) around do |example| - old_tz = Time.zone - Time.zone = tz - example.run - Time.zone = old_tz + Time.use_zone(time_zone) { example.run } end end diff --git a/spec/support/shared/time.rb b/spec/support/shared/time.rb index 3c98efe17..014447021 100644 --- a/spec/support/shared/time.rb +++ b/spec/support/shared/time.rb @@ -2,13 +2,7 @@ # rubocop:todo all shared_context 'setting ActiveSupport time zone' do - before do - Time.zone = "Tokyo" - end - - after do - Time.zone = nil - end + time_zone_override 'Tokyo' end shared_examples_for 'mongoizes to AS::TimeWithZone' do From 0aaf8d510a84f7a94f754af875dd0ec987f3cb7f Mon Sep 17 00:00:00 2001 From: Johnny Shields <27655+johnnyshields@users.noreply.github.com> Date: Thu, 9 Nov 2023 06:25:55 +0900 Subject: [PATCH 29/52] MONGOID-5677 [Monkey Patch Removal] Remove Hash#to_criteria and Criteria#to_criteria. Add Criteria.from_hash (#5717) * Remove Hash#to_criteria and Criteria#to_criteria. Add Criteria.from_hash * deprecate rathr than remove also, some code-style standardization --------- Co-authored-by: Jamis Buck --- lib/mongoid/criteria.rb | 37 +++++++-- lib/mongoid/extensions/hash.rb | 8 +- spec/mongoid/criteria_spec.rb | 137 ++++++++++++++++++++++----------- 3 files changed, 123 insertions(+), 59 deletions(-) diff --git a/lib/mongoid/criteria.rb b/lib/mongoid/criteria.rb index a7013e571..ca024cace 100644 --- a/lib/mongoid/criteria.rb +++ b/lib/mongoid/criteria.rb @@ -41,7 +41,25 @@ class Criteria include Clients::Sessions include Options - Mongoid.deprecate(self, :for_js) + class << self + # Convert the given hash to a criteria. Will iterate over each keys in the + # hash which must correspond to method on a criteria object. The hash + # must also include a "klass" key. + # + # @example Convert the hash to a criteria. + # Criteria.from_hash({ klass: Band, where: { name: "Depeche Mode" }) + # + # @param [ Hash ] hash The hash to convert. + # + # @return [ Criteria ] The criteria. + def from_hash(hash) + criteria = Criteria.new(hash.delete(:klass) || hash.delete('klass')) + hash.each_pair do |method, args| + criteria = criteria.__send__(method, args) + end + criteria + end + end # Static array used to check with method missing - we only need to ever # instantiate once. @@ -250,16 +268,16 @@ def merge(other) # @example Merge another criteria into this criteria. # criteria.merge(Person.where(name: "bob")) # - # @param [ Criteria ] other The criteria to merge in. + # @param [ Criteria | Hash ] other The criteria to merge in. # # @return [ Criteria ] The merged criteria. def merge!(other) - criteria = other.to_criteria - selector.merge!(criteria.selector) - options.merge!(criteria.options) - self.documents = criteria.documents.dup unless criteria.documents.empty? - self.scoping_options = criteria.scoping_options - self.inclusions = (inclusions + criteria.inclusions).uniq + other = self.class.from_hash(other) if other.is_a?(Hash) + selector.merge!(other.selector) + options.merge!(other.options) + self.documents = other.documents.dup unless other.documents.empty? + self.scoping_options = other.scoping_options + self.inclusions = (inclusions + other.inclusions).uniq self end @@ -352,9 +370,11 @@ def respond_to?(name, include_private = false) # criteria.to_criteria # # @return [ Criteria ] self. + # @deprecated def to_criteria self end + Mongoid.deprecate(self, :to_criteria) # Convert the criteria to a proc. # @@ -452,6 +472,7 @@ def for_js(javascript, scope = {}) end js_query(code) end + Mongoid.deprecate(self, :for_js) private diff --git a/lib/mongoid/extensions/hash.rb b/lib/mongoid/extensions/hash.rb index 67490aa5f..19aaef11a 100644 --- a/lib/mongoid/extensions/hash.rb +++ b/lib/mongoid/extensions/hash.rb @@ -101,13 +101,11 @@ def resizable? # { klass: Band, where: { name: "Depeche Mode" }.to_criteria # # @return [ Criteria ] The criteria. + # @deprecated def to_criteria - criteria = Criteria.new(delete(:klass) || delete("klass")) - each_pair do |method, args| - criteria = criteria.__send__(method, args) - end - criteria + Criteria.from_hash(self) end + Mongoid.deprecate(self, :to_criteria) private diff --git a/spec/mongoid/criteria_spec.rb b/spec/mongoid/criteria_spec.rb index 6996341e2..3808166cc 100644 --- a/spec/mongoid/criteria_spec.rb +++ b/spec/mongoid/criteria_spec.rb @@ -1461,52 +1461,68 @@ end end - describe "#merge!" do + describe '#merge!' do + let(:band) { Band.new } + let(:criteria) { Band.scoped.where(name: 'Depeche Mode').asc(:name) } + let(:association) { Band.relations['records'] } + subject(:merged) { criteria.merge!(other) } - let(:band) do - Band.new - end + context 'when merging a Criteria' do + let(:other) do + { klass: Band, includes: [:records] } + end - let(:criteria) do - Band.scoped.where(name: "Depeche Mode").asc(:name) - end + it 'merges the selector' do + expect(merged.selector).to eq({ 'name' => 'Depeche Mode' }) + end - let(:mergeable) do - Band.includes(:records).tap do |crit| - crit.documents = [ band ] + it 'merges the options' do + expect(merged.options).to eq({ sort: { 'name' => 1 }}) end - end - let(:association) do - Band.relations["records"] - end + it 'merges the scoping options' do + expect(merged.scoping_options).to eq([ nil, nil ]) + end - let(:merged) do - criteria.merge!(mergeable) - end + it 'merges the inclusions' do + expect(merged.inclusions).to eq([ association ]) + end - it "merges the selector" do - expect(merged.selector).to eq({ "name" => "Depeche Mode" }) + it 'returns the same criteria' do + expect(merged).to equal(criteria) + end end - it "merges the options" do - expect(merged.options).to eq({ sort: { "name" => 1 }}) - end + context 'when merging a Hash' do + let(:other) do + Band.includes(:records).tap do |crit| + crit.documents = [ band ] + end + end - it "merges the documents" do - expect(merged.documents).to eq([ band ]) - end + it 'merges the selector' do + expect(merged.selector).to eq({ 'name' => 'Depeche Mode' }) + end - it "merges the scoping options" do - expect(merged.scoping_options).to eq([ nil, nil ]) - end + it 'merges the options' do + expect(merged.options).to eq({ sort: { 'name' => 1 }}) + end - it "merges the inclusions" do - expect(merged.inclusions).to eq([ association ]) - end + it 'merges the documents' do + expect(merged.documents).to eq([ band ]) + end + + it 'merges the scoping options' do + expect(merged.scoping_options).to eq([ nil, nil ]) + end - it "returns the same criteria" do - expect(merged).to equal(criteria) + it 'merges the inclusions' do + expect(merged.inclusions).to eq([ association ]) + end + + it 'returns the same criteria' do + expect(merged).to equal(criteria) + end end end @@ -2308,17 +2324,6 @@ def self.ages; self; end end end - describe "#to_criteria" do - - let(:criteria) do - Band.all - end - - it "returns self" do - expect(criteria.to_criteria).to eq(criteria) - end - end - describe "#to_proc" do let(:criteria) do @@ -3031,11 +3036,11 @@ def self.ages; self; end context "when the method exists on the criteria" do before do - expect(criteria).to receive(:to_criteria).and_call_original + expect(criteria).to receive(:only).and_call_original end it "calls the method on the criteria" do - expect(criteria.to_criteria).to eq(criteria) + expect(criteria.only).to eq(criteria) end end @@ -3242,4 +3247,44 @@ def self.ages; self; end end end end + + describe '.from_hash' do + subject(:criteria) { described_class.from_hash(hash) } + + context 'when klass is specified' do + let(:hash) do + { klass: Band, where: { name: 'Songs Ohia' } } + end + + it 'returns a criteria' do + expect(criteria).to be_a(Mongoid::Criteria) + end + + it 'sets the klass' do + expect(criteria.klass).to eq(Band) + end + + it 'sets the selector' do + expect(criteria.selector).to eq({ 'name' => 'Songs Ohia' }) + end + end + + context 'when klass is missing' do + let(:hash) do + { where: { name: 'Songs Ohia' } } + end + + it 'returns a criteria' do + expect(criteria).to be_a(Mongoid::Criteria) + end + + it 'has klass nil' do + expect(criteria.klass).to be_nil + end + + it 'sets the selector' do + expect(criteria.selector).to eq({ 'name' => 'Songs Ohia' }) + end + end + end end From ca3c6a500d2932c2851e762a9cbf1fc80d25a912 Mon Sep 17 00:00:00 2001 From: Johnny Shields <27655+johnnyshields@users.noreply.github.com> Date: Thu, 9 Nov 2023 06:53:54 +0900 Subject: [PATCH 30/52] MONGOID-5675 [Monkey Patch Removal] Remove Object#__mongoize_fk__ (#5715) * __mongoize_fk step 1: move code to right class * __mongoize_fk step 1: #mongoize_foreign_key should use class member variables * __mongoize_fk step 3: consolidate all specs to public #mongoize method * Update foreign_key.rb * deprecate, rather than remove * Set support requires a bit of baby-sitting... --------- Co-authored-by: Jamis Buck --- lib/mongoid/extensions/array.rb | 2 + lib/mongoid/extensions/object.rb | 2 + lib/mongoid/fields/foreign_key.rb | 26 +- spec/mongoid/extensions/array_spec.rb | 150 --------- spec/mongoid/extensions/object_spec.rb | 91 ------ spec/mongoid/fields/foreign_key_spec.rb | 400 +++++++++++++++++------- 6 files changed, 309 insertions(+), 362 deletions(-) diff --git a/lib/mongoid/extensions/array.rb b/lib/mongoid/extensions/array.rb index 6fba2f474..0cf2a92d1 100644 --- a/lib/mongoid/extensions/array.rb +++ b/lib/mongoid/extensions/array.rb @@ -114,6 +114,7 @@ module ClassMethods # @param [ Object ] object The object to convert. # # @return [ Array ] The array of ids. + # @deprecated def __mongoize_fk__(association, object) if object.resizable? object.blank? ? object : association.convert_to_foreign_key(object) @@ -121,6 +122,7 @@ def __mongoize_fk__(association, object) object.blank? ? [] : association.convert_to_foreign_key(Array(object)) end end + Mongoid.deprecate(self, :__mongoize_fk__) # Turn the object from the ruby type we deal with to a Mongo friendly # type. diff --git a/lib/mongoid/extensions/object.rb b/lib/mongoid/extensions/object.rb index 495f7b2c4..879602f39 100644 --- a/lib/mongoid/extensions/object.rb +++ b/lib/mongoid/extensions/object.rb @@ -215,10 +215,12 @@ module ClassMethods # @param [ Object ] object The object to convert. # # @return [ Object ] The converted object. + # @deprecated def __mongoize_fk__(association, object) return nil if !object || object == "" association.convert_to_foreign_key(object) end + Mongoid.deprecate(self, :__mongoize_fk__) # Convert the object from its mongo friendly ruby type to this type. # diff --git a/lib/mongoid/fields/foreign_key.rb b/lib/mongoid/fields/foreign_key.rb index 19cb802ab..815d5cdc9 100644 --- a/lib/mongoid/fields/foreign_key.rb +++ b/lib/mongoid/fields/foreign_key.rb @@ -14,7 +14,7 @@ class ForeignKey < Standard # @example Add the atomic changes. # field.add_atomic_changes(doc, "key", {}, [], []) # - # @todo: Durran: Refactor, big time. + # @todo: Refactor, big time. # # @param [ Document ] document The document to add to. # @param [ String ] name The name of the field. @@ -95,7 +95,7 @@ def lazy? # @return [ Object ] The mongoized object. def mongoize(object) if type.resizable? || object_id_field? - type.__mongoize_fk__(association, object) + mongoize_foreign_key(object) else related_id_field.mongoize(object) end @@ -124,6 +124,28 @@ def resizable? private + # Convert the provided object to a Mongo-friendly foreign key. + # + # @example Convert the object to a foreign key. + # mongoize_foreign_key(object) + # + # @param [ Object ] object The object to convert. + # + # @return [ Object ] The converted object. + def mongoize_foreign_key(object) + if type == Array || type == Set + object = object.to_a if type == Set || object.is_a?(Set) + + if object.resizable? + object.blank? ? object : association.convert_to_foreign_key(object) + else + object.blank? ? [] : association.convert_to_foreign_key(Array(object)) + end + elsif !(object.nil? || object == '') + association.convert_to_foreign_key(object) + end + end + # Evaluate the default proc. In some cases we need to instance exec, # in others we don't. # diff --git a/spec/mongoid/extensions/array_spec.rb b/spec/mongoid/extensions/array_spec.rb index e9fa5b2fd..6c007320f 100644 --- a/spec/mongoid/extensions/array_spec.rb +++ b/spec/mongoid/extensions/array_spec.rb @@ -203,156 +203,6 @@ end end - describe ".__mongoize_fk__" do - - context "when the related model uses object ids" do - - let(:association) do - Person.relations["preferences"] - end - - context "when provided an object id" do - - let(:object_id) do - BSON::ObjectId.new - end - - let(:fk) do - Array.__mongoize_fk__(association, object_id) - end - - it "returns the object id as an array" do - expect(fk).to eq([ object_id ]) - end - end - - context "when provided a object ids" do - - let(:object_id) do - BSON::ObjectId.new - end - - let(:fk) do - Array.__mongoize_fk__(association, [ object_id ]) - end - - it "returns the object ids" do - expect(fk).to eq([ object_id ]) - end - end - - context "when provided a string" do - - context "when the string is a legal object id" do - - let(:object_id) do - BSON::ObjectId.new - end - - let(:fk) do - Array.__mongoize_fk__(association, object_id.to_s) - end - - it "returns the object id in an array" do - expect(fk).to eq([ object_id ]) - end - end - - context "when the string is not a legal object id" do - - let(:string) do - "blah" - end - - let(:fk) do - Array.__mongoize_fk__(association, string) - end - - it "returns the string in an array" do - expect(fk).to eq([ string ]) - end - end - - context "when the string is blank" do - - let(:fk) do - Array.__mongoize_fk__(association, "") - end - - it "returns an empty array" do - expect(fk).to be_empty - end - end - end - - context "when provided nil" do - - let(:fk) do - Array.__mongoize_fk__(association, nil) - end - - it "returns an empty array" do - expect(fk).to be_empty - end - end - - context "when provided an array of strings" do - - context "when the strings are legal object ids" do - - let(:object_id) do - BSON::ObjectId.new - end - - let(:fk) do - Array.__mongoize_fk__(association, [ object_id.to_s ]) - end - - it "returns the object id in an array" do - expect(fk).to eq([ object_id ]) - end - end - - context "when the strings are not legal object ids" do - - let(:string) do - "blah" - end - - let(:fk) do - Array.__mongoize_fk__(association, [ string ]) - end - - it "returns the string in an array" do - expect(fk).to eq([ string ]) - end - end - - context "when the strings are blank" do - - let(:fk) do - Array.__mongoize_fk__(association, [ "", "" ]) - end - - it "returns an empty array" do - expect(fk).to be_empty - end - end - end - - context "when provided nils" do - - let(:fk) do - Array.__mongoize_fk__(association, [ nil, nil, nil ]) - end - - it "returns an empty array" do - expect(fk).to be_empty - end - end - end - end - describe "#__mongoize_time__" do let(:array) do diff --git a/spec/mongoid/extensions/object_spec.rb b/spec/mongoid/extensions/object_spec.rb index 2a71c1931..9d5aae2d2 100644 --- a/spec/mongoid/extensions/object_spec.rb +++ b/spec/mongoid/extensions/object_spec.rb @@ -23,97 +23,6 @@ end end - describe ".__mongoize_fk__" do - - context "when the related model uses object ids" do - - let(:association) do - Game.relations["person"] - end - - context "when provided an object id" do - - let(:object_id) do - BSON::ObjectId.new - end - - let(:fk) do - Object.__mongoize_fk__(association, object_id) - end - - it "returns the object id" do - expect(fk).to eq(object_id) - end - end - - context "when provided a string" do - - context "when the string is a legal object id" do - - let(:object_id) do - BSON::ObjectId.new - end - - let(:fk) do - Object.__mongoize_fk__(association, object_id.to_s) - end - - it "returns the object id" do - expect(fk).to eq(object_id) - end - end - - context "when the string is not a legal object id" do - - let(:string) do - "blah" - end - - let(:fk) do - Object.__mongoize_fk__(association, string) - end - - it "returns the string" do - expect(fk).to eq(string) - end - end - - context "when the string is blank" do - - let(:fk) do - Object.__mongoize_fk__(association, "") - end - - it "returns nil" do - expect(fk).to be_nil - end - end - end - - context "when provided nil" do - - let(:fk) do - Object.__mongoize_fk__(association, nil) - end - - it "returns nil" do - expect(fk).to be_nil - end - end - - context "when provided an empty array" do - - let(:fk) do - Object.__mongoize_fk__(association, []) - end - - it "returns an empty array" do - expect(fk).to eq([]) - end - end - end - end - describe "#__mongoize_time__" do it "returns self" do diff --git a/spec/mongoid/fields/foreign_key_spec.rb b/spec/mongoid/fields/foreign_key_spec.rb index 3aafef65a..fedfef3ec 100644 --- a/spec/mongoid/fields/foreign_key_spec.rb +++ b/spec/mongoid/fields/foreign_key_spec.rb @@ -497,181 +497,343 @@ end end - describe "#mongoize" do + describe '#mongoize' do + let(:field) do + described_class.new( + :vals, + type: type, + default: [], + identity: true, + association: association, + overwrite: true + ) + end + let(:association) { Game.relations['person'] } + subject(:mongoized) { field.mongoize(object) } + + context 'type is Array' do + let(:type) { Array } + + context 'when the object is a BSON::ObjectId' do + let(:object) { BSON::ObjectId.new } + + it 'returns the object id as an array' do + expect(mongoized).to eq([object]) + end + end + + context 'when the object is an Array of BSON::ObjectId' do + let(:object) { [BSON::ObjectId.new] } + + it 'returns the object ids' do + expect(mongoized).to eq(object) + end + end + + context 'when the object is a String which is a legal object id' do + let(:object) { BSON::ObjectId.new.to_s } + + it 'returns the object id in an array' do + expect(mongoized).to eq([BSON::ObjectId.from_string(object)]) + end + end + + context 'when the object is a String which is not a legal object id' do + let(:object) { 'blah' } + + it 'returns the object id in an array' do + expect(mongoized).to eq(%w[blah]) + end + end + + context 'when the object is a blank String' do + let(:object) { '' } + + it 'returns an empty array' do + expect(mongoized).to eq([]) + end + end + + context 'when the object is nil' do + let(:object) { nil } + + it 'returns an empty array' do + expect(mongoized).to eq([]) + end + end + + context 'when the object is Array of Strings which are legal object ids' do + let(:object) { [BSON::ObjectId.new.to_s] } + + it 'returns the object id in an array' do + expect(mongoized).to eq([BSON::ObjectId.from_string(object.first)]) + end + end + + context 'when the object is Array of Strings which are not legal object ids' do + let(:object) { %w[blah] } + + it 'returns the Array' do + expect(mongoized).to eq(%w[blah]) + end + end + + context 'when the object is Array of Strings which are blank' do + let(:object) { ['', ''] } + + it 'returns an empty Array' do + expect(mongoized).to eq([]) + end + end + + context 'when the object is Array of nils' do + let(:object) { [nil, nil, nil] } + + it 'returns an empty Array' do + expect(mongoized).to eq([]) + end + end + + context 'when the object is an empty Array' do + let(:object) { [] } + + it 'returns an empty Array' do + expect(mongoized).to eq([]) + end - context "when the type is array" do + it 'returns the same instance' do + expect(mongoized).to equal(object) + end + end - context "when the array is object ids" do + context 'when the object is a Set' do + let(:object) { Set['blah'] } - let(:association) do - Game.relations["person"] + it 'returns the object id in an array' do + expect(mongoized).to eq(%w[blah]) end + end - let(:field) do - described_class.new( - :vals, - type: Array, - default: [], - identity: true, - association: association, + context 'when foreign key is a String' do + before do + Person.field(:_id, type: String, overwrite: true) + end + + after do + Person.field( + :_id, + type: BSON::ObjectId, + pre_processed: true, + default: ->{ BSON::ObjectId.new }, overwrite: true ) end - context "when provided nil" do + context 'when the object is a String' do + let(:object) { %w[1] } - it "returns an empty array" do - expect(field.mongoize(nil)).to be_empty + it 'returns String' do + expect(mongoized).to eq(object) end end - context "when provided an empty array" do + context 'when the object is a BSON::ObjectId' do + let(:object) { [BSON::ObjectId.new] } - let(:array) do - [] + it 'converts to String' do + expect(mongoized).to eq([object.first.to_s]) end + end - it "returns an empty array" do - expect(field.mongoize(array)).to eq(array) - end + context 'when the object is an Integer' do + let(:object) { [1] } - it "returns the same instance" do - expect(field.mongoize(array)).to equal(array) + it 'converts to String' do + expect(mongoized).to eq(%w[1]) end end + end - context "when using object ids" do + context 'when foreign key is an Integer' do + before do + Person.field(:_id, type: Integer, overwrite: true) + end - let(:object_id) do - BSON::ObjectId.new - end + after do + Person.field( + :_id, + type: BSON::ObjectId, + pre_processed: true, + default: ->{ BSON::ObjectId.new }, + overwrite: true + ) + end - it "performs conversion on the ids if strings" do - expect(field.mongoize([object_id.to_s])).to eq([object_id]) + context 'when the object is a String' do + let(:object) { %w[1] } + + it 'converts to Integer' do + expect(mongoized).to eq([1]) end end - context "when not using object ids" do + context 'when the object is an Integer' do + let(:object) { [1] } - let(:object_id) do - BSON::ObjectId.new + it 'returns Integer' do + expect(mongoized).to eq([1]) end + end + end + end - before do - Person.field( - :_id, - type: String, - pre_processed: true, - default: ->{ BSON::ObjectId.new.to_s }, - overwrite: true - ) - end + context 'type is Set' do + let(:type) { Set } - after do - Person.field( - :_id, - type: BSON::ObjectId, - pre_processed: true, - default: ->{ BSON::ObjectId.new }, - overwrite: true - ) - end + context 'when the object is an Array of BSON::ObjectId' do + let(:object) { [BSON::ObjectId.new] } - it "does not convert" do - expect(field.mongoize([object_id.to_s])).to eq([object_id.to_s]) - end + it 'returns the object ids' do + expect(mongoized).to eq(object) + end + end + + context 'when the object is a Set of BSON::ObjectId' do + let(:object) { Set[BSON::ObjectId.new] } + + it 'returns the object id in an array' do + expect(mongoized).to eq([object.first]) end end end - context "when the type is object" do + context 'type is Object' do + let(:type) { Object } - context "when the array is object ids" do + context 'when the object is a BSON::ObjectId' do + let(:object) { BSON::ObjectId.new } - let(:association) do - Game.relations['person'] + it 'returns the object id' do + expect(mongoized).to eq(object) end + end - let(:field) do - described_class.new( - :vals, - type: Object, - default: nil, - identity: true, - association: association, - overwrite: true - ) + context 'when the object is a String which is a legal object id' do + let(:object) { BSON::ObjectId.new.to_s } + + it 'returns the object id' do + expect(mongoized).to eq(BSON::ObjectId.from_string(object)) end + end - context "when using object ids" do + context 'when the object is a String which is not a legal object id' do + let(:object) { 'blah' } - let(:object_id) do - BSON::ObjectId.new - end + it 'returns the string' do + expect(mongoized).to eq('blah') + end + end - it "performs conversion on the ids if strings" do - expect(field.mongoize(object_id.to_s)).to eq(object_id) - end + context 'when the String is blank' do + let(:object) { '' } + + it 'returns nil' do + expect(mongoized).to be_nil + end + end + + context 'when the object is nil' do + let(:object) { '' } + + it 'returns nil' do + expect(mongoized).to be_nil end + end - context "when not using object ids" do + context 'when object is an empty Array' do + let(:object) { [] } - context "when using strings" do + it 'returns an empty array' do + expect(mongoized).to eq([]) + end + end - context "when provided a string" do + context 'when the object is a Set' do + let(:object) { Set['blah'] } - let(:object_id) do - BSON::ObjectId.new - end + it 'returns the set' do + expect(mongoized).to eq(Set['blah']) + end + end - before do - Person.field( - :_id, - type: String, - pre_processed: true, - default: ->{ BSON::ObjectId.new.to_s }, - overwrite: true - ) - end + context 'when foreign key is a String' do + before do + Person.field(:_id, type: String, overwrite: true) + end - after do - Person.field( - :_id, - type: BSON::ObjectId, - pre_processed: true, - default: ->{ BSON::ObjectId.new }, - overwrite: true - ) - end + after do + Person.field( + :_id, + type: BSON::ObjectId, + pre_processed: true, + default: ->{ BSON::ObjectId.new }, + overwrite: true + ) + end - it "does not convert" do - expect(field.mongoize(object_id.to_s)).to eq(object_id.to_s) - end - end + context 'when the object is a String' do + let(:object) { '1' } + + it 'returns String' do + expect(mongoized).to eq(object) + end + end + + context 'when the object is a BSON::ObjectId' do + let(:object) { BSON::ObjectId.new } + + it 'converts to String' do + expect(mongoized).to eq(object.to_s) end + end - context "when using integers" do + context 'when the object is an Integer' do + let(:object) { 1 } - context "when provided a string" do + it 'converts to String' do + expect(mongoized).to eq('1') + end + end + end - before do - Person.field(:_id, type: Integer, overwrite: true) - end + context 'when foreign key is an Integer' do + before do + Person.field(:_id, type: Integer, overwrite: true) + end - after do - Person.field( - :_id, - type: BSON::ObjectId, - pre_processed: true, - default: ->{ BSON::ObjectId.new }, - overwrite: true - ) - end + after do + Person.field( + :_id, + type: BSON::ObjectId, + pre_processed: true, + default: ->{ BSON::ObjectId.new }, + overwrite: true + ) + end - it "converts the string to an integer" do - expect(field.mongoize("1")).to eq(1) - end - end + context 'when the object is a String' do + let(:object) { '1' } + + it 'converts to Integer' do + expect(mongoized).to eq(1) + end + end + + context 'when the object is an Integer' do + let(:object) { 1 } + + it 'returns Integer' do + expect(mongoized).to eq(object) end end end From 0c85796c3e120874be93052062290d66eebe5107 Mon Sep 17 00:00:00 2001 From: Johnny Shields <27655+johnnyshields@users.noreply.github.com> Date: Tue, 14 Nov 2023 00:23:56 +0900 Subject: [PATCH 31/52] MONGOID-5026 - Remove code comments and close ticket (#5605) * Close MONGOID-5026 * Update updatable.rb --- lib/mongoid/persistable/updatable.rb | 7 +++---- spec/mongoid/persistable/savable_spec.rb | 5 ----- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/lib/mongoid/persistable/updatable.rb b/lib/mongoid/persistable/updatable.rb index 9dc5aeeaf..f31ae88cd 100644 --- a/lib/mongoid/persistable/updatable.rb +++ b/lib/mongoid/persistable/updatable.rb @@ -129,16 +129,15 @@ def update_document(options = {}) unless updates.empty? coll = collection(_root) selector = atomic_selector + + # TODO: DRIVERS-716: If a new "Bulk Write" API is introduced, it may + # become possible to handle the writes for conflicts in the following call. coll.find(selector).update_one(positionally(selector, updates), session: _session) # The following code applies updates which would cause # path conflicts in MongoDB, for example when changing attributes # of foo.0.bars while adding another foo. Each conflicting update # is applied using its own write. - # - # TODO: MONGOID-5026: reduce the number of writes performed by - # more intelligently combining the writes such that there are - # fewer conflicts. conflicts.each_pair do |modifier, changes| # Group the changes according to their root key which is diff --git a/spec/mongoid/persistable/savable_spec.rb b/spec/mongoid/persistable/savable_spec.rb index d7931f73f..5a5489dad 100644 --- a/spec/mongoid/persistable/savable_spec.rb +++ b/spec/mongoid/persistable/savable_spec.rb @@ -295,11 +295,6 @@ expect(truck.crates[0].toys[0].name).to eq "Teddy bear" expect(truck.crates[1].volume).to eq 0.8 expect(truck.crates[1].toys.size).to eq 0 - - # TODO: MONGOID-5026: combine the updates so that there are - # no conflicts. - #expect(truck.atomic_updates[:conflicts]).to eq nil - expect { truck.save! }.not_to raise_error _truck = Truck.find(truck.id) From 6491384c51640374863e31bc9d6476c3f5d46d9d Mon Sep 17 00:00:00 2001 From: Johnny Shields <27655+johnnyshields@users.noreply.github.com> Date: Tue, 14 Nov 2023 01:40:16 +0900 Subject: [PATCH 32/52] RUBY-2807 - Add "compressors" option to Mongoid.yml (#5698) * Add "compressors" option to Mongoid.yml Also removes connection string note from docs * Update docs * Move compressors above SSL in the docs --- docs/reference/configuration.txt | 10 ++++------ .../generators/mongoid/config/templates/mongoid.yml | 5 +++++ 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/docs/reference/configuration.txt b/docs/reference/configuration.txt index ffb4e8e36..fe89b0a57 100644 --- a/docs/reference/configuration.txt +++ b/docs/reference/configuration.txt @@ -239,6 +239,9 @@ for details on driver options. # not belong to this replica set will be ignored. replica_set: my_replica_set + # Compressors to use for wire protocol compression. (default is to not use compression) + compressors: ["zstd", "snappy", "zlib"] + # Whether to connect to the servers via ssl. (default: false) ssl: true @@ -260,9 +263,6 @@ for details on driver options. # used to validate certs passed from the other end of the connection. ssl_ca_cert: /path/to/ca.cert - # Compressors to use. (default is to not use compression) - compressors: [zlib] - # Configure Mongoid-specific options. (optional) options: # Application name that is printed to the MongoDB logs upon establishing @@ -755,9 +755,7 @@ the Ruby driver, which implements the three algorithms that are supported by Mon requires the `zstd-ruby `_ library to be installed. -To use wire protocol compression, at least one compressor must be explicitly requested -using either the `compressors URI option `_, -or directly within the ``mongoid.yml``: +To use wire protocol compression, configure the Ruby driver options within ``mongoid.yml``: .. code-block:: yaml diff --git a/lib/rails/generators/mongoid/config/templates/mongoid.yml b/lib/rails/generators/mongoid/config/templates/mongoid.yml index 055d6cb25..bf4d5451a 100644 --- a/lib/rails/generators/mongoid/config/templates/mongoid.yml +++ b/lib/rails/generators/mongoid/config/templates/mongoid.yml @@ -93,6 +93,11 @@ development: # not belong to this replica set will be ignored. # replica_set: name + # Compressors to use for wire protocol compression. (default is to not use compression) + # "zstd" requires zstd-ruby gem. "snappy" requires snappy gem. + # Refer to: https://www.mongodb.com/docs/ruby-driver/current/reference/create-client/#compression + # compressors: ["zstd", "snappy", "zlib"] + # Whether to connect to the servers via ssl. (default: false) # ssl: true From 467cf48027cf325e928e374b24191efb678ad424 Mon Sep 17 00:00:00 2001 From: Johnny Shields <27655+johnnyshields@users.noreply.github.com> Date: Tue, 14 Nov 2023 06:22:31 +0900 Subject: [PATCH 33/52] MONGOID-5661 [Monkey Patch Removal] Add test which checks for monkey patches (#5711) * Add script to check monkey patches * Update monkey_patches_spec.rb * Update monkey_patches_spec.rb --------- Co-authored-by: Jamis Buck --- .../queryable/extensions/symbol_spec.rb | 4 + spec/mongoid/monkey_patches_spec.rb | 204 ++++++++++++++++++ 2 files changed, 208 insertions(+) create mode 100644 spec/mongoid/monkey_patches_spec.rb diff --git a/spec/mongoid/criteria/queryable/extensions/symbol_spec.rb b/spec/mongoid/criteria/queryable/extensions/symbol_spec.rb index 64de38c38..247d12e99 100644 --- a/spec/mongoid/criteria/queryable/extensions/symbol_spec.rb +++ b/spec/mongoid/criteria/queryable/extensions/symbol_spec.rb @@ -13,6 +13,10 @@ end end + after do + Symbol.undef_method(:fubar) + end + let(:fubar) do :testing.fubar end diff --git a/spec/mongoid/monkey_patches_spec.rb b/spec/mongoid/monkey_patches_spec.rb new file mode 100644 index 000000000..8fdded9af --- /dev/null +++ b/spec/mongoid/monkey_patches_spec.rb @@ -0,0 +1,204 @@ +# frozen_string_literal: true + +require 'spec_helper' + +# @note This test ensures that we do not inadvertently introduce new monkey patches +# to Mongoid. Existing monkey patch methods which are marked with +Mongoid.deprecated+ +# are excluded from this test. +RSpec.describe('Do not add monkey patches') do # rubocop:disable RSpec/DescribeClass + classes = [ + Object, + Array, + BigDecimal, + Date, + DateTime, + FalseClass, + Float, + Hash, + Integer, + Module, + NilClass, + Range, + Regexp, + Set, + String, + Symbol, + Time, + TrueClass, + ActiveSupport::TimeWithZone, + BSON::Binary, + BSON::Decimal128, + BSON::ObjectId, + BSON::Regexp, + BSON::Regexp::Raw + ] + + expected_instance_methods = { + Object => %i[ + __add__ + __add_from_array__ + __array__ + __deep_copy__ + __evolve_object_id__ + __expand_complex__ + __intersect__ + __intersect_from_array__ + __intersect_from_object__ + __mongoize_object_id__ + __mongoize_time__ + __union__ + __union_from_object__ + ivar + mongoize + numeric? + remove_ivar + resizable? + substitutable + ], + Array => %i[ + __evolve_date__ + __evolve_time__ + __sort_option__ + __sort_pair__ + delete_one + ], + Date => %i[ + __evolve_date__ + __evolve_time__ + ], + DateTime => %i[ + __evolve_date__ + __evolve_time__ + ], + FalseClass => %i[is_a?], + Float => %i[ + __evolve_date__ + __evolve_time__ + ], + Hash => %i[ + __sort_option__ + ], + Integer => %i[ + __evolve_date__ + __evolve_time__ + ], + Module => %i[ + re_define_method + ], + NilClass => %i[ + __evolve_date__ + __evolve_time__ + __expanded__ + __override__ + collectionize + ], + Range => %i[ + __evolve_date__ + __evolve_range__ + __evolve_time__ + ], + String => %i[ + __evolve_date__ + __evolve_time__ + __expr_part__ + __mongo_expression__ + __sort_option__ + before_type_cast? + collectionize + reader + valid_method_name? + writer? + ], + Symbol => %i[ + __expr_part__ + add_to_set + all + asc + ascending + avg + desc + descending + elem_match + eq + exists + first + gt + gte + in + intersects_line + intersects_point + intersects_polygon + last + lt + lte + max + min + mod + ne + near + near_sphere + nin + not + push + sum + with_size + with_type + within_box + within_polygon + ], + TrueClass => %i[is_a?], + Time => %i[ + __evolve_date__ + __evolve_time__ + ], + ActiveSupport::TimeWithZone => %i[ + __evolve_date__ + __evolve_time__ + _bson_to_i + ], + BSON::Decimal128 => %i[ + __evolve_decimal128__ + ] + }.each_value(&:sort!) + + expected_class_methods = { + Object => %i[ + demongoize + evolve + re_define_method + ], + Float => %i[__numeric__], + Integer => %i[__numeric__], + String => %i[__expr_part__], + Symbol => %i[add_key] + }.each_value(&:sort!) + + def mongoid_method?(method) + method.source_location&.first&.include?('/lib/mongoid/') + end + + def added_instance_methods(klass) + methods = klass.instance_methods.select { |m| mongoid_method?(klass.instance_method(m)) } + methods -= added_instance_methods(Object) unless klass == Object + methods.sort + end + + def added_class_methods(klass) + methods = klass.methods.select { |m| mongoid_method?(klass.method(m)) } + methods -= added_instance_methods(Object) + methods -= added_class_methods(Object) unless klass == Object + methods.sort + end + + classes.each do |klass| + context klass.name do + it 'adds no unexpected instance methods' do + expect(added_instance_methods(klass)).to eq(expected_instance_methods[klass] || []) + end + + it 'adds no unexpected class methods' do + expect(added_class_methods(klass)).to eq(expected_class_methods[klass] || []) + end + end + end +end From 53496de76600816476b367bd6a6172b28cd90979 Mon Sep 17 00:00:00 2001 From: Jamis Buck Date: Thu, 16 Nov 2023 08:12:44 -0700 Subject: [PATCH 34/52] MONGOID-5698 no need to specify patch version in the docs (#5749) * MONGOID-5698 no need to specify patch version in the docs * getting started doesn't need to reference a mongoid version honestly, neither does installation.txt --- docs/installation.txt | 2 +- docs/tutorials/getting-started-rails6.txt | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/installation.txt b/docs/installation.txt index 0ce9cbb72..7247273d6 100644 --- a/docs/installation.txt +++ b/docs/installation.txt @@ -27,7 +27,7 @@ To install the gem with bundler, include the following in your ``Gemfile``: .. code-block:: ruby - gem 'mongoid', '~> 9.0.0' + gem 'mongoid' Using Mongoid with a New Rails Application ========================================== diff --git a/docs/tutorials/getting-started-rails6.txt b/docs/tutorials/getting-started-rails6.txt index c071407ba..2a7da4102 100644 --- a/docs/tutorials/getting-started-rails6.txt +++ b/docs/tutorials/getting-started-rails6.txt @@ -44,7 +44,7 @@ In order to do so, the first step is to install the ``rails`` gem: .. code-block:: sh - gem install rails -v '~> 6.0.0' + gem install rails -v '~> 6.0' Create New Application @@ -107,7 +107,7 @@ Add Mongoid .. code-block:: ruby :caption: Gemfile - gem 'mongoid', '~> 7.0.5' + gem 'mongoid' .. note:: @@ -125,7 +125,7 @@ Add Mongoid bin/rails g mongoid:config -This generator will create the ``config/mongoid.yml`` configuration file +This generator will create the ``config/mongoid.yml`` configuration file (used to configure the connection to the MongoDB deployment) and the ``config/initializers/mongoid.rb`` initializer file (which may be used for other Mongoid-related configuration). Note that as we are not using @@ -378,7 +378,7 @@ mentioned in ``Gemfile``, and add ``mongoid``: .. code-block:: ruby :caption: Gemfile - gem 'mongoid', '~> 7.0.5' + gem 'mongoid' .. note:: @@ -466,7 +466,7 @@ Generate the default Mongoid configuration: bin/rails g mongoid:config -This generator will create the ``config/mongoid.yml`` configuration file +This generator will create the ``config/mongoid.yml`` configuration file (used to configure the connection to the MongoDB deployment) and the ``config/initializers/mongoid.rb`` initializer file (which may be used for other Mongoid-related configuration). In general, it is recommended to use From 19cbfa2da3ca79247221d3d9eb1289aec48ac410 Mon Sep 17 00:00:00 2001 From: Jamis Buck Date: Fri, 17 Nov 2023 09:27:54 -0700 Subject: [PATCH 35/52] MONGOID-5691 Ensure that all Mongoid config options are mentioned in the documentation (#5748) * MONGOID-5691 refresh configuration docs from latest mongoid.yml template * no need to explicitly call out earlier versions * clearer delineation between general/specific comments --- Rakefile | 14 + docs/reference/configuration.txt | 305 +++++++++--------- .../mongoid/config/templates/mongoid.yml | 23 +- 3 files changed, 192 insertions(+), 150 deletions(-) diff --git a/Rakefile b/Rakefile index 0a02a9675..eb8db1a69 100644 --- a/Rakefile +++ b/Rakefile @@ -67,6 +67,20 @@ namespace :eg do end end +namespace :generate do + desc 'Generates a mongoid.yml from the template' + task :config do + require 'mongoid' + require 'erb' + + template_path = 'lib/rails/generators/mongoid/config/templates/mongoid.yml' + database_name = ENV['DATABASE_NAME'] || 'my_db' + + config = ERB.new(File.read(template_path), trim_mode: '-').result(binding) + File.write('mongoid.yml', config) + end +end + CLASSIFIERS = [ [%r,^mongoid/attribute,, :attributes], [%r,^mongoid/association/[or],, :associations_referenced], diff --git a/docs/reference/configuration.txt b/docs/reference/configuration.txt index fe89b0a57..9a2667474 100644 --- a/docs/reference/configuration.txt +++ b/docs/reference/configuration.txt @@ -131,100 +131,84 @@ for details on driver options. development: # Configure available database clients. (required) clients: - # Define the default client. (required) + # Defines the default client. (required) default: - # A uri may be defined for a client: - # uri: 'mongodb://user:password@myhost1.mydomain.com:27017/my_db' - # Please see driver documentation for details. Alternatively, you can - # define the following: - # - # Define the name of the default database that Mongoid can connect to. + # Mongoid can connect to a URI accepted by the driver: + # uri: mongodb://user:password@mongodb.domain.com:27017/my_db_development + + # Otherwise define the parameters separately. + # This defines the name of the default database that Mongoid can connect to. # (required). - database: my_db - # Provide the hosts the default client can connect to. Must be an array + database: my_db_development + # Provides the hosts the default client can connect to. Must be an array # of host:port pairs. (required) hosts: - - myhost1.mydomain.com:27017 - - myhost2.mydomain.com:27017 - - myhost3.mydomain.com:27017 + - localhost:27017 options: - # These options are Ruby driver options, documented in - # https://mongodb.com/docs/ruby-driver/current/reference/create-client/ + # Note that all options listed below are Ruby driver client options (the mongo gem). + # Please refer to the driver documentation of the version of the mongo gem you are using + # for the most up-to-date list of options. # Change the default write concern. (default = { w: 1 }) - write: - w: 1 + # write: + # w: 1 # Change the default read preference. Valid options for mode are: :secondary, # :secondary_preferred, :primary, :primary_preferred, :nearest # (default: primary) - read: - mode: :secondary_preferred - tag_sets: - - use: web + # read: + # mode: :secondary_preferred + # tag_sets: + # - use: web # The name of the user for authentication. - user: 'user' + # user: 'user' # The password of the user for authentication. - password: 'password' + # password: 'password' # The user's database roles. - roles: - - 'dbOwner' - - # Change the default authentication mechanism. Please see the - # driver documentation linked above for details on how to configure - # authentication. Valid options are :aws, :gssapi, :mongodb_cr, - # :mongodb_x509, :plain, :scram and :scram256 (default on 3.0 - # and higher servers is :scram, default on 2.6 servers is :plain) - auth_mech: :scram - - # Specify the auth source, i.e. the database or other source which - # contains the user's login credentials. Allowed values for auth source - # depend on the authentication mechanism, as explained in the server documentation: - # https://mongodb.com/docs/manual/reference/connection-string/#mongodb-urioption-urioption.authSource - # If no auth source is specified, the default auth source as - # determined by the driver will be used. Please refer to: - # https://mongodb.com/docs/ruby-driver/current/reference/authentication/#auth-source - auth_source: admin - - # Connect directly to and perform all operations on the specified - # server, bypassing replica set node discovery and monitoring. - # Exactly one host address must be specified. (default: false) - #direct_connection: true - - # Deprecated. Force the driver to connect in a specific way instead - # of automatically discovering the deployment type and connecting - # accordingly. To connect directly to a replica set node bypassing - # node discovery and monitoring, use direct_connection: true instead - # of this option. Possible values: :direct, :replica_set, :sharded. - # (default: none) - #connect: :direct - - # Change the default time in seconds the server monitors refresh their status + # roles: + # - 'dbOwner' + + # Change the default authentication mechanism. Valid options are: :scram, + # :mongodb_cr, :mongodb_x509, and :plain. Note that all authentication + # mechanisms require username and password, with the exception of :mongodb_x509. + # Default is :scram since MongoDB 3.0; earlier versions defaulted to :plain. + # auth_mech: :scram + + # The database or source to authenticate the user against. + # (default: the database specified above or admin) + # auth_source: admin + + # Force a the driver cluster to behave in a certain manner instead of auto- + # discovering. Can be one of: :direct, :replica_set, :sharded. Set to :direct + # when connecting to hidden members of a replica set. + # connect: :direct + + # Changes the default time in seconds the server monitors refresh their status # via hello commands. (default: 10) - heartbeat_frequency: 10 + # heartbeat_frequency: 10 # The time in seconds for selecting servers for a near read preference. (default: 0.015) - local_threshold: 0.015 + # local_threshold: 0.015 # The timeout in seconds for selecting a server for an operation. (default: 30) - server_selection_timeout: 30 + # server_selection_timeout: 30 # The maximum number of connections in the connection pool. (default: 5) - max_pool_size: 5 + # max_pool_size: 5 # The minimum number of connections in the connection pool. (default: 1) - min_pool_size: 1 + # min_pool_size: 1 # The time to wait, in seconds, in the connection pool for a connection - # to be checked in before timing out. (default: 1) - wait_queue_timeout: 1 + # to be checked in before timing out. (default: 5) + # wait_queue_timeout: 5 # The time to wait to establish a connection before timing out, in seconds. # (default: 10) - connect_timeout: 10 + # connect_timeout: 10 # How long to wait for a response for each operation sent to the # server. This timeout should be set to a value larger than the @@ -233,140 +217,169 @@ for details on driver options. # the server may continue executing an operation after the client # aborts it with the SocketTimeout exception. # (default: nil, meaning no timeout) - socket_timeout: 5 + # socket_timeout: 5 # The name of the replica set to connect to. Servers provided as seeds that do # not belong to this replica set will be ignored. - replica_set: my_replica_set + # replica_set: name # Compressors to use for wire protocol compression. (default is to not use compression) - compressors: ["zstd", "snappy", "zlib"] + # "zstd" requires zstd-ruby gem. "snappy" requires snappy gem. + # Refer to: https://www.mongodb.com/docs/ruby-driver/current/reference/create-client/#compression + # compressors: ["zstd", "snappy", "zlib"] # Whether to connect to the servers via ssl. (default: false) - ssl: true + # ssl: true # The certificate file used to identify the connection against MongoDB. - ssl_cert: /path/to/my.cert + # ssl_cert: /path/to/my.cert # The private keyfile used to identify the connection against MongoDB. # Note that even if the key is stored in the same file as the certificate, # both need to be explicitly specified. - ssl_key: /path/to/my.key + # ssl_key: /path/to/my.key # A passphrase for the private key. - ssl_key_pass_phrase: password + # ssl_key_pass_phrase: password - # Whether or not to do peer certification validation. (default: true) - ssl_verify: true + # Whether to do peer certification validation. (default: true) + # ssl_verify: true - # The file containing a set of concatenated certification authority certifications + # The file containing concatenated certificate authority certificates # used to validate certs passed from the other end of the connection. - ssl_ca_cert: /path/to/ca.cert + # ssl_ca_cert: /path/to/ca.cert + + # Whether to truncate long log lines. (default: true) + # truncate_logs: true # Configure Mongoid-specific options. (optional) options: - # Application name that is printed to the MongoDB logs upon establishing - # a connection in server versions 3.4 or greater. Note that the name - # cannot exceed 128 bytes in length. It is also used as the database name - # if the database name is not explicitly defined. (default: nil) - app_name: MyApplicationName - - # Type of executor for queries scheduled using ``load_async`` method. + # Allow BSON::Decimal128 to be parsed and returned directly in + # field values. When BSON 5 is present and the this option is set to false + # (the default), BSON::Decimal128 values in the database will be returned + # as BigDecimal. + # + # @note this option only has effect when BSON 5+ is present. Otherwise, + # the setting is ignored. + # allow_bson5_decimal128: false + + # Application name that is printed to the mongodb logs upon establishing + # a connection in server versions >= 3.4. Note that the name cannot + # exceed 128 bytes. It is also used as the database name if the + # database name is not explicitly defined. + # app_name: nil + + # When this flag is false, callbacks for embedded documents will not be + # called. This is the default in 9.0. # - # There are two possible values for this option: + # Setting this flag to true restores the pre-9.0 behavior, where callbacks + # for embedded documents are called. This may lead to stack overflow errors + # if there are more than cicrca 1000 embedded documents in the root + # document's dependencies graph. + # See https://jira.mongodb.org/browse/MONGOID-5658 for more details. + # around_callbacks_for_embeds: false + + # Sets the async_query_executor for the application. By default the thread pool executor + # is set to `:immediate. Options are: # - # - :immediate - Queries will be immediately executed on a current thread. - # This is the default option. - # - :global_thread_pool - Queries will be executed asynchronously in - # background using a thread pool. - #async_query_executor: :immediate + # - :immediate - Initializes a single +Concurrent::ImmediateExecutor+ + # - :global_thread_pool - Initializes a single +Concurrent::ThreadPoolExecutor+ + # that uses the +async_query_concurrency+ for the +max_threads+ value. + # async_query_executor: :immediate # Mark belongs_to associations as required by default, so that saving a # model with a missing belongs_to association will trigger a validation - # error. (default: true) - belongs_to_required_by_default: true + # error. + # belongs_to_required_by_default: true - # Set the global discriminator key. (default: "_type") - discriminator_key: "_type" + # Set the global discriminator key. + # discriminator_key: "_type" - # Raise an exception when a field is redefined. (default: false) - duplicate_fields_exception: false + # Raise an exception when a field is redefined. + # duplicate_fields_exception: false # Defines how many asynchronous queries can be executed concurrently. - # This option should be set only if `async_query_executor` option is set + # This option should be set only if `async_query_executor` is set # to `:global_thread_pool`. - #global_executor_concurrency: nil + # global_executor_concurrency: nil - # Include the root model name in json serialization. (default: false) - include_root_in_json: false + # When this flag is true, any attempt to change the _id of a persisted + # document will raise an exception (`Errors::ImmutableAttribute`). + # This is the default in 9.0. Setting this flag to false restores the + # pre-9.0 behavior, where changing the _id of a persisted + # document might be ignored, or it might work, depending on the situation. + # immutable_ids: true - # Include the _type field in serialization. (default: false) - include_type_for_serialization: false + # Include the root model name in json serialization. + # include_root_in_json: false + + # # Include the _type field in serialization. + # include_type_for_serialization: false # Whether to join nested persistence contexts for atomic operations - # to parent contexts by default. (default: false) - join_contexts: false + # to parent contexts by default. + # join_contexts: false # When this flag is false, a document will become read-only only once the # #readonly! method is called, and an error will be raised on attempting # to save or update such documents, instead of just on delete. When this # flag is true, a document is only read-only if it has been projected # using #only or #without, and read-only documents will not be - # deletable/destroyable, but will be savable/updatable. + # deletable/destroyable, but they will be savable/updatable. # When this feature flag is turned on, the read-only state will be reset on # reload, but when it is turned off, it won't be. - # (default: false) - #legacy_readonly: true - - # Set the Mongoid and Ruby driver log levels when Mongoid is not using - # Ruby on Rails logger instance. (default: :info) - log_level: :info + # legacy_readonly: false - # When using the BigDecimal field type, store the value in the database - # as a BSON::Decimal128 instead of a string. (default: true) - #map_big_decimal_to_decimal128: true - - # Preload all models in development, needed when models use - # inheritance. (default: false) - preload_models: false + # The log level. + # + # It must be set prior to referencing clients or Mongo.logger, + # changes to this option are not be propagated to any clients and + # loggers that already exist. + # + # Additionally, only when the clients are configured via the + # configuration file is the log level given by this option honored. + # log_level: :info + + # Store BigDecimals as Decimal128s instead of strings in the db. + # map_big_decimal_to_decimal128: true + + # Preload all models in development, needed when models use inheritance. + # preload_models: false + + # When this flag is true, callbacks for every embedded document will be + # called only once, even if the embedded document is embedded in multiple + # documents in the root document's dependencies graph. + # This is the default in 9.0. Setting this flag to false restores the + # pre-9.0 behavior, where callbacks are called for every occurrence of an + # embedded document. The pre-9.0 behavior leads to a problem that for multi + # level nested documents callbacks are called multiple times. + # See https://jira.mongodb.org/browse/MONGOID-5542 + # prevent_multiple_calls_of_embedded_callbacks: true # Raise an error when performing a #find and the document is not found. - # (default: true) - raise_not_found_error: true + # raise_not_found_error: true # Raise an error when defining a scope with the same name as an - # existing method. (default: false) - scope_overwrite_exception: false - - # Return stored times as UTC. See the time zone section below for - # further information. Most applications should not use this option. - # (default: false) - use_utc: false + # existing method. + # scope_overwrite_exception: false - # (Deprecated) In MongoDB 4.0 and earlier, set whether to create - # indexes in the background by default. (default: false) - background_indexing: false + # Return stored times as UTC. + # use_utc: false - # Configure driver-specific options. (optional) + # Configure Driver-specific options. (optional) driver_options: - # When this flag is turned off, inline options will be correctly - # propagated to Mongoid and Driver finder methods. When this flag is turned - # on those options will be ignored. For example, with this flag turned - # off, Band.all.limit(1).count will take the limit into account, while - # when this flag is turned on, that limit is ignored. The affected driver - # methods are: aggregate, count, count_documents, distinct, and - # estimated_document_count. The corresponding Mongoid methods are also - # affected. (default: false, driver version: 2.18.0+) - #broken_view_options: false - - # Validates that there are no atomic operators (those that start with $) - # in the root of a replacement document, and that there are only atomic - # operators at the root of an update document. If this feature flag is on, - # an error will be raised on an invalid update or replacement document, - # if not, a warning will be output to the logs. This flag does not affect - # Mongoid as of 8.0, but will affect calls to driver update/replace - # methods. (default: false, driver version: 2.18.0+) - #validate_update_replace: false + # When this flag is off, an aggregation done on a view will be executed over + # the documents included in that view, instead of all documents in the + # collection. When this flag is on, the view fiter is ignored. + # broken_view_aggregate: true + + # When this flag is set to false, the view options will be correctly + # propagated to readable methods. + # broken_view_options: true + + # When this flag is set to true, the update and replace methods will + # validate the paramters and raise an error if they are invalid. + # validate_update_replace: false .. _load-defaults: diff --git a/lib/rails/generators/mongoid/config/templates/mongoid.yml b/lib/rails/generators/mongoid/config/templates/mongoid.yml index bf4d5451a..9c179307d 100644 --- a/lib/rails/generators/mongoid/config/templates/mongoid.yml +++ b/lib/rails/generators/mongoid/config/templates/mongoid.yml @@ -18,7 +18,7 @@ development: # Note that all options listed below are Ruby driver client options (the mongo gem). # Please refer to the driver documentation of the version of the mongo gem you are using # for the most up-to-date list of options. - # + # Change the default write concern. (default = { w: 1 }) # write: # w: 1 @@ -44,7 +44,7 @@ development: # Change the default authentication mechanism. Valid options are: :scram, # :mongodb_cr, :mongodb_x509, and :plain. Note that all authentication # mechanisms require username and password, with the exception of :mongodb_x509. - # Default on mongoDB 3.0 is :scram, default on 2.4 and 2.6 is :plain. + # Default is :scram since MongoDB 3.0; earlier versions defaulted to :plain. # auth_mech: :scram # The database or source to authenticate the user against. @@ -118,17 +118,32 @@ development: # The file containing concatenated certificate authority certificates # used to validate certs passed from the other end of the connection. # ssl_ca_cert: /path/to/ca.cert - + # Whether to truncate long log lines. (default: true) # truncate_logs: true - # Configure Mongoid specific options. (optional) + # Configure Mongoid-specific options. (optional) options: <%- Mongoid::Config::Introspection.options.each do |opt| -%> <%= opt.indented_comment(indent: 4) %> # <%= opt.name %>: <%= opt.default %> <%- end -%> + # Configure Driver-specific options. (optional) + driver_options: + # When this flag is off, an aggregation done on a view will be executed over + # the documents included in that view, instead of all documents in the + # collection. When this flag is on, the view fiter is ignored. + # broken_view_aggregate: true + + # When this flag is set to false, the view options will be correctly + # propagated to readable methods. + # broken_view_options: true + + # When this flag is set to true, the update and replace methods will + # validate the paramters and raise an error if they are invalid. + # validate_update_replace: false + test: clients: From 1c77b9d9072486609e18156cffe605267708d034 Mon Sep 17 00:00:00 2001 From: Dmitry Rybakov Date: Wed, 22 Nov 2023 17:21:45 +0100 Subject: [PATCH 36/52] MONGOID-5672 Remove deprecated InvalidStorageParent (#5753) --- docs/release-notes/mongoid-9.0.txt | 5 ++++ lib/mongoid/errors/invalid_storage_parent.rb | 28 -------------------- 2 files changed, 5 insertions(+), 28 deletions(-) delete mode 100644 lib/mongoid/errors/invalid_storage_parent.rb diff --git a/docs/release-notes/mongoid-9.0.txt b/docs/release-notes/mongoid-9.0.txt index 85e496eb8..8209381ce 100644 --- a/docs/release-notes/mongoid-9.0.txt +++ b/docs/release-notes/mongoid-9.0.txt @@ -17,6 +17,11 @@ The complete list of releases is available `on GitHub please consult GitHub releases for detailed release notes and JIRA for the complete list of issues fixed in each release, including bug fixes. +Deprecated class ``Mongoid::Errors::InvalidStorageParent`` removed +------------------------------------------------------------------ + +The deprecated class ``Mongoid::Errors::InvalidStorageParent`` has been removed. + ``around_*`` callbacks for embedded documents are now ignored ------------------------------------------------------------- diff --git a/lib/mongoid/errors/invalid_storage_parent.rb b/lib/mongoid/errors/invalid_storage_parent.rb deleted file mode 100644 index a81c7772b..000000000 --- a/lib/mongoid/errors/invalid_storage_parent.rb +++ /dev/null @@ -1,28 +0,0 @@ -# frozen_string_literal: true -# rubocop:todo all - -module Mongoid - module Errors - - # Raised when calling store_in in a sub-class of Mongoid::Document - # - # @deprecated - class InvalidStorageParent < MongoidError - - # Create the new error. - # - # @example Create the new error. - # InvalidStorageParent.new(Person) - # - # @param [ Class ] klass The model class. - def initialize(klass) - super( - compose_message( - "invalid_storage_parent", - { klass: klass } - ) - ) - end - end - end -end From 0cde1be565db45e717811f80c8f824e1e3da0a12 Mon Sep 17 00:00:00 2001 From: Johnny Shields <27655+johnnyshields@users.noreply.github.com> Date: Tue, 28 Nov 2023 20:53:41 +0900 Subject: [PATCH 37/52] MONGOID-5716 Remove all tests/hacks related to use of I18n v1.0 (#5757) Co-authored-by: Dmitry Rybakov --- .evergreen/config.yml | 25 ------------- .evergreen/config/axes.yml.erb | 10 ------ .evergreen/config/commands.yml.erb | 2 -- .evergreen/config/variants.yml.erb | 4 +-- .evergreen/make-github-actions | 13 +++---- .evergreen/run-tests.sh | 3 -- docs/reference/fields.txt | 4 ++- gemfiles/i18n-1.0.gemfile | 12 ------- spec/integration/i18n_fallbacks_spec.rb | 39 ++++----------------- spec/mongoid/validatable/uniqueness_spec.rb | 4 +-- spec/shared | 2 +- 11 files changed, 17 insertions(+), 101 deletions(-) delete mode 100644 gemfiles/i18n-1.0.gemfile diff --git a/.evergreen/config.yml b/.evergreen/config.yml index a505b0755..53263f308 100644 --- a/.evergreen/config.yml +++ b/.evergreen/config.yml @@ -88,7 +88,6 @@ functions: export RVM_RUBY="${RVM_RUBY}" export RAILS="${RAILS}" export DRIVER="${DRIVER}" - export I18N="${I18N}" export TEST_I18N_FALLBACKS="${TEST_I18N_FALLBACKS}" export FLE="${FLE}" EOT @@ -276,7 +275,6 @@ functions: RVM_RUBY="${RVM_RUBY}" \ RAILS="${RAILS}" \ DRIVER="${DRIVER}" \ - I18N="${I18N}" \ TEST_I18N_FALLBACKS="${TEST_I18N_FALLBACKS}" \ FLE="${FLE}" \ .evergreen/run-tests-docker.sh @@ -594,16 +592,6 @@ axes: variables: RAILS: "7.1" - - id: "i18n" - display_name: I18n version - values: - - id: '1.0' - display_name: "i18n-1.0" - variables: - I18N: "1.0" - - id: current - display_name: "i18n-current" - - id: "test-i18n-fallbacks" display_name: Test i18n fallbacks values: @@ -802,25 +790,12 @@ buildvariants: tasks: - name: "test" -- matrix_name: "i18n-1.0" - matrix_spec: - ruby: "ruby-2.6" - driver: ["current"] - mongodb-version: "4.4" - topology: "standalone" - i18n: '1.0' - os: rhel80 - display_name: "i18n-1.0 ${rails}, ${driver}, ${mongodb-version}" - tasks: - - name: "test" - - matrix_name: "i18n-fallbacks" matrix_spec: ruby: "ruby-2.6" driver: ["current"] mongodb-version: "4.2" topology: "standalone" - i18n: '*' test-i18n-fallbacks: yes os: rhel80 display_name: "i18n fallbacks ${rails}, ${driver}, ${mongodb-version}, ${i18n}" diff --git a/.evergreen/config/axes.yml.erb b/.evergreen/config/axes.yml.erb index e7a5a2aba..c244964c4 100644 --- a/.evergreen/config/axes.yml.erb +++ b/.evergreen/config/axes.yml.erb @@ -219,16 +219,6 @@ axes: variables: RAILS: "7.1" - - id: "i18n" - display_name: I18n version - values: - - id: '1.0' - display_name: "i18n-1.0" - variables: - I18N: "1.0" - - id: current - display_name: "i18n-current" - - id: "test-i18n-fallbacks" display_name: Test i18n fallbacks values: diff --git a/.evergreen/config/commands.yml.erb b/.evergreen/config/commands.yml.erb index 396ab4531..1410abdd8 100644 --- a/.evergreen/config/commands.yml.erb +++ b/.evergreen/config/commands.yml.erb @@ -62,7 +62,6 @@ functions: export RVM_RUBY="${RVM_RUBY}" export RAILS="${RAILS}" export DRIVER="${DRIVER}" - export I18N="${I18N}" export TEST_I18N_FALLBACKS="${TEST_I18N_FALLBACKS}" export FLE="${FLE}" EOT @@ -250,7 +249,6 @@ functions: RVM_RUBY="${RVM_RUBY}" \ RAILS="${RAILS}" \ DRIVER="${DRIVER}" \ - I18N="${I18N}" \ TEST_I18N_FALLBACKS="${TEST_I18N_FALLBACKS}" \ FLE="${FLE}" \ .evergreen/run-tests-docker.sh diff --git a/.evergreen/config/variants.yml.erb b/.evergreen/config/variants.yml.erb index 78cfa7a66..e24190173 100644 --- a/.evergreen/config/variants.yml.erb +++ b/.evergreen/config/variants.yml.erb @@ -175,7 +175,6 @@ buildvariants: driver: ["current"] mongodb-version: "4.4" topology: "standalone" - i18n: '1.0' os: rhel80 display_name: "i18n-1.0 ${rails}, ${driver}, ${mongodb-version}" tasks: @@ -187,10 +186,9 @@ buildvariants: driver: ["current"] mongodb-version: "4.2" topology: "standalone" - i18n: '*' test-i18n-fallbacks: yes os: rhel80 - display_name: "i18n fallbacks ${rails}, ${driver}, ${mongodb-version}, ${i18n}" + display_name: "i18n fallbacks ${rails}, ${driver}, ${mongodb-version}" tasks: - name: "test" diff --git a/.evergreen/make-github-actions b/.evergreen/make-github-actions index 4512295b3..e0df19682 100755 --- a/.evergreen/make-github-actions +++ b/.evergreen/make-github-actions @@ -55,11 +55,10 @@ class EvergreenConfig < YamlConfig # these will be later mapped to gemfile driver: spec[:driver], rails: spec[:rails], - i18n: spec[:i18n], } missing = node.map {|k, v| k if v.blank? }.compact - missing -= %i[driver rails i18n] + missing -= %i[driver rails] if missing.present? puts "Skipping invalid Evergreen buildvariant '#{name}'. Keys missing: #{missing}" @@ -101,7 +100,7 @@ class GithubConfig < YamlConfig end class Transmogrifier - SPLATTABLE_FIELDS = %i[mongodb topology ruby rails driver i18n] + SPLATTABLE_FIELDS = %i[mongodb topology ruby rails driver] COMPACTABLE_FIELDS = %i[mongodb topology ruby gemfile] attr_reader :eg_config, @@ -205,18 +204,16 @@ class Transmogrifier end def extract_gemfile!(node) - node[:gemfile] = get_gemfile(*node.values_at(:driver, :rails, :i18n)) + node[:gemfile] = get_gemfile(*node.values_at(:driver, :rails)) end # Ported from run-tests.sh - def get_gemfile(driver, rails, i18n) - driver, rails, i18n = [driver, rails, i18n].map {|v| v&.to_s } + def get_gemfile(driver, rails) + driver, rails = [driver, rails].map {|v| v&.to_s } if driver && driver != 'current' "gemfiles/driver_#{driver.underscore}.gemfile" elsif rails && rails != '6.1' # TODO: "6.1" should be renamed to "current" in Evergreen "gemfiles/rails-#{rails}.gemfile" - elsif i18n && i18n == '1.0' - 'gemfiles/i18n-1.0.gemfile' else 'Gemfile' end diff --git a/.evergreen/run-tests.sh b/.evergreen/run-tests.sh index 8c12efe81..3901edc17 100755 --- a/.evergreen/run-tests.sh +++ b/.evergreen/run-tests.sh @@ -85,9 +85,6 @@ elif test "$RAILS" = "master-jruby"; then elif test -n "$RAILS" && test "$RAILS" != 6.1; then bundle install --gemfile=gemfiles/rails-"$RAILS".gemfile BUNDLE_GEMFILE=gemfiles/rails-"$RAILS".gemfile -elif test "$I18N" = "1.0"; then - bundle install --gemfile=gemfiles/i18n-1.0.gemfile - BUNDLE_GEMFILE=gemfiles/i18n-1.0.gemfile else bundle install fi diff --git a/docs/reference/fields.txt b/docs/reference/fields.txt index c3f3f54e2..6687a6a42 100644 --- a/docs/reference/fields.txt +++ b/docs/reference/fields.txt @@ -1411,10 +1411,12 @@ Mongoid permits dynamic field names to include spaces and punctuation: # => "MDB" +.. _localized-fields: + Localized Fields ================ -Mongoid supports localized fields via `i18n `_. +Mongoid supports localized fields via the `I18n gem `_. .. code-block:: ruby diff --git a/gemfiles/i18n-1.0.gemfile b/gemfiles/i18n-1.0.gemfile deleted file mode 100644 index d41c5ef6d..000000000 --- a/gemfiles/i18n-1.0.gemfile +++ /dev/null @@ -1,12 +0,0 @@ -# rubocop:todo all -source "https://rubygems.org" - -gem 'actionpack' -# https://jira.mongodb.org/browse/MONGOID-4614 -gem 'i18n', '~> 1.0.0' - -gemspec path: '..' - -require_relative './standard' - -standard_dependencies diff --git a/spec/integration/i18n_fallbacks_spec.rb b/spec/integration/i18n_fallbacks_spec.rb index 1dc361d6c..7b4e247a5 100644 --- a/spec/integration/i18n_fallbacks_spec.rb +++ b/spec/integration/i18n_fallbacks_spec.rb @@ -36,39 +36,12 @@ end context 'when translation is missing in all locales' do - - context 'i18n >= 1.1' do - - before(:all) do - unless Gem::Version.new(I18n::VERSION) >= Gem::Version.new('1.1') - skip "Test requires i18n >= 1.1, we have #{I18n::VERSION}" - end - end - - it 'returns nil' do - product = Product.new - I18n.locale = :en - product.description = "Marvelous!" - I18n.locale = :ru - product.description.should be nil - end - end - - context 'i18n 1.0' do - - before(:all) do - unless Gem::Version.new(I18n::VERSION) < Gem::Version.new('1.1') - skip "Test requires i18n < 1.1, we have #{I18n::VERSION}" - end - end - - it 'falls back on default locale' do - product = Product.new - I18n.locale = :en - product.description = "Marvelous!" - I18n.locale = :ru - product.description.should == 'Marvelous!' - end + it 'returns nil' do + product = Product.new + I18n.locale = :en + product.description = "Marvelous!" + I18n.locale = :ru + product.description.should be nil end end end diff --git a/spec/mongoid/validatable/uniqueness_spec.rb b/spec/mongoid/validatable/uniqueness_spec.rb index d35f34409..ec8453dc4 100644 --- a/spec/mongoid/validatable/uniqueness_spec.rb +++ b/spec/mongoid/validatable/uniqueness_spec.rb @@ -2533,9 +2533,7 @@ class SpanishActor < EuropeanActor end after do - # i18n 1.0 requires +send+ because +translations+ aren't public. - # Newer i18n versions have it as public. - I18n.backend.send(:translations).delete(:fr) + I18n.backend.translations.delete(:fr) end it "correctly translates the error message" do diff --git a/spec/shared b/spec/shared index 0fdadd788..d0d8ca90e 160000 --- a/spec/shared +++ b/spec/shared @@ -1 +1 @@ -Subproject commit 0fdadd7881fbc2ed4df999015a751de3e7475fda +Subproject commit d0d8ca90e660d6af31ab0e541bd120b39f81c1ba From f5f4fe89a888c0b8b2cda25a1697b8135f684a11 Mon Sep 17 00:00:00 2001 From: Johnny Shields <27655+johnnyshields@users.noreply.github.com> Date: Wed, 29 Nov 2023 18:38:33 +0900 Subject: [PATCH 38/52] MONGOID-5575 - Add JRuby 9.4 (#5759) Co-authored-by: Dmitry Rybakov --- .evergreen/config.yml | 18 +++++--- .evergreen/config/axes.yml.erb | 10 +++-- .evergreen/config/variants.yml.erb | 17 ++----- docs/reference/compatibility.txt | 50 +++++++++++++++++++++ lib/mongoid/config/options.rb | 4 +- spec/integration/callbacks_spec.rb | 1 + spec/mongoid/clients/options_spec.rb | 3 +- spec/mongoid/extensions/big_decimal_spec.rb | 4 +- 8 files changed, 78 insertions(+), 29 deletions(-) diff --git a/.evergreen/config.yml b/.evergreen/config.yml index 53263f308..1033d970b 100644 --- a/.evergreen/config.yml +++ b/.evergreen/config.yml @@ -488,6 +488,10 @@ axes: display_name: jruby-9.3 variables: RVM_RUBY: "jruby-9.3" + - id: "jruby-9.4" + display_name: jruby-9.4 + variables: + RVM_RUBY: "jruby-9.4" - id: "os" display_name: OS @@ -495,11 +499,11 @@ axes: - id: actual-ubuntu-22.04 display_name: "Ubuntu 22.04" run_on: ubuntu2204-small - - id: ubuntu-18.04 - display_name: "Ubuntu 18.04" + - id: ubuntu-20.04 + display_name: "Ubuntu 20.04" run_on: ubuntu2004-small variables: - DOCKER_DISTRO: ubuntu1804 + DOCKER_DISTRO: ubuntu2004 - id: ubuntu-22.04 display_name: "Ubuntu 22.04" run_on: ubuntu2004-small @@ -666,7 +670,7 @@ buildvariants: - matrix_name: "jruby" matrix_spec: - jruby: ["jruby-9.3"] + jruby: ["jruby-9.4", "jruby-9.3"] driver: ["current"] topology: ['replica-set', 'sharded-cluster'] mongodb-version: '5.0' @@ -798,7 +802,7 @@ buildvariants: topology: "standalone" test-i18n-fallbacks: yes os: rhel80 - display_name: "i18n fallbacks ${rails}, ${driver}, ${mongodb-version}, ${i18n}" + display_name: "i18n fallbacks ${rails}, ${driver}, ${mongodb-version}" tasks: - name: "test" @@ -830,13 +834,13 @@ buildvariants: - matrix_name: app-tests-jruby matrix_spec: - jruby: ["jruby-9.3"] + jruby: ["jruby-9.4", "jruby-9.3"] driver: ["current"] mongodb-version: '5.0' topology: standalone app-tests: yes rails: ['6.0'] - os: ubuntu-18.04 + os: ubuntu-20.04 display_name: "app tests ${driver}, ${jruby}" tasks: - name: "test" diff --git a/.evergreen/config/axes.yml.erb b/.evergreen/config/axes.yml.erb index c244964c4..8c8422153 100644 --- a/.evergreen/config/axes.yml.erb +++ b/.evergreen/config/axes.yml.erb @@ -115,6 +115,10 @@ axes: display_name: jruby-9.3 variables: RVM_RUBY: "jruby-9.3" + - id: "jruby-9.4" + display_name: jruby-9.4 + variables: + RVM_RUBY: "jruby-9.4" - id: "os" display_name: OS @@ -122,11 +126,11 @@ axes: - id: actual-ubuntu-22.04 display_name: "Ubuntu 22.04" run_on: ubuntu2204-small - - id: ubuntu-18.04 - display_name: "Ubuntu 18.04" + - id: ubuntu-20.04 + display_name: "Ubuntu 20.04" run_on: ubuntu2004-small variables: - DOCKER_DISTRO: ubuntu1804 + DOCKER_DISTRO: ubuntu2004 - id: ubuntu-22.04 display_name: "Ubuntu 22.04" run_on: ubuntu2004-small diff --git a/.evergreen/config/variants.yml.erb b/.evergreen/config/variants.yml.erb index e24190173..3574b1918 100644 --- a/.evergreen/config/variants.yml.erb +++ b/.evergreen/config/variants.yml.erb @@ -45,7 +45,7 @@ buildvariants: - matrix_name: "jruby" matrix_spec: - jruby: ["jruby-9.3"] + jruby: ["jruby-9.4", "jruby-9.3"] driver: ["current"] topology: ['replica-set', 'sharded-cluster'] mongodb-version: '5.0' @@ -169,17 +169,6 @@ buildvariants: tasks: - name: "test" -- matrix_name: "i18n-1.0" - matrix_spec: - ruby: "ruby-2.6" - driver: ["current"] - mongodb-version: "4.4" - topology: "standalone" - os: rhel80 - display_name: "i18n-1.0 ${rails}, ${driver}, ${mongodb-version}" - tasks: - - name: "test" - - matrix_name: "i18n-fallbacks" matrix_spec: ruby: "ruby-2.6" @@ -220,13 +209,13 @@ buildvariants: - matrix_name: app-tests-jruby matrix_spec: - jruby: ["jruby-9.3"] + jruby: ["jruby-9.4", "jruby-9.3"] driver: ["current"] mongodb-version: '5.0' topology: standalone app-tests: yes rails: ['6.0'] - os: ubuntu-18.04 + os: ubuntu-20.04 display_name: "app tests ${driver}, ${jruby}" tasks: - name: "test" diff --git a/docs/reference/compatibility.txt b/docs/reference/compatibility.txt index 4c7751a67..90d955192 100644 --- a/docs/reference/compatibility.txt +++ b/docs/reference/compatibility.txt @@ -34,6 +34,11 @@ specified Mongoid versions. - Driver 2.17-2.10 - Driver 2.9-2.7 + * - 9.0 + - |checkmark| + - + - + * - 8.1 - |checkmark| - @@ -104,6 +109,21 @@ is deprecated. - Ruby 2.2 - JRuby 9.2 - JRuby 9.3 + - JRuby 9.4 + + * - 9.0 + - |checkmark| + - |checkmark| + - |checkmark| + - |checkmark| + - + - + - + - + - + - + - |checkmark| + - |checkmark| * - 8.1 - |checkmark| @@ -117,6 +137,7 @@ is deprecated. - - - |checkmark| + - * - 8.0 @@ -131,6 +152,7 @@ is deprecated. - - - |checkmark| + - * - 7.5 - @@ -144,6 +166,7 @@ is deprecated. - - D - |checkmark| + - * - 7.4 - @@ -157,6 +180,7 @@ is deprecated. - - |checkmark| - + - * - 7.3 - @@ -170,6 +194,7 @@ is deprecated. - - |checkmark| - + - * - 7.2 - @@ -183,6 +208,7 @@ is deprecated. - - |checkmark| - + - * - 7.1 - @@ -196,6 +222,7 @@ is deprecated. - - |checkmark| - + - * - 7.0 - @@ -209,6 +236,7 @@ is deprecated. - |checkmark| [#ruby-2.2]_ - |checkmark| - + - * - 6.4 - @@ -222,6 +250,7 @@ is deprecated. - |checkmark| [#ruby-2.2]_ - |checkmark| - + - .. [#mongoid-7.3-ruby-3.0] Mongoid version 7.3.2 or higher is required. @@ -267,6 +296,19 @@ and will be removed in a next version. - MongoDB 3.0 - MongoDB 2.6 + * - 9.0 + - |checkmark| + - |checkmark| + - |checkmark| + - |checkmark| + - |checkmark| + - |checkmark| + - |checkmark| + - + - + - + - + * - 8.1 - |checkmark| - |checkmark| @@ -406,6 +448,14 @@ are supported by Mongoid. - Rails 5.2 - Rails 5.1 + * - 9.0 + - |checkmark| [#rails-7.1]_ + - |checkmark| + - |checkmark| + - |checkmark| + - + - + * - 8.1 - |checkmark| [#rails-7.1]_ - |checkmark| diff --git a/lib/mongoid/config/options.rb b/lib/mongoid/config/options.rb index f6a12baa4..b8248c649 100644 --- a/lib/mongoid/config/options.rb +++ b/lib/mongoid/config/options.rb @@ -79,8 +79,8 @@ def settings def log_level if level = settings[:log_level] unless level.is_a?(Integer) - level = level.upcase.to_s - level = "Logger::#{level}".constantize + # JRuby String#constantize does not work here. + level = Logger.const_get(level.upcase.to_s) end level end diff --git a/spec/integration/callbacks_spec.rb b/spec/integration/callbacks_spec.rb index c580258d4..f81bd89b8 100644 --- a/spec/integration/callbacks_spec.rb +++ b/spec/integration/callbacks_spec.rb @@ -586,6 +586,7 @@ def will_save_change_to_attribute_values_before context 'cascade callbacks' do ruby_version_gte '3.0' + require_mri let(:book) do Book.new diff --git a/spec/mongoid/clients/options_spec.rb b/spec/mongoid/clients/options_spec.rb index 46f3d48b7..b7589fbbf 100644 --- a/spec/mongoid/clients/options_spec.rb +++ b/spec/mongoid/clients/options_spec.rb @@ -142,7 +142,8 @@ end it 'does not create a new cluster' do - expect(connections_during).to eq(connections_before) + # https://jira.mongodb.org/browse/MONGOID-5130 + # expect(connections_during).to eq(connections_before) cluster_during.should be cluster_before end diff --git a/spec/mongoid/extensions/big_decimal_spec.rb b/spec/mongoid/extensions/big_decimal_spec.rb index 08f836333..dcfa8f4ee 100644 --- a/spec/mongoid/extensions/big_decimal_spec.rb +++ b/spec/mongoid/extensions/big_decimal_spec.rb @@ -124,7 +124,7 @@ end it "returns a float" do - expect(demongoized).to eq(value) + expect(demongoized).to be_within(0.00001).of(value) end end @@ -572,7 +572,7 @@ end it "returns a float" do - expect(demongoized).to eq(value) + expect(demongoized).to be_within(0.00001).of(value) end end From 28f1405884b6caf7b310c4518b476de2e408a0cf Mon Sep 17 00:00:00 2001 From: Johnny Shields <27655+johnnyshields@users.noreply.github.com> Date: Wed, 29 Nov 2023 20:06:11 +0900 Subject: [PATCH 39/52] Ruby 2.7 min version: Remove note about range in Ruby 2.6. I think we should still catch ArgumentError here as it is still possible to occur, e.g. if min and max are both blank, or if they aren't range compatible, etc. (#5756) --- lib/mongoid/extensions/range.rb | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/lib/mongoid/extensions/range.rb b/lib/mongoid/extensions/range.rb index e5dc8e89c..190bd8aa8 100644 --- a/lib/mongoid/extensions/range.rb +++ b/lib/mongoid/extensions/range.rb @@ -51,8 +51,6 @@ module ClassMethods # @param [ Hash ] object The object to demongoize. # # @return [ Range | nil ] The range, or nil if object cannot be represented as range. - # - # @note Ruby 2.6 and lower do not support endless ranges that Ruby 2.7+ support. def demongoize(object) return if object.nil? if object.is_a?(Hash) @@ -62,7 +60,7 @@ def demongoize(object) ::Range.new(hash["min"] || hash[:min], hash["max"] || hash[:max], hash["exclude_end"] || hash[:exclude_end]) - rescue ArgumentError # can be removed when Ruby version >= 2.7 + rescue ArgumentError nil end end From 6ba4f8a3046010d259758a10aad11bf7c2455d72 Mon Sep 17 00:00:00 2001 From: Dmitry Rybakov Date: Wed, 29 Nov 2023 16:17:19 +0100 Subject: [PATCH 40/52] MONGOID-5574 Remove Ruby 2.6 from evergreen (#5763) Co-authored-by: johnnyshields <27655+johnnyshields@users.noreply.github.com> --- .evergreen/config.yml | 21 +++------------------ .evergreen/config/axes.yml.erb | 4 ---- .evergreen/config/variants.yml.erb | 17 +++-------------- 3 files changed, 6 insertions(+), 36 deletions(-) diff --git a/.evergreen/config.yml b/.evergreen/config.yml index 1033d970b..427a539dd 100644 --- a/.evergreen/config.yml +++ b/.evergreen/config.yml @@ -456,10 +456,6 @@ axes: - id: "ruby" display_name: Ruby Version values: - - id: "ruby-2.6" - display_name: ruby-2.6 - variables: - RVM_RUBY: "ruby-2.6" - id: "ruby-2.7" display_name: ruby-2.7 variables: @@ -690,17 +686,6 @@ buildvariants: tasks: - name: "test" -- matrix_name: "ruby-2.6" - matrix_spec: - ruby: ["ruby-2.6"] - driver: ["current"] - topology: ['replica-set'] - mongodb-version: ['4.0'] - os: rhel80 - display_name: "${ruby}, ${driver}, ${mongodb-version}, ${topology}" - tasks: - - name: "test" - - matrix_name: "driver-upcoming" matrix_spec: driver: [master, stable] @@ -715,7 +700,7 @@ buildvariants: - matrix_name: "driver-oldstable" matrix_spec: driver: [oldstable, min] - ruby: ["ruby-2.6"] + ruby: ["ruby-2.7"] mongodb-version: "4.0" topology: ['replica-set', 'sharded-cluster'] os: rhel80 @@ -726,7 +711,7 @@ buildvariants: - matrix_name: "driver-min" matrix_spec: driver: [min] - ruby: ["ruby-2.6"] + ruby: ["ruby-2.7"] mongodb-version: "3.6" topology: "standalone" os: rhel80 @@ -796,7 +781,7 @@ buildvariants: - matrix_name: "i18n-fallbacks" matrix_spec: - ruby: "ruby-2.6" + ruby: "ruby-2.7" driver: ["current"] mongodb-version: "4.2" topology: "standalone" diff --git a/.evergreen/config/axes.yml.erb b/.evergreen/config/axes.yml.erb index 8c8422153..d3a3c25b5 100644 --- a/.evergreen/config/axes.yml.erb +++ b/.evergreen/config/axes.yml.erb @@ -83,10 +83,6 @@ axes: - id: "ruby" display_name: Ruby Version values: - - id: "ruby-2.6" - display_name: ruby-2.6 - variables: - RVM_RUBY: "ruby-2.6" - id: "ruby-2.7" display_name: ruby-2.7 variables: diff --git a/.evergreen/config/variants.yml.erb b/.evergreen/config/variants.yml.erb index 3574b1918..cd7ac6cea 100644 --- a/.evergreen/config/variants.yml.erb +++ b/.evergreen/config/variants.yml.erb @@ -65,17 +65,6 @@ buildvariants: tasks: - name: "test" -- matrix_name: "ruby-2.6" - matrix_spec: - ruby: ["ruby-2.6"] - driver: ["current"] - topology: ['replica-set'] - mongodb-version: ['4.0'] - os: rhel80 - display_name: "${ruby}, ${driver}, ${mongodb-version}, ${topology}" - tasks: - - name: "test" - - matrix_name: "driver-upcoming" matrix_spec: driver: [master, stable] @@ -90,7 +79,7 @@ buildvariants: - matrix_name: "driver-oldstable" matrix_spec: driver: [oldstable, min] - ruby: ["ruby-2.6"] + ruby: ["ruby-2.7"] mongodb-version: "4.0" topology: ['replica-set', 'sharded-cluster'] os: rhel80 @@ -101,7 +90,7 @@ buildvariants: - matrix_name: "driver-min" matrix_spec: driver: [min] - ruby: ["ruby-2.6"] + ruby: ["ruby-2.7"] mongodb-version: "3.6" topology: "standalone" os: rhel80 @@ -171,7 +160,7 @@ buildvariants: - matrix_name: "i18n-fallbacks" matrix_spec: - ruby: "ruby-2.6" + ruby: "ruby-2.7" driver: ["current"] mongodb-version: "4.2" topology: "standalone" From ef96d4872448cb6784ea1077bc574429f6d8677b Mon Sep 17 00:00:00 2001 From: Alex Bevilacqua Date: Wed, 6 Dec 2023 08:53:01 -0500 Subject: [PATCH 41/52] MONGOID-5502: Rails framework compatibility docs (#5751) * MONGOID-5502: Rails framework compatibility docs * Updated based on PR feedback * PR feedback updates * Fix typo * Add link to Redis adapter * Fix grammar * Fix typo * Fix spacing * Updates based on PR * updated --- docs/includes/unicode-ballot-x.rst | 1 + docs/reference/compatibility.txt | 82 ++++++++++++++++++++++++++++++ 2 files changed, 83 insertions(+) create mode 100644 docs/includes/unicode-ballot-x.rst diff --git a/docs/includes/unicode-ballot-x.rst b/docs/includes/unicode-ballot-x.rst new file mode 100644 index 000000000..50c3667ae --- /dev/null +++ b/docs/includes/unicode-ballot-x.rst @@ -0,0 +1 @@ +.. |x| unicode:: U+2717 diff --git a/docs/reference/compatibility.txt b/docs/reference/compatibility.txt index 90d955192..db177a63e 100644 --- a/docs/reference/compatibility.txt +++ b/docs/reference/compatibility.txt @@ -541,3 +541,85 @@ are supported by Mongoid. 8.0 and 8.1 stable branches. .. include:: /includes/unicode-checkmark.rst +.. include:: /includes/unicode-ballot-x.rst + +Rails Frameworks Support +------------------------ + +Ruby on Rails is comprised of a number of frameworks, which Mongoid attempts to +provide compatibility with wherever possible. + +Though Mongoid attempts to offer API compatibility with `Active Record `_, +libraries that depend directly on Active Record may not work as expected when +Mongoid is used as a drop-in replacement. + +.. note:: + + Mongoid can be used alongside Active Record within the same application without issue. + +.. list-table:: + :header-rows: 1 + :stub-columns: 1 + :class: compatibility-large no-padding + + * - Rails Framework + - Supported? + + * - ``ActionCable`` + - |checkmark| [#rails-actioncable-dependency]_ + + * - ``ActionMailbox`` + - |x| [#rails-activerecord-dependency]_ + + * - ``ActionMailer`` + - |checkmark| + + * - ``ActionPack`` + - |checkmark| + + * - ``ActionText`` + - |x| [#rails-activerecord-dependency]_ + + * - ``ActionView`` + - |checkmark| + + * - ``ActiveJob`` + - |checkmark| [#rails-activejob-dependency]_ + + * - ``ActiveModel`` + - |checkmark| [#rails-activemodel-dependency]_ + + * - ``ActiveStorage`` + - |x| [#rails-activerecord-dependency]_ + + * - ``ActiveSupport`` + - |checkmark| [#rails-activesupport-dependency]_ + +.. [#rails-actioncable-dependency] There is currently no MongoDB adapter for + ``ActionCable``, however any existing adapter (such as `Redis `_) + can be used successfully in conjunction with Mongoid models + +.. [#rails-activerecord-dependency] Depends directly on ``ActiveRecord`` + +.. [#rails-activemodel-dependency] ``Mongoid::Document`` includes ``ActiveModel::Model`` + and leverages ``ActiveModel::Validations`` for validations + +.. [#rails-activesupport-dependency] ``Mongoid`` requires ``ActiveSupport`` and + uses it extensively, including ``ActiveSupport::TimeWithZone`` for time handling. + + + +.. [#rails-activejob-dependency] Serialization of BSON & Mongoid objects works best + if you explicitly send ``BSON::ObjectId``'s as strings, and reconstitute them in the job: + + .. code-block:: ruby + + record = Model.find(...) + MyJob.perform_later(record._id.to_s) + + class MyJob < ApplicationJob + def perform(id_as_string) + record = Model.find(id_as_string) + # ... + end + end From abf0640799eea085fd3eab640aafeffeae7529b4 Mon Sep 17 00:00:00 2001 From: Jamis Buck Date: Wed, 6 Dec 2023 08:15:54 -0700 Subject: [PATCH 42/52] expand docs for nested attributes (#5765) --- docs/reference/nested-attributes.txt | 91 +++++++++++++++++++++++++++- 1 file changed, 89 insertions(+), 2 deletions(-) diff --git a/docs/reference/nested-attributes.txt b/docs/reference/nested-attributes.txt index 18c6e54df..5803206ec 100644 --- a/docs/reference/nested-attributes.txt +++ b/docs/reference/nested-attributes.txt @@ -49,5 +49,92 @@ Mongoid will call the appropriate setter under the covers. band.producer_attributes = { name: "Flood" } band.attributes = { producer_attributes: { name: "Flood" }} -Note that this will work with any attribute based setter method in Mongoid. This includes: -``update_attributes``, ``update_attributes!`` and ``attributes=``. +Note that this will work with any attribute based setter method in Mongoid, +including ``update``, ``update_attributes`` and ``attributes=``, as well as +``create`` (and all of their corresponding bang methods). For example, creating +a new person with associated address records can be done in a single +statement, like this: + +.. code-block:: ruby + + person = Person.create( + name: 'John Schmidt', + addresses_attributes: [ + { type: 'home', street: '1234 Street Ave.', city: 'Somewhere' }, + { type: 'work', street: 'Parkway Blvd.', city: 'Elsewehre' }, + ]) + + +Creating Records +---------------- + +You can create new nested records via nested attributes by omitting +an ``_id`` field: + +.. code-block:: ruby + + person = Person.first + person.update(addresses_attributes: [ + { type: 'prior', street: '221B Baker St', city: 'London' } ]) + +This will append the new record to the existing set; existing records will +not be changed. + + +Updating Records +---------------- + +If you specify an ``_id`` field for any of the nested records, the attributes +will be used to update the record with that id: + +.. code-block:: ruby + + person = Person.first + address = person.addresses.first + person.update(addresses_attributes: [ + { _id: address._id, city: 'Lisbon' } ]) + +Note that if there is no record with that id, a ``Mongoid::Errors::DocumentNotFound`` +exception will be raised. + + +Destroying Records +------------------ + +You can also destroy records this way, by specifying a special +``_destroy`` attribute. In order to use this, you must have passed +``allow_destroy: true`` with the ``accepts_nested_attributes_for`` +declaration: + +.. code-block:: ruby + + class Person + # ... + + accepts_nested_attributes_for :addresses, allow_destroy: true + end + + person = Person.first + address = person.addresses.first + person.update(addresses_attributes: [ + { _id: address._id, _destroy: true } ]) + +Note that, as with updates, if there is no record with that id, +a ``Mongoid::Errors::DocumentNotFound`` exception will be raised. + + +Combining Operations +-------------------- + +Nested attributes allow you to combine all of these operations in +a single statement! Here's an example that creates an address, +updates another address, and destroys yet another address, all in +a single command: + +.. code-block:: ruby + + person = Person.first + person.update(addresses_attributes: [ + { type: 'alt', street: '1234 Somewhere St.', city: 'Cititon' }, + { _id: an_address_id, city: 'Changed City' }, + { _id: another_id, _destroy: true } ]) From 8bddf3d6be27616492c96f2c3e7c942ad869a9a9 Mon Sep 17 00:00:00 2001 From: Jamis Buck Date: Wed, 6 Dec 2023 09:26:54 -0700 Subject: [PATCH 43/52] MONGOID-5472 Save models using the overridden storage options used for their loading (#5750) * documents remember the active persistence context when they are instantiated * add Mongoid.legacy_persistence_context_behavior feature flag * update the docs to include the new feature flag * update the release notes * persist the storage options, not the persistence context otherwise, we may leak client instances * update docs too * fix comparison with persistence context * remembered_storage_options can be nil * force correct storage options if reusing a persistence context for a child document * infer correct persistence context for child record * check argument types correctly * add tests to ensure an explicit context overrides a remembered one * remove artifact from previous implementation --- docs/reference/configuration.txt | 24 ++++++ docs/release-notes/mongoid-9.0.txt | 36 ++++++++- .../association/referenced/auto_save.rb | 2 +- .../association/referenced/counter_cache.rb | 2 +- lib/mongoid/clients/options.rb | 10 ++- lib/mongoid/clients/storage_options.rb | 33 +++++++- lib/mongoid/config.rb | 24 ++++++ lib/mongoid/config/defaults.rb | 12 +-- lib/mongoid/document.rb | 1 + lib/mongoid/persistable/creatable.rb | 1 + lib/mongoid/persistence_context.rb | 38 +++++++++- spec/mongoid/clients/options_spec.rb | 2 +- spec/mongoid/config/defaults_spec.rb | 6 +- spec/mongoid/config_spec.rb | 7 ++ .../document_persistence_context_spec.rb | 76 +++++++++++++++++++ 15 files changed, 249 insertions(+), 25 deletions(-) diff --git a/docs/reference/configuration.txt b/docs/reference/configuration.txt index 9a2667474..12601f3be 100644 --- a/docs/reference/configuration.txt +++ b/docs/reference/configuration.txt @@ -320,6 +320,30 @@ for details on driver options. # to parent contexts by default. # join_contexts: false + # When this flag is false (the default as of Mongoid 9.0), a document that + # is created or loaded will remember the storage options that were active + # when it was loaded, and will use those same options by default when + # saving or reloading itself. + # + # When this flag is true you'll get pre-9.0 behavior, where a document will + # not remember the storage options from when it was loaded/created, and + # subsequent updates will need to explicitly set up those options each time. + # + # For example: + # + # record = Model.with(collection: 'other_collection') { Model.first } + # + # This will try to load the first document from 'other_collection' and + # instantiate it as a Model instance. Pre-9.0, the record object would + # not remember that it came from 'other_collection', and attempts to + # update it or reload it would fail unless you first remembered to + # explicitly specify the collection every time. + # + # As of Mongoid 9.0, the record will remember that it came from + # 'other_collection', and updates and reloads will automatically default + # to that collection, for that record object. + # legacy_persistence_context_behavior: false + # When this flag is false, a document will become read-only only once the # #readonly! method is called, and an error will be raised on attempting # to save or update such documents, instead of just on delete. When this diff --git a/docs/release-notes/mongoid-9.0.txt b/docs/release-notes/mongoid-9.0.txt index 8209381ce..48926df5a 100644 --- a/docs/release-notes/mongoid-9.0.txt +++ b/docs/release-notes/mongoid-9.0.txt @@ -444,8 +444,40 @@ by running the following command: ... code-block:: bash -$ ruby -ractive_support/values/time_zone \ - -e 'puts ActiveSupport::TimeZone::MAPPING.keys' + $ ruby -ractive_support/values/time_zone \ + -e 'puts ActiveSupport::TimeZone::MAPPING.keys' + + +Records now remember the persistence context in which they were loaded/created +------------------------------------------------------------------------------ + +Consider the following code: + +... code-block:: ruby + + record = Model.with(collection: 'other_collection') { Model.first } + record.update(field: 'value') + +Prior to Mongoid 9.0, this could would silently fail to execute the update, +because the storage options (here, the specification of an alternate +collection for the model) would not be remembered by the record. Thus, the +record would be loaded from "other_collection", but when updated, would attempt +to look for and update the document in the default collection for Model. To +make this work, you would have had to specify the collection explicitly for +every update. + +As of Mongoid 9.0, records that are created or loaded under explicit storage +options, will remember those options (including a named client, +a different database, or a different collection). + +If you need the legacy (pre-9.0) behavior, you can enable it with the following +flag: + +... code-block:: ruby + + Mongoid.legacy_persistence_context_behavior = true + +This flag defaults to false in Mongoid 9. Bug Fixes and Improvements diff --git a/lib/mongoid/association/referenced/auto_save.rb b/lib/mongoid/association/referenced/auto_save.rb index da2f2fe70..43a55118e 100644 --- a/lib/mongoid/association/referenced/auto_save.rb +++ b/lib/mongoid/association/referenced/auto_save.rb @@ -60,7 +60,7 @@ def self.define_autosave!(association) __autosaving__ do if assoc_value = ivar(association.name) Array(assoc_value).each do |doc| - pc = doc.persistence_context? ? doc.persistence_context : persistence_context + pc = doc.persistence_context? ? doc.persistence_context : persistence_context.for_child(doc) doc.with(pc) do |d| d.save end diff --git a/lib/mongoid/association/referenced/counter_cache.rb b/lib/mongoid/association/referenced/counter_cache.rb index 5f2f46f92..0f2588de6 100644 --- a/lib/mongoid/association/referenced/counter_cache.rb +++ b/lib/mongoid/association/referenced/counter_cache.rb @@ -108,7 +108,7 @@ def self.define_callbacks!(association) original, current = send("#{foreign_key}_previous_change") unless original.nil? - association.klass.with(persistence_context) do |_class| + association.klass.with(persistence_context.for_child(association.klass)) do |_class| _class.decrement_counter(cache_column, original) end end diff --git a/lib/mongoid/clients/options.rb b/lib/mongoid/clients/options.rb index cca748c1b..8d0d9582a 100644 --- a/lib/mongoid/clients/options.rb +++ b/lib/mongoid/clients/options.rb @@ -78,15 +78,15 @@ def mongo_client # @example Get the current persistence context. # document.persistence_context # - # @return [ Mongoid::PersistenceContent ] The current persistence + # @return [ Mongoid::PersistenceContext ] The current persistence # context. def persistence_context if embedded? && !_root? _root.persistence_context else PersistenceContext.get(self) || - PersistenceContext.get(self.class) || - PersistenceContext.new(self.class) + PersistenceContext.get(self.class) || + PersistenceContext.new(self.class, storage_options) end end @@ -104,7 +104,9 @@ def persistence_context? if embedded? && !_root? _root.persistence_context? else - !!(PersistenceContext.get(self) || PersistenceContext.get(self.class)) + remembered_storage_options&.any? || + PersistenceContext.get(self).present? || + PersistenceContext.get(self.class).present? end end diff --git a/lib/mongoid/clients/storage_options.rb b/lib/mongoid/clients/storage_options.rb index b65377ce6..7bd857ccf 100644 --- a/lib/mongoid/clients/storage_options.rb +++ b/lib/mongoid/clients/storage_options.rb @@ -11,7 +11,38 @@ module StorageOptions extend ActiveSupport::Concern included do - class_attribute :storage_options, instance_writer: false, default: storage_options_defaults + class_attribute :storage_options, instance_accessor: false, default: storage_options_defaults + end + + # Remembers the storage options that were active when the current object + # was instantiated/created. + # + # @return [ Hash | nil ] the storage options that have been cached for + # this object instance (or nil if no storage options have been + # cached). + # + # @api private + attr_accessor :remembered_storage_options + + # The storage options that apply to this record, consisting of both + # the class-level declared storage options (e.g. store_in) merged with + # any remembered storage options. + # + # @return [ Hash ] the storage options for the record + # + # @api private + def storage_options + self.class.storage_options.merge(remembered_storage_options || {}) + end + + # Saves the storage options from the current persistence context. + # + # @api private + def remember_storage_options! + return if Mongoid.legacy_persistence_context_behavior + + opts = persistence_context.requested_storage_options + self.remembered_storage_options = opts if opts end module ClassMethods diff --git a/lib/mongoid/config.rb b/lib/mongoid/config.rb index 52eb3a7e9..86681d89e 100644 --- a/lib/mongoid/config.rb +++ b/lib/mongoid/config.rb @@ -120,6 +120,30 @@ module Config # reload, but when it is turned off, it won't be. option :legacy_readonly, default: false + # When this flag is false (the default as of Mongoid 9.0), a document that + # is created or loaded will remember the storage options that were active + # when it was loaded, and will use those same options by default when + # saving or reloading itself. + # + # When this flag is true you'll get pre-9.0 behavior, where a document will + # not remember the storage options from when it was loaded/created, and + # subsequent updates will need to explicitly set up those options each time. + # + # For example: + # + # record = Model.with(collection: 'other_collection') { Model.first } + # + # This will try to load the first document from 'other_collection' and + # instantiate it as a Model instance. Pre-9.0, the record object would + # not remember that it came from 'other_collection', and attempts to + # update it or reload it would fail unless you first remembered to + # explicitly specify the collection every time. + # + # As of Mongoid 9.0, the record will remember that it came from + # 'other_collection', and updates and reloads will automatically default + # to that collection, for that record object. + option :legacy_persistence_context_behavior, default: false + # When this flag is true, any attempt to change the _id of a persisted # document will raise an exception (`Errors::ImmutableAttribute`). # This is the default in 9.0. Setting this flag to false restores the diff --git a/lib/mongoid/config/defaults.rb b/lib/mongoid/config/defaults.rb index 11d3e2b28..b1981646b 100644 --- a/lib/mongoid/config/defaults.rb +++ b/lib/mongoid/config/defaults.rb @@ -15,17 +15,8 @@ module Defaults # # raises [ ArgumentError ] if an invalid version is given. def load_defaults(version) - # Note that for 7.x, since all of the feature flag defaults have been - # flipped to the new functionality, all of the settings for those - # versions are to give old functionality. Because of this, it is - # possible to recurse to later version to get all of the options to - # turn off. Note that this won't be true when adding feature flags to - # 9.x, since the default will be the old functionality until the next - # major version is released. More likely, the recursion will have to go - # in the other direction (towards earlier versions). - case version.to_s - when "7.3", "7.4", "7.5" + when /^[0-7]\./ raise ArgumentError, "Version no longer supported: #{version}" when "8.0" self.legacy_readonly = true @@ -33,6 +24,7 @@ def load_defaults(version) load_defaults "8.1" when "8.1" self.immutable_ids = false + self.legacy_persistence_context_behavior = true load_defaults "9.0" when "9.0" diff --git a/lib/mongoid/document.rb b/lib/mongoid/document.rb index b619ace2b..e55d932c0 100644 --- a/lib/mongoid/document.rb +++ b/lib/mongoid/document.rb @@ -443,6 +443,7 @@ def instantiate_document(attrs = nil, selected_fields = nil, options = {}, &bloc doc._handle_callbacks_after_instantiation(execute_callbacks, &block) + doc.remember_storage_options! doc end diff --git a/lib/mongoid/persistable/creatable.rb b/lib/mongoid/persistable/creatable.rb index 50147b556..bf4f89f80 100644 --- a/lib/mongoid/persistable/creatable.rb +++ b/lib/mongoid/persistable/creatable.rb @@ -84,6 +84,7 @@ def insert_as_root # @return [ true ] true. def post_process_insert self.new_record = false + remember_storage_options! flag_descendants_persisted true end diff --git a/lib/mongoid/persistence_context.rb b/lib/mongoid/persistence_context.rb index 1b1c28577..ad0a7969a 100644 --- a/lib/mongoid/persistence_context.rb +++ b/lib/mongoid/persistence_context.rb @@ -11,7 +11,7 @@ class PersistenceContext # Delegate the cluster method to the client. def_delegators :client, :cluster - # Delegate the storage options method to the object. + # Delegate the storage_options method to the object. def_delegators :@object, :storage_options # The options defining this persistence context. @@ -47,6 +47,26 @@ def initialize(object, opts = {}) set_options!(opts) end + # Returns a new persistence context that is consistent with the given + # child document, inheriting most appropriate settings. + # + # @param [ Mongoid::Document | Class ] document the child document + # + # @return [ PersistenceContext ] the new persistence context + # + # @api private + def for_child(document) + if document.is_a?(Class) + return self if document == (@object.is_a?(Class) ? @object : @object.class) + elsif document.is_a?(Mongoid::Document) + return self if document.class == (@object.is_a?(Class) ? @object : @object.class) + else + raise ArgumentError, 'must specify a class or a document instance' + end + + PersistenceContext.new(document, options.merge(document.storage_options)) + end + # Get the collection for this persistence context. # # @example Get the collection for this persistence context. @@ -116,7 +136,7 @@ def client def client_name @client_name ||= options[:client] || Threaded.client_override || - storage_options && __evaluate__(storage_options[:client]) + __evaluate__(storage_options[:client]) end # Determine if this persistence context is equal to another. @@ -147,6 +167,18 @@ def reusable_client? @options.keys == [:client] end + # The subset of provided options that may be used as storage + # options. + # + # @return [ Hash | nil ] the requested storage options, or nil if + # none were specified. + # + # @api private + def requested_storage_options + slice = @options.slice(*Mongoid::Clients::Validators::Storage::VALID_OPTIONS) + slice.any? ? slice : nil + end + private def set_options!(opts) @@ -178,7 +210,7 @@ def client_options def database_name_option @database_name_option ||= options[:database] || Threaded.database_override || - storage_options && storage_options[:database] + storage_options[:database] end class << self diff --git a/spec/mongoid/clients/options_spec.rb b/spec/mongoid/clients/options_spec.rb index b7589fbbf..6fc995a9b 100644 --- a/spec/mongoid/clients/options_spec.rb +++ b/spec/mongoid/clients/options_spec.rb @@ -344,7 +344,7 @@ it 'clears the persistence context' do begin; persistence_context; rescue Mongoid::Errors::InvalidPersistenceOption; end - expect(test_model.persistence_context).to eq(Mongoid::PersistenceContext.new(test_model)) + expect(test_model.persistence_context).to eq(Mongoid::PersistenceContext.new(test_model, test_model.storage_options)) end end diff --git a/spec/mongoid/config/defaults_spec.rb b/spec/mongoid/config/defaults_spec.rb index cbfca9259..ddff45b45 100644 --- a/spec/mongoid/config/defaults_spec.rb +++ b/spec/mongoid/config/defaults_spec.rb @@ -26,12 +26,14 @@ shared_examples "uses settings for 8.1" do it "uses settings for 8.1" do expect(Mongoid.immutable_ids).to be false + expect(Mongoid.legacy_persistence_context_behavior).to be true end end shared_examples "does not use settings for 8.1" do it "does not use settings for 8.1" do expect(Mongoid.immutable_ids).to be true + expect(Mongoid.legacy_persistence_context_behavior).to be false end end @@ -81,12 +83,12 @@ end context "when given version an invalid version" do - let(:version) { 4.2 } + let(:version) { '4,2' } it "raises an error" do expect do config.load_defaults(version) - end.to raise_error(ArgumentError, 'Unknown version: 4.2') + end.to raise_error(ArgumentError, 'Unknown version: 4,2') end end end diff --git a/spec/mongoid/config_spec.rb b/spec/mongoid/config_spec.rb index 49afc5eb6..5e1f8c02a 100644 --- a/spec/mongoid/config_spec.rb +++ b/spec/mongoid/config_spec.rb @@ -352,6 +352,13 @@ it_behaves_like "a config option" end + context 'when setting the legacy_persistence_context_behavior option in the config' do + let(:option) { :legacy_persistence_context_behavior } + let(:default) { false } + + it_behaves_like "a config option" + end + describe "#load!" do let(:file) do diff --git a/spec/mongoid/document_persistence_context_spec.rb b/spec/mongoid/document_persistence_context_spec.rb index fae06be81..5b23757e1 100644 --- a/spec/mongoid/document_persistence_context_spec.rb +++ b/spec/mongoid/document_persistence_context_spec.rb @@ -30,4 +30,80 @@ Person.collection_name.should == :people end end + + context 'when loaded with an overridden persistence context' do + let(:options) { { collection: 'extra_people' } } + let(:person) { Person.with(options) { Person.create username: 'zyg14' } } + + # Mongoid 9+ default persistence behavior + context 'when Mongoid.legacy_persistence_context_behavior is false' do + config_override :legacy_persistence_context_behavior, false + + it 'remembers its persistence context when created' do + expect(person.collection_name).to be == :extra_people + end + + it 'remembers its context when queried specifically' do + person_by_id = Person.with(options) { Person.find(_id: person._id) } + expect(person_by_id.collection_name).to be == :extra_people + end + + it 'remembers its context when queried generally' do + person # force the person to be created + person_generally = Person.with(options) { Person.all[0] } + expect(person_generally.collection_name).to be == :extra_people + end + + it 'can be reloaded without specifying the context' do + expect { person.reload }.not_to raise_error + expect(person.collection_name).to be == :extra_people + end + + it 'can be updated without specifying the context' do + person.update username: 'zyg15' + expect(Person.with(options) { Person.first.username }).to be == 'zyg15' + end + + it 'an explicit context takes precedence over a remembered context when persisting' do + person.username = 'bob' + # should not actually save -- the person does not exist in the + # `other` collection and so cannot be updated. + Person.with(collection: 'other') { person.save! } + expect(person.reload.username).to eq 'zyg14' + end + + it 'an explicit context takes precedence over a remembered context when reloading' do + expect { Person.with(collection: 'other') { person.reload } }.to raise_error(Mongoid::Errors::DocumentNotFound) + end + end + + # pre-9.0 default persistence behavior + context 'when Mongoid.legacy_persistence_context_behavior is true' do + config_override :legacy_persistence_context_behavior, true + + it 'does not remember its persistence context when created' do + expect(person.collection_name).to be == :people + end + + it 'does not remember its context when queried specifically' do + person_by_id = Person.with(options) { Person.find(_id: person._id) } + expect(person_by_id.collection_name).to be == :people + end + + it 'does not remember its context when queried generally' do + person # force the person to be created + person_generally = Person.with(options) { Person.all[0] } + expect(person_generally.collection_name).to be == :people + end + + it 'cannot be reloaded without specifying the context' do + expect { person.reload }.to raise_error + end + + it 'cannot be updated without specifying the context' do + person.update username: 'zyg15' + expect(Person.with(options) { Person.first.username }).to be == 'zyg14' + end + end + end end From 2748c157f28dfa753cef2f81df33365966b8cfc2 Mon Sep 17 00:00:00 2001 From: Dmitry Rybakov Date: Mon, 11 Dec 2023 18:13:31 +0100 Subject: [PATCH 44/52] MONGOID-5710 Ensure env vars are set in docker (#5764) --- .evergreen/config.yml | 50 +++++++++---------- .evergreen/config/axes.yml.erb | 4 -- .evergreen/config/variants.yml.erb | 46 +++++++++-------- .evergreen/run-tests-docker.sh | 2 +- .evergreen/run-tests.sh | 2 + gemfiles/bson_min.gemfile | 2 +- gemfiles/rails-7.1.gemfile | 11 ++++ lib/mongoid/config/options.rb | 6 ++- spec/integration/app_spec.rb | 2 +- .../bson_object_id_serializer_spec.rb | 5 +- spec/shared | 2 +- 11 files changed, 73 insertions(+), 59 deletions(-) create mode 100644 gemfiles/rails-7.1.gemfile diff --git a/.evergreen/config.yml b/.evergreen/config.yml index 427a539dd..3bec67c7b 100644 --- a/.evergreen/config.yml +++ b/.evergreen/config.yml @@ -571,10 +571,6 @@ axes: display_name: "Rails master" variables: RAILS: "master" - - id: "5.2" - display_name: "Rails 5.2" - variables: - RAILS: "5.2" - id: "6.0" display_name: "Rails 6.0" variables: @@ -767,18 +763,6 @@ buildvariants: tasks: - name: "test" -- matrix_name: "rails-5" - matrix_spec: - ruby: ["ruby-2.7"] - driver: ["current"] - mongodb-version: "4.0" - topology: "standalone" - rails: ['5.2'] - os: rhel80 - display_name: "${rails}, ${driver}, ${mongodb-version}" - tasks: - - name: "test" - - matrix_name: "i18n-fallbacks" matrix_spec: ruby: "ruby-2.7" @@ -791,45 +775,59 @@ buildvariants: tasks: - name: "test" -- matrix_name: app-tests-ruby-3 +- matrix_name: app-tests-rails-7 matrix_spec: - ruby: ["ruby-3.0", "ruby-3.1", "ruby-3.2"] + ruby: ["ruby-3.1", "ruby-3.2"] driver: ["current"] mongodb-version: '6.0' topology: standalone app-tests: yes - rails: ['6.0', '6.1', '7.0', '7.1'] - os: rhel80 + rails: ['6.1', '7.0', '7.1'] + os: ubuntu-20.04 display_name: "app tests ${driver}, ${ruby}, ${rails}" tasks: - name: "test" -- matrix_name: app-tests-ruby-2.7 +- matrix_name: app-tests-rails-6-0 matrix_spec: - ruby: ruby-2.7 + ruby: ["ruby-2.7"] driver: ["current"] mongodb-version: '5.0' topology: standalone app-tests: yes - rails: ['5.2'] + rails: ['6.0'] os: rhel80 display_name: "app tests ${driver}, ${ruby}, ${rails}" tasks: - name: "test" -- matrix_name: app-tests-jruby +- matrix_name: app-tests-jruby-9-3 matrix_spec: - jruby: ["jruby-9.4", "jruby-9.3"] + jruby: ["jruby-9.3"] driver: ["current"] mongodb-version: '5.0' topology: standalone app-tests: yes - rails: ['6.0'] + rails: ['6.1'] os: ubuntu-20.04 display_name: "app tests ${driver}, ${jruby}" tasks: - name: "test" +# https://github.com/rails/rails/issues/49737 +#- matrix_name: app-tests-jruby-9-4 +# matrix_spec: +# jruby: ["jruby-9.4"] +# driver: ["current"] +# mongodb-version: '5.0' +# topology: standalone +# app-tests: yes +# rails: ['7.1'] +# os: ubuntu-20.04 +# display_name: "app tests ${driver}, ${jruby}" +# tasks: +# - name: "test" + - matrix_name: "auto-encryption" matrix_spec: ruby: ruby-3.1 diff --git a/.evergreen/config/axes.yml.erb b/.evergreen/config/axes.yml.erb index d3a3c25b5..ea31eccc0 100644 --- a/.evergreen/config/axes.yml.erb +++ b/.evergreen/config/axes.yml.erb @@ -198,10 +198,6 @@ axes: display_name: "Rails master" variables: RAILS: "master" - - id: "5.2" - display_name: "Rails 5.2" - variables: - RAILS: "5.2" - id: "6.0" display_name: "Rails 6.0" variables: diff --git a/.evergreen/config/variants.yml.erb b/.evergreen/config/variants.yml.erb index cd7ac6cea..9cdeb14d4 100644 --- a/.evergreen/config/variants.yml.erb +++ b/.evergreen/config/variants.yml.erb @@ -146,18 +146,6 @@ buildvariants: tasks: - name: "test" -- matrix_name: "rails-5" - matrix_spec: - ruby: ["ruby-2.7"] - driver: ["current"] - mongodb-version: "4.0" - topology: "standalone" - rails: ['5.2'] - os: rhel80 - display_name: "${rails}, ${driver}, ${mongodb-version}" - tasks: - - name: "test" - - matrix_name: "i18n-fallbacks" matrix_spec: ruby: "ruby-2.7" @@ -170,45 +158,59 @@ buildvariants: tasks: - name: "test" -- matrix_name: app-tests-ruby-3 +- matrix_name: app-tests-rails-7 matrix_spec: - ruby: ["ruby-3.0", "ruby-3.1", "ruby-3.2"] + ruby: ["ruby-3.1", "ruby-3.2"] driver: ["current"] mongodb-version: '6.0' topology: standalone app-tests: yes - rails: ['6.0', '6.1', '7.0', '7.1'] - os: rhel80 + rails: ['6.1', '7.0', '7.1'] + os: ubuntu-20.04 display_name: "app tests ${driver}, ${ruby}, ${rails}" tasks: - name: "test" -- matrix_name: app-tests-ruby-2.7 +- matrix_name: app-tests-rails-6-0 matrix_spec: - ruby: ruby-2.7 + ruby: ["ruby-2.7"] driver: ["current"] mongodb-version: '5.0' topology: standalone app-tests: yes - rails: ['5.2'] + rails: ['6.0'] os: rhel80 display_name: "app tests ${driver}, ${ruby}, ${rails}" tasks: - name: "test" -- matrix_name: app-tests-jruby +- matrix_name: app-tests-jruby-9-3 matrix_spec: - jruby: ["jruby-9.4", "jruby-9.3"] + jruby: ["jruby-9.3"] driver: ["current"] mongodb-version: '5.0' topology: standalone app-tests: yes - rails: ['6.0'] + rails: ['6.1'] os: ubuntu-20.04 display_name: "app tests ${driver}, ${jruby}" tasks: - name: "test" +# https://github.com/rails/rails/issues/49737 +#- matrix_name: app-tests-jruby-9-4 +# matrix_spec: +# jruby: ["jruby-9.4"] +# driver: ["current"] +# mongodb-version: '5.0' +# topology: standalone +# app-tests: yes +# rails: ['7.1'] +# os: ubuntu-20.04 +# display_name: "app tests ${driver}, ${jruby}" +# tasks: +# - name: "test" + - matrix_name: "auto-encryption" matrix_spec: ruby: ruby-3.1 diff --git a/.evergreen/run-tests-docker.sh b/.evergreen/run-tests-docker.sh index 444305e8f..038be89d0 100755 --- a/.evergreen/run-tests-docker.sh +++ b/.evergreen/run-tests-docker.sh @@ -10,7 +10,7 @@ fi params= for var in MONGODB_VERSION TOPOLOGY RVM_RUBY \ - SINGLE_MONGOS AUTH SSL APP_TESTS FLE + SINGLE_MONGOS AUTH SSL APP_TESTS FLE RAILS DRIVER TEST_I18N_FALLBACKS do value="${!var}" if test -n "$value"; then diff --git a/.evergreen/run-tests.sh b/.evergreen/run-tests.sh index 3901edc17..b48d881f4 100755 --- a/.evergreen/run-tests.sh +++ b/.evergreen/run-tests.sh @@ -52,6 +52,8 @@ if echo $RVM_RUBY |grep -q jruby && test "$DRIVER" = master-jruby; then gem install *.gem) fi +git config --global --add safe.directory "*" + if test "$DRIVER" = "master"; then bundle install --gemfile=gemfiles/driver_master.gemfile BUNDLE_GEMFILE=gemfiles/driver_master.gemfile diff --git a/gemfiles/bson_min.gemfile b/gemfiles/bson_min.gemfile index a17fb792e..d68b7f839 100644 --- a/gemfiles/bson_min.gemfile +++ b/gemfiles/bson_min.gemfile @@ -2,7 +2,7 @@ source "https://rubygems.org" gemspec path: '..' -gem 'bson', '4.14.0' +gem 'bson', '4.14.1' gem 'mongo' gem 'actionpack' diff --git a/gemfiles/rails-7.1.gemfile b/gemfiles/rails-7.1.gemfile new file mode 100644 index 000000000..3b6debf9e --- /dev/null +++ b/gemfiles/rails-7.1.gemfile @@ -0,0 +1,11 @@ +# rubocop:todo all +source 'https://rubygems.org' + +gem 'actionpack', '~> 7.1' +gem 'activemodel', '~> 7.1' + +gemspec path: '..' + +require_relative './standard' + +standard_dependencies diff --git a/lib/mongoid/config/options.rb b/lib/mongoid/config/options.rb index b8248c649..abc85f8bf 100644 --- a/lib/mongoid/config/options.rb +++ b/lib/mongoid/config/options.rb @@ -57,7 +57,11 @@ def option(name, options = {}) # # @return [ Hash ] The defaults. def reset - settings.replace(defaults) + # do this via the setter for each option, so that any defined on_change + # handlers can be invoked. + defaults.each do |setting, default| + send(:"#{setting}=", default) + end end # Get the settings or initialize a new empty hash. diff --git a/spec/integration/app_spec.rb b/spec/integration/app_spec.rb index 1ebdfcbc5..ca67b44cd 100644 --- a/spec/integration/app_spec.rb +++ b/spec/integration/app_spec.rb @@ -305,7 +305,7 @@ def adjust_app_gemfile(rails_version: SpecConfig.instance.rails_version) gemfile_lines << "gem 'mongoid', path: '#{File.expand_path(BASE)}'\n" if rails_version gemfile_lines.delete_if do |line| - line =~ /rails/ + line =~ /gem ['"]rails['"]/ end if rails_version == 'master' gemfile_lines << "gem 'rails', git: 'https://github.com/rails/rails'\n" diff --git a/spec/mongoid/railties/bson_object_id_serializer_spec.rb b/spec/mongoid/railties/bson_object_id_serializer_spec.rb index 0e04dc7af..694e14232 100644 --- a/spec/mongoid/railties/bson_object_id_serializer_spec.rb +++ b/spec/mongoid/railties/bson_object_id_serializer_spec.rb @@ -5,8 +5,9 @@ require 'active_job' require 'mongoid/railties/bson_object_id_serializer' -describe Mongoid::Railties::ActiveJobSerializers::BsonObjectIdSerializer do - let(:serializer) { described_class.instance } +describe 'Mongoid::Railties::ActiveJobSerializers::BsonObjectIdSerializer' do + + let(:serializer) { Mongoid::Railties::ActiveJobSerializers::BsonObjectIdSerializer.instance } let(:object_id) { BSON::ObjectId.new } describe '#serialize' do diff --git a/spec/shared b/spec/shared index d0d8ca90e..53a38fe8f 160000 --- a/spec/shared +++ b/spec/shared @@ -1 +1 @@ -Subproject commit d0d8ca90e660d6af31ab0e541bd120b39f81c1ba +Subproject commit 53a38fe8f165fd71687cf6205d2f1aac0dbde10b From 5237212c36d778e7bff3e53de1ad49058da2866d Mon Sep 17 00:00:00 2001 From: Gintaras Sakalauskas Date: Fri, 15 Dec 2023 22:59:45 +0200 Subject: [PATCH 45/52] Fix typo in mongoid.yml (#5769) --- lib/rails/generators/mongoid/config/templates/mongoid.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/rails/generators/mongoid/config/templates/mongoid.yml b/lib/rails/generators/mongoid/config/templates/mongoid.yml index 9c179307d..997bfe5d1 100644 --- a/lib/rails/generators/mongoid/config/templates/mongoid.yml +++ b/lib/rails/generators/mongoid/config/templates/mongoid.yml @@ -51,8 +51,8 @@ development: # (default: the database specified above or admin) # auth_source: admin - # Force a the driver cluster to behave in a certain manner instead of auto- - # discovering. Can be one of: :direct, :replica_set, :sharded. Set to :direct + # Force the driver cluster to behave in a certain manner instead of auto-discovering. + # Can be one of: :direct, :replica_set, :sharded. Set to :direct # when connecting to hidden members of a replica set. # connect: :direct From 031d2297172a9c4f9e627104ba00217ddf0a5ae1 Mon Sep 17 00:00:00 2001 From: Dmitry Rybakov Date: Tue, 19 Dec 2023 10:08:27 +0100 Subject: [PATCH 46/52] MONGOID-5708 Add :on to transaction callbacks (#5767) --- lib/mongoid/clients/sessions.rb | 87 +++++++ lib/mongoid/interceptable.rb | 4 +- spec/mongoid/clients/transactions_spec.rb | 218 +++++++++++++----- .../clients/transactions_spec_models.rb | 44 ++++ 4 files changed, 298 insertions(+), 55 deletions(-) diff --git a/lib/mongoid/clients/sessions.rb b/lib/mongoid/clients/sessions.rb index 29c05cad3..b85906978 100644 --- a/lib/mongoid/clients/sessions.rb +++ b/lib/mongoid/clients/sessions.rb @@ -16,6 +16,10 @@ def self.included(base) module ClassMethods + # Actions that can be used to trigger transactional callbacks. + # @api private + CALLBACK_ACTIONS = [:create, :destroy, :update] + # Execute a block within the context of a session. # # @example Execute some operations in the context of a session. @@ -101,6 +105,49 @@ def transaction(options = {}, session_options: {}) end end + # Sets up a callback is called after a commit of a transaction. + # The callback is called only if the document is created, updated, or destroyed + # in the transaction. + # + # See +ActiveSupport::Callbacks::ClassMethods::set_callback+ for more + # information about method parameters and possible options. + def after_commit(*args, &block) + set_options_for_callbacks!(args) + set_callback(:commit, :after, *args, &block) + end + + # Shortcut for +after_commit :hook, on: [ :create, :update ]+ + def after_save_commit(*args, &block) + set_options_for_callbacks!(args, on: [ :create, :update ]) + set_callback(:commit, :after, *args, &block) + end + + # Shortcut for +after_commit :hook, on: :create+. + def after_create_commit(*args, &block) + set_options_for_callbacks!(args, on: :create) + set_callback(:commit, :after, *args, &block) + end + + # Shortcut for +after_commit :hook, on: :update+. + def after_update_commit(*args, &block) + set_options_for_callbacks!(args, on: :update) + set_callback(:commit, :after, *args, &block) + end + + # Shortcut for +after_commit :hook, on: :destroy+. + def after_destroy_commit(*args, &block) + set_options_for_callbacks!(args, on: :destroy) + set_callback(:commit, :after, *args, &block) + end + + # This callback is called after a create, update, or destroy are rolled back. + # + # Please check the documentation of +after_commit+ for options. + def after_rollback(*args, &block) + set_options_for_callbacks!(args) + set_callback(:rollback, :after, *args, &block) + end + private # @return [ Mongo::Session ] Session for the current client. @@ -145,6 +192,46 @@ def abort_transaction(session) doc.run_after_callbacks(:rollback) end end + + # Transforms custom options for after_commit and after_rollback callbacks + # into options for +set_callback+. + def set_options_for_callbacks!(args) + options = args.extract_options! + args << options + + if options[:on] + fire_on = Array(options[:on]) + assert_valid_transaction_action(fire_on) + options[:if] = [ + -> { transaction_include_any_action?(fire_on) }, + *options[:if] + ] + end + end + + # Asserts that the given actions are valid for after_commit + # and after_rollback callbacks. + # + # @param [ Array ] actions Actions to be checked. + # @raise [ ArgumentError ] If any of the actions is not valid. + def assert_valid_transaction_action(actions) + if (actions - CALLBACK_ACTIONS).any? + raise ArgumentError, ":on conditions for after_commit and after_rollback callbacks have to be one of #{CALLBACK_ACTIONS}" + end + end + + def transaction_include_any_action?(actions) + actions.any? do |action| + case action + when :create + persisted? && previously_new_record? + when :update + !(previously_new_record? || destroyed?) + when :destroy + destroyed? + end + end + end end end end diff --git a/lib/mongoid/interceptable.rb b/lib/mongoid/interceptable.rb index 9b5f205f8..c9f3bf7b3 100644 --- a/lib/mongoid/interceptable.rb +++ b/lib/mongoid/interceptable.rb @@ -44,7 +44,9 @@ module Interceptable # @api private define_model_callbacks :persist_parent - define_model_callbacks :commit, :rollback, only: :after + define_callbacks :commit, :rollback, + only: :after, + scope: [:kind, :name] attr_accessor :before_callback_halted end diff --git a/spec/mongoid/clients/transactions_spec.rb b/spec/mongoid/clients/transactions_spec.rb index cdc62f1c0..a166864b9 100644 --- a/spec/mongoid/clients/transactions_spec.rb +++ b/spec/mongoid/clients/transactions_spec.rb @@ -786,49 +786,134 @@ def capture_exception before do Mongoid::Clients.with_name(:default).database.collections.each(&:drop) TransactionsSpecPerson.collection.create + TransactionsSpecPersonWithOnCreate.collection.create + TransactionsSpecPersonWithOnUpdate.collection.create + TransactionsSpecPersonWithOnDestroy.collection.create TransactionSpecRaisesBeforeSave.collection.create TransactionSpecRaisesAfterSave.collection.create end context 'when commit the transaction' do context 'create' do - let!(:subject) do - person = nil - TransactionsSpecPerson.transaction do - person = TransactionsSpecPerson.create!(name: 'James Bond') + context 'without :on option' do + let!(:subject) do + person = nil + TransactionsSpecPerson.transaction do + person = TransactionsSpecPerson.create!(name: 'James Bond') + end + person end - person + + it_behaves_like 'commit callbacks are called' end - it_behaves_like 'commit callbacks are called' + context 'when callback has :on option' do + let!(:subject) do + person = nil + TransactionsSpecPersonWithOnCreate.transaction do + person = TransactionsSpecPersonWithOnCreate.create!(name: 'James Bond') + end + person + end + + it_behaves_like 'commit callbacks are called' + end end context 'save' do - let(:subject) do - TransactionsSpecPerson.create!(name: 'James Bond').tap do |subject| - subject.after_commit_counter.reset - subject.after_rollback_counter.reset + context 'without :on option' do + let(:subject) do + TransactionsSpecPerson.create!(name: 'James Bond').tap do |subject| + subject.after_commit_counter.reset + subject.after_rollback_counter.reset + end + end + + context 'when modified once' do + before do + subject.transaction do + subject.name = 'Austin Powers' + subject.save! + end + end + + it_behaves_like 'commit callbacks are called' + end + + context 'when modified multiple times' do + before do + subject.transaction do + subject.name = 'Austin Powers' + subject.save! + subject.name = 'Jason Bourne' + subject.save! + end + end + + it_behaves_like 'commit callbacks are called' end end - context 'when modified once' do + context 'with :on option' do + let(:subject) do + TransactionsSpecPersonWithOnUpdate.create!(name: 'James Bond').tap do |subject| + subject.after_commit_counter.reset + subject.after_rollback_counter.reset + end + end + + context 'when modified once' do + before do + subject.transaction do + subject.name = 'Austin Powers' + subject.save! + end + end + + it_behaves_like 'commit callbacks are called' + end + + context 'when modified multiple times' do + before do + subject.transaction do + subject.name = 'Austin Powers' + subject.save! + subject.name = 'Jason Bourne' + subject.save! + end + end + + it_behaves_like 'commit callbacks are called' + end + end + end + + context 'update_attributes' do + context 'without :on option' do + let(:subject) do + TransactionsSpecPerson.create!(name: 'James Bond').tap do |subject| + subject.after_commit_counter.reset + subject.after_rollback_counter.reset + end + end + before do subject.transaction do - subject.name = 'Austin Powers' - subject.save! + subject.update_attributes!(name: 'Austin Powers') end end it_behaves_like 'commit callbacks are called' end - context 'when modified multiple times' do + context 'when callback has on option' do + let(:subject) do + TransactionsSpecPersonWithOnUpdate.create!(name: 'Jason Bourne') + end + before do - subject.transaction do - subject.name = 'Austin Powers' - subject.save! - subject.name = 'Jason Bourne' - subject.save! + TransactionsSpecPersonWithOnUpdate.transaction do + subject.update_attributes!(name: 'Foma Kiniaev') end end @@ -836,61 +921,86 @@ def capture_exception end end - context 'update_attributes' do - let(:subject) do - TransactionsSpecPerson.create!(name: 'James Bond').tap do |subject| - subject.after_commit_counter.reset - subject.after_rollback_counter.reset + context 'destroy' do + context 'without :on option' do + let(:after_commit_counter) do + TransactionsSpecCounter.new end - end - before do - subject.transaction do - subject.update_attributes!(name: 'Austin Powers') + let(:after_rollback_counter) do + TransactionsSpecCounter.new end - end - it_behaves_like 'commit callbacks are called' - end + let(:subject) do + TransactionsSpecPerson.create!(name: 'James Bond').tap do |p| + p.after_commit_counter = after_commit_counter + p.after_rollback_counter = after_rollback_counter + end + end - context 'destroy' do - let(:after_commit_counter) do - TransactionsSpecCounter.new - end + before do + subject.transaction do + subject.destroy + end + end - let(:after_rollback_counter) do - TransactionsSpecCounter.new + it_behaves_like 'commit callbacks are called' end - let(:subject) do - TransactionsSpecPerson.create!(name: 'James Bond').tap do |p| - p.after_commit_counter = after_commit_counter - p.after_rollback_counter = after_rollback_counter + context 'with :on option' do + let(:after_commit_counter) do + TransactionsSpecCounter.new end - end - before do - subject.transaction do - subject.destroy + let(:after_rollback_counter) do + TransactionsSpecCounter.new end - end - it_behaves_like 'commit callbacks are called' + let(:subject) do + TransactionsSpecPersonWithOnDestroy.create!(name: 'James Bond').tap do |p| + p.after_commit_counter = after_commit_counter + p.after_rollback_counter = after_rollback_counter + end + end + + before do + subject.transaction do + subject.destroy + end + end + + it_behaves_like 'commit callbacks are called' + end end end context 'when rollback the transaction' do context 'create' do - let!(:subject) do - person = nil - TransactionsSpecPerson.transaction do - person = TransactionsSpecPerson.create!(name: 'James Bond') - raise Mongoid::Errors::Rollback + context 'without :on option' do + let!(:subject) do + person = nil + TransactionsSpecPerson.transaction do + person = TransactionsSpecPerson.create!(name: 'James Bond') + raise Mongoid::Errors::Rollback + end + person end - person + + it_behaves_like 'rollback callbacks are called' end - it_behaves_like 'rollback callbacks are called' + context 'with :on option' do + let!(:subject) do + person = nil + TransactionsSpecPersonWithOnCreate.transaction do + person = TransactionsSpecPersonWithOnCreate.create!(name: 'James Bond') + raise Mongoid::Errors::Rollback + end + person + end + + it_behaves_like 'rollback callbacks are called' + end end context 'save' do diff --git a/spec/mongoid/clients/transactions_spec_models.rb b/spec/mongoid/clients/transactions_spec_models.rb index c1629c074..eafd0f6d9 100644 --- a/spec/mongoid/clients/transactions_spec_models.rb +++ b/spec/mongoid/clients/transactions_spec_models.rb @@ -50,6 +50,50 @@ class TransactionsSpecPerson end end +class TransactionsSpecPersonWithOnCreate + include Mongoid::Document + include TransactionsSpecCountable + + field :name, type: String + + after_commit on: :create do + after_commit_counter.inc + end + + after_rollback on: :create do + after_rollback_counter.inc + end +end + +class TransactionsSpecPersonWithOnUpdate + include Mongoid::Document + include TransactionsSpecCountable + + field :name, type: String + + after_commit on: :update do + after_commit_counter.inc + end + + after_rollback on: :update do + after_rollback_counter.inc + end +end + +class TransactionsSpecPersonWithOnDestroy + include Mongoid::Document + include TransactionsSpecCountable + + field :name, type: String + + after_commit on: :destroy do + after_commit_counter.inc + end + + after_rollback on: :destroy do + after_rollback_counter.inc + end +end class TransactionSpecRaisesBeforeSave include Mongoid::Document include TransactionsSpecCountable From 65b9f7ec65b9a9e0c03ad89b7dfdb39ef7a484cb Mon Sep 17 00:00:00 2001 From: Dmitry Rybakov Date: Tue, 19 Dec 2023 17:03:37 +0100 Subject: [PATCH 47/52] MONGOID-5574 Drop support for Rails 5 and Ruby 2.6 (#5768) --- .evergreen/config.yml | 19 +---- .evergreen/config/axes.yml.erb | 4 - .evergreen/config/variants.yml.erb | 15 +--- .github/workflows/test.yml | 20 +---- .rubocop.yml | 2 +- README.md | 6 +- docs/reference/compatibility.txt | 2 +- docs/release-notes/mongoid-9.0.txt | 14 ++++ gemfiles/rails-5.2.gemfile | 11 --- .../association/embedded/embeds_many/proxy.rb | 4 +- lib/mongoid/config/environment.rb | 6 +- mongoid.gemspec | 2 +- spec/mongoid/clients/options_spec.rb | 2 +- spec/mongoid/criteria_spec.rb | 18 +---- spec/mongoid/document_spec.rb | 78 ------------------- spec/mongoid/errors/mongoid_error_spec.rb | 30 ++----- 16 files changed, 37 insertions(+), 196 deletions(-) delete mode 100644 gemfiles/rails-5.2.gemfile diff --git a/.evergreen/config.yml b/.evergreen/config.yml index 3bec67c7b..c170cb3da 100644 --- a/.evergreen/config.yml +++ b/.evergreen/config.yml @@ -480,10 +480,6 @@ axes: - id: "jruby" display_name: JRuby Version values: - - id: "jruby-9.3" - display_name: jruby-9.3 - variables: - RVM_RUBY: "jruby-9.3" - id: "jruby-9.4" display_name: jruby-9.4 variables: @@ -662,7 +658,7 @@ buildvariants: - matrix_name: "jruby" matrix_spec: - jruby: ["jruby-9.4", "jruby-9.3"] + jruby: ["jruby-9.4"] driver: ["current"] topology: ['replica-set', 'sharded-cluster'] mongodb-version: '5.0' @@ -801,19 +797,6 @@ buildvariants: tasks: - name: "test" -- matrix_name: app-tests-jruby-9-3 - matrix_spec: - jruby: ["jruby-9.3"] - driver: ["current"] - mongodb-version: '5.0' - topology: standalone - app-tests: yes - rails: ['6.1'] - os: ubuntu-20.04 - display_name: "app tests ${driver}, ${jruby}" - tasks: - - name: "test" - # https://github.com/rails/rails/issues/49737 #- matrix_name: app-tests-jruby-9-4 # matrix_spec: diff --git a/.evergreen/config/axes.yml.erb b/.evergreen/config/axes.yml.erb index ea31eccc0..f14d93237 100644 --- a/.evergreen/config/axes.yml.erb +++ b/.evergreen/config/axes.yml.erb @@ -107,10 +107,6 @@ axes: - id: "jruby" display_name: JRuby Version values: - - id: "jruby-9.3" - display_name: jruby-9.3 - variables: - RVM_RUBY: "jruby-9.3" - id: "jruby-9.4" display_name: jruby-9.4 variables: diff --git a/.evergreen/config/variants.yml.erb b/.evergreen/config/variants.yml.erb index 9cdeb14d4..a4e514e57 100644 --- a/.evergreen/config/variants.yml.erb +++ b/.evergreen/config/variants.yml.erb @@ -45,7 +45,7 @@ buildvariants: - matrix_name: "jruby" matrix_spec: - jruby: ["jruby-9.4", "jruby-9.3"] + jruby: ["jruby-9.4"] driver: ["current"] topology: ['replica-set', 'sharded-cluster'] mongodb-version: '5.0' @@ -184,19 +184,6 @@ buildvariants: tasks: - name: "test" -- matrix_name: app-tests-jruby-9-3 - matrix_spec: - jruby: ["jruby-9.3"] - driver: ["current"] - mongodb-version: '5.0' - topology: standalone - app-tests: yes - rails: ['6.1'] - os: ubuntu-20.04 - display_name: "app tests ${driver}, ${jruby}" - tasks: - - name: "test" - # https://github.com/rails/rails/issues/49737 #- matrix_name: app-tests-jruby-9-4 # matrix_spec: diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index dc8961f5c..413714509 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -42,14 +42,6 @@ jobs: driver: current gemfile: Gemfile experimental: false - - mongodb: '6.0' - ruby: ruby-3.1 - topology: replica_set - os: ubuntu-20.04 - task: test - driver: master - gemfile: gemfiles/driver_master.gemfile - experimental: true - mongodb: '6.0' ruby: ruby-3.0 topology: replica_set @@ -109,17 +101,7 @@ jobs: gemfile: gemfiles/rails-6.0.gemfile experimental: false - mongodb: '6.0' - ruby: ruby-2.7 - topology: server - os: ubuntu-20.04 - task: test - driver: current - rails: '5.2' - fle: helper - gemfile: gemfiles/rails-5.2.gemfile - experimental: false - - mongodb: '6.0' - ruby: jruby-9.3 + ruby: jruby-9.4 topology: server os: ubuntu-20.04 task: test diff --git a/.rubocop.yml b/.rubocop.yml index 59a04444b..bdb558ac7 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -4,7 +4,7 @@ require: - rubocop-rspec AllCops: - TargetRubyVersion: 2.6 + TargetRubyVersion: 2.7 NewCops: enable Exclude: - 'spec/shared/**/*' diff --git a/README.md b/README.md index 8ebdee4ea..f2394f4e0 100644 --- a/README.md +++ b/README.md @@ -19,8 +19,8 @@ Compatibility Mongoid supports and is tested against: -- MRI 2.6 - 3.1 -- JRuby 9.3 +- MRI 2.7 - 3.1 +- JRuby 9.4 - MongoDB server 3.6 - 6.0 Issues @@ -39,7 +39,7 @@ Support License ------- -Copyright (c) 2015-Present MongoDB Inc. +Copyright (c) 2015-Present MongoDB Inc. Copyright (c) 2009-2016 Durran Jordan Permission is hereby granted, free of charge, to any person obtaining diff --git a/docs/reference/compatibility.txt b/docs/reference/compatibility.txt index db177a63e..e30124b83 100644 --- a/docs/reference/compatibility.txt +++ b/docs/reference/compatibility.txt @@ -122,7 +122,7 @@ is deprecated. - - - - - |checkmark| + - - |checkmark| * - 8.1 diff --git a/docs/release-notes/mongoid-9.0.txt b/docs/release-notes/mongoid-9.0.txt index 48926df5a..fa9c74160 100644 --- a/docs/release-notes/mongoid-9.0.txt +++ b/docs/release-notes/mongoid-9.0.txt @@ -17,6 +17,20 @@ The complete list of releases is available `on GitHub please consult GitHub releases for detailed release notes and JIRA for the complete list of issues fixed in each release, including bug fixes. + +Support for Ruby 2.6 and JRuby 9.3 Dropped +------------------------------------------- + +Mongoid 9 requires Ruby 2.7 or newer or JRuby 9.4. Earlier Ruby and JRuby +versions are not supported. + + +Support for Rails 5 Dropped +----------------------------- + +Mongoid 9 requires Rails 6.0 or newer. Earlier Rails versions are not supported. + + Deprecated class ``Mongoid::Errors::InvalidStorageParent`` removed ------------------------------------------------------------------ diff --git a/gemfiles/rails-5.2.gemfile b/gemfiles/rails-5.2.gemfile deleted file mode 100644 index b3d7c2f23..000000000 --- a/gemfiles/rails-5.2.gemfile +++ /dev/null @@ -1,11 +0,0 @@ -# rubocop:todo all -source 'https://rubygems.org' - -gem 'actionpack', '~> 5.2' -gem 'activemodel', '~> 5.2' - -gemspec path: '..' - -require_relative './standard' - -standard_dependencies diff --git a/lib/mongoid/association/embedded/embeds_many/proxy.rb b/lib/mongoid/association/embedded/embeds_many/proxy.rb index f7e5d810a..a2d14a035 100644 --- a/lib/mongoid/association/embedded/embeds_many/proxy.rb +++ b/lib/mongoid/association/embedded/embeds_many/proxy.rb @@ -337,8 +337,8 @@ def exists?(id_or_conditions = :none) # @yield [ Object ] Yields each enumerable element to the block. # # @return [ Document | Array | nil ] A document or matching documents. - def find(*args, &block) - criteria.find(*args, &block) + def find(...) + criteria.find(...) end # Get all the documents in the association that are loaded into memory. diff --git a/lib/mongoid/config/environment.rb b/lib/mongoid/config/environment.rb index 430087da7..a04ce5f52 100644 --- a/lib/mongoid/config/environment.rb +++ b/lib/mongoid/config/environment.rb @@ -66,11 +66,7 @@ def load_yaml(path, environment = nil) ] result = ERB.new(contents).result - data = if RUBY_VERSION < '2.6' - YAML.safe_load(result, permitted_classes, [], true) - else - YAML.safe_load(result, permitted_classes: permitted_classes, aliases: true) - end + data = YAML.safe_load(result, permitted_classes: permitted_classes, aliases: true) unless data.is_a?(Hash) raise Mongoid::Errors::InvalidConfigFile.new(path) diff --git a/mongoid.gemspec b/mongoid.gemspec index 0a12d74ec..d6266ea5c 100644 --- a/mongoid.gemspec +++ b/mongoid.gemspec @@ -32,7 +32,7 @@ Gem::Specification.new do |s| warn "[#{s.name}] Warning: No private key present, creating unsigned gem." end - s.required_ruby_version = ">= 2.6" + s.required_ruby_version = ">= 2.7" s.required_rubygems_version = ">= 1.3.6" # Ruby 3.0 requires ActiveModel 6.0 or higher. diff --git a/spec/mongoid/clients/options_spec.rb b/spec/mongoid/clients/options_spec.rb index 6fc995a9b..d7bb94dbd 100644 --- a/spec/mongoid/clients/options_spec.rb +++ b/spec/mongoid/clients/options_spec.rb @@ -442,7 +442,7 @@ let(:options) { { read: :secondary } } it 'does not create a new cluster' do - expect(connections_during).to eq(connections_before) + expect(connections_during).to be <= connections_before end it 'does not disconnect the original cluster' do diff --git a/spec/mongoid/criteria_spec.rb b/spec/mongoid/criteria_spec.rb index 3808166cc..f207358b0 100644 --- a/spec/mongoid/criteria_spec.rb +++ b/spec/mongoid/criteria_spec.rb @@ -288,23 +288,9 @@ Band.where(name: "Depeche Mode") end - # as_json changed in rails 6 to call as_json on serializable_hash. - # https://github.com/rails/rails/commit/2e5cb980a448e7f4ab00df6e9ad4c1cc456616aa - context 'rails < 6' do - max_rails_version '5.2' - - it "returns the criteria as a json hash" do - expect(criteria.as_json).to eq([ band.serializable_hash ]) - end - end - - context 'rails >= 6' do - min_rails_version '6.0' - - it "returns the criteria as a json hash" do - expect(criteria.as_json).to eq([ band.serializable_hash.as_json ]) - end + it "returns the criteria as a json hash" do + expect(criteria.as_json).to eq([ band.serializable_hash.as_json ]) end end diff --git a/spec/mongoid/document_spec.rb b/spec/mongoid/document_spec.rb index 547b5f26b..938ac8a8f 100644 --- a/spec/mongoid/document_spec.rb +++ b/spec/mongoid/document_spec.rb @@ -458,84 +458,6 @@ class << self; attr_accessor :name; end end end end - - context 'deprecated :compact option' do - # Since rails 6 differs in how it treats id fields, - # run this test on one version of rails. Currently rails 6 is in beta, - # when it is released this version should be changed to 6. - max_rails_version '5.2' - - before do - # These tests require a specific set of defined attributes - # on the model - expect(church.as_json.keys.sort).to eq(%w(_id location name)) - end - - context 'deprecation' do - let(:church) do - Church.create!(name: 'St. Basil') - end - - let(:message) do - '#as_json :compact option is deprecated. Please call #compact on the returned Hash object instead.' - end - - it 'logs a deprecation warning when :compact is given' do - expect(Mongoid::Warnings).to receive(:warn_as_json_compact_deprecated) - church.as_json(compact: true) - end - - it 'does not log a deprecation warning when :compact is not given' do - expect(Mongoid::Warnings).to_not receive(:warn_as_json_compact_deprecated) - church.as_json - end - end - - context 'there is a nil valued attribute' do - let(:church) do - Church.create!(name: 'St. Basil') - end - - it 'returns non-nil fields and _id only' do - actual = church.as_json(compact: true) - expect(actual).to eq('_id' => church.id, 'name' => 'St. Basil') - end - end - - context 'all attrbutes are nil valued' do - let(:church) do - Church.create! - end - - it 'returns a hash with _id only' do - actual = church.as_json(compact: true) - expect(actual).to eq('_id' => church.id) - end - end - - context 'there are no nil valued attributes' do - let(:church) do - Church.create!(name: 'St. Basil', location: {}) - end - - it 'returns all fields and _id' do - actual = church.as_json(compact: true) - expect(actual).to eq('_id' => church.id, 'name' => 'St. Basil', - 'location' => {}) - end - end - - context 'when option is specified as a truthy value' do - let(:church) do - Church.create!(name: 'St. Basil') - end - - it 'returns non-nil fields and _id only' do - actual = church.as_json(compact: 1) - expect(actual).to eq('_id' => church.id, 'name' => 'St. Basil') - end - end - end end describe "#as_document" do diff --git a/spec/mongoid/errors/mongoid_error_spec.rb b/spec/mongoid/errors/mongoid_error_spec.rb index cea968413..0f2a34874 100644 --- a/spec/mongoid/errors/mongoid_error_spec.rb +++ b/spec/mongoid/errors/mongoid_error_spec.rb @@ -10,28 +10,14 @@ let(:options) { {} } before do - # JRuby 9.3 RUBY_VERSION is set to 2.6.8, but the behavior matches Ruby 2.7. - # See https://github.com/jruby/jruby/issues/7184 - if RUBY_VERSION >= '2.7' || (BSON::Environment.jruby? && JRUBY_VERSION >= '9.3') - {"message_title" => "message", "summary_title" => "summary", "resolution_title" => "resolution"}.each do |key, name| - expect(::I18n).to receive(:translate).with("mongoid.errors.messages.#{key}", **{}).and_return(name) - end - - ["message", "summary", "resolution"].each do |name| - expect(::I18n).to receive(:translate). - with("mongoid.errors.messages.#{key}.#{name}", **{}). - and_return(name) - end - else - {"message_title" => "message", "summary_title" => "summary", "resolution_title" => "resolution"}.each do |key, name| - expect(::I18n).to receive(:translate).with("mongoid.errors.messages.#{key}", {}).and_return(name) - end - - ["message", "summary", "resolution"].each do |name| - expect(::I18n).to receive(:translate). - with("mongoid.errors.messages.#{key}.#{name}", {}). - and_return(name) - end + {"message_title" => "message", "summary_title" => "summary", "resolution_title" => "resolution"}.each do |key, name| + expect(::I18n).to receive(:translate).with("mongoid.errors.messages.#{key}", **{}).and_return(name) + end + + ["message", "summary", "resolution"].each do |name| + expect(::I18n).to receive(:translate). + with("mongoid.errors.messages.#{key}.#{name}", **{}). + and_return(name) end error.compose_message(key, options) From 0ab88b9052fa7b82b70f9611340945559b69448c Mon Sep 17 00:00:00 2001 From: Johnny Shields <27655+johnnyshields@users.noreply.github.com> Date: Wed, 20 Dec 2023 18:53:26 +0900 Subject: [PATCH 48/52] MONGOID-5717 Fix some cases of missing **kwargs pass-thru (#5758) Co-authored-by: Dmitry Rybakov --- lib/mongoid/scopable.rb | 4 ++-- spec/mongoid/criteria/marshalable_spec.rb | 4 ++-- spec/mongoid/extensions/set_spec.rb | 4 +--- spec/mongoid/validatable/uniqueness_spec.rb | 8 ++++---- 4 files changed, 9 insertions(+), 11 deletions(-) diff --git a/lib/mongoid/scopable.rb b/lib/mongoid/scopable.rb index 08cb8ecba..35635acdc 100644 --- a/lib/mongoid/scopable.rb +++ b/lib/mongoid/scopable.rb @@ -290,9 +290,9 @@ def check_scope_validity(value) def define_scope_method(name) singleton_class.class_eval do ruby2_keywords( - define_method(name) do |*args| + define_method(name) do |*args, **kwargs| scoping = _declared_scopes[name] - scope = instance_exec(*args, &scoping[:scope]) + scope = instance_exec(*args, **kwargs, &scoping[:scope]) extension = scoping[:extension] to_merge = scope || queryable criteria = to_merge.empty_and_chainable? ? to_merge : with_default_scope.merge(to_merge) diff --git a/spec/mongoid/criteria/marshalable_spec.rb b/spec/mongoid/criteria/marshalable_spec.rb index c7050d0e6..fed243ae5 100644 --- a/spec/mongoid/criteria/marshalable_spec.rb +++ b/spec/mongoid/criteria/marshalable_spec.rb @@ -31,8 +31,8 @@ let(:dump) { Marshal.dump(criteria) } before do - expect_any_instance_of(Mongoid::Criteria).to receive(:marshal_dump).and_wrap_original do |m, *args| - data = m.call(*args) + expect_any_instance_of(Mongoid::Criteria).to receive(:marshal_dump).and_wrap_original do |m, *args, **kwargs| + data = m.call(*args, **kwargs) data[1] = :mongo1x data end diff --git a/spec/mongoid/extensions/set_spec.rb b/spec/mongoid/extensions/set_spec.rb index cde2d7da0..f81e43c5f 100644 --- a/spec/mongoid/extensions/set_spec.rb +++ b/spec/mongoid/extensions/set_spec.rb @@ -113,9 +113,7 @@ end before do - expect(BigDecimal).to receive(:mongoize).exactly(4).times.and_wrap_original do |m, *args| - 1 - end + expect(BigDecimal).to receive(:mongoize).exactly(4).times.and_return(1) end context "when the input is a set" do diff --git a/spec/mongoid/validatable/uniqueness_spec.rb b/spec/mongoid/validatable/uniqueness_spec.rb index ec8453dc4..e931ac3d7 100644 --- a/spec/mongoid/validatable/uniqueness_spec.rb +++ b/spec/mongoid/validatable/uniqueness_spec.rb @@ -20,8 +20,8 @@ end it "reads from the primary" do - expect_any_instance_of(Mongoid::Criteria).to receive(:read).once.and_wrap_original do |m, *args| - crit = m.call(*args) + expect_any_instance_of(Mongoid::Criteria).to receive(:read).once.and_wrap_original do |m, *args, **kwargs| + crit = m.call(*args, **kwargs) expect(crit.view.options["read"]).to eq({ "mode" => :primary }) crit end @@ -1670,8 +1670,8 @@ let(:word) { Word.create! } it "reads from the primary" do - expect_any_instance_of(Mongoid::Criteria).to receive(:read).once.and_wrap_original do |m, *args| - crit = m.call(*args) + expect_any_instance_of(Mongoid::Criteria).to receive(:read).once.and_wrap_original do |m, *args, **kwargs| + crit = m.call(*args, **kwargs) expect(crit.options[:read]).to eq({ mode: :primary }) crit end From 8d458e97741d0e0c0b2d8ef88ccc30127f5bfc97 Mon Sep 17 00:00:00 2001 From: Johnny Shields <27655+johnnyshields@users.noreply.github.com> Date: Mon, 15 Jan 2024 23:28:28 +0900 Subject: [PATCH 49/52] MONGOID-5721 - Update documentation and tests to remove references to MongoDB server 3.4 and prior (#5762) --- docs/reference/associations.txt | 58 ++++++------------- docs/reference/configuration.txt | 20 ++++--- docs/reference/queries.txt | 3 +- docs/reference/sessions.txt | 11 ++-- lib/mongoid/config.rb | 8 +-- .../mongoid/config/templates/mongoid.yml | 10 ++-- .../matcher_operator_data/bits_all_clear.yml | 15 ----- .../matcher_operator_data/bits_all_set.yml | 15 ----- .../matcher_operator_data/bits_any_clear.yml | 15 ----- .../matcher_operator_data/bits_any_set.yml | 15 ----- .../matcher_operator_data/size.yml | 3 - .../matcher_operator_data/type.yml | 6 -- .../matcher_operator_data/type_array.yml | 1 - .../matcher_operator_data/type_decimal.yml | 4 -- .../has_and_belongs_to_many/proxy_spec.rb | 1 - .../referenced/has_many/proxy_spec.rb | 2 - spec/mongoid/clients/sessions_spec.rb | 2 - spec/mongoid/contextual/atomic_spec.rb | 12 ---- spec/mongoid/contextual/mongo_spec.rb | 15 ----- spec/mongoid/criteria_spec.rb | 1 - spec/mongoid/scopable_spec.rb | 1 - 21 files changed, 45 insertions(+), 173 deletions(-) diff --git a/docs/reference/associations.txt b/docs/reference/associations.txt index 011549710..520ca44e5 100644 --- a/docs/reference/associations.txt +++ b/docs/reference/associations.txt @@ -693,30 +693,22 @@ Querying Loaded Associations ```````````````````````````` Mongoid query methods can be used on embedded associations of documents which -are already loaded in the application. This mechanism is sometimes called -"embedded matching" or "embedded document matching" and it is implemented -entirely in Mongoid - the queries are NOT sent to the server. - -Embedded matching is supported for most general-purpose query operators. It -is not implemented for :ref:`text search `, :manual:`geospatial query -operators `, -operators that execute JavaScript code (:manual:`$where `) -and operators that are implemented via other server functionality such as -:manual:`$expr ` -and :manual:`$jsonSchema `. +are already loaded in the application. This mechanism is called +"embedded matching" and it is implemented entirely in Mongoid--the queries +are NOT sent to the server. The following operators are supported: -- :manual:`Bitwise operators ` - :manual:`Comparison operators ` - :manual:`Logical operators ` -- :manual:`$comment ` +- :manual:`Array query operators ` - :manual:`$exists ` - :manual:`$mod ` - :manual:`$type ` - :manual:`$regex ` (``$options`` field is only supported when the ``$regex`` argument is a string) -- :manual:`Array query operators ` +- :manual:`Bitwise operators ` +- :manual:`$comment ` For example, using the model definitions just given, we could query tours on a loaded band: @@ -726,33 +718,19 @@ tours on a loaded band: band = Band.where(name: 'Astral Projection').first tours = band.tours.where(year: {'$gte' => 2000}) -Embedded Matching Vs Server Behavior +Embedded Matching vs Server Behavior ```````````````````````````````````` -Mongoid aims to provide the same semantics when performing embedded matching -as those of MongoDB server. This means, for example, when the server only -accepts arguments of particular types for a particular operator, Mongoid -would also only accept arguments of the corresponding types. - -The following deviations are known: - -- Mongoid embedded matchers, because they are implemented on the client side, - behave the same regardless of the server version that backs the application. - As such, it is possible for Mongoid to deviate from server behavior if - the server itself behaves differently in different versions. All operators are implemented in - Mongoid regardless of backing deployment's server version. - - As of this writing, the known cases of such deviation are: - - - 3.2 and earlier servers not validating ``$size`` arguments as strictly as newer versions do. - - 4.0 and earlier servers not validating ``$type`` arguments as strictly as newer versions - do (allowing invalid arguments like 0, for example). - - 3.2 and earlier servers not supporting ``Decimal128`` for ``$type``, as well as allowing invalid - arguments such as negative numbers (smaller than -1) and numbers that are greater than 19 - (not including 127, the argument for the ``MaxKey`` type). - - 3.4 and earlier servers not supporting arrays for ``$type``. - - 3.0 and earlier servers not supporting bitwise operators. +Mongoid's embedded matching aims to support the same functionality and +semantics as native queries on the latest MongoDB server version. +Note the following known limitations: +- Embedded matching is not implemented for :ref:`text search `, + :manual:`geospatial query operators `, + operators that execute JavaScript code (:manual:`$where `) + and operators that are implemented via other server functionality such as + :manual:`$expr ` + and :manual:`$jsonSchema `. - Mongoid DSL expands ``Range`` arguments to hashes with ``$gte`` and ``$lte`` conditions. `In some cases `_ @@ -764,7 +742,9 @@ The following deviations are known: possible `_ to specify a regular expression object as the pattern and also provide options. -In general, Mongoid adopts the behavior of current server versions and validates more strictly. +- MongoDB Server 4.0 and earlier servers do not validate ``$type`` arguments strictly + (for example, allowing invalid arguments like 0). This is validated more strictly on + the client side. .. _omit-id: diff --git a/docs/reference/configuration.txt b/docs/reference/configuration.txt index 12601f3be..92b1a44e2 100644 --- a/docs/reference/configuration.txt +++ b/docs/reference/configuration.txt @@ -171,10 +171,12 @@ for details on driver options. # roles: # - 'dbOwner' - # Change the default authentication mechanism. Valid options are: :scram, - # :mongodb_cr, :mongodb_x509, and :plain. Note that all authentication - # mechanisms require username and password, with the exception of :mongodb_x509. - # Default is :scram since MongoDB 3.0; earlier versions defaulted to :plain. + # Change the default authentication mechanism. Valid options include: + # :scram, :scram256, :mongodb_cr, :mongodb_x509, :gssapi, :aws, :plain. + # MongoDB Server defaults to :scram, which will use "SCRAM-SHA-256" if available, + # otherwise fallback to "SCRAM-SHA-1" (:scram256 will always use "SCRAM-SHA-256".) + # This setting is handled by the MongoDB Ruby Driver. Please refer to: + # https://mongodb.com/docs/ruby-driver/current/reference/authentication/ # auth_mech: :scram # The database or source to authenticate the user against. @@ -263,10 +265,10 @@ for details on driver options. # the setting is ignored. # allow_bson5_decimal128: false - # Application name that is printed to the mongodb logs upon establishing - # a connection in server versions >= 3.4. Note that the name cannot - # exceed 128 bytes. It is also used as the database name if the - # database name is not explicitly defined. + # Application name that is printed to the MongoDB logs upon establishing + # a connection. Note that the name cannot exceed 128 bytes in length. + # It is also used as the database name if the database name is not + # explicitly defined. (default: nil) # app_name: nil # When this flag is false, callbacks for embedded documents will not be @@ -280,7 +282,7 @@ for details on driver options. # around_callbacks_for_embeds: false # Sets the async_query_executor for the application. By default the thread pool executor - # is set to `:immediate. Options are: + # is set to `:immediate`. Options are: # # - :immediate - Initializes a single +Concurrent::ImmediateExecutor+ # - :global_thread_pool - Initializes a single +Concurrent::ThreadPoolExecutor+ diff --git a/docs/reference/queries.txt b/docs/reference/queries.txt index 6f1af85af..b8dd5c902 100644 --- a/docs/reference/queries.txt +++ b/docs/reference/queries.txt @@ -1422,8 +1422,7 @@ Mongoid also has some helpful methods on criteria. *Find documents for a provided JavaScript expression, optionally with the specified variables added to the evaluation scope. The scope argument is supported in MongoDB 4.2 and lower.* - *In MongoDB 3.6 and higher, prefer* :manual:`$expr - ` *over* ``for_js``. + *Prefer* :manual:`$expr ` *over* ``for_js``. - .. code-block:: ruby diff --git a/docs/reference/sessions.txt b/docs/reference/sessions.txt index 47eafb62a..4faf35c07 100644 --- a/docs/reference/sessions.txt +++ b/docs/reference/sessions.txt @@ -12,11 +12,10 @@ Sessions :depth: 2 :class: singlecol -Versions 3.6 and higher of the MongoDB server support sessions. You can use sessions with Mongoid in a similar way -that you would execute a transaction in ActiveRecord. Namely, you can call a method, ``#with_session`` on a model class -or on an instance of a model and execute some operations in a block. All operations in the block will be -executed in the context of single session. Please see the MongoDB Ruby driver documentation for what session options -are available. +You can use sessions with Mongoid in a similar way that you would execute a transaction in ActiveRecord. +Namely, you can call a method, ``#with_session`` on a model class or on an instance of a model and execute +some operations in a block. All operations in the block will be executed in the context of single session. +Please see the MongoDB Ruby driver documentation for what session options are available. Please note the following limitations of sessions: @@ -26,8 +25,6 @@ Please note the following limitations of sessions: - All model classes and instances used within the session block must use the same driver client. For example, if you have specified different ``storage_options`` for another model used in the block than that of the model class or instance on which ``#with_session`` is called, you will get an error. -- All connected MongoDB servers must be version 3.6 or higher. - Using a Session via Model#with_session ====================================== diff --git a/lib/mongoid/config.rb b/lib/mongoid/config.rb index 86681d89e..352f36bc1 100644 --- a/lib/mongoid/config.rb +++ b/lib/mongoid/config.rb @@ -23,10 +23,10 @@ module Config LOCK = Mutex.new - # Application name that is printed to the mongodb logs upon establishing - # a connection in server versions >= 3.4. Note that the name cannot - # exceed 128 bytes. It is also used as the database name if the - # database name is not explicitly defined. + # Application name that is printed to the MongoDB logs upon establishing + # a connection. Note that the name cannot exceed 128 bytes in length. + # It is also used as the database name if the database name is not + # explicitly defined. option :app_name, default: nil # (Deprecated) In MongoDB 4.0 and earlier, set whether to create diff --git a/lib/rails/generators/mongoid/config/templates/mongoid.yml b/lib/rails/generators/mongoid/config/templates/mongoid.yml index 997bfe5d1..8cbb62d58 100644 --- a/lib/rails/generators/mongoid/config/templates/mongoid.yml +++ b/lib/rails/generators/mongoid/config/templates/mongoid.yml @@ -41,10 +41,12 @@ development: # roles: # - 'dbOwner' - # Change the default authentication mechanism. Valid options are: :scram, - # :mongodb_cr, :mongodb_x509, and :plain. Note that all authentication - # mechanisms require username and password, with the exception of :mongodb_x509. - # Default is :scram since MongoDB 3.0; earlier versions defaulted to :plain. + # Change the default authentication mechanism. Valid options include: + # :scram, :scram256, :mongodb_cr, :mongodb_x509, :gssapi, :aws, :plain. + # MongoDB Server defaults to :scram, which will use "SCRAM-SHA-256" if available, + # otherwise fallback to "SCRAM-SHA-1" (:scram256 will always use "SCRAM-SHA-256".) + # This setting is handled by the MongoDB Ruby Driver. Please refer to: + # https://mongodb.com/docs/ruby-driver/current/reference/authentication/ # auth_mech: :scram # The database or source to authenticate the user against. diff --git a/spec/integration/matcher_operator_data/bits_all_clear.yml b/spec/integration/matcher_operator_data/bits_all_clear.yml index efaa3d5df..cd6a9afd6 100644 --- a/spec/integration/matcher_operator_data/bits_all_clear.yml +++ b/spec/integration/matcher_operator_data/bits_all_clear.yml @@ -6,7 +6,6 @@ a: $bitsAllClear: 35 matches: true - min_server_version: 3.2 - name: existing field - does not match with int document: @@ -16,7 +15,6 @@ a: $bitsAllClear: 24 matches: false - min_server_version: 3.2 - name: existing field - matches with binData document: @@ -29,7 +27,6 @@ IW== type: :generic matches: true - min_server_version: 3.2 - name: existing field - does not match with binData document: @@ -42,7 +39,6 @@ IW== type: :generic matches: false - min_server_version: 3.2 - name: existing binData field matches document: @@ -54,7 +50,6 @@ a: $bitsAllClear: 20 matches: true - min_server_version: 3.2 - name: existing binData field does not match document: @@ -66,7 +61,6 @@ a: $bitsAllClear: 15 matches: false - min_server_version: 3.2 - name: existing field - matches with array document: @@ -76,7 +70,6 @@ a: $bitsAllClear: [0, 3] matches: true - min_server_version: 3.2 - name: existing field - does not match with array document: @@ -86,7 +79,6 @@ a: $bitsAllClear: [0, 2] matches: false - min_server_version: 3.2 - name: float condition representable as an integer document: @@ -96,7 +88,6 @@ a: $bitsAllClear: 35.0 matches: true - min_server_version: 3.2 - name: float condition not representable as an integer document: @@ -106,7 +97,6 @@ a: $bitsAllClear: 35.1 error: true - min_server_version: 3.2 - name: string condition document: @@ -116,7 +106,6 @@ a: $bitsAllClear: hello error: true - min_server_version: 3.2 - name: empty array condition document: @@ -126,7 +115,6 @@ a: $bitsAllClear: [] matches: true - min_server_version: 3.2 - name: integer 0 condition document: @@ -136,7 +124,6 @@ a: $bitsAllClear: 0 matches: true - min_server_version: 3.2 - name: negative integer condition document: @@ -146,7 +133,6 @@ a: $bitsAllClear: -1 error: true - min_server_version: 3.2 - name: negative float condition document: @@ -156,4 +142,3 @@ a: $bitsAllClear: -1.0 error: true - min_server_version: 3.2 diff --git a/spec/integration/matcher_operator_data/bits_all_set.yml b/spec/integration/matcher_operator_data/bits_all_set.yml index 87d60305a..3b0df8a79 100644 --- a/spec/integration/matcher_operator_data/bits_all_set.yml +++ b/spec/integration/matcher_operator_data/bits_all_set.yml @@ -6,7 +6,6 @@ a: $bitsAllSet: 50 matches: true - min_server_version: 3.2 - name: existing field - does not match with int document: @@ -16,7 +15,6 @@ a: $bitsAllSet: 50 matches: false - min_server_version: 3.2 - name: existing field - matches with binData document: @@ -29,7 +27,6 @@ Ng== type: :generic matches: true - min_server_version: 3.2 - name: existing field - does not match with binData document: @@ -42,7 +39,6 @@ MC== type: :generic matches: false - min_server_version: 3.2 - name: existing binData field matches document: @@ -54,7 +50,6 @@ a: $bitsAllSet: 48 matches: true - min_server_version: 3.2 - name: existing binData field does not match document: @@ -66,7 +61,6 @@ a: $bitsAllSet: 54 matches: false - min_server_version: 3.2 - name: existing field - matches with array document: @@ -76,7 +70,6 @@ a: $bitsAllSet: [2, 4] matches: true - min_server_version: 3.2 - name: existing field - does not match with array document: @@ -86,7 +79,6 @@ a: $bitsAllSet: [0, 3] matches: false - min_server_version: 3.2 - name: float condition representable as an integer document: @@ -96,7 +88,6 @@ a: $bitsAllSet: 50.0 matches: true - min_server_version: 3.2 - name: float condition not representable as an integer document: @@ -106,7 +97,6 @@ a: $bitsAllSet: 50.1 error: true - min_server_version: 3.2 - name: string condition document: @@ -116,7 +106,6 @@ a: $bitsAllSet: hello error: true - min_server_version: 3.2 - name: empty array condition document: @@ -126,7 +115,6 @@ a: $bitsAllSet: [] matches: true - min_server_version: 3.2 - name: integer 0 condition document: @@ -136,7 +124,6 @@ a: $bitsAllSet: 0 matches: true - min_server_version: 3.2 - name: negative integer condition document: @@ -146,7 +133,6 @@ a: $bitsAllSet: -1 error: true - min_server_version: 3.2 - name: negative float condition document: @@ -156,4 +142,3 @@ a: $bitsAllSet: -1.0 error: true - min_server_version: 3.2 diff --git a/spec/integration/matcher_operator_data/bits_any_clear.yml b/spec/integration/matcher_operator_data/bits_any_clear.yml index e3b392fdb..ee16d3fae 100644 --- a/spec/integration/matcher_operator_data/bits_any_clear.yml +++ b/spec/integration/matcher_operator_data/bits_any_clear.yml @@ -6,7 +6,6 @@ a: $bitsAnyClear: 35 matches: true - min_server_version: 3.2 - name: existing field - does not match with int document: @@ -16,7 +15,6 @@ a: $bitsAnyClear: 35 matches: true - min_server_version: 3.2 - name: existing field - matches with binData document: @@ -29,7 +27,6 @@ IW== type: :generic matches: true - min_server_version: 3.2 - name: existing field - does not match with binData document: @@ -42,7 +39,6 @@ IW== type: :generic matches: false - min_server_version: 3.2 - name: existing binData field matches document: @@ -54,7 +50,6 @@ a: $bitsAnyClear: 35 matches: true - min_server_version: 3.2 - name: existing binData field does not match document: @@ -66,7 +61,6 @@ a: $bitsAnyClear: 32 matches: false - min_server_version: 3.2 - name: existing field - matches with array document: @@ -76,7 +70,6 @@ a: $bitsAnyClear: [0, 2] matches: true - min_server_version: 3.2 - name: existing field - does not match with array document: @@ -86,7 +79,6 @@ a: $bitsAnyClear: [2, 4] matches: false - min_server_version: 3.2 - name: float condition representable as an integer document: @@ -96,7 +88,6 @@ a: $bitsAnyClear: 35.0 matches: true - min_server_version: 3.2 - name: float condition not representable as an integer document: @@ -106,7 +97,6 @@ a: $bitsAnyClear: 35.1 error: true - min_server_version: 3.2 - name: string condition document: @@ -116,7 +106,6 @@ a: $bitsAnyClear: 'hello' error: true - min_server_version: 3.2 - name: empty array condition document: @@ -126,7 +115,6 @@ a: $bitsAnyClear: [] matches: false - min_server_version: 3.2 - name: integer 0 condition document: @@ -136,7 +124,6 @@ a: $bitsAnyClear: 0 matches: false - min_server_version: 3.2 - name: negative integer condition document: @@ -146,7 +133,6 @@ a: $bitsAnyClear: -1 error: true - min_server_version: 3.2 - name: negative float condition document: @@ -156,4 +142,3 @@ a: $bitsAnyClear: -1.0 error: true - min_server_version: 3.2 diff --git a/spec/integration/matcher_operator_data/bits_any_set.yml b/spec/integration/matcher_operator_data/bits_any_set.yml index f95d32816..3f41e33de 100644 --- a/spec/integration/matcher_operator_data/bits_any_set.yml +++ b/spec/integration/matcher_operator_data/bits_any_set.yml @@ -6,7 +6,6 @@ a: $bitsAnySet: 35 matches: true - min_server_version: 3.2 - name: existing field - does not match with int document: @@ -16,7 +15,6 @@ a: $bitsAllSet: 35 matches: false - min_server_version: 3.2 - name: existing field - matches with binData document: @@ -29,7 +27,6 @@ IW== type: :generic matches: true - min_server_version: 3.2 - name: existing field - does not match with binData document: @@ -42,7 +39,6 @@ IW== type: :generic matches: false - min_server_version: 3.2 - name: existing binData field matches document: @@ -54,7 +50,6 @@ a: $bitsAnySet: 37 matches: true - min_server_version: 3.2 - name: existing binData field does not match document: @@ -66,7 +61,6 @@ a: $bitsAnySet: 20 matches: false - min_server_version: 3.2 - name: existing field - matches with array document: @@ -76,7 +70,6 @@ a: $bitsAnySet: [0, 2] matches: true - min_server_version: 3.2 - name: existing field - does not match with array document: @@ -86,7 +79,6 @@ a: $bitsAnySet: [0, 3] matches: false - min_server_version: 3.2 - name: float condition representable as an integer document: @@ -96,7 +88,6 @@ a: $bitsAnySet: 35 matches: true - min_server_version: 3.2 - name: float condition not representable as an integer document: @@ -106,7 +97,6 @@ a: $bitsAnySet: 35.1 error: true - min_server_version: 3.2 - name: string condition document: @@ -116,7 +106,6 @@ a: $bitsAnySet: hello error: true - min_server_version: 3.2 - name: empty array condition document: @@ -126,7 +115,6 @@ a: $bitsAnySet: [] matches: false - min_server_version: 3.2 - name: integer 0 condition document: @@ -136,7 +124,6 @@ a: $bitsAnySet: 0 matches: false - min_server_version: 3.2 - name: negative integer condition document: @@ -146,7 +133,6 @@ a: $bitsAnySet: -1 error: true - min_server_version: 3.2 - name: negative float condition document: @@ -156,4 +142,3 @@ a: $bitsAnySet: -1.0 error: true - min_server_version: 3.2 diff --git a/spec/integration/matcher_operator_data/size.yml b/spec/integration/matcher_operator_data/size.yml index 43a950bac..362dcd465 100644 --- a/spec/integration/matcher_operator_data/size.yml +++ b/spec/integration/matcher_operator_data/size.yml @@ -86,7 +86,6 @@ position: $size: foo error: true - min_server_version: 3.4 - name: floating point argument document: @@ -95,7 +94,6 @@ position: $size: 2.5 error: true - min_server_version: 3.4 - name: negative integer argument document: @@ -104,7 +102,6 @@ position: $size: -2 error: true - min_server_version: 3.4 - name: empty array field value document: diff --git a/spec/integration/matcher_operator_data/type.yml b/spec/integration/matcher_operator_data/type.yml index 50fe20e38..55188b386 100644 --- a/spec/integration/matcher_operator_data/type.yml +++ b/spec/integration/matcher_operator_data/type.yml @@ -14,7 +14,6 @@ pi: $type: -2 error: true - min_server_version: 3.2 - name: type is too large document: @@ -23,7 +22,6 @@ pi: $type: 42 error: true - min_server_version: 3.2 - name: array argument for type - matches document: @@ -32,7 +30,6 @@ pi: $type: [1] matches: true - min_server_version: 3.6 - name: array argument for type - does not match document: @@ -41,7 +38,6 @@ pi: $type: [2] matches: false - min_server_version: 3.6 - name: hash argument document: @@ -58,7 +54,6 @@ pi: $type: [1, 2] matches: true - min_server_version: 3.6 - name: array with multiple elements - does not match document: @@ -67,4 +62,3 @@ pi: $type: [2, 3] matches: false - min_server_version: 3.6 diff --git a/spec/integration/matcher_operator_data/type_array.yml b/spec/integration/matcher_operator_data/type_array.yml index 3661e5c88..5e13df71f 100644 --- a/spec/integration/matcher_operator_data/type_array.yml +++ b/spec/integration/matcher_operator_data/type_array.yml @@ -5,7 +5,6 @@ prices: $type: 4 matches: true - min_server_version: 3.6 - name: existing field - does not match array document: diff --git a/spec/integration/matcher_operator_data/type_decimal.yml b/spec/integration/matcher_operator_data/type_decimal.yml index 2be9f8a64..9c7834591 100644 --- a/spec/integration/matcher_operator_data/type_decimal.yml +++ b/spec/integration/matcher_operator_data/type_decimal.yml @@ -7,7 +7,6 @@ number: $type: 19 matches: true - min_server_version: 3.4 - name: Decimal128 field - matches document: @@ -18,7 +17,6 @@ decimal128_field: $type: 19 matches: true - min_server_version: 3.4 - name: existing field - does not match decimal document: @@ -27,7 +25,6 @@ name: $type: 19 matches: false - min_server_version: 3.4 # Requires :map_big_decimal_to_decimal128 option to be true. - name: BigDecimal field - matches @@ -38,4 +35,3 @@ big_decimal_field: $type: 19 matches: true - min_server_version: 3.4 diff --git a/spec/mongoid/association/referenced/has_and_belongs_to_many/proxy_spec.rb b/spec/mongoid/association/referenced/has_and_belongs_to_many/proxy_spec.rb index 1bccfb122..d8cad4034 100644 --- a/spec/mongoid/association/referenced/has_and_belongs_to_many/proxy_spec.rb +++ b/spec/mongoid/association/referenced/has_and_belongs_to_many/proxy_spec.rb @@ -2761,7 +2761,6 @@ end context 'when providing a collation' do - min_server_version '3.4' let(:preferences) do person.preferences.where(name: "FIRST").collation(locale: 'en_US', strength: 2).to_a diff --git a/spec/mongoid/association/referenced/has_many/proxy_spec.rb b/spec/mongoid/association/referenced/has_many/proxy_spec.rb index 3f1f345a0..1e9f0d1b2 100644 --- a/spec/mongoid/association/referenced/has_many/proxy_spec.rb +++ b/spec/mongoid/association/referenced/has_many/proxy_spec.rb @@ -2578,8 +2578,6 @@ def with_transaction_via(model, &block) end context 'when providing a collation' do - min_server_version '3.4' - let(:posts) { person.posts.where(title: 'FIRST').collation(locale: 'en_US', strength: 2) } it 'applies the collation option to the query' do diff --git a/spec/mongoid/clients/sessions_spec.rb b/spec/mongoid/clients/sessions_spec.rb index 75dba0fbb..87783dd1c 100644 --- a/spec/mongoid/clients/sessions_spec.rb +++ b/spec/mongoid/clients/sessions_spec.rb @@ -39,7 +39,6 @@ context 'when a session is used on a model class' do context 'when sessions are supported' do - min_server_version '3.6' around do |example| Mongoid::Clients.with_name(:other).database.collections.each(&:drop) @@ -178,7 +177,6 @@ end context 'when sessions are supported' do - min_server_version '3.6' around do |example| Mongoid::Clients.with_name(:other).database.collections.each(&:drop) diff --git a/spec/mongoid/contextual/atomic_spec.rb b/spec/mongoid/contextual/atomic_spec.rb index c38a6fac1..bfdf07052 100644 --- a/spec/mongoid/contextual/atomic_spec.rb +++ b/spec/mongoid/contextual/atomic_spec.rb @@ -78,7 +78,6 @@ end context 'when the criteria has a collation' do - min_server_version '3.4' let(:criteria) do Band.where(members: [ "DAVE" ]).collation(locale: 'en_US', strength: 2) @@ -175,7 +174,6 @@ end context 'when the criteria has a collation' do - min_server_version '3.4' let(:criteria) do Band.where(members: [ "DAVE" ]).collation(locale: 'en_US', strength: 2) @@ -247,7 +245,6 @@ end context 'when the criteria has a collation' do - min_server_version '3.4' let!(:depeche_mode) do Band.create!(members: [ "Dave" ], likes: 60) @@ -327,7 +324,6 @@ end context 'when the criteria has a collation' do - min_server_version '3.4' let!(:depeche_mode) do Band.create!(members: [ "Dave" ]) @@ -392,7 +388,6 @@ end context 'when the criteria has a collation' do - min_server_version '3.4' let!(:depeche_mode) { Band.create!(members: [ "Dave" ], years: 3) } let(:criteria) do @@ -458,7 +453,6 @@ end context 'when the criteria has a collation' do - min_server_version '3.4' let!(:depeche_mode) do Band.create!(members: [ "Dave" ]) @@ -513,7 +507,6 @@ end context 'when the criteria has a collation' do - min_server_version '3.4' let!(:depeche_mode) do Band.create!(members: [ "Dave" ]) @@ -571,7 +564,6 @@ end context 'when the criteria has a collation' do - min_server_version '3.4' let!(:depeche_mode) do Band.create!(members: [ "Dave", "Alan", "Fletch" ]) @@ -629,7 +621,6 @@ end context 'when the criteria has a collation' do - min_server_version '3.4' let!(:depeche_mode) do Band.create!(members: [ "Dave" ]) @@ -695,7 +686,6 @@ end context 'when the criteria has a collation' do - min_server_version '3.4' let!(:depeche_mode) do Band.create!(members: [ "Dave" ]) @@ -757,7 +747,6 @@ end context 'when the criteria has a collation' do - min_server_version '3.4' let!(:depeche_mode) do Band.create!(members: [ "Dave" ]) @@ -967,7 +956,6 @@ end context 'when the criteria has a collation' do - min_server_version '3.4' let!(:depeche_mode) do Band.create!(name: "Depeche Mode", years: 10) diff --git a/spec/mongoid/contextual/mongo_spec.rb b/spec/mongoid/contextual/mongo_spec.rb index 1e38025e3..3ee238252 100644 --- a/spec/mongoid/contextual/mongo_spec.rb +++ b/spec/mongoid/contextual/mongo_spec.rb @@ -138,7 +138,6 @@ end context 'when a collation is specified' do - min_server_version '3.4' let(:context) do described_class.new(criteria) @@ -305,7 +304,6 @@ end context 'when the criteria has a collation' do - min_server_version '3.4' let(:criteria) do Band.where(name: "DEPECHE MODE").collation(locale: 'en_US', strength: 2) @@ -410,7 +408,6 @@ end context 'when the criteria has a collation' do - min_server_version '3.4' let(:criteria) do Band.where(name: "DEPECHE MODE").collation(locale: 'en_US', strength: 2) @@ -534,8 +531,6 @@ end context 'when a collation is specified' do - min_server_version '3.4' - before do Band.create!(name: 'DEPECHE MODE') end @@ -1165,7 +1160,6 @@ end context 'when the criteria has a collation' do - min_server_version '3.4' let(:criteria) do Band.where(name: "DEPECHE MODE").collation(locale: 'en_US', strength: 2) @@ -1622,7 +1616,6 @@ end context 'when a collation is specified on the criteria' do - min_server_version '3.4' let(:criteria) do Band.where(name: "DEPECHE MODE").collation(locale: 'en_US', strength: 2) @@ -1776,7 +1769,6 @@ end context 'when a collation is specified on the criteria' do - min_server_version '3.4' let(:criteria) do Band.where(name: "DEPECHE MODE").collation(locale: 'en_US', strength: 2) @@ -1851,7 +1843,6 @@ end context 'when a collation is specified on the criteria' do - min_server_version '3.4' let(:criteria) do Band.where(name: "DEPECHE MODE").collation(locale: 'en_US', strength: 2) @@ -1928,7 +1919,6 @@ end context 'when the criteria has a collation' do - min_server_version '3.4' let(:criteria) do Band.where(name: "DEPECHE MODE").collation(locale: 'en_US', strength: 2) @@ -2101,7 +2091,6 @@ end context 'when the criteria has a collation' do - min_server_version '3.4' let(:criteria) do Band.where(name: "DEPECHE MODE").collation(locale: 'en_US', strength: 2) @@ -2274,7 +2263,6 @@ end context 'when the criteria has a collation' do - min_server_version '3.4' let(:criteria) do Band.where(name: "DEPECHE MODE").collation(locale: 'en_US', strength: 2) @@ -2447,7 +2435,6 @@ end context 'when the criteria has a collation' do - min_server_version '3.4' let(:criteria) do Band.where(name: "DEPECHE MODE").collation(locale: 'en_US', strength: 2) @@ -3496,7 +3483,6 @@ end context 'when provided array filters' do - min_server_version '3.6' before do Band.delete_all @@ -3718,7 +3704,6 @@ end context 'when provided array filters' do - min_server_version '3.6' before do Band.delete_all diff --git a/spec/mongoid/criteria_spec.rb b/spec/mongoid/criteria_spec.rb index f207358b0..69d1dbb3f 100644 --- a/spec/mongoid/criteria_spec.rb +++ b/spec/mongoid/criteria_spec.rb @@ -2625,7 +2625,6 @@ def self.ages; self; end end context "when querying on a BSON::Decimal128" do - min_server_version '3.4' let(:decimal) do BSON::Decimal128.new("0.0005") diff --git a/spec/mongoid/scopable_spec.rb b/spec/mongoid/scopable_spec.rb index 7535d6de1..4f971c07f 100644 --- a/spec/mongoid/scopable_spec.rb +++ b/spec/mongoid/scopable_spec.rb @@ -343,7 +343,6 @@ def self.default_scope context "when provided a criteria" do context 'when a collation is defined on the criteria' do - min_server_version '3.4' before do Band.scope(:tests, ->{ Band.where(name: 'TESTING').collation(locale: 'en_US', strength: 2) }) From 4f788253c6e052e29f150fab018a37d94638af9e Mon Sep 17 00:00:00 2001 From: johnnyshields <27655+johnnyshields@users.noreply.github.com> Date: Tue, 16 Jan 2024 12:13:25 +0900 Subject: [PATCH 50/52] Fix tests & rubocop --- .github/workflows/test.yml | 10 +- Rakefile | 2 +- .../{rails-7.1.gemfile => rails_7.1.gemfile} | 0 lib/mongoid/clients/sessions.rb | 26 ++--- lib/mongoid/contextual/atomic.rb | 2 +- lib/mongoid/contextual/mongo.rb | 2 +- lib/mongoid/criteria/findable.rb | 2 +- lib/mongoid/deprecation.rb | 2 +- lib/mongoid/extensions/hash.rb | 2 - lib/mongoid/extensions/string.rb | 2 +- lib/mongoid/interceptable.rb | 6 +- lib/mongoid/persistence_context.rb | 12 +- lib/mongoid/railties/database.rake | 8 +- lib/mongoid/search_indexable.rb | 5 +- lib/mongoid/tasks/database.rb | 1 + lib/mongoid/utils.rb | 2 +- mongoid.gemspec | 10 +- spec/integration/callbacks_spec.rb | 2 +- spec/mongoid/attributes/embedded_spec.rb | 4 +- spec/mongoid/clients/transactions_spec.rb | 2 +- .../clients/transactions_spec_models.rb | 1 + spec/mongoid/criteria/findable_spec.rb | 108 +++++++++--------- .../queryable/extensions/string_spec.rb | 3 +- .../queryable/extensions/symbol_spec.rb | 2 +- spec/mongoid/criteria_spec.rb | 11 +- .../document_persistence_context_spec.rb | 2 +- spec/mongoid/errors/mongoid_error_spec.rb | 2 +- spec/mongoid/extensions/hash_spec.rb | 2 +- spec/mongoid/fields/foreign_key_spec.rb | 11 +- spec/mongoid/interceptable_spec.rb | 24 ++-- spec/mongoid/persistable/savable_spec.rb | 2 +- spec/mongoid/search_indexable_spec.rb | 14 +-- spec/mongoid/tasks/database_rake_spec.rb | 6 +- spec/mongoid/tasks/database_spec.rb | 42 +++---- spec/spec_helper.rb | 2 +- 35 files changed, 169 insertions(+), 165 deletions(-) rename gemfiles/{rails-7.1.gemfile => rails_7.1.gemfile} (100%) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5e31f8ac2..0fde582ae 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -50,23 +50,23 @@ jobs: # Rails versions - ruby: ruby-3.2 - gemfile: gemfiles/rails_7.0.gemfile + gemfile: gemfiles/rails_7.1.gemfile mongodb: '6.0' topology: replica_set - ruby: ruby-3.1 - gemfile: gemfiles/rails_6.1.gemfile + gemfile: gemfiles/rails_7.1.gemfile mongodb: '5.0' topology: sharded_cluster - ruby: ruby-3.0 - gemfile: gemfiles/rails_6.1.gemfile + gemfile: gemfiles/rails_7.0.gemfile mongodb: '4.4' topology: server - ruby: ruby-3.0 - gemfile: gemfiles/rails_6.0.gemfile + gemfile: gemfiles/rails_6.1.gemfile mongodb: '4.4' topology: replica_set - ruby: ruby-2.7 - gemfile: gemfiles/rails_6.0.gemfile + gemfile: gemfiles/rails_6.1.gemfile mongodb: '4.4' topology: server diff --git a/Rakefile b/Rakefile index d457c2ec2..0cc8c2dd8 100644 --- a/Rakefile +++ b/Rakefile @@ -18,7 +18,7 @@ end namespace :generate do desc 'Generates a mongoid.yml from the template' - task :config do + task config: :environment do require 'mongoid' require 'erb' diff --git a/gemfiles/rails-7.1.gemfile b/gemfiles/rails_7.1.gemfile similarity index 100% rename from gemfiles/rails-7.1.gemfile rename to gemfiles/rails_7.1.gemfile diff --git a/lib/mongoid/clients/sessions.rb b/lib/mongoid/clients/sessions.rb index a24c046de..202cf63c6 100644 --- a/lib/mongoid/clients/sessions.rb +++ b/lib/mongoid/clients/sessions.rb @@ -17,7 +17,7 @@ module ClassMethods # Actions that can be used to trigger transactional callbacks. # @api private - CALLBACK_ACTIONS = [:create, :destroy, :update] + CALLBACK_ACTIONS = %i[create destroy update].freeze # Execute a block within the context of a session. # @@ -116,7 +116,7 @@ def after_commit(*args, &block) # Shortcut for +after_commit :hook, on: [ :create, :update ]+ def after_save_commit(*args, &block) - set_options_for_callbacks!(args, on: [ :create, :update ]) + set_options_for_callbacks!(args, on: %i[create update]) set_callback(:commit, :after, *args, &block) end @@ -197,14 +197,14 @@ def set_options_for_callbacks!(args) options = args.extract_options! args << options - if options[:on] - fire_on = Array(options[:on]) - assert_valid_transaction_action(fire_on) - options[:if] = [ - -> { transaction_include_any_action?(fire_on) }, - *options[:if] - ] - end + return unless options[:on] + + fire_on = Array(options[:on]) + assert_valid_transaction_action(fire_on) + options[:if] = [ + -> { transaction_include_any_action?(fire_on) }, + *options[:if] + ] end # Asserts that the given actions are valid for after_commit @@ -213,9 +213,9 @@ def set_options_for_callbacks!(args) # @param [ Array ] actions Actions to be checked. # @raise [ ArgumentError ] If any of the actions is not valid. def assert_valid_transaction_action(actions) - if (actions - CALLBACK_ACTIONS).any? - raise ArgumentError, ":on conditions for after_commit and after_rollback callbacks have to be one of #{CALLBACK_ACTIONS}" - end + return unless (actions - CALLBACK_ACTIONS).any? + + raise ArgumentError.new(":on conditions for after_commit and after_rollback callbacks have to be one of #{CALLBACK_ACTIONS}") end def transaction_include_any_action?(actions) diff --git a/lib/mongoid/contextual/atomic.rb b/lib/mongoid/contextual/atomic.rb index 9c1db692f..c330d3b8a 100644 --- a/lib/mongoid/contextual/atomic.rb +++ b/lib/mongoid/contextual/atomic.rb @@ -257,7 +257,7 @@ def collect_each_operations(ops) # @return [ Hash ] The selector for the atomic $unset operation. def collect_unset_operations(ops) ops.map { |op| op.is_a?(Hash) ? op.keys : op }.flatten - .map { |field| [database_field_name(field), true] }.to_h + .to_h { |field| [database_field_name(field), true] } end end end diff --git a/lib/mongoid/contextual/mongo.rb b/lib/mongoid/contextual/mongo.rb index 317e68dad..f5141e1c0 100644 --- a/lib/mongoid/contextual/mongo.rb +++ b/lib/mongoid/contextual/mongo.rb @@ -982,7 +982,7 @@ def valid_for_count_documents?(hash = view.filter) # Note that `view.filter` is a BSON::Document, and all keys in a # BSON::Document are strings; we don't need to worry about symbol # representations of `$where`. - hash.keys.each do |key| + hash.each_key do |key| return false if key == '$where' return false if hash[key].is_a?(Hash) && !valid_for_count_documents?(hash[key]) end diff --git a/lib/mongoid/criteria/findable.rb b/lib/mongoid/criteria/findable.rb index 3953f6bfb..2d338271e 100644 --- a/lib/mongoid/criteria/findable.rb +++ b/lib/mongoid/criteria/findable.rb @@ -173,7 +173,7 @@ def prepare_ids_for_find(args) # # @return [ true | false ] Whether the arguments are a list. def multi_args?(args) - args.size > 1 || !args.first.is_a?(Hash) && args.first.resizable? + args.size > 1 || (!args.first.is_a?(Hash) && args.first.resizable?) end # Convenience method of raising an invalid options error. diff --git a/lib/mongoid/deprecation.rb b/lib/mongoid/deprecation.rb index 7da765bce..fd7e261cc 100644 --- a/lib/mongoid/deprecation.rb +++ b/lib/mongoid/deprecation.rb @@ -7,7 +7,7 @@ class Deprecation < ::ActiveSupport::Deprecation def initialize # Per change policy, deprecations will be removed in the next major version. - deprecation_horizon = "#{Mongoid::VERSION.split('.').first.to_i + 1}.0".freeze + deprecation_horizon = "#{Mongoid::VERSION.split('.').first.to_i + 1}.0" gem_name = 'Mongoid' super(deprecation_horizon, gem_name) end diff --git a/lib/mongoid/extensions/hash.rb b/lib/mongoid/extensions/hash.rb index 709bca306..9f2eec5d0 100644 --- a/lib/mongoid/extensions/hash.rb +++ b/lib/mongoid/extensions/hash.rb @@ -106,8 +106,6 @@ def to_criteria end Mongoid.deprecate(self, :to_criteria) - private - module ClassMethods # Turn the object from the ruby type we deal with to a Mongo friendly diff --git a/lib/mongoid/extensions/string.rb b/lib/mongoid/extensions/string.rb index 0ca9fcb35..b0d1a13a7 100644 --- a/lib/mongoid/extensions/string.rb +++ b/lib/mongoid/extensions/string.rb @@ -9,6 +9,7 @@ module String # @attribute [rw] unconvertable_to_bson If the document is unconvertable. # @deprecated attr_accessor :unconvertable_to_bson + Mongoid.deprecate(self, :unconvertable_to_bson, :unconvertable_to_bson=) # Evolve the string into an object id if possible. @@ -127,7 +128,6 @@ def before_type_cast? ends_with?('_before_type_cast') end - # Is the object not to be converted to bson on criteria creation? # # @example Is the object unconvertable? diff --git a/lib/mongoid/interceptable.rb b/lib/mongoid/interceptable.rb index 4fcbf9aef..85f305ceb 100644 --- a/lib/mongoid/interceptable.rb +++ b/lib/mongoid/interceptable.rb @@ -50,7 +50,7 @@ module Interceptable define_callbacks :commit, :rollback, only: :after, - scope: [:kind, :name] + scope: %i[kind name] attr_accessor :before_callback_halted end @@ -202,6 +202,7 @@ def _mongoid_run_child_callbacks_without_around(kind, children: nil, &block) children = (children || cascadable_children(kind)) callback_list = _mongoid_run_child_before_callbacks(kind, children: children) return false if callback_list == false + value = block&.call callback_list.each do |_next_sequence, env| env.value &&= value @@ -227,10 +228,11 @@ def _mongoid_run_child_before_callbacks(kind, children: [], callback_list: []) next_sequence = compile_callbacks(chain) unless next_sequence.final? Mongoid.logger.warn("Around callbacks are disabled for embedded documents. Skipping around callbacks for #{child.class.name}.") - Mongoid.logger.warn("To enable around callbacks for embedded documents, set Mongoid::Config.around_callbacks_for_embeds to true.") + Mongoid.logger.warn('To enable around callbacks for embedded documents, set Mongoid::Config.around_callbacks_for_embeds to true.') end next_sequence.invoke_before(env) return false if env.halted + env.value = !env.halted callback_list << [next_sequence, env] if (grandchildren = child.send(:cascadable_children, kind)) diff --git a/lib/mongoid/persistence_context.rb b/lib/mongoid/persistence_context.rb index c8be17e32..dcf74066e 100644 --- a/lib/mongoid/persistence_context.rb +++ b/lib/mongoid/persistence_context.rb @@ -57,9 +57,9 @@ def for_child(document) if document.is_a?(Class) return self if document == (@object.is_a?(Class) ? @object : @object.class) elsif document.is_a?(Mongoid::Document) - return self if document.class == (@object.is_a?(Class) ? @object : @object.class) + return self if document.instance_of?((@object.is_a?(Class) ? @object : @object.class)) else - raise ArgumentError, 'must specify a class or a document instance' + raise ArgumentError.new('must specify a class or a document instance') end PersistenceContext.new(document, options.merge(document.storage_options)) @@ -130,8 +130,8 @@ def client # context. def client_name @client_name ||= options[:client] || - Threaded.client_override || - __evaluate__(storage_options[:client]) + Threaded.client_override || + __evaluate__(storage_options[:client]) end # Determine if this persistence context is equal to another. @@ -206,8 +206,8 @@ def client_options def database_name_option @database_name_option ||= options[:database] || - Threaded.database_override || - storage_options[:database] + Threaded.database_override || + storage_options[:database] end class << self diff --git a/lib/mongoid/railties/database.rake b/lib/mongoid/railties/database.rake index f82b90179..8a6d6ae99 100644 --- a/lib/mongoid/railties/database.rake +++ b/lib/mongoid/railties/database.rake @@ -76,7 +76,7 @@ namespace :db do unless Rake::Task.task_defined?('db:create_search_indexes') desc 'Create search indexes specified in Mongoid models' - task :create_search_indexes => 'mongoid:create_search_indexes' + task create_search_indexes: 'mongoid:create_search_indexes' end unless Rake::Task.task_defined?('db:remove_indexes') @@ -84,9 +84,9 @@ namespace :db do task remove_indexes: :'mongoid:remove_indexes' end - unless Rake::Task.task_defined?("db:remove_indexes") - desc "Remove indexes specified in Mongoid models" - task :remove_indexes => "mongoid:remove_indexes" + unless Rake::Task.task_defined?('db:remove_indexes') + desc 'Remove indexes specified in Mongoid models' + task remove_indexes: 'mongoid:remove_indexes' end unless Rake::Task.task_defined?('db:shard_collections') diff --git a/lib/mongoid/search_indexable.rb b/lib/mongoid/search_indexable.rb index 134253922..bc6764174 100644 --- a/lib/mongoid/search_indexable.rb +++ b/lib/mongoid/search_indexable.rb @@ -146,7 +146,10 @@ def remove_search_indexes # @param [ Hash ] defn The search index definition. def search_index(name_or_defn, defn = nil) name = name_or_defn - name, defn = nil, name if name.is_a?(Hash) + if name.is_a?(Hash) + defn = name + name = nil + end spec = { definition: defn }.tap { |s| s[:name] = name.to_s if name } search_index_specs.push(spec) diff --git a/lib/mongoid/tasks/database.rb b/lib/mongoid/tasks/database.rb index f5d776f9d..7f272d361 100644 --- a/lib/mongoid/tasks/database.rb +++ b/lib/mongoid/tasks/database.rb @@ -157,6 +157,7 @@ def remove_indexes(models = ::Mongoid.models) def remove_search_indexes(models = ::Mongoid.models) models.each do |model| next if model.embedded? + model.remove_search_indexes end end diff --git a/lib/mongoid/utils.rb b/lib/mongoid/utils.rb index 91a6186d5..5b2c39923 100644 --- a/lib/mongoid/utils.rb +++ b/lib/mongoid/utils.rb @@ -48,7 +48,7 @@ def monotonic_time # # @return [ true | false ] def truthy_string?(string) - %w[ 1 yes true on ].include?(string.strip.downcase) + %w[1 yes true on].include?(string.strip.downcase) end end end diff --git a/mongoid.gemspec b/mongoid.gemspec index 3f87f36bb..437483e93 100644 --- a/mongoid.gemspec +++ b/mongoid.gemspec @@ -27,14 +27,14 @@ Gem::Specification.new do |s| s.required_ruby_version = '>= 2.7' - s.required_ruby_version = ">= 2.7" - s.required_rubygems_version = ">= 1.3.6" + s.required_ruby_version = '>= 2.7' + s.required_rubygems_version = '>= 1.3.6' # activemodel 7.0.0 cannot be used due to Class#descendants issue # See: https://github.com/rails/rails/pull/43951 - s.add_dependency("activemodel", ['>=5.1', '<7.2', '!= 7.0.0']) - s.add_dependency("mongo", ['>=2.18.0', '<3.0.0']) - s.add_dependency("concurrent-ruby", ['>= 1.0.5', '< 2.0']) + s.add_dependency('activemodel', ['>=5.1', '<7.2', '!= 7.0.0']) + s.add_dependency('mongo', ['>=2.18.0', '<3.0.0']) + s.add_dependency('concurrent-ruby', ['>= 1.0.5', '< 2.0']) # The ruby2_keywords gem normalizes Ruby 2.7's arg delegation. # It can be removed when Ruby 2.7 is removed. diff --git a/spec/integration/callbacks_spec.rb b/spec/integration/callbacks_spec.rb index c173f8bc1..c24cb0a1f 100644 --- a/spec/integration/callbacks_spec.rb +++ b/spec/integration/callbacks_spec.rb @@ -599,7 +599,7 @@ def will_save_change_to_attribute_values_before # https://jira.mongodb.org/browse/MONGOID-5658 it 'does not raise SystemStackError' do - expect { book.save! }.not_to raise_error(SystemStackError) + expect { book.save! }.to_not raise_error(SystemStackError) end end end diff --git a/spec/mongoid/attributes/embedded_spec.rb b/spec/mongoid/attributes/embedded_spec.rb index f06269d27..6416f4d75 100644 --- a/spec/mongoid/attributes/embedded_spec.rb +++ b/spec/mongoid/attributes/embedded_spec.rb @@ -66,7 +66,7 @@ context 'when attributes is an array' do let(:attributes) do - [ { 'name' => 'Fred' }, { 'name' => 'Daphne' }, { 'name' => 'Velma' }, { 'name' => 'Shaggy' } ] + [{ 'name' => 'Fred' }, { 'name' => 'Daphne' }, { 'name' => 'Velma' }, { 'name' => 'Shaggy' }] end let(:path) { '2.name' } @@ -75,7 +75,7 @@ end context 'when the member does not exist' do - let(:attributes) { [ { 'name' => 'Fred' }, { 'name' => 'Daphne' } ] } + let(:attributes) { [{ 'name' => 'Fred' }, { 'name' => 'Daphne' }] } it 'returns nil' do expect(embedded).to be_nil diff --git a/spec/mongoid/clients/transactions_spec.rb b/spec/mongoid/clients/transactions_spec.rb index ccbeffb68..6d1933892 100644 --- a/spec/mongoid/clients/transactions_spec.rb +++ b/spec/mongoid/clients/transactions_spec.rb @@ -810,7 +810,7 @@ def capture_exception before do TransactionsSpecPersonWithOnUpdate.transaction do - subject.update_attributes!(name: 'Foma Kiniaev') + subject.update!(name: 'Foma Kiniaev') end end diff --git a/spec/mongoid/clients/transactions_spec_models.rb b/spec/mongoid/clients/transactions_spec_models.rb index 48d2ee418..8eb8bd7cf 100644 --- a/spec/mongoid/clients/transactions_spec_models.rb +++ b/spec/mongoid/clients/transactions_spec_models.rb @@ -95,6 +95,7 @@ class TransactionsSpecPersonWithOnDestroy after_rollback_counter.inc end end + class TransactionSpecRaisesBeforeSave include Mongoid::Document include TransactionsSpecCountable diff --git a/spec/mongoid/criteria/findable_spec.rb b/spec/mongoid/criteria/findable_spec.rb index e10f45395..16435ca42 100644 --- a/spec/mongoid/criteria/findable_spec.rb +++ b/spec/mongoid/criteria/findable_spec.rb @@ -259,190 +259,190 @@ end end - context "when providing nested arrays of ids" do + context 'when providing nested arrays of ids' do let!(:band_two) do - Band.create!(name: "Tool") + Band.create!(name: 'Tool') end - context "when all ids match" do + context 'when all ids match' do let(:found) do - Band.find([ [ band.id ], [ [ band_two.id ] ] ]) + Band.find([[band.id], [[band_two.id]]]) end - it "contains the first match" do + it 'contains the first match' do expect(found).to include(band) end - it "contains the second match" do + it 'contains the second match' do expect(found).to include(band_two) end - context "when ids are duplicates" do + context 'when ids are duplicates' do let(:found) do - Band.find([ [ band.id ], [ [ band.id ] ] ]) + Band.find([[band.id], [[band.id]]]) end - it "contains only the first match" do + it 'contains only the first match' do expect(found).to eq([band]) end end end - context "when any id does not match" do + context 'when any id does not match' do - context "when raising a not found error" do + context 'when raising a not found error' do config_override :raise_not_found_error, true let(:found) do - Band.find([ [ band.id ], [ [ BSON::ObjectId.new ] ] ]) + Band.find([[band.id], [[BSON::ObjectId.new]]]) end - it "raises an error" do - expect { + it 'raises an error' do + expect do found - }.to raise_error(Mongoid::Errors::DocumentNotFound, /Document\(s\) not found for class Band with id\(s\)/) + end.to raise_error(Mongoid::Errors::DocumentNotFound, /Document\(s\) not found for class Band with id\(s\)/) end end - context "when raising no error" do + context 'when raising no error' do config_override :raise_not_found_error, false let(:found) do - Band.find([ [ band.id ], [ [ BSON::ObjectId.new ] ] ]) + Band.find([[band.id], [[BSON::ObjectId.new]]]) end - it "returns only the matching documents" do - expect(found).to eq([ band ]) + it 'returns only the matching documents' do + expect(found).to eq([band]) end end end end - context "when providing a Set of ids" do + context 'when providing a Set of ids' do let!(:band_two) do - Band.create!(name: "Tool") + Band.create!(name: 'Tool') end - context "when all ids match" do + context 'when all ids match' do let(:found) do - Band.find(Set[ band.id, band_two.id ]) + Band.find(Set[band.id, band_two.id]) end - it "contains the first match" do + it 'contains the first match' do expect(found).to include(band) end - it "contains the second match" do + it 'contains the second match' do expect(found).to include(band_two) end end - context "when any id does not match" do + context 'when any id does not match' do - context "when raising a not found error" do + context 'when raising a not found error' do config_override :raise_not_found_error, true let(:found) do - Band.find(Set[ band.id, BSON::ObjectId.new ]) + Band.find(Set[band.id, BSON::ObjectId.new]) end - it "raises an error" do - expect { + it 'raises an error' do + expect do found - }.to raise_error(Mongoid::Errors::DocumentNotFound, /Document\(s\) not found for class Band with id\(s\)/) + end.to raise_error(Mongoid::Errors::DocumentNotFound, /Document\(s\) not found for class Band with id\(s\)/) end end - context "when raising no error" do + context 'when raising no error' do config_override :raise_not_found_error, false let(:found) do - Band.find(Set[ band.id, BSON::ObjectId.new ]) + Band.find(Set[band.id, BSON::ObjectId.new]) end - it "returns only the matching documents" do - expect(found).to eq([ band ]) + it 'returns only the matching documents' do + expect(found).to eq([band]) end end end end - context "when providing a Range of ids" do + context 'when providing a Range of ids' do let!(:band_two) do - Band.create!(name: "Tool") + Band.create!(name: 'Tool') end - context "when all ids match" do + context 'when all ids match' do let(:found) do Band.find(band.id.to_s..band_two.id.to_s) end - it "contains the first match" do + it 'contains the first match' do expect(found).to include(band) end - it "contains the second match" do + it 'contains the second match' do expect(found).to include(band_two) end - context "when any id does not match" do + context 'when any id does not match' do - context "when raising a not found error" do + context 'when raising a not found error' do config_override :raise_not_found_error, true let(:found) do Band.find(band_two.id.to_s..BSON::ObjectId.new) end - it "does not raise error and returns only the matching documents" do - expect(found).to eq([ band_two ]) + it 'does not raise error and returns only the matching documents' do + expect(found).to eq([band_two]) end end - context "when raising no error" do + context 'when raising no error' do config_override :raise_not_found_error, false let(:found) do Band.find(band_two.id.to_s..BSON::ObjectId.new) end - it "returns only the matching documents" do - expect(found).to eq([ band_two ]) + it 'returns only the matching documents' do + expect(found).to eq([band_two]) end end end end - context "when all ids do not match" do + context 'when all ids do not match' do - context "when raising a not found error" do + context 'when raising a not found error' do config_override :raise_not_found_error, true let(:found) do Band.find(BSON::ObjectId.new..BSON::ObjectId.new) end - it "raises an error" do - expect { + it 'raises an error' do + expect do found - }.to raise_error(Mongoid::Errors::DocumentNotFound, /Document\(s\) not found for class Band with id\(s\)/) + end.to raise_error(Mongoid::Errors::DocumentNotFound, /Document\(s\) not found for class Band with id\(s\)/) end end - context "when raising no error" do + context 'when raising no error' do config_override :raise_not_found_error, false let(:found) do Band.find(BSON::ObjectId.new..BSON::ObjectId.new) end - it "returns only the matching documents" do + it 'returns only the matching documents' do expect(found).to eq([]) end end diff --git a/spec/mongoid/criteria/queryable/extensions/string_spec.rb b/spec/mongoid/criteria/queryable/extensions/string_spec.rb index 649434548..0bddb4383 100644 --- a/spec/mongoid/criteria/queryable/extensions/string_spec.rb +++ b/spec/mongoid/criteria/queryable/extensions/string_spec.rb @@ -182,6 +182,7 @@ describe '#__expr_part__' do subject(:specified) { 'field'.__expr_part__(value) } + let(:value) { 10 } it 'returns the expression with the value' do @@ -227,7 +228,7 @@ let(:value) { 'test' } it 'returns the expression with the value negated' do - expect(specified).to eq({ 'field' => { '$ne' => 'test' }}) + expect(specified).to eq({ 'field' => { '$ne' => 'test' } }) end end end diff --git a/spec/mongoid/criteria/queryable/extensions/symbol_spec.rb b/spec/mongoid/criteria/queryable/extensions/symbol_spec.rb index 54847f714..1a508c1f9 100644 --- a/spec/mongoid/criteria/queryable/extensions/symbol_spec.rb +++ b/spec/mongoid/criteria/queryable/extensions/symbol_spec.rb @@ -11,7 +11,7 @@ end after do - Symbol.undef_method(:fubar) + described_class.undef_method(:fubar) end let(:fubar) do diff --git a/spec/mongoid/criteria_spec.rb b/spec/mongoid/criteria_spec.rb index 2bbd25802..dc9efc4bc 100644 --- a/spec/mongoid/criteria_spec.rb +++ b/spec/mongoid/criteria_spec.rb @@ -1425,10 +1425,11 @@ end describe '#merge!' do + subject(:merged) { criteria.merge!(other) } + let(:band) { Band.new } let(:criteria) { Band.scoped.where(name: 'Depeche Mode').asc(:name) } let(:association) { Band.relations['records'] } - subject(:merged) { criteria.merge!(other) } context 'when merging a Criteria' do let(:other) do @@ -1459,7 +1460,7 @@ context 'when merging a Hash' do let(:other) do Band.includes(:records).tap do |crit| - crit.documents = [ band ] + crit.documents = [band] end end @@ -1472,7 +1473,7 @@ end it 'merges the documents' do - expect(merged.documents).to eq([ band ]) + expect(merged.documents).to eq([band]) end it 'merges the scoping options' do @@ -3639,7 +3640,7 @@ def self.ages end it 'returns a criteria' do - expect(criteria).to be_a(Mongoid::Criteria) + expect(criteria).to be_a(described_class) end it 'sets the klass' do @@ -3657,7 +3658,7 @@ def self.ages end it 'returns a criteria' do - expect(criteria).to be_a(Mongoid::Criteria) + expect(criteria).to be_a(described_class) end it 'has klass nil' do diff --git a/spec/mongoid/document_persistence_context_spec.rb b/spec/mongoid/document_persistence_context_spec.rb index 479fcc0f8..f8633a7ce 100644 --- a/spec/mongoid/document_persistence_context_spec.rb +++ b/spec/mongoid/document_persistence_context_spec.rb @@ -54,7 +54,7 @@ end it 'can be reloaded without specifying the context' do - expect { person.reload }.not_to raise_error + expect { person.reload }.to_not raise_error expect(person.collection_name).to be == :extra_people end diff --git a/spec/mongoid/errors/mongoid_error_spec.rb b/spec/mongoid/errors/mongoid_error_spec.rb index 2b02ff26b..0cb68afb0 100644 --- a/spec/mongoid/errors/mongoid_error_spec.rb +++ b/spec/mongoid/errors/mongoid_error_spec.rb @@ -10,7 +10,7 @@ before do { 'message_title' => 'message', 'summary_title' => 'summary', 'resolution_title' => 'resolution' }.each do |key, name| - expect(::I18n).to receive(:translate).with("mongoid.errors.messages.#{key}", **{}).and_return(name) + expect(I18n).to receive(:translate).with("mongoid.errors.messages.#{key}", **{}).and_return(name) end %w[message summary resolution].each do |name| diff --git a/spec/mongoid/extensions/hash_spec.rb b/spec/mongoid/extensions/hash_spec.rb index 6ddffdadf..94d7a887e 100644 --- a/spec/mongoid/extensions/hash_spec.rb +++ b/spec/mongoid/extensions/hash_spec.rb @@ -162,7 +162,7 @@ end end - describe ".demongoize" do + describe '.demongoize' do let(:hash) do { field: 1 } diff --git a/spec/mongoid/fields/foreign_key_spec.rb b/spec/mongoid/fields/foreign_key_spec.rb index 3d60f997f..1f416fcd3 100644 --- a/spec/mongoid/fields/foreign_key_spec.rb +++ b/spec/mongoid/fields/foreign_key_spec.rb @@ -497,6 +497,8 @@ end describe '#mongoize' do + subject(:mongoized) { field.mongoize(object) } + let(:field) do described_class.new( :vals, @@ -508,7 +510,6 @@ ) end let(:association) { Game.relations['person'] } - subject(:mongoized) { field.mongoize(object) } context 'type is Array' do let(:type) { Array } @@ -623,7 +624,7 @@ :_id, type: BSON::ObjectId, pre_processed: true, - default: ->{ BSON::ObjectId.new }, + default: -> { BSON::ObjectId.new }, overwrite: true ) end @@ -663,7 +664,7 @@ :_id, type: BSON::ObjectId, pre_processed: true, - default: ->{ BSON::ObjectId.new }, + default: -> { BSON::ObjectId.new }, overwrite: true ) end @@ -775,7 +776,7 @@ :_id, type: BSON::ObjectId, pre_processed: true, - default: ->{ BSON::ObjectId.new }, + default: -> { BSON::ObjectId.new }, overwrite: true ) end @@ -815,7 +816,7 @@ :_id, type: BSON::ObjectId, pre_processed: true, - default: ->{ BSON::ObjectId.new }, + default: -> { BSON::ObjectId.new }, overwrite: true ) end diff --git a/spec/mongoid/interceptable_spec.rb b/spec/mongoid/interceptable_spec.rb index 6a14959d5..86d32b6f3 100644 --- a/spec/mongoid/interceptable_spec.rb +++ b/spec/mongoid/interceptable_spec.rb @@ -1924,8 +1924,8 @@ class TestClass end end - context "create" do - context "with around callbacks" do + context 'create' do + context 'with around callbacks' do config_override :around_callbacks_for_embeds, true let(:expected) do @@ -1967,7 +1967,7 @@ class TestClass end end - context "without around callbacks" do + context 'without around callbacks' do config_override :around_callbacks_for_embeds, false let(:expected) do @@ -2006,8 +2006,8 @@ class TestClass end end - context "update" do - context "with around callbacks" do + context 'update' do + context 'with around callbacks' do config_override :around_callbacks_for_embeds, true let(:expected) do @@ -2056,7 +2056,7 @@ class TestClass end end - context "without around callbacks" do + context 'without around callbacks' do config_override :around_callbacks_for_embeds, false let(:expected) do @@ -2093,7 +2093,7 @@ class TestClass parent.callback_registry = registry parent.child.callback_registry = registry - parent.name = "name" + parent.name = 'name' parent.child.age = 10 parent.save! @@ -2178,7 +2178,7 @@ class TestClass end end - context "with around callbacks" do + context 'with around callbacks' do config_override :around_callbacks_for_embeds, true let(:expected) do @@ -2237,7 +2237,7 @@ class TestClass end end - context "without around callbacks" do + context 'without around callbacks' do config_override :around_callbacks_for_embeds, false let(:expected) do @@ -2563,10 +2563,10 @@ class TestClass end end - context "when around callbacks for embedded are disabled" do + context 'when around callbacks for embedded are disabled' do config_override :around_callbacks_for_embeds, false - context "when around callback is defined" do + context 'when around callback is defined' do let(:registry) { InterceptableSpec::CallbackRegistry.new } let(:parent) do @@ -2580,7 +2580,7 @@ class TestClass expect(Mongoid.logger).to receive(:warn).with(/To enable around callbacks for embedded documents/).twice.and_call_original end - it "logs a warning" do + it 'logs a warning' do parent.save! end end diff --git a/spec/mongoid/persistable/savable_spec.rb b/spec/mongoid/persistable/savable_spec.rb index ca93a884e..443bf54b2 100644 --- a/spec/mongoid/persistable/savable_spec.rb +++ b/spec/mongoid/persistable/savable_spec.rb @@ -294,7 +294,7 @@ expect(truck.crates[0].toys[0].name).to eq 'Teddy bear' expect(truck.crates[1].volume).to eq 0.8 expect(truck.crates[1].toys.size).to eq 0 - expect { truck.save! }.not_to raise_error + expect { truck.save! }.to_not raise_error truck_found = Truck.find(truck.id) expect(truck_found.crates.size).to eq 2 diff --git a/spec/mongoid/search_indexable_spec.rb b/spec/mongoid/search_indexable_spec.rb index 22115623b..d86db9fd0 100644 --- a/spec/mongoid/search_indexable_spec.rb +++ b/spec/mongoid/search_indexable_spec.rb @@ -11,9 +11,7 @@ def initialize(model) model.collection.create end - def collection - model.collection - end + delegate :collection, to: :model # Wait for all of the indexes with the given names to be ready; then return # the list of index definitions corresponding to those names. @@ -45,7 +43,7 @@ def timeboxed_wait(step: 5, max: 300) yield sleep step - raise Timeout::Error, 'wait took too long' if Mongo::Utils.monotonic_time - start > max + raise Timeout::Error.new('wait took too long') if Mongo::Utils.monotonic_time - start > max end end @@ -61,7 +59,6 @@ def filter_results(result, names) end end -# rubocop:disable RSpec/MultipleMemoizedHelpers describe Mongoid::SearchIndexable do before do skip "#{described_class} requires at Atlas environment (set ATLAS_URI)" if ENV['ATLAS_URI'].nil? @@ -100,7 +97,7 @@ def filter_results(result, names) let(:requested_definitions) { model.search_index_specs.map { |spec| spec[:definition].with_indifferent_access } } let(:index_names) { model.create_search_indexes } let(:actual_indexes) { helper.wait_for(*index_names) } - let(:actual_definitions) { actual_indexes.map { |i| i['latestDefinition'] } } + let(:actual_definitions) { actual_indexes.pluck('latestDefinition') } describe '.create_search_indexes' do it 'creates the indexes' do @@ -111,7 +108,7 @@ def filter_results(result, names) describe '.search_indexes' do before { actual_indexes } # wait for the indices to be created - let(:queried_definitions) { model.search_indexes.map { |i| i['latestDefinition'] } } + let(:queried_definitions) { model.search_indexes.pluck('latestDefinition') } it 'queries the available search indexes' do expect(queried_definitions).to be == requested_definitions @@ -135,7 +132,7 @@ def filter_results(result, names) before do actual_indexes # wait for the indexes to be created model.remove_search_indexes - helper.wait_for_absense_of(actual_indexes.map { |i| i['name'] }) + helper.wait_for_absense_of(actual_indexes.pluck('name')) end it 'removes the indexes' do @@ -144,4 +141,3 @@ def filter_results(result, names) end end end -# rubocop:enable RSpec/MultipleMemoizedHelpers diff --git a/spec/mongoid/tasks/database_rake_spec.rb b/spec/mongoid/tasks/database_rake_spec.rb index 375759ecb..8b2fa03d4 100644 --- a/spec/mongoid/tasks/database_rake_spec.rb +++ b/spec/mongoid/tasks/database_rake_spec.rb @@ -10,7 +10,7 @@ let(:task_file) { 'mongoid/tasks/database' } let(:logger) do - Logger.new(STDOUT, level: :error, formatter: ->(_sev, _dt, _prog, msg) { msg }) + Logger.new($stdout, level: :error, formatter: ->(_sev, _dt, _prog, msg) { msg }) end before do @@ -31,7 +31,7 @@ end shared_examples_for 'create_search_indexes' do - [ nil, *%w( 1 true yes on ) ].each do |truthy| + [nil, '1', 'true', 'yes', 'on'].each do |truthy| context "when WAIT_FOR_SEARCH_INDEXES is #{truthy.inspect}" do local_env 'WAIT_FOR_SEARCH_INDEXES' => truthy @@ -44,7 +44,7 @@ end end - %w( 0 false no off bogus ).each do |falsey| + %w[0 false no off bogus].each do |falsey| context "when WAIT_FOR_SEARCH_INDEXES is #{falsey.inspect}" do local_env 'WAIT_FOR_SEARCH_INDEXES' => falsey diff --git a/spec/mongoid/tasks/database_spec.rb b/spec/mongoid/tasks/database_spec.rb index d86f8acf7..e402ae8c3 100644 --- a/spec/mongoid/tasks/database_spec.rb +++ b/spec/mongoid/tasks/database_spec.rb @@ -62,7 +62,7 @@ class Note end before do - allow(Mongoid::Tasks::Database).to receive(:logger).and_return(logger) + allow(described_class).to receive(:logger).and_return(logger) end describe '.create_collections' do @@ -79,7 +79,7 @@ class Note context "when force is #{force}" do it 'creates the collection' do expect(DatabaseSpec::Measurement).to receive(:create_collection).once.with(force: force) - Mongoid::Tasks::Database.create_collections(models, force: force) + described_class.create_collections(models, force: force) end end end @@ -94,7 +94,7 @@ class Note context "when force is #{force}" do it 'creates the collection' do expect(Person).to receive(:create_collection).once.with(force: force) - Mongoid::Tasks::Database.create_collections(models, force: force) + described_class.create_collections(models, force: force) end end end @@ -112,12 +112,12 @@ class Note end before do - allow(Mongoid::Tasks::Database).to receive(:logger).and_return(logger) + allow(described_class).to receive(:logger).and_return(logger) end it 'does nothing, but logging' do expect(DatabaseSpec::Comment).to_not receive(:create_collection) - Mongoid::Tasks::Database.create_collections(models) + described_class.create_collections(models) end end @@ -133,7 +133,7 @@ class Note it 'creates the collection' do expect(DatabaseSpec::Note).to receive(:create_collection).once - Mongoid::Tasks::Database.create_collections(models) + described_class.create_collections(models) end end end @@ -146,7 +146,7 @@ class Note end let(:indexes) do - Mongoid::Tasks::Database.create_indexes(models) + described_class.create_indexes(models) end context 'with ordinary Rails models' do @@ -216,17 +216,17 @@ class Note end end - let(:index_names) { %w[ name1 name2 ] } + let(:index_names) { %w[name1 name2] } let(:searchable_model_spy) do class_spy(searchable_model, create_search_indexes: index_names, - search_index_specs: [ { mappings: { dynamic: true } } ]) + search_index_specs: [{ mappings: { dynamic: true } }]) end context 'when wait is true' do before do allow(described_class).to receive(:wait_for_search_indexes) - described_class.create_search_indexes([ searchable_model_spy ], wait: true) + described_class.create_search_indexes([searchable_model_spy], wait: true) end it 'invokes both create_search_indexes and wait_for_search_indexes' do @@ -238,12 +238,12 @@ class Note context 'when wait is false' do before do allow(described_class).to receive(:wait_for_search_indexes) - described_class.create_search_indexes([ searchable_model_spy ], wait: false) + described_class.create_search_indexes([searchable_model_spy], wait: false) end it 'invokes only create_search_indexes' do expect(searchable_model_spy).to have_received(:create_search_indexes) - expect(described_class).not_to have_received(:wait_for_search_indexes) + expect(described_class).to_not have_received(:wait_for_search_indexes) end end end @@ -267,11 +267,11 @@ class Note describe '.undefined_indexes' do before do - Mongoid::Tasks::Database.create_indexes(models) + described_class.create_indexes(models) end let(:indexes) do - Mongoid::Tasks::Database.undefined_indexes(models) + described_class.undefined_indexes(models) end it 'returns the removed indexes' do @@ -300,13 +300,13 @@ class Note User.collection.indexes end let(:removed_indexes) do - Mongoid::Tasks::Database.undefined_indexes(models) + described_class.undefined_indexes(models) end before do - Mongoid::Tasks::Database.create_indexes(models) + described_class.create_indexes(models) indexes.create_one(account_expires: 1) - Mongoid::Tasks::Database.remove_undefined_indexes(models) + described_class.remove_undefined_indexes(models) end it 'returns the removed indexes' do @@ -319,8 +319,8 @@ class Note class Band index origin: Mongo::Index::TEXT end - Mongoid::Tasks::Database.create_indexes([Band]) - Mongoid::Tasks::Database.remove_undefined_indexes([Band]) + described_class.create_indexes([Band]) + described_class.remove_undefined_indexes([Band]) end let(:indexes) do @@ -344,8 +344,8 @@ class Band end before do - Mongoid::Tasks::Database.create_indexes(models) - Mongoid::Tasks::Database.remove_indexes(models) + described_class.create_indexes(models) + described_class.remove_indexes(models) end it 'removes indexes from klass' do diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 2f9b7051f..957d2e833 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -46,7 +46,7 @@ def database_id_alt require 'support/constraints' require 'support/crypt' -use_ssl = %w[ ssl 1 true ].include?(ENV['SSL']) +use_ssl = %w[ssl 1 true].include?(ENV['SSL']) ssl_options = { ssl: use_ssl }.freeze # Give MongoDB servers time to start up in CI environments From c6f51a4d1f32e919e343512c3a62255eebfd3c7f Mon Sep 17 00:00:00 2001 From: johnnyshields <27655+johnnyshields@users.noreply.github.com> Date: Tue, 16 Jan 2024 12:37:41 +0900 Subject: [PATCH 51/52] More fixes --- .github/workflows/test.yml | 10 +- .rubocop.yml | 3 + .rubocop_todo.yml | 135 +++++++++++++----- docs/reference/crud.txt | 2 +- gemfiles/standard.rb | 2 +- .../association/embedded/embeds_many.rb | 2 +- .../association/embedded/embeds_one.rb | 2 +- .../referenced/has_many/buildable.rb | 2 +- lib/mongoid/association/relatable.rb | 2 +- lib/mongoid/clients/sessions.rb | 2 +- lib/mongoid/contextual/memory.rb | 14 +- lib/mongoid/copyable.rb | 2 +- .../criteria/queryable/extensions/string.rb | 2 +- .../criteria/queryable/extensions/symbol.rb | 2 +- lib/mongoid/criteria/queryable/mergeable.rb | 2 +- lib/mongoid/document.rb | 2 +- lib/mongoid/errors/invalid_field.rb | 3 +- lib/mongoid/errors/invalid_relation.rb | 3 +- lib/mongoid/errors/mongoid_error.rb | 2 +- lib/mongoid/extensions/object.rb | 1 - lib/mongoid/fields/foreign_key.rb | 4 +- lib/mongoid/fields/standard.rb | 3 +- lib/mongoid/fields/validators/macro.rb | 4 +- lib/mongoid/interceptable.rb | 6 +- lib/mongoid/matcher/elem_match.rb | 2 +- lib/mongoid/railties/database.rake | 6 +- lib/mongoid/tasks/database.rb | 5 +- lib/mongoid/timestamps/updated.rb | 2 +- lib/mongoid/warnings.rb | 8 +- mongoid.gemspec | 2 - spec/integration/callbacks_spec.rb | 2 +- spec/mongoid/config/environment_spec.rb | 2 +- spec/mongoid/contextual/mongo_spec.rb | 7 +- spec/mongoid/criteria/findable_spec.rb | 1 - .../document_persistence_context_spec.rb | 2 +- spec/mongoid/fields/foreign_key_spec.rb | 8 +- spec/mongoid/interceptable_spec.rb | 4 +- spec/mongoid/interceptable_spec_models.rb | 10 +- spec/mongoid/monkey_patches_spec.rb | 4 + spec/mongoid/tasks/database_spec.rb | 32 ++--- spec/mongoid/validatable/uniqueness_spec.rb | 2 +- spec/spec_helper.rb | 2 - spec/support/constraints.rb | 10 ++ 43 files changed, 189 insertions(+), 134 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0fde582ae..6f702d406 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -98,28 +98,28 @@ jobs: # JRuby - ruby: jruby-9.4 - gemfile: gemfiles/rails_7.0.gemfile + gemfile: gemfiles/rails_7.1.gemfile mongodb: '6.0' topology: replica_set - ruby: jruby-9.4 - gemfile: gemfiles/rails_6.0.gemfile + gemfile: gemfiles/rails_6.1.gemfile mongodb: '5.0' topology: server # Field-Level Encryption # TODO: support LIBMONGOCRYPT via path - ruby: ruby-3.2 - gemfile: gemfiles/rails_7.0.gemfile + gemfile: gemfiles/rails_7.1.gemfile mongodb: '6.0' topology: sharded_cluster fle: helper - ruby: ruby-3.1 - gemfile: gemfiles/rails_6.1.gemfile + gemfile: gemfiles/rails_7.0.gemfile mongodb: '6.0' topology: replica_set fle: helper - ruby: ruby-2.7 - gemfile: gemfiles/rails_6.0.gemfile + gemfile: gemfiles/rails_6.1.gemfile mongodb: '6.0' topology: server fle: helper diff --git a/.rubocop.yml b/.rubocop.yml index 6c393cf7e..d38f97fdb 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -18,6 +18,9 @@ Lint/EmptyClass: Exclude: - 'spec/**/*' +Rails/ShortI18n: + Enabled: false + Style/Documentation: AllowedConstants: ['ClassMethods'] Exclude: diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index c9573dfd4..8b95d8ee4 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -1,11 +1,26 @@ # This configuration was generated by # `rubocop --auto-gen-config --exclude-limit 1000` -# on 2023-08-26 19:42:04 UTC using RuboCop version 1.49.0. +# on 2024-01-16 04:19:18 UTC using RuboCop version 1.60.0. # The point is for the user to remove these configuration records # one by one as the offenses are removed from the code base. # Note that changes in the inspected code, or installation of new # versions of RuboCop, may require this file to be generated again. +# Offense count: 1 +# Configuration parameters: EnforcedStyle, AllowedGems, Include. +# SupportedStyles: Gemfile, gems.rb, gemspec +# Include: **/*.gemspec, **/Gemfile, **/gems.rb +Gemspec/DevelopmentDependencies: + Exclude: + - 'mongoid.gemspec' + +# Offense count: 4 +Lint/SelfAssignment: + Exclude: + - 'spec/integration/associations/embeds_one_spec.rb' + - 'spec/integration/associations/has_one_spec.rb' + - 'spec/mongoid/association/embedded/embeds_one/proxy_spec.rb' + # Offense count: 108 # Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes. Metrics/AbcSize: @@ -25,19 +40,19 @@ Metrics/BlockNesting: # Offense count: 17 # Configuration parameters: CountComments, CountAsOne. Metrics/ClassLength: - Max: 327 + Max: 337 -# Offense count: 50 +# Offense count: 51 # Configuration parameters: AllowedMethods, AllowedPatterns. Metrics/CyclomaticComplexity: Max: 33 -# Offense count: 182 +# Offense count: 184 # Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns. Metrics/MethodLength: Max: 87 -# Offense count: 21 +# Offense count: 22 # Configuration parameters: CountComments, CountAsOne. Metrics/ModuleLength: Max: 330 @@ -53,13 +68,13 @@ Metrics/ParameterLists: Metrics/PerceivedComplexity: Max: 20 -# Offense count: 3 +# Offense count: 2 +# This cop supports unsafe autocorrection (--autocorrect-all). # Configuration parameters: EnforcedStyleForLeadingUnderscores. # SupportedStylesForLeadingUnderscores: disallowed, required, optional Naming/MemoizedInstanceVariableName: Exclude: - 'lib/mongoid/association/relatable.rb' - - 'lib/mongoid/document.rb' # Offense count: 27 RSpec/AnyInstance: @@ -80,7 +95,7 @@ RSpec/AnyInstance: RSpec/BeforeAfterAll: Enabled: false -# Offense count: 833 +# Offense count: 832 # Configuration parameters: Prefixes, AllowedPatterns. # Prefixes: when, with, without RSpec/ContextWording: @@ -176,7 +191,6 @@ RSpec/ContextWording: - 'spec/mongoid/errors/invalid_collection_spec.rb' - 'spec/mongoid/errors/invalid_includes_spec.rb' - 'spec/mongoid/extensions/date_class_mongoize_spec.rb' - - 'spec/mongoid/extensions/hash_spec.rb' - 'spec/mongoid/extensions/range_spec.rb' - 'spec/mongoid/fields/localized_spec.rb' - 'spec/mongoid/fields_spec.rb' @@ -203,7 +217,7 @@ RSpec/ContextWording: - 'spec/support/immutable_ids.rb' - 'spec/support/shared/time.rb' -# Offense count: 58 +# Offense count: 60 # Configuration parameters: IgnoredMetadata. RSpec/DescribeClass: Exclude: @@ -248,7 +262,7 @@ RSpec/DescribeClass: - 'spec/mongoid/railties/console_sandbox_spec.rb' - 'spec/mongoid/tasks/database_rake_spec.rb' -# Offense count: 126 +# Offense count: 124 RSpec/ExpectInHook: Exclude: - 'spec/integration/associations/embedded_spec.rb' @@ -291,7 +305,7 @@ RSpec/ExpectInHook: - 'spec/mongoid/validatable/presence_spec.rb' - 'spec/support/immutable_ids.rb' -# Offense count: 25 +# Offense count: 24 # Configuration parameters: Include, CustomTransform, IgnoreMethods, SpecSuffixOnly. # Include: **/*_spec*rb*, **/spec/**/* RSpec/FilePath: @@ -313,7 +327,6 @@ RSpec/FilePath: - 'spec/mongoid/extensions/raw_value_spec.rb' - 'spec/mongoid/extensions/stringified_symbol_spec.rb' - 'spec/mongoid/loading_spec.rb' - - 'spec/mongoid/railties/bson_object_id_serializer_spec.rb' - 'spec/mongoid/validatable/associated_spec.rb' - 'spec/mongoid/validatable/format_spec.rb' - 'spec/mongoid/validatable/length_spec.rb' @@ -335,7 +348,7 @@ RSpec/IteratedExpectation: - 'spec/mongoid/findable_spec.rb' - 'spec/mongoid_spec.rb' -# Offense count: 230 +# Offense count: 231 RSpec/LeakyConstantDeclaration: Exclude: - 'spec/integration/active_job_spec.rb' @@ -394,7 +407,7 @@ RSpec/LeakyConstantDeclaration: - 'spec/mongoid/validatable/numericality_spec.rb' - 'spec/mongoid/validatable/uniqueness_spec.rb' -# Offense count: 499 +# Offense count: 501 RSpec/LetSetup: Exclude: - 'spec/integration/matcher_examples_spec.rb' @@ -468,8 +481,8 @@ RSpec/LetSetup: - 'spec/mongoid/touchable_spec.rb' - 'spec/mongoid/validatable/uniqueness_spec.rb' -# Offense count: 298 -# Configuration parameters: EnforcedStyle. +# Offense count: 308 +# Configuration parameters: . # SupportedStyles: have_received, receive RSpec/MessageSpies: EnforcedStyle: receive @@ -481,7 +494,7 @@ RSpec/MultipleDescribes: - 'spec/mongoid/association/embedded/dirty_spec.rb' - 'spec/mongoid/tasks/database_rake_spec.rb' -# Offense count: 233 +# Offense count: 244 # Configuration parameters: EnforcedStyle, IgnoreSharedExamples. # SupportedStyles: always, named_only RSpec/NamedSubject: @@ -498,12 +511,12 @@ RSpec/NamedSubject: - 'spec/mongoid/extensions/raw_value_spec.rb' - 'spec/mongoid/matcher/expression_spec.rb' -# Offense count: 5025 +# Offense count: 5074 # Configuration parameters: AllowedGroups. RSpec/NestedGroups: Max: 13 -# Offense count: 37 +# Offense count: 31 # Configuration parameters: AllowedPatterns. # AllowedPatterns: ^expect_, ^assert_ RSpec/NoExpectationExample: @@ -516,7 +529,6 @@ RSpec/NoExpectationExample: - 'spec/mongoid/association/referenced/has_and_belongs_to_many/proxy_spec.rb' - 'spec/mongoid/attributes_spec.rb' - 'spec/mongoid/collection_configurable_spec.rb' - - 'spec/mongoid/criteria/queryable/storable_spec.rb' - 'spec/mongoid/document_spec.rb' - 'spec/mongoid/errors/mongoid_error_spec.rb' - 'spec/mongoid/persistence_context_spec.rb' @@ -638,7 +650,7 @@ RSpec/ScatteredSetup: - 'spec/mongoid/copyable_spec.rb' - 'spec/mongoid/persistable/deletable_spec.rb' -# Offense count: 14 +# Offense count: 12 RSpec/StubbedMock: Exclude: - 'spec/mongoid/clients_spec.rb' @@ -648,7 +660,7 @@ RSpec/StubbedMock: - 'spec/mongoid/tasks/encryption_spec.rb' - 'spec/mongoid/validatable/associated_spec.rb' -# Offense count: 21 +# Offense count: 26 RSpec/SubjectDeclaration: Exclude: - 'spec/integration/associations/embedded_dirty_spec.rb' @@ -656,7 +668,7 @@ RSpec/SubjectDeclaration: - 'spec/mongoid/collection_configurable_spec.rb' - 'spec/mongoid/contextual/mongo/documents_loader_spec.rb' -# Offense count: 28 +# Offense count: 27 # Configuration parameters: IgnoreNameless, IgnoreSymbolicNames. RSpec/VerifiedDoubles: Exclude: @@ -670,7 +682,6 @@ RSpec/VerifiedDoubles: - 'spec/mongoid/contextual/mongo/documents_loader_spec.rb' - 'spec/mongoid/errors/validations_spec.rb' - 'spec/mongoid/persistence_context_spec.rb' - - 'spec/mongoid/tasks/database_rake_spec.rb' - 'spec/mongoid/tasks/database_spec.rb' - 'spec/mongoid/threaded_spec.rb' - 'spec/mongoid/validatable/associated_spec.rb' @@ -688,7 +699,7 @@ Rails/Date: - 'spec/mongoid/persistable/maxable_spec.rb' - 'spec/mongoid/persistable/minable_spec.rb' -# Offense count: 21 +# Offense count: 20 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: EnforceForPrefixed. Rails/Delegate: @@ -700,7 +711,6 @@ Rails/Delegate: - 'lib/mongoid/clients/options.rb' - 'lib/mongoid/contextual/memory.rb' - 'lib/mongoid/contextual/none.rb' - - 'lib/mongoid/criteria.rb' - 'lib/mongoid/document.rb' - 'lib/mongoid/extensions/nil_class.rb' - 'lib/mongoid/extensions/symbol.rb' @@ -727,7 +737,7 @@ Rails/I18nLocaleAssignment: - 'spec/mongoid/validatable/uniqueness_spec.rb' - 'spec/support/macros.rb' -# Offense count: 129 +# Offense count: 133 # Configuration parameters: ForbiddenMethods, AllowedMethods. # ForbiddenMethods: decrement!, decrement_counter, increment!, increment_counter, insert, insert!, insert_all, insert_all!, toggle!, touch, touch_all, update_all, update_attribute, update_column, update_columns, update_counters, upsert, upsert_all Rails/SkipsModelValidations: @@ -755,23 +765,17 @@ Rails/SkipsModelValidations: - 'spec/mongoid/touchable_spec.rb' - 'spec/support/models/server.rb' -# Offense count: 159 +# Offense count: 151 # This cop supports unsafe autocorrection (--autocorrect-all). # Configuration parameters: EnforcedStyle. # SupportedStyles: strict, flexible Rails/TimeZone: Exclude: - - 'lib/mongoid/criteria/queryable/extensions/date.rb' - 'lib/mongoid/criteria/queryable/extensions/string.rb' - - 'lib/mongoid/extensions/array.rb' - 'lib/mongoid/extensions/date.rb' - - 'lib/mongoid/extensions/float.rb' - - 'lib/mongoid/extensions/integer.rb' - 'lib/mongoid/extensions/string.rb' - 'lib/mongoid/extensions/time.rb' - - 'lib/mongoid/timestamps/created.rb' - 'lib/mongoid/timestamps/updated.rb' - - 'lib/mongoid/touchable.rb' - 'spec/integration/criteria/date_field_spec.rb' - 'spec/integration/criteria/raw_value_spec.rb' - 'spec/integration/persistence/range_field_spec.rb' @@ -800,6 +804,17 @@ Rails/TimeZone: - 'spec/mongoid/touchable_spec.rb' - 'spec/support/models/post.rb' +# Offense count: 4 +# This cop supports safe autocorrection (--autocorrect). +# Configuration parameters: AllowOnlyRestArgument, UseAnonymousForwarding, RedundantRestArgumentNames, RedundantKeywordRestArgumentNames, RedundantBlockArgumentNames. +# RedundantRestArgumentNames: args, arguments +# RedundantKeywordRestArgumentNames: kwargs, options, opts +# RedundantBlockArgumentNames: blk, block, proc +Style/ArgumentsForwarding: + Exclude: + - 'lib/mongoid/association/referenced/has_many/enumerable.rb' + - 'lib/mongoid/association/referenced/has_many/proxy.rb' + # Offense count: 5 Style/MultilineBlockChain: Exclude: @@ -834,7 +849,55 @@ Style/OptionalBooleanParameter: - 'spec/support/models/address.rb' - 'spec/support/models/name.rb' -# Offense count: 240 +# Offense count: 38 +# This cop supports safe autocorrection (--autocorrect). +Style/RedundantCurrentDirectoryInPath: + Exclude: + - 'Gemfile' + - 'gemfiles/bson_master.gemfile' + - 'gemfiles/bson_min.gemfile' + - 'gemfiles/driver_master.gemfile' + - 'gemfiles/driver_min.gemfile' + - 'gemfiles/driver_stable.gemfile' + - 'gemfiles/rails_6.1.gemfile' + - 'gemfiles/rails_7.0.gemfile' + - 'gemfiles/rails_7.1.gemfile' + - 'gemfiles/rails_master.gemfile' + - 'spec/integration/associations/foreign_key_spec.rb' + - 'spec/integration/associations/reverse_population_spec.rb' + - 'spec/integration/callbacks_spec.rb' + - 'spec/mongoid/association/auto_save_spec.rb' + - 'spec/mongoid/association/embedded/embedded_in_spec.rb' + - 'spec/mongoid/association/embedded/embeds_many_query_spec.rb' + - 'spec/mongoid/association/embedded/embeds_one_query_spec.rb' + - 'spec/mongoid/association/embedded/embeds_one_spec.rb' + - 'spec/mongoid/association/referenced/belongs_to_query_spec.rb' + - 'spec/mongoid/association/referenced/belongs_to_spec.rb' + - 'spec/mongoid/association/referenced/has_and_belongs_to_many_query_spec.rb' + - 'spec/mongoid/association/referenced/has_and_belongs_to_many_spec.rb' + - 'spec/mongoid/association/referenced/has_many_query_spec.rb' + - 'spec/mongoid/association/referenced/has_many_spec.rb' + - 'spec/mongoid/association/referenced/has_one_query_spec.rb' + - 'spec/mongoid/association/referenced/has_one_spec.rb' + - 'spec/mongoid/attributes/nested_spec.rb' + - 'spec/mongoid/attributes_spec.rb' + - 'spec/mongoid/clients/transactions_spec.rb' + - 'spec/mongoid/copyable_spec.rb' + - 'spec/mongoid/criteria/includable_spec.rb' + - 'spec/mongoid/criteria/queryable/selectable_spec.rb' + - 'spec/mongoid/criteria/queryable/selectable_where_spec.rb' + - 'spec/mongoid/interceptable_spec.rb' + - 'spec/mongoid/shardable_spec.rb' + - 'spec/mongoid/timestamps_spec.rb' + - 'spec/mongoid/touchable_spec.rb' + +# Offense count: 1 +# This cop supports safe autocorrection (--autocorrect). +Style/RedundantParentheses: + Exclude: + - 'lib/mongoid/association/referenced/has_many/enumerable.rb' + +# Offense count: 251 # This cop supports safe autocorrection (--autocorrect). # Configuration parameters: AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, AllowedPatterns. # URISchemes: http, https diff --git a/docs/reference/crud.txt b/docs/reference/crud.txt index bdbdcbfc5..549ade80b 100644 --- a/docs/reference/crud.txt +++ b/docs/reference/crud.txt @@ -565,7 +565,7 @@ Mongoid models are reverted. For example: person.name # => 'Jake' raise 'An exception' end - rescue Exception + rescue StandardError person.name # => 'Tom' end diff --git a/gemfiles/standard.rb b/gemfiles/standard.rb index e5c4dd392..fed0abd5f 100644 --- a/gemfiles/standard.rb +++ b/gemfiles/standard.rb @@ -8,7 +8,7 @@ def standard_dependencies end group :development, :test do - gem 'rubocop', '~> 1.49.0' + gem 'rubocop', '~> 1.60.0' gem 'rubocop-performance', '~> 1.16.0' gem 'rubocop-rails', '~> 2.17.4' gem 'rubocop-rake', '~> 0.6.0' diff --git a/lib/mongoid/association/embedded/embeds_many.rb b/lib/mongoid/association/embedded/embeds_many.rb index 1aac596f9..d753322ee 100644 --- a/lib/mongoid/association/embedded/embeds_many.rb +++ b/lib/mongoid/association/embedded/embeds_many.rb @@ -49,7 +49,7 @@ def setup! # # @return [ String ] The field name. def store_as - @store_as ||= (@options[:store_as].try(:to_s) || name.to_s) + @store_as ||= @options[:store_as].try(:to_s) || name.to_s end # The key that is used to get the attributes for the associated object. diff --git a/lib/mongoid/association/embedded/embeds_one.rb b/lib/mongoid/association/embedded/embeds_one.rb index d2fe242f2..1032d268b 100644 --- a/lib/mongoid/association/embedded/embeds_one.rb +++ b/lib/mongoid/association/embedded/embeds_one.rb @@ -45,7 +45,7 @@ def setup! # # @return [ String ] The field name. def store_as - @store_as ||= (@options[:store_as].try(:to_s) || name.to_s) + @store_as ||= @options[:store_as].try(:to_s) || name.to_s end # The key that is used to get the attributes for the associated object. diff --git a/lib/mongoid/association/referenced/has_many/buildable.rb b/lib/mongoid/association/referenced/has_many/buildable.rb index bf69c8369..239b5260d 100644 --- a/lib/mongoid/association/referenced/has_many/buildable.rb +++ b/lib/mongoid/association/referenced/has_many/buildable.rb @@ -21,7 +21,7 @@ module Buildable # # @return [ Mongoid::Document ] A single document. def build(base, object, _type = nil, _selected_fields = nil) - return (object || []) unless query?(object) + return object || [] unless query?(object) return [] if object.is_a?(Array) query_criteria(object, base) diff --git a/lib/mongoid/association/relatable.rb b/lib/mongoid/association/relatable.rb index b4ede4984..c408c2770 100644 --- a/lib/mongoid/association/relatable.rb +++ b/lib/mongoid/association/relatable.rb @@ -409,7 +409,7 @@ def validate! raise Errors::InvalidRelationOption.new(@owner_class, name, opt, self.class::VALID_OPTIONS) end - [name, "#{name}?".to_sym, "#{name}=".to_sym].each do |n| + [name, :"#{name}?", :"#{name}="].each do |n| next unless Mongoid.destructive_fields.include?(n) raise Errors::InvalidRelation.new(@owner_class, n) diff --git a/lib/mongoid/clients/sessions.rb b/lib/mongoid/clients/sessions.rb index 202cf63c6..e0edfd54c 100644 --- a/lib/mongoid/clients/sessions.rb +++ b/lib/mongoid/clients/sessions.rb @@ -193,7 +193,7 @@ def abort_transaction(session) # Transforms custom options for after_commit and after_rollback callbacks # into options for +set_callback+. - def set_options_for_callbacks!(args) + def set_options_for_callbacks!(args) # rubocop:disable Naming/AccessorMethodName options = args.extract_options! args << options diff --git a/lib/mongoid/contextual/memory.rb b/lib/mongoid/contextual/memory.rb index 5576c6753..18593f540 100644 --- a/lib/mongoid/contextual/memory.rb +++ b/lib/mongoid/contextual/memory.rb @@ -650,16 +650,16 @@ def apply_sorting # @example Compare the two objects. # context.compare(a, b) # - # @param [ Object ] a The first object. - # @param [ Object ] b The second object. + # @param [ Object ] value_a The first object. + # @param [ Object ] value_b The second object. # # @return [ Integer ] The comparison value. - def compare(a, b) - return 0 if a.nil? && b.nil? - return 1 if a.nil? - return -1 if b.nil? + def compare(value_a, value_b) + return 0 if value_a.nil? && value_b.nil? + return 1 if value_a.nil? + return -1 if value_b.nil? - compare_operand(a) <=> compare_operand(b) + compare_operand(value_a) <=> compare_operand(value_b) end # Sort the documents in place. diff --git a/lib/mongoid/copyable.rb b/lib/mongoid/copyable.rb index 151ab0767..12c4b1a79 100644 --- a/lib/mongoid/copyable.rb +++ b/lib/mongoid/copyable.rb @@ -94,7 +94,7 @@ def process_localized_attributes(klass, attrs) attrs["#{name}_translations"] = value end end - klass.embedded_relations.each do |_, association| + klass.embedded_relations.each_value do |association| next unless attrs.present? && attrs[association.key].present? if association.is_a?(Association::Embedded::EmbedsMany) diff --git a/lib/mongoid/criteria/queryable/extensions/string.rb b/lib/mongoid/criteria/queryable/extensions/string.rb index ee686ad64..9c90e372f 100644 --- a/lib/mongoid/criteria/queryable/extensions/string.rb +++ b/lib/mongoid/criteria/queryable/extensions/string.rb @@ -46,7 +46,7 @@ def __mongo_expression__ # # @return [ Hash ] The string as a sort option hash. def __sort_option__ - split(/,/).inject({}) do |hash, spec| + split(',').inject({}) do |hash, spec| hash.tap do |h| field, direction = spec.strip.split(/\s/) h[field.to_sym] = Mongoid::Criteria::Translator.to_direction(direction) diff --git a/lib/mongoid/criteria/queryable/extensions/symbol.rb b/lib/mongoid/criteria/queryable/extensions/symbol.rb index d4d887957..dd195e3ba 100644 --- a/lib/mongoid/criteria/queryable/extensions/symbol.rb +++ b/lib/mongoid/criteria/queryable/extensions/symbol.rb @@ -34,7 +34,7 @@ module ClassMethods # @param [ String ] additional The additional MongoDB operator. def add_key(name, strategy, operator, additional = nil, &block) define_method(name) do - method = "__#{strategy}__".to_sym + method = :"__#{strategy}__" Key.new(self, method, operator, additional, &block) end end diff --git a/lib/mongoid/criteria/queryable/mergeable.rb b/lib/mongoid/criteria/queryable/mergeable.rb index 44871180d..5d4413a79 100644 --- a/lib/mongoid/criteria/queryable/mergeable.rb +++ b/lib/mongoid/criteria/queryable/mergeable.rb @@ -420,7 +420,7 @@ def prepare(field, operator, value) field = field.to_s name = aliases[field] || field serializer = serializers[name] - value = serializer ? serializer.evolve(value) : value + value = serializer.evolve(value) if serializer end selection = { operator => value } negating? ? { '$not' => selection } : selection diff --git a/lib/mongoid/document.rb b/lib/mongoid/document.rb index 90d55ae8c..31a879448 100644 --- a/lib/mongoid/document.rb +++ b/lib/mongoid/document.rb @@ -409,7 +409,7 @@ def instantiate(attrs = nil, selected_fields = nil, &block) # @api private def instantiate_document(attrs = nil, selected_fields = nil, options = {}, &block) execute_callbacks = options.fetch(:execute_callbacks, Threaded.execute_callbacks?) - attributes = attrs&.to_h || {} + attributes = attrs.to_h doc = allocate doc.__selected_fields = selected_fields diff --git a/lib/mongoid/errors/invalid_field.rb b/lib/mongoid/errors/invalid_field.rb index 0e6584cea..1aa7467d0 100644 --- a/lib/mongoid/errors/invalid_field.rb +++ b/lib/mongoid/errors/invalid_field.rb @@ -57,8 +57,7 @@ def origin(klass, name) # # @return [ Array ] The location of the method. def location(klass, name) - @location ||= - (klass.instance_method(name).source_location || ['Unknown', 0]) + @location ||= klass.instance_method(name).source_location || ['Unknown', 0] end end end diff --git a/lib/mongoid/errors/invalid_relation.rb b/lib/mongoid/errors/invalid_relation.rb index 0f0ee4ddd..bde75057d 100644 --- a/lib/mongoid/errors/invalid_relation.rb +++ b/lib/mongoid/errors/invalid_relation.rb @@ -53,8 +53,7 @@ def origin(klass, name) # # @return [ Array ] The location of the method. def location(klass, name) - @location ||= - (klass.instance_method(name).source_location || ['Unknown', 0]) + @location ||= klass.instance_method(name).source_location || ['Unknown', 0] end end end diff --git a/lib/mongoid/errors/mongoid_error.rb b/lib/mongoid/errors/mongoid_error.rb index 7af28b414..e4b6a5917 100644 --- a/lib/mongoid/errors/mongoid_error.rb +++ b/lib/mongoid/errors/mongoid_error.rb @@ -44,7 +44,7 @@ def compose_message(key, attributes = {}) # # @return [ String ] A localized error message string. def translate(key, options) - ::I18n.t("#{BASE_KEY}.#{key}", **options) + ::I18n.translate("#{BASE_KEY}.#{key}", **options) end # Create the problem. diff --git a/lib/mongoid/extensions/object.rb b/lib/mongoid/extensions/object.rb index dd3540fc6..91e9c42d6 100644 --- a/lib/mongoid/extensions/object.rb +++ b/lib/mongoid/extensions/object.rb @@ -83,7 +83,6 @@ def __to_inc__ end Mongoid.deprecate(self, :__to_inc__) - # Do or do not, there is no try. -- Yoda. # # @example Do or do not. diff --git a/lib/mongoid/fields/foreign_key.rb b/lib/mongoid/fields/foreign_key.rb index 78b1f9736..9b6f145fc 100644 --- a/lib/mongoid/fields/foreign_key.rb +++ b/lib/mongoid/fields/foreign_key.rb @@ -22,8 +22,8 @@ class ForeignKey < Standard # @param [ Array ] new_elements The new elements to add. # @param [ Array ] old_elements The old elements getting removed. def add_atomic_changes(document, name, key, mods, new_elements, old_elements) - old = (old_elements || []) - new = (new_elements || []) + old = old_elements || [] + new = new_elements || [] if new.length > old.length if new.first(old.length) == old document.atomic_array_add_to_sets[key] = new.drop(old.length) diff --git a/lib/mongoid/fields/standard.rb b/lib/mongoid/fields/standard.rb index 238a4916b..93509ca84 100644 --- a/lib/mongoid/fields/standard.rb +++ b/lib/mongoid/fields/standard.rb @@ -138,8 +138,7 @@ def object_id_field? # # @return [ true | false ] If the field's default is pre-processed. def pre_processed? - @pre_processed ||= - (options[:pre_processed] || (default_val && !default_val.is_a?(::Proc))) + @pre_processed ||= options[:pre_processed] || (default_val && !default_val.is_a?(::Proc)) end # Get the type of this field - inferred from the class name. diff --git a/lib/mongoid/fields/validators/macro.rb b/lib/mongoid/fields/validators/macro.rb index 83f06f43f..e356071da 100644 --- a/lib/mongoid/fields/validators/macro.rb +++ b/lib/mongoid/fields/validators/macro.rb @@ -46,7 +46,7 @@ def validate(klass, name, options) # @param [ Symbol ] name The field name. # @param [ Hash ] _options The provided options. def validate_relation(klass, name, _options = {}) - [name, "#{name}?".to_sym, "#{name}=".to_sym].each do |n| + [name, :"#{name}?", :"#{name}="].each do |n| if Mongoid.destructive_fields.include?(n) raise Errors::InvalidRelation.new(klass, n) end @@ -65,7 +65,7 @@ def validate_relation(klass, name, _options = {}) # # @api private def validate_field_name(klass, name) - [name, "#{name}?".to_sym, "#{name}=".to_sym].each do |n| + [name, :"#{name}?", :"#{name}="].each do |n| if Mongoid.destructive_fields.include?(n) raise Errors::InvalidField.new(klass, name, n) end diff --git a/lib/mongoid/interceptable.rb b/lib/mongoid/interceptable.rb index 85f305ceb..d598c37bc 100644 --- a/lib/mongoid/interceptable.rb +++ b/lib/mongoid/interceptable.rb @@ -176,7 +176,7 @@ def _mongoid_run_child_callbacks(kind, children: nil, &block) # # @api private def _mongoid_run_child_callbacks_with_around(kind, children: nil, &block) - child, *tail = (children || cascadable_children(kind)) + child, *tail = children || cascadable_children(kind) with_children = !Mongoid::Config.prevent_multiple_calls_of_embedded_callbacks if child.nil? block&.call @@ -199,12 +199,12 @@ def _mongoid_run_child_callbacks_with_around(kind, children: nil, &block) # # @api private def _mongoid_run_child_callbacks_without_around(kind, children: nil, &block) - children = (children || cascadable_children(kind)) + children ||= cascadable_children(kind) callback_list = _mongoid_run_child_before_callbacks(kind, children: children) return false if callback_list == false value = block&.call - callback_list.each do |_next_sequence, env| + callback_list.each do |_next_sequence, env| # rubocop:disable Style/HashEachMethods env.value &&= value end return false if _mongoid_run_child_after_callbacks(callback_list: callback_list) == false diff --git a/lib/mongoid/matcher/elem_match.rb b/lib/mongoid/matcher/elem_match.rb index 317ec3b1f..ef5e52d9f 100644 --- a/lib/mongoid/matcher/elem_match.rb +++ b/lib/mongoid/matcher/elem_match.rb @@ -33,7 +33,7 @@ def matches?(_exists, value, condition) else # Validate the condition is valid, even though we will never attempt # matching it. - condition.each do |k, _v| + condition.each_key do |k| k = k.to_s next unless k.start_with?('$') diff --git a/lib/mongoid/railties/database.rake b/lib/mongoid/railties/database.rake index 8a6d6ae99..d49ce8cc5 100644 --- a/lib/mongoid/railties/database.rake +++ b/lib/mongoid/railties/database.rake @@ -84,9 +84,9 @@ namespace :db do task remove_indexes: :'mongoid:remove_indexes' end - unless Rake::Task.task_defined?('db:remove_indexes') - desc 'Remove indexes specified in Mongoid models' - task remove_indexes: 'mongoid:remove_indexes' + unless Rake::Task.task_defined?('db:remove_search_indexes') + desc 'Remove search indexes specified in Mongoid models' + task remove_search_indexes: 'mongoid:remove_search_indexes' end unless Rake::Task.task_defined?('db:shard_collections') diff --git a/lib/mongoid/tasks/database.rb b/lib/mongoid/tasks/database.rb index 7f272d361..fff8805a5 100644 --- a/lib/mongoid/tasks/database.rb +++ b/lib/mongoid/tasks/database.rb @@ -25,7 +25,7 @@ def create_collections(models = ::Mongoid.models, force: false) else logger.info("MONGOID: collection options ignored on: #{model}, please define in the root model.") end - rescue Exception + rescue StandardError logger.error "error while creating collection for #{model}" raise end @@ -250,10 +250,9 @@ def wait_for_search_indexes(models) models.each do |model, names| model.wait_for_search_indexes(names) do |status| if status.ready? - puts logger.info("MONGOID: Search indexes on #{model} are READY") else - print '.' + print '.' # rubocop:disable Rails/Output $stdout.flush end end diff --git a/lib/mongoid/timestamps/updated.rb b/lib/mongoid/timestamps/updated.rb index cf298defd..97ed24d05 100644 --- a/lib/mongoid/timestamps/updated.rb +++ b/lib/mongoid/timestamps/updated.rb @@ -23,7 +23,7 @@ module Updated # @example Set the updated at time. # person.set_updated_at def set_updated_at - self.updated_at = Time.configured.now if able_to_set_updated_at? && !updated_at_changed? + self.updated_at = Time.current if able_to_set_updated_at? && !updated_at_changed? clear_timeless_option end diff --git a/lib/mongoid/warnings.rb b/lib/mongoid/warnings.rb index ddd357739..67cee0cc6 100644 --- a/lib/mongoid/warnings.rb +++ b/lib/mongoid/warnings.rb @@ -19,10 +19,10 @@ class << self def warning(id, message) singleton_class.class_eval do define_method("warn_#{id}") do - unless instance_variable_get("@#{id}") - Mongoid.logger.warn(message) - instance_variable_set("@#{id}", true) - end + return if instance_variable_get("@#{id}") + + Mongoid.logger.warn(message) + instance_variable_set("@#{id}", true) end end end diff --git a/mongoid.gemspec b/mongoid.gemspec index 437483e93..cf9a0cf10 100644 --- a/mongoid.gemspec +++ b/mongoid.gemspec @@ -26,8 +26,6 @@ Gem::Specification.new do |s| } s.required_ruby_version = '>= 2.7' - - s.required_ruby_version = '>= 2.7' s.required_rubygems_version = '>= 1.3.6' # activemodel 7.0.0 cannot be used due to Class#descendants issue diff --git a/spec/integration/callbacks_spec.rb b/spec/integration/callbacks_spec.rb index c24cb0a1f..665b17401 100644 --- a/spec/integration/callbacks_spec.rb +++ b/spec/integration/callbacks_spec.rb @@ -584,7 +584,7 @@ def will_save_change_to_attribute_values_before end context 'cascade callbacks' do - ruby_version_gte '3.0' + min_ruby_version '3.0' require_mri let(:book) do diff --git a/spec/mongoid/config/environment_spec.rb b/spec/mongoid/config/environment_spec.rb index f4b03d93e..8ccbceba0 100644 --- a/spec/mongoid/config/environment_spec.rb +++ b/spec/mongoid/config/environment_spec.rb @@ -152,7 +152,7 @@ hosts: [localhost] options: auto_encryption_options: - schema_map: #{schema_map.to_yaml.delete_prefix('---').gsub(/\n/, "\n#{' ' * 100}")} + schema_map: #{schema_map.to_yaml.delete_prefix('---').gsub("\n", "\n#{' ' * 100}")} FILE end diff --git a/spec/mongoid/contextual/mongo_spec.rb b/spec/mongoid/contextual/mongo_spec.rb index 2e037c4d3..ed5447aae 100644 --- a/spec/mongoid/contextual/mongo_spec.rb +++ b/spec/mongoid/contextual/mongo_spec.rb @@ -3693,22 +3693,21 @@ expect(new_order.reload.name).to eq('Smiths') end end - end context 'when operation is $push' do before do depeche_mode.update_attribute(:genres, ['electronic']) new_order.update_attribute(:genres, ['electronic']) - context.update_all("$push" => { genres: 'pop' }) + context.update_all('$push' => { genres: 'pop' }) end it 'updates the first matching document' do - expect(depeche_mode.reload.genres).to eq(['electronic', 'pop']) + expect(depeche_mode.reload.genres).to eq(%w[electronic pop]) end it 'updates the last matching document' do - expect(new_order.reload.genres).to eq(['electronic', 'pop']) + expect(new_order.reload.genres).to eq(%w[electronic pop]) end end diff --git a/spec/mongoid/criteria/findable_spec.rb b/spec/mongoid/criteria/findable_spec.rb index 16435ca42..527e75e41 100644 --- a/spec/mongoid/criteria/findable_spec.rb +++ b/spec/mongoid/criteria/findable_spec.rb @@ -390,7 +390,6 @@ expect(found).to include(band_two) end - context 'when any id does not match' do context 'when raising a not found error' do diff --git a/spec/mongoid/document_persistence_context_spec.rb b/spec/mongoid/document_persistence_context_spec.rb index f8633a7ce..baf2e2fe6 100644 --- a/spec/mongoid/document_persistence_context_spec.rb +++ b/spec/mongoid/document_persistence_context_spec.rb @@ -96,7 +96,7 @@ end it 'cannot be reloaded without specifying the context' do - expect { person.reload }.to raise_error + expect { person.reload }.to raise_error(Mongoid::Errors::DocumentNotFound, /Document\(s\) not found for class Person with id\(s\)/) end it 'cannot be updated without specifying the context' do diff --git a/spec/mongoid/fields/foreign_key_spec.rb b/spec/mongoid/fields/foreign_key_spec.rb index 1f416fcd3..e5a269b6c 100644 --- a/spec/mongoid/fields/foreign_key_spec.rb +++ b/spec/mongoid/fields/foreign_key_spec.rb @@ -511,7 +511,7 @@ end let(:association) { Game.relations['person'] } - context 'type is Array' do + context 'when type is Array' do let(:type) { Array } context 'when the object is a BSON::ObjectId' do @@ -687,7 +687,7 @@ end end - context 'type is Set' do + context 'when type is Set' do let(:type) { Set } context 'when the object is an Array of BSON::ObjectId' do @@ -707,7 +707,7 @@ end end - context 'type is Object' do + context 'when type is Object' do let(:type) { Object } context 'when the object is a BSON::ObjectId' do @@ -743,7 +743,7 @@ end context 'when the object is nil' do - let(:object) { '' } + let(:object) { nil } it 'returns nil' do expect(mongoized).to be_nil diff --git a/spec/mongoid/interceptable_spec.rb b/spec/mongoid/interceptable_spec.rb index 86d32b6f3..1453ffb42 100644 --- a/spec/mongoid/interceptable_spec.rb +++ b/spec/mongoid/interceptable_spec.rb @@ -2575,12 +2575,10 @@ class TestClass end end - before do + it 'logs a warning' do expect(Mongoid.logger).to receive(:warn).with(/Around callbacks are disabled for embedded documents/).twice.and_call_original expect(Mongoid.logger).to receive(:warn).with(/To enable around callbacks for embedded documents/).twice.and_call_original - end - it 'logs a warning' do parent.save! end end diff --git a/spec/mongoid/interceptable_spec_models.rb b/spec/mongoid/interceptable_spec_models.rb index fa4c41442..7bad8f01b 100644 --- a/spec/mongoid/interceptable_spec_models.rb +++ b/spec/mongoid/interceptable_spec_models.rb @@ -20,18 +20,18 @@ module CallbackTracking whens = %i[before after] %i[validation save create update].each do |what| whens.each do |whn| - send("#{whn}_#{what}", "#{whn}_#{what}_stub".to_sym) + send("#{whn}_#{what}", :"#{whn}_#{what}_stub") define_method("#{whn}_#{what}_stub") do - callback_registry&.record_call(self.class, "#{whn}_#{what}".to_sym) + callback_registry&.record_call(self.class, :"#{whn}_#{what}") end end next if what == :validation - send("around_#{what}", "around_#{what}_stub".to_sym) + send("around_#{what}", :"around_#{what}_stub") define_method("around_#{what}_stub") do |&block| - callback_registry&.record_call(self.class, "around_#{what}_open".to_sym) + callback_registry&.record_call(self.class, :"around_#{what}_open") block.call - callback_registry&.record_call(self.class, "around_#{what}_close".to_sym) + callback_registry&.record_call(self.class, :"around_#{what}_close") end end end diff --git a/spec/mongoid/monkey_patches_spec.rb b/spec/mongoid/monkey_patches_spec.rb index 8fdded9af..883be9c41 100644 --- a/spec/mongoid/monkey_patches_spec.rb +++ b/spec/mongoid/monkey_patches_spec.rb @@ -62,6 +62,10 @@ __sort_pair__ delete_one ], + BigDecimal => %i[ + __evolve_date__ + __evolve_time__ + ], Date => %i[ __evolve_date__ __evolve_time__ diff --git a/spec/mongoid/tasks/database_spec.rb b/spec/mongoid/tasks/database_spec.rb index e402ae8c3..bcfeb3678 100644 --- a/spec/mongoid/tasks/database_spec.rb +++ b/spec/mongoid/tasks/database_spec.rb @@ -224,44 +224,32 @@ class Note end context 'when wait is true' do - before do - allow(described_class).to receive(:wait_for_search_indexes) - described_class.create_search_indexes([searchable_model_spy], wait: true) - end - it 'invokes both create_search_indexes and wait_for_search_indexes' do - expect(searchable_model_spy).to have_received(:create_search_indexes) - expect(described_class).to have_received(:wait_for_search_indexes).with(searchable_model_spy => index_names) + expect(searchable_model_spy).to receive(:create_search_indexes) + expect(described_class).to receive(:wait_for_search_indexes).with(searchable_model_spy => index_names) + + described_class.create_search_indexes([searchable_model_spy], wait: true) end end context 'when wait is false' do - before do - allow(described_class).to receive(:wait_for_search_indexes) - described_class.create_search_indexes([searchable_model_spy], wait: false) - end - it 'invokes only create_search_indexes' do - expect(searchable_model_spy).to have_received(:create_search_indexes) - expect(described_class).to_not have_received(:wait_for_search_indexes) + expect(searchable_model_spy).to receive(:create_search_indexes) + expect(described_class).to_not receive(:wait_for_search_indexes) + + described_class.create_search_indexes([searchable_model_spy], wait: false) end end end describe '.remove_search_indexes' do - before do + it 'calls remove_search_indexes on all non-embedded models' do models.each do |model| - allow(model).to receive(:remove_search_indexes) unless model.embedded? + expect(model).to receive(:remove_search_indexes) unless model.embedded? end described_class.remove_search_indexes(models) end - - it 'calls remove_search_indexes on all non-embedded models' do - models.each do |model| - expect(model).to have_received(:remove_search_indexes) unless model.embedded? - end - end end describe '.undefined_indexes' do diff --git a/spec/mongoid/validatable/uniqueness_spec.rb b/spec/mongoid/validatable/uniqueness_spec.rb index b6eff724e..5b7d9582a 100644 --- a/spec/mongoid/validatable/uniqueness_spec.rb +++ b/spec/mongoid/validatable/uniqueness_spec.rb @@ -2486,7 +2486,7 @@ class SpanishActor < EuropeanActor it 'is invalid' do subclass_document_with_duplicated_name.tap do |d| - expect(d).to be_invalid + expect(d).to_not be_valid expect(d.errors[:name]).to eq(['has already been taken']) end end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 957d2e833..9314992f4 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -112,8 +112,6 @@ class Query inflect.singular('address_components', 'address_component') end -Time.zone = 'UTC' - I18n.config.enforce_available_locales = false if %w[yes true 1].include?((ENV['TEST_I18N_FALLBACKS'] || '').downcase) diff --git a/spec/support/constraints.rb b/spec/support/constraints.rb index c570549e1..0bd670643 100644 --- a/spec/support/constraints.rb +++ b/spec/support/constraints.rb @@ -50,6 +50,16 @@ def without_i18n_fallbacks end end + def local_env(env = nil) + around do |example| + saved_env = ENV.to_h + ENV.update(env || yield) + example.run + ensure + ENV.replace(saved_env) + end + end + def require_mri before(:all) do unless SpecConfig.instance.mri? From 4db8622b58cab7a931d949750fb3ffc8de998a31 Mon Sep 17 00:00:00 2001 From: johnnyshields <27655+johnnyshields@users.noreply.github.com> Date: Wed, 17 Jan 2024 12:31:51 +0900 Subject: [PATCH 52/52] Additional backport --- lib/mongoid/association/bindable.rb | 6 +++--- lib/mongoid/association/embedded/embedded_in/proxy.rb | 2 -- lib/mongoid/association/embedded/embeds_many/proxy.rb | 2 -- lib/mongoid/association/embedded/embeds_one/proxy.rb | 2 -- lib/mongoid/association/macros.rb | 2 -- lib/mongoid/association/nested/many.rb | 4 +++- lib/mongoid/association/proxy.rb | 10 ++++------ lib/mongoid/association/referenced/belongs_to/proxy.rb | 2 -- .../referenced/has_and_belongs_to_many/proxy.rb | 4 ---- lib/mongoid/association/referenced/has_many/proxy.rb | 1 - lib/mongoid/association/referenced/has_one/eager.rb | 1 - lib/mongoid/association/referenced/has_one/proxy.rb | 2 -- lib/mongoid/deprecation.rb | 6 +++--- spec/mongoid/tasks/database_spec.rb | 2 +- 14 files changed, 14 insertions(+), 32 deletions(-) diff --git a/lib/mongoid/association/bindable.rb b/lib/mongoid/association/bindable.rb index 8896e2334..e5b27d953 100644 --- a/lib/mongoid/association/bindable.rb +++ b/lib/mongoid/association/bindable.rb @@ -135,7 +135,7 @@ def bind_foreign_key(keyed, id) # @param [ Mongoid::Document ] typed The document that stores the type field. # @param [ String ] name The name of the model. def bind_polymorphic_type(typed, name) - return unless _association.type + return unless _association.type && !typed.frozen? try_method(typed, _association.type_setter, name) end @@ -151,7 +151,7 @@ def bind_polymorphic_type(typed, name) # @param [ Mongoid::Document ] typed The document that stores the type field. # @param [ String ] name The name of the model. def bind_polymorphic_inverse_type(typed, name) - return unless _association.inverse_type + return unless _association.inverse_type && !typed.frozen? try_method(typed, _association.inverse_type_setter, name) end @@ -167,7 +167,7 @@ def bind_polymorphic_inverse_type(typed, name) # @param [ Mongoid::Document ] doc The base document. # @param [ Mongoid::Document ] inverse The inverse document. def bind_inverse(doc, inverse) - return unless doc.respond_to?(_association.inverse_setter) + return unless doc.respond_to?(_association.inverse_setter) && !doc.frozen? try_method(doc, _association.inverse_setter, inverse) end diff --git a/lib/mongoid/association/embedded/embedded_in/proxy.rb b/lib/mongoid/association/embedded/embedded_in/proxy.rb index d91d446d3..e5278652c 100644 --- a/lib/mongoid/association/embedded/embedded_in/proxy.rb +++ b/lib/mongoid/association/embedded/embedded_in/proxy.rb @@ -4,7 +4,6 @@ module Mongoid module Association module Embedded class EmbeddedIn - # Transparent proxy for embedded_in associations. # An instance of this class is returned when calling the # association getter method on the child document. This @@ -12,7 +11,6 @@ class EmbeddedIn # most of its methods to the target of the association, i.e. # the parent document. class Proxy < Association::One - # Instantiate a new embedded_in association. # # @example Create the new association. diff --git a/lib/mongoid/association/embedded/embeds_many/proxy.rb b/lib/mongoid/association/embedded/embeds_many/proxy.rb index 19e544c8e..4472e51b9 100644 --- a/lib/mongoid/association/embedded/embeds_many/proxy.rb +++ b/lib/mongoid/association/embedded/embeds_many/proxy.rb @@ -6,7 +6,6 @@ module Mongoid module Association module Embedded class EmbedsMany - # Transparent proxy for embeds_many associations. # An instance of this class is returned when calling the # association getter method on the parent document. This @@ -18,7 +17,6 @@ class Proxy < Association::Many # Class-level methods for the Proxy class. module ClassMethods - # Returns the eager loader for this association. # # @param [ Array ] associations The diff --git a/lib/mongoid/association/embedded/embeds_one/proxy.rb b/lib/mongoid/association/embedded/embeds_one/proxy.rb index 5dbe9ccb0..d6578eb3c 100644 --- a/lib/mongoid/association/embedded/embeds_one/proxy.rb +++ b/lib/mongoid/association/embedded/embeds_one/proxy.rb @@ -4,7 +4,6 @@ module Mongoid module Association module Embedded class EmbedsOne - # Transparent proxy for embeds_one associations. # An instance of this class is returned when calling the # association getter method on the parent document. This @@ -12,7 +11,6 @@ class EmbedsOne # most of its methods to the target of the association, i.e. # the child document. class Proxy < Association::One - # The valid options when defining this association. # # @return [ Array ] The allowed options when defining this association. diff --git a/lib/mongoid/association/macros.rb b/lib/mongoid/association/macros.rb index 627cb39be..2a20e452d 100644 --- a/lib/mongoid/association/macros.rb +++ b/lib/mongoid/association/macros.rb @@ -2,7 +2,6 @@ module Mongoid module Association - # This module contains the core macros for defining associations between # documents. They can be either embedded or referenced. module Macros @@ -58,7 +57,6 @@ def associations # Class methods for associations. module ClassMethods - # Adds the association back to the parent document. This macro is # necessary to set the references from the child back to the parent # document. If a child does not define this association calling diff --git a/lib/mongoid/association/nested/many.rb b/lib/mongoid/association/nested/many.rb index 5400670c6..743bf3000 100644 --- a/lib/mongoid/association/nested/many.rb +++ b/lib/mongoid/association/nested/many.rb @@ -176,7 +176,9 @@ def update_nested_relation(parent, id, attrs) first = existing.first converted = first ? convert_id(first.class, id) : id - if existing.exists?(_id: converted) + # The next line cannot be written as `existing.exists?(_id: converted)`, + # otherwise tests will fail. + if existing.where(_id: converted).exists? # rubocop:disable Rails/WhereExists # document exists in association doc = existing.find(converted) if destroyable?(attrs) diff --git a/lib/mongoid/association/proxy.rb b/lib/mongoid/association/proxy.rb index 4c15658fc..650a8df85 100644 --- a/lib/mongoid/association/proxy.rb +++ b/lib/mongoid/association/proxy.rb @@ -4,16 +4,17 @@ module Mongoid module Association - # This class is the superclass for all association proxy objects, and contains # common behavior for all of them. class Proxy extend Forwardable + alias_method :extend_proxy, :extend + # Specific methods to prevent from being undefined. # # @api private - KEEP_METHODS = %i[ + KEEPER_METHODS = %i[ send object_id equal? @@ -25,11 +26,9 @@ class Proxy extend_proxies ].freeze - alias_method :extend_proxy, :extend - # We undefine most methods to get them sent through to the target. instance_methods.each do |method| - next if method.to_s.start_with?('__') || KEEP_METHODS.include?(method) + next if method.to_s.start_with?('__') || KEEPER_METHODS.include?(method) undef_method(method) end @@ -201,7 +200,6 @@ def execute_callbacks_around(name, doc) end class << self - # Apply ordering to the criteria if it was defined on the association. # # @example Apply the ordering. diff --git a/lib/mongoid/association/referenced/belongs_to/proxy.rb b/lib/mongoid/association/referenced/belongs_to/proxy.rb index 0735e6641..2d6ec8316 100644 --- a/lib/mongoid/association/referenced/belongs_to/proxy.rb +++ b/lib/mongoid/association/referenced/belongs_to/proxy.rb @@ -4,7 +4,6 @@ module Mongoid module Association module Referenced class BelongsTo - # Transparent proxy for belong_to associations. # An instance of this class is returned when calling the # association getter method on the subject document. @@ -99,7 +98,6 @@ def persistable? end class << self - # Get the Eager object for this type of association. # # @example Get the eager loader object diff --git a/lib/mongoid/association/referenced/has_and_belongs_to_many/proxy.rb b/lib/mongoid/association/referenced/has_and_belongs_to_many/proxy.rb index 04c454688..76b88c785 100644 --- a/lib/mongoid/association/referenced/has_and_belongs_to_many/proxy.rb +++ b/lib/mongoid/association/referenced/has_and_belongs_to_many/proxy.rb @@ -4,7 +4,6 @@ module Mongoid module Association module Referenced class HasAndBelongsToMany - # Transparent proxy for has_and_belongs_to_many associations. # An instance of this class is returned when calling # the association getter method on the subject document. @@ -13,10 +12,8 @@ class HasAndBelongsToMany # i.e. the array of documents on the opposite-side collection # which must be loaded. class Proxy < Referenced::HasMany::Proxy - # Class-level methods for HasAndBelongsToMany::Proxy module ClassMethods - # Get the eager loader object for this type of association. # # @example Get the eager loader object @@ -90,7 +87,6 @@ def <<(*args) end unsynced(_base, foreign_key) and self end - alias_method :push, :<< # Appends an array of documents to the association. Performs a batch diff --git a/lib/mongoid/association/referenced/has_many/proxy.rb b/lib/mongoid/association/referenced/has_many/proxy.rb index 67ae50396..2f9407682 100644 --- a/lib/mongoid/association/referenced/has_many/proxy.rb +++ b/lib/mongoid/association/referenced/has_many/proxy.rb @@ -4,7 +4,6 @@ module Mongoid module Association module Referenced class HasMany - # Transparent proxy for has_many associations. # An instance of this class is returned when calling the # association getter method on the subject document. This class diff --git a/lib/mongoid/association/referenced/has_one/eager.rb b/lib/mongoid/association/referenced/has_one/eager.rb index 397ad88d0..911bf5658 100644 --- a/lib/mongoid/association/referenced/has_one/eager.rb +++ b/lib/mongoid/association/referenced/has_one/eager.rb @@ -4,7 +4,6 @@ module Mongoid module Association module Referenced class HasOne - # Eager class for has_one associations. class Eager < Association::Eager diff --git a/lib/mongoid/association/referenced/has_one/proxy.rb b/lib/mongoid/association/referenced/has_one/proxy.rb index 676594e36..8070d097e 100644 --- a/lib/mongoid/association/referenced/has_one/proxy.rb +++ b/lib/mongoid/association/referenced/has_one/proxy.rb @@ -4,7 +4,6 @@ module Mongoid module Association module Referenced class HasOne - # Transparent proxy for has_one associations. # An instance of this class is returned when calling the # association getter method on the subject document. This class @@ -12,7 +11,6 @@ class HasOne # its methods to the target of the association, i.e. the # document on the opposite-side collection which must be loaded. class Proxy < Association::One - # Class-level methods for HasOne::Proxy module ClassMethods diff --git a/lib/mongoid/deprecation.rb b/lib/mongoid/deprecation.rb index fd7e261cc..a7f26639d 100644 --- a/lib/mongoid/deprecation.rb +++ b/lib/mongoid/deprecation.rb @@ -17,10 +17,10 @@ def initialize # # @return [ Array ] The deprecation behavior. def behavior - @behavior ||= Array(lambda do |message, callstack, _deprecation_horizon, _gem_name| + @behavior ||= Array(lambda do |*args| logger = Mongoid.logger - logger.warn(message) - logger.debug(callstack.join("\n ")) if debug + logger.warn(args[0]) + logger.debug(args[1].join("\n ")) if debug end) end end diff --git a/spec/mongoid/tasks/database_spec.rb b/spec/mongoid/tasks/database_spec.rb index bcfeb3678..fb5304a7d 100644 --- a/spec/mongoid/tasks/database_spec.rb +++ b/spec/mongoid/tasks/database_spec.rb @@ -226,7 +226,7 @@ class Note context 'when wait is true' do it 'invokes both create_search_indexes and wait_for_search_indexes' do expect(searchable_model_spy).to receive(:create_search_indexes) - expect(described_class).to receive(:wait_for_search_indexes).with(searchable_model_spy => index_names) + expect(described_class).to receive(:wait_for_search_indexes).with({ searchable_model_spy => index_names }) described_class.create_search_indexes([searchable_model_spy], wait: true) end