diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7a03c6c33..6f702d406 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -24,10 +24,10 @@ jobs: test: name: "${{matrix.ruby}} ${{matrix.gemfile || 'Gemfile'}} - mongodb-${{matrix.mongodb || '6.0'}} - ${{matrix.topology || 'server'}} - ${{matrix.fle && 'fle=' || ''}}${{matrix.fle || ''}} - ${{matrix.os || 'ubuntu-20.04'}}" + db:${{matrix.mongodb || '6.0'}} + fle:${{matrix.fle && 'fle=' || ''}}${{matrix.fle || ''}} + top:${{matrix.topology || 'server'}} + os:${{matrix.os || 'ubuntu-20.04'}}" env: CI: true TESTOPTS: '-v' @@ -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 @@ -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 69758c7ed..20a57954d 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' @@ -708,6 +718,7 @@ Rails/Delegate: - 'lib/mongoid/findable.rb' - 'lib/mongoid/scopable.rb' +# 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: @@ -735,23 +746,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' @@ -780,6 +785,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: @@ -814,7 +830,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/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 diff --git a/Rakefile b/Rakefile index 7e7b6c9a7..0cc8c2dd8 100644 --- a/Rakefile +++ b/Rakefile @@ -16,6 +16,18 @@ RSpec::Core::RakeTask.new('spec:progress') do |spec| spec.pattern = 'spec/**/*_spec.rb' end +namespace :generate do + desc 'Generates a mongoid.yml from the template' + task config: :environment do + require 'mongoid' + require 'erb' + + template_path = 'lib/rails/generators/mongoid/config/templates/mongoid.yml' + config = ERB.new(File.read(template_path), trim_mode: '-').result(binding) + File.write('mongoid.yml', config) + end +end + $LOAD_PATH.unshift File.expand_path('lib', __dir__) require 'mongoid/version' 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/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/reference/associations.txt b/docs/reference/associations.txt index ab3deb411..18e787475 100644 --- a/docs/reference/associations.txt +++ b/docs/reference/associations.txt @@ -693,7 +693,7 @@ Querying Loaded Associations ```````````````````````````` Mongoid query methods can be used on embedded associations of documents which -are already loaded in the application. This mechanism is referred to as +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. @@ -707,8 +707,8 @@ The following operators are supported: - :manual:`$type ` - :manual:`$regex ` (``$options`` field is only supported when the ``$regex`` argument is a string) -- :manual:`$comment ` - :manual:`Bitwise operators ` +- :manual:`$comment ` For example, using the model definitions just given, we could query tours on a loaded band: @@ -723,7 +723,7 @@ Embedded Matching vs Server Behavior Mongoid's embedded matching aims to support the same functionality and semantics as native queries on the latest MongoDB server version. -The following limitations are known: +Note the following known limitations: - Embedded matching is not implemented for :ref:`text search `, :manual:`geospatial query operators `, diff --git a/docs/reference/compatibility.txt b/docs/reference/compatibility.txt index d02bffc9d..7f616c315 100644 --- a/docs/reference/compatibility.txt +++ b/docs/reference/compatibility.txt @@ -1,5 +1,3 @@ -.. _compatibility: - ************* Compatibility ************* @@ -36,17 +34,52 @@ specified Mongoid versions. - Driver 2.17-2.10 - Driver 2.9-2.7 - * - 8.0 thru 9.0 + * - 9.0 + - |checkmark| + - + - + + * - 8.1 + - |checkmark| + - + - + + * - 8.0 + - |checkmark| + - + - + + * - 7.5 + - |checkmark| + - |checkmark| + - + + * - 7.4 + - |checkmark| - |checkmark| - + + * - 7.3 + - |checkmark| + - |checkmark| - - * - 7.2 thru 7.5 + * - 7.2 - |checkmark| - |checkmark| - - * - 6.4 thru 7.1 + * - 7.1 + - |checkmark| + - |checkmark| + - |checkmark| + + * - 7.0 + - |checkmark| + - |checkmark| + - |checkmark| + + * - 6.4 - |checkmark| - |checkmark| - |checkmark| @@ -74,23 +107,23 @@ is deprecated. - Ruby 2.4 - Ruby 2.3 - Ruby 2.2 - - JRuby 9.4 - - JRuby 9.3 - JRuby 9.2 + - JRuby 9.3 + - JRuby 9.4 * - 9.0 - |checkmark| - |checkmark| - |checkmark| - - D - - D + - |checkmark| + - - - - - - - |checkmark| - - + - |checkmark| * - 8.1 - |checkmark| @@ -106,6 +139,7 @@ is deprecated. - |checkmark| - + * - 8.0 - - |checkmark| @@ -130,9 +164,9 @@ is deprecated. - - - - - - - |checkmark| - D + - |checkmark| + - * - 7.4 - @@ -144,9 +178,9 @@ is deprecated. - - - + - |checkmark| - - - - |checkmark| * - 7.3 - @@ -158,9 +192,9 @@ is deprecated. - D - D - + - |checkmark| - - - - |checkmark| * - 7.2 - @@ -172,9 +206,9 @@ is deprecated. - D - D - + - |checkmark| - - - - |checkmark| * - 7.1 - @@ -186,9 +220,9 @@ is deprecated. - |checkmark| [#ruby-2.4]_ - |checkmark| - + - |checkmark| - - - - |checkmark| * - 7.0 - @@ -200,9 +234,9 @@ is deprecated. - |checkmark| [#ruby-2.4]_ - |checkmark| - |checkmark| [#ruby-2.2]_ + - |checkmark| - - - - |checkmark| * - 6.4 - @@ -214,9 +248,9 @@ is deprecated. - |checkmark| [#ruby-2.4]_ - |checkmark| - |checkmark| [#ruby-2.2]_ + - |checkmark| - - - - |checkmark| .. [#mongoid-7.3-ruby-3.0] Mongoid version 7.3.2 or higher is required. @@ -266,10 +300,10 @@ and will be removed in a next version. - |checkmark| - |checkmark| - |checkmark| - - D - - - - - - + - |checkmark| + - |checkmark| + - |checkmark| + - |checkmark| - - - @@ -280,8 +314,9 @@ and will be removed in a next version. - |checkmark| - |checkmark| - |checkmark| - - D - - D + - |checkmark| + - |checkmark| + - |checkmark| - - - @@ -294,12 +329,13 @@ and will be removed in a next version. - |checkmark| - |checkmark| - |checkmark| + - |checkmark| - - - - - * - 7.4 thru 7.5 + * - 7.5 - |checkmark| - |checkmark| - |checkmark| @@ -312,8 +348,72 @@ and will be removed in a next version. - D - D - * - 6.4 thru 7.3 + * - 7.4 + - |checkmark| + - |checkmark| + - |checkmark| + - |checkmark| + - |checkmark| + - |checkmark| + - |checkmark| + - D + - D + - D + - D + + * - 7.3 - + - + - |checkmark| + - |checkmark| + - |checkmark| + - |checkmark| + - |checkmark| + - D + - D + - D + - D + + * - 7.2 + - + - + - |checkmark| + - |checkmark| + - |checkmark| + - |checkmark| + - |checkmark| + - D + - D + - D + - D + + * - 7.1 + - + - + - |checkmark| + - |checkmark| + - |checkmark| + - |checkmark| + - |checkmark| + - D + - D + - D + - D + + * - 7.0 + - + - + - |checkmark| + - |checkmark| + - |checkmark| + - |checkmark| + - |checkmark| + - D + - D + - D + - D + + * - 6.4 - - - |checkmark| @@ -341,6 +441,7 @@ are supported by Mongoid. :class: compatibility-large no-padding * - Mongoid + - Rails 7.1 - Rails 7.0 - Rails 6.1 - Rails 6.0 @@ -348,13 +449,15 @@ are supported by Mongoid. - Rails 5.1 * - 9.0 + - |checkmark| [#rails-7.1]_ + - |checkmark| - |checkmark| - |checkmark| - - D - - * - 8.1 + - |checkmark| [#rails-7.1]_ - |checkmark| - |checkmark| - |checkmark| @@ -362,6 +465,7 @@ are supported by Mongoid. - * - 8.0 + - |checkmark| [#rails-7.1]_ - |checkmark| - |checkmark| - |checkmark| @@ -369,6 +473,7 @@ are supported by Mongoid. - * - 7.5 + - - |checkmark| - |checkmark| - |checkmark| @@ -376,6 +481,7 @@ are supported by Mongoid. - D * - 7.4 + - - |checkmark| - |checkmark| - |checkmark| @@ -383,6 +489,7 @@ are supported by Mongoid. - |checkmark| [#rails-5-ruby-3.0]_ * - 7.3 + - - |checkmark| [#rails-7-Mongoid-7.3]_ - |checkmark| - |checkmark| @@ -390,6 +497,7 @@ are supported by Mongoid. - |checkmark| [#rails-5-ruby-3.0]_ * - 7.2 + - - - |checkmark| [#rails-6.1]_ - |checkmark| @@ -397,6 +505,7 @@ are supported by Mongoid. - |checkmark| [#rails-5-ruby-3.0]_ * - 7.1 + - - - |checkmark| [#rails-6.1]_ - |checkmark| @@ -404,6 +513,7 @@ are supported by Mongoid. - |checkmark| * - 7.0 + - - - |checkmark| [#rails-6.1]_ - |checkmark| [#rails-6]_ @@ -414,6 +524,7 @@ are supported by Mongoid. - - - + - - |checkmark| - |checkmark| @@ -426,4 +537,87 @@ 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 +.. 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 diff --git a/docs/reference/configuration.txt b/docs/reference/configuration.txt index af7fa7a95..0014cb452 100644 --- a/docs/reference/configuration.txt +++ b/docs/reference/configuration.txt @@ -131,101 +131,86 @@ 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' + # roles: + # - 'dbOwner' - # Change the default authentication mechanism. Valid options are: - # :scram, :scram256, :mongodb_x509, :gssapi, :aws, and :plain. - # Default on MongoDB is :scram, which will use "SCRAM-SHA-256" if available, - # otherwise fallback to "SCRAM-SHA-1"; :scram256 will always use "SCRAM-SHA-256". + # 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 - - # 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 + # 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 @@ -234,136 +219,193 @@ 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) + # "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 - # Compressors to use. (default is to not use compression) - compressors: [zlib] + # Whether to truncate long log lines. (default: true) + # truncate_logs: true # Configure Mongoid-specific options. (optional) options: + # 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. 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 + # app_name: nil - # Type of executor for queries scheduled using ``load_async`` method. + # 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' - # 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 (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 # 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 - - # 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 + # legacy_readonly: false - # 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 + # existing method. + # 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 + # 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: @@ -735,6 +777,53 @@ 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, configure the Ruby driver options within ``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 ====================== 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/docs/reference/indexes.txt b/docs/reference/indexes.txt index 8e45403cd..5e17d18e3 100644 --- a/docs/reference/indexes.txt +++ b/docs/reference/indexes.txt @@ -113,6 +113,28 @@ This only works on the association macro that the foreign key is stored on: end +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 =========================== @@ -144,6 +166,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/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 } ]) diff --git a/docs/release-notes/mongoid-9.0.txt b/docs/release-notes/mongoid-9.0.txt index 7dd57d659..09fa47fb2 100644 --- a/docs/release-notes/mongoid-9.0.txt +++ b/docs/release-notes/mongoid-9.0.txt @@ -43,6 +43,48 @@ Consider using `MongoDB Atlas `_ to automate your MongoDB server upgrades. +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 +------------------------------------------------------------------ + +The deprecated class ``Mongoid::Errors::InvalidStorageParent`` has been removed. + + +``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 +------------------------------- + +The ``for_js`` method is deprecated and will be removed in Mongoid 10.0. + + Deprecated options removed -------------------------- @@ -78,6 +120,7 @@ The following previously deprecated functionality is now 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.) - ``Document#as_json :compact`` option is removed. Please call ```#compact`` on the returned ``Hash`` object instead. - ``Criteria#geo_near`` is removed as MongoDB server versions 4.2 @@ -403,6 +446,125 @@ 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 + + +``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' + + +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/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 diff --git a/gemfiles/rails_6.0.gemfile b/gemfiles/rails_7.1.gemfile similarity index 70% rename from gemfiles/rails_6.0.gemfile rename to gemfiles/rails_7.1.gemfile index 4a19cdfda..6eb4eed82 100644 --- a/gemfiles/rails_6.0.gemfile +++ b/gemfiles/rails_7.1.gemfile @@ -3,8 +3,8 @@ source 'https://rubygems.org' gemspec path: '..' -gem 'actionpack', '~> 6.0' -gem 'activemodel', '~> 6.0' +gem 'actionpack', '~> 7.1' +gem 'activemodel', '~> 7.1' require_relative './standard' standard_dependencies 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/bindable.rb b/lib/mongoid/association/bindable.rb index 80463b9be..e5b27d953 100644 --- a/lib/mongoid/association/bindable.rb +++ b/lib/mongoid/association/bindable.rb @@ -121,7 +121,7 @@ def remove_associated_in_to(doc, inverse) def bind_foreign_key(keyed, id) return if keyed.frozen? - keyed.you_must(_association.foreign_key_setter, id) + try_method(keyed, _association.foreign_key_setter, id) end # Set the type of the related document on the foreign type field, used @@ -135,9 +135,9 @@ 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? - typed.you_must(_association.type_setter, name) + try_method(typed, _association.type_setter, name) end # Set the type of the related document on the foreign type field, used @@ -151,9 +151,9 @@ 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? - typed.you_must(_association.inverse_type_setter, name) + try_method(typed, _association.inverse_type_setter, name) end # Bind the inverse document to the child document so that the in memory @@ -167,9 +167,9 @@ 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? - doc.you_must(_association.inverse_setter, inverse) + try_method(doc, _association.inverse_setter, inverse) end # Bind the provided document with the base from the parent association. @@ -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 96ccb818e..830009ccb 100644 --- a/lib/mongoid/association/embedded/embedded_in/binding.rb +++ b/lib/mongoid/association/embedded/embedded_in/binding.rb @@ -24,10 +24,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 @@ -41,9 +41,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/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.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_many/binding.rb b/lib/mongoid/association/embedded/embeds_many/binding.rb index ac895b0b4..8623bfba3 100644 --- a/lib/mongoid/association/embedded/embeds_many/binding.rb +++ b/lib/mongoid/association/embedded/embeds_many/binding.rb @@ -20,7 +20,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 @@ -32,7 +32,7 @@ def bind_one(doc) # @param [ Mongoid::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_many/proxy.rb b/lib/mongoid/association/embedded/embeds_many/proxy.rb index a3afc96fb..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 @@ -294,14 +292,24 @@ def destroy_all(conditions = {}) # @example Are there persisted documents? # person.posts.exists? # - # @param [ Hash | Object | false ] id_or_conditions an _id to - # search for, a hash of conditions, nil or false. - # - # @return [ true | false ] True is persisted documents exist, false if not. + # @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) - return _target.any?(&:persisted?) if id_or_conditions == :none - - criteria.exists?(id_or_conditions) + 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/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/embedded/embeds_one/binding.rb b/lib/mongoid/association/embedded/embeds_one/binding.rb index 0850ffc89..1ed7b8706 100644 --- a/lib/mongoid/association/embedded/embeds_one/binding.rb +++ b/lib/mongoid/association/embedded/embeds_one/binding.rb @@ -21,7 +21,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 @@ -33,7 +33,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/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 6d138c8a4..743bf3000 100644 --- a/lib/mongoid/association/nested/many.rb +++ b/lib/mongoid/association/nested/many.rb @@ -102,7 +102,7 @@ def over_limit?(attributes) 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) @@ -154,7 +154,7 @@ def destroy_document(relation, doc) # @param [ Mongoid::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 @@ -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/nested/nested_buildable.rb b/lib/mongoid/association/nested/nested_buildable.rb index 924a554a9..2a9af658c 100644 --- a/lib/mongoid/association/nested/nested_buildable.rb +++ b/lib/mongoid/association/nested/nested_buildable.rb @@ -64,6 +64,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 fa598243f..564139288 100644 --- a/lib/mongoid/association/nested/one.rb +++ b/lib/mongoid/association/nested/one.rb @@ -29,7 +29,7 @@ def build(parent) @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/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/auto_save.rb b/lib/mongoid/association/referenced/auto_save.rb index 3f11dcd95..a19b60557 100644 --- a/lib/mongoid/association/referenced/auto_save.rb +++ b/lib/mongoid/association/referenced/auto_save.rb @@ -59,7 +59,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, &:save) end end 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/counter_cache.rb b/lib/mongoid/association/referenced/counter_cache.rb index 055e80b87..af15089df 100644 --- a/lib/mongoid/association/referenced/counter_cache.rb +++ b/lib/mongoid/association/referenced/counter_cache.rb @@ -107,7 +107,7 @@ def self.define_callbacks!(association) original, current = send("#{foreign_key}_previous_change") unless original.nil? - association.klass.with(persistence_context) do |k| + association.klass.with(persistence_context.for_child(association.klass)) do |k| k.decrement_counter(cache_column, original) 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 2c1c35562..0418bcb98 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 @@ -18,11 +18,11 @@ class Binding # @param [ Mongoid::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 @@ -38,7 +38,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/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/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/referenced/has_many/enumerable.rb b/lib/mongoid/association/referenced/has_many/enumerable.rb index 1599937df..9ccb3d2e0 100644 --- a/lib/mongoid/association/referenced/has_many/enumerable.rb +++ b/lib/mongoid/association/referenced/has_many/enumerable.rb @@ -490,12 +490,48 @@ def respond_to_missing?(name, _include_private = false) 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/association/referenced/has_many/proxy.rb b/lib/mongoid/association/referenced/has_many/proxy.rb index b9a75fe34..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 @@ -227,8 +226,14 @@ def each(&block) # @example Are there persisted documents? # person.posts.exists? # - # @param [ Hash | Object | false ] id_or_conditions an _id to - # search for, a hash of conditions, nil or false. + # @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?(id_or_conditions = :none) 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/association/relatable.rb b/lib/mongoid/association/relatable.rb index 64fadbe84..c408c2770 100644 --- a/lib/mongoid/association/relatable.rb +++ b/lib/mongoid/association/relatable.rb @@ -80,7 +80,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 @@ -234,7 +234,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. @@ -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/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/attributes.rb b/lib/mongoid/attributes.rb index a439244a2..6e585acce 100644 --- a/lib/mongoid/attributes.rb +++ b/lib/mongoid/attributes.rb @@ -2,6 +2,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' @@ -294,7 +295,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 8ddd2c2ca..dc5117a0b 100644 --- a/lib/mongoid/attributes/nested.rb +++ b/lib/mongoid/attributes/nested.rb @@ -3,7 +3,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/cacheable.rb b/lib/mongoid/cacheable.rb index 752c38bc7..1e3e3e7c6 100644 --- a/lib/mongoid/cacheable.rb +++ b/lib/mongoid/cacheable.rb @@ -26,7 +26,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 diff --git a/lib/mongoid/clients/options.rb b/lib/mongoid/clients/options.rb index b8de545e4..1f8489916 100644 --- a/lib/mongoid/clients/options.rb +++ b/lib/mongoid/clients/options.rb @@ -77,7 +77,7 @@ 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? @@ -85,7 +85,7 @@ def persistence_context else PersistenceContext.get(self) || PersistenceContext.get(self.class) || - PersistenceContext.new(self.class) + PersistenceContext.new(self.class, storage_options) end end @@ -103,7 +103,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/sessions.rb b/lib/mongoid/clients/sessions.rb index ef0b42369..e0edfd54c 100644 --- a/lib/mongoid/clients/sessions.rb +++ b/lib/mongoid/clients/sessions.rb @@ -15,6 +15,10 @@ def self.included(base) module ClassMethods + # Actions that can be used to trigger transactional callbacks. + # @api private + CALLBACK_ACTIONS = %i[create destroy update].freeze + # Execute a block within the context of a session. # # @example Execute some operations in the context of a session. @@ -99,6 +103,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: %i[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. @@ -143,6 +190,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) # rubocop:disable Naming/AccessorMethodName + options = args.extract_options! + args << options + + 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 + # 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) + 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) + 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/clients/storage_options.rb b/lib/mongoid/clients/storage_options.rb index 5f44b5dcf..f695811ef 100644 --- a/lib/mongoid/clients/storage_options.rb +++ b/lib/mongoid/clients/storage_options.rb @@ -10,7 +10,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/composable.rb b/lib/mongoid/composable.rb index 97757a203..98f8dd467 100644 --- a/lib/mongoid/composable.rb +++ b/lib/mongoid/composable.rb @@ -11,6 +11,7 @@ require 'mongoid/matchable' require 'mongoid/persistable' require 'mongoid/reloadable' +require 'mongoid/search_indexable' require 'mongoid/selectable' require 'mongoid/scopable' require 'mongoid/serializable' @@ -49,6 +50,7 @@ module Composable include Association include Reloadable include Scopable + include SearchIndexable include Selectable include Serializable include Shardable diff --git a/lib/mongoid/config.rb b/lib/mongoid/config.rb index 275465e26..7864e0c24 100644 --- a/lib/mongoid/config.rb +++ b/lib/mongoid/config.rb @@ -111,6 +111,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 @@ -128,6 +152,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/config/defaults.rb b/lib/mongoid/config/defaults.rb index ef624adfd..9b4ea50b0 100644 --- a/lib/mongoid/config/defaults.rb +++ b/lib/mongoid/config/defaults.rb @@ -14,17 +14,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.new("Version no longer supported: #{version}") when '8.0' self.legacy_readonly = true @@ -32,6 +23,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/config/environment.rb b/lib/mongoid/config/environment.rb index 32bfbd968..438084c0e 100644 --- a/lib/mongoid/config/environment.rb +++ b/lib/mongoid/config/environment.rb @@ -60,11 +60,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) raise Mongoid::Errors::InvalidConfigFile.new(path) unless data.is_a?(Hash) diff --git a/lib/mongoid/config/options.rb b/lib/mongoid/config/options.rb index f8cd84e04..befb6e108 100644 --- a/lib/mongoid/config/options.rb +++ b/lib/mongoid/config/options.rb @@ -56,7 +56,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/lib/mongoid/contextual/atomic.rb b/lib/mongoid/contextual/atomic.rb index f9fa4a4ac..c330d3b8a 100644 --- a/lib/mongoid/contextual/atomic.rb +++ b/lib/mongoid/contextual/atomic.rb @@ -165,17 +165,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' => fields.to_h) + def unset(*unsets) + view.update_many('$unset' => collect_unset_operations(unsets)) end # Performs an atomic $min update operation on the given field or fields. @@ -243,6 +240,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 + .to_h { |field| [database_field_name(field), true] } + end end end end diff --git a/lib/mongoid/contextual/memory.rb b/lib/mongoid/contextual/memory.rb index ad7fa549b..18593f540 100644 --- a/lib/mongoid/contextual/memory.rb +++ b/lib/mongoid/contextual/memory.rb @@ -642,25 +642,24 @@ def apply_sorting in_place_sort(spec) end - # Compare two values, checking for nil. + # Compare two values, handling the cases when + # either value is nil. # # @api private # # @example Compare the two objects. # context.compare(a, b) # - # @param [ Object ] a The first object. - # @param [ Object ] b The first object. + # @param [ Object ] value_a The first object. + # @param [ Object ] value_b The second object. # # @return [ Integer ] The comparison value. - def compare(a, b) # rubocop:disable Naming/MethodParameterName - if a.nil? - b.nil? ? 0 : 1 - elsif b.nil? - -1 - else - a <=> b - end + 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(value_a) <=> compare_operand(value_b) end # Sort the documents in place. @@ -672,10 +671,8 @@ def compare(a, b) # rubocop:disable Naming/MethodParameterName def in_place_sort(values) documents.sort! do |a, b| values.map do |field, direction| - a_value = a[field] - b_value = 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 @@ -699,6 +696,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/contextual/mongo.rb b/lib/mongoid/contextual/mongo.rb index 7333f059a..f5141e1c0 100644 --- a/lib/mongoid/contextual/mongo.rb +++ b/lib/mongoid/contextual/mongo.rb @@ -1,5 +1,6 @@ # frozen_string_literal: true +require 'mongoid/atomic_update_preparer' require 'mongoid/contextual/mongo/documents_loader' require 'mongoid/contextual/atomic' require 'mongoid/contextual/aggregable/mongo' @@ -68,7 +69,13 @@ class Mongo def count(options = {}, &block) return super(&block) if block - view.count_documents(options) + 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 # Get the estimated number of documents matching the query. @@ -803,8 +810,7 @@ def documents_loader def update_documents(attributes, method = :update_one, opts = {}) return false unless attributes - attributes = attributes.transform_keys { |k| klass.database_field_name(k.to_s) } - view.send(method, attributes.__consolidate__(klass), opts) + view.send(method, AtomicUpdatePreparer.prepare(attributes, klass), opts) end # Apply the field limitations. @@ -963,6 +969,27 @@ 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. + # + # 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 + # representations of `$where`. + 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 + + true + end + def raise_document_not_found_error raise Errors::DocumentNotFound.new(klass, nil, nil) end 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.rb b/lib/mongoid/criteria.rb index 2ca68a411..a4bda938a 100644 --- a/lib/mongoid/criteria.rb +++ b/lib/mongoid/criteria.rb @@ -40,6 +40,26 @@ class Criteria include Clients::Sessions include Options + 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. CHECK = [].freeze @@ -158,7 +178,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 @@ -221,7 +241,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. @@ -246,16 +266,16 @@ def merge(other) # @example Merge another criteria into this criteria. # criteria.merge(Person.where(name: "bob")) # - # @param [ Mongoid::Criteria ] other The criteria to merge in. + # @param [ Mongoid::Criteria | Hash ] other The criteria to merge in. # # @return [ Mongoid::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 @@ -346,9 +366,11 @@ def respond_to?(name, include_private = false) # criteria.to_criteria # # @return [ Mongoid::Criteria ] self. + # @deprecated def to_criteria self end + Mongoid.deprecate(self, :to_criteria) # Convert the criteria to a proc. # @@ -437,6 +459,8 @@ def without_options # @param [ Hash ] scope The scope for the code. # # @return [ Mongoid::Criteria ] The criteria. + # + # @deprecated def for_js(javascript, scope = {}) code = if scope.empty? # CodeWithScope is not supported for $where as of MongoDB 4.4 @@ -446,6 +470,7 @@ def for_js(javascript, scope = {}) end js_query(code) end + Mongoid.deprecate(self, :for_js) private diff --git a/lib/mongoid/criteria/findable.rb b/lib/mongoid/criteria/findable.rb index 086257249..2d338271e 100644 --- a/lib/mongoid/criteria/findable.rb +++ b/lib/mongoid/criteria/findable.rb @@ -13,7 +13,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. # @@ -39,9 +40,9 @@ def execute_or_raise(ids, multi) # # @return [ Mongoid::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, 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. @@ -140,6 +141,41 @@ 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. + # + # @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/criteria/queryable/extensions/date.rb b/lib/mongoid/criteria/queryable/extensions/date.rb index 449036d3d..b25fd6b38 100644 --- a/lib/mongoid/criteria/queryable/extensions/date.rb +++ b/lib/mongoid/criteria/queryable/extensions/date.rb @@ -25,7 +25,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/criteria/queryable/extensions/object.rb b/lib/mongoid/criteria/queryable/extensions/object.rb index a4c190270..f578b2a94 100644 --- a/lib/mongoid/criteria/queryable/extensions/object.rb +++ b/lib/mongoid/criteria/queryable/extensions/object.rb @@ -129,9 +129,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 36bff0998..ad3d17f42 100644 --- a/lib/mongoid/criteria/queryable/extensions/regexp.rb +++ b/lib/mongoid/criteria/queryable/extensions/regexp.rb @@ -14,9 +14,11 @@ module Regexp # /\A[123]/.regexp? # # @return [ true ] Always true. + # @deprecated def regexp? true end + Mongoid.deprecate(self, :regexp?) module ClassMethods @@ -44,9 +46,11 @@ module RawExt # 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 5ab139eac..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) @@ -81,7 +81,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 @@ -98,9 +98,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/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/deprecable.rb b/lib/mongoid/deprecable.rb index fb21f3beb..a0e5201a9 100644 --- a/lib/mongoid/deprecable.rb +++ b/lib/mongoid/deprecable.rb @@ -27,7 +27,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 6c8963a18..a7f26639d 100644 --- a/lib/mongoid/deprecation.rb +++ b/lib/mongoid/deprecation.rb @@ -5,20 +5,22 @@ module Mongoid # Utility class for logging deprecation warnings. class Deprecation < ::ActiveSupport::Deprecation - @gem_name = 'Mongoid' - - # Per change policy, deprecations will be removed in the next major version. - @deprecation_horizon = "#{Mongoid::VERSION.split('.').first.to_i + 1}.0".freeze # rubocop:disable Style/RedundantFreeze + def initialize + # Per change policy, deprecations will be removed in the next major version. + deprecation_horizon = "#{Mongoid::VERSION.split('.').first.to_i + 1}.0" + gem_name = 'Mongoid' + super(deprecation_horizon, gem_name) + end # Overrides default ActiveSupport::Deprecation behavior # to use Mongoid's logger. # # @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/lib/mongoid/document.rb b/lib/mongoid/document.rb index 327d8d245..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 @@ -418,6 +418,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/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/invalid_storage_parent.rb b/lib/mongoid/errors/invalid_storage_parent.rb deleted file mode 100644 index 2a0a8d703..000000000 --- a/lib/mongoid/errors/invalid_storage_parent.rb +++ /dev/null @@ -1,27 +0,0 @@ -# frozen_string_literal: true - -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 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/array.rb b/lib/mongoid/extensions/array.rb index 62855fd9e..0738f975d 100644 --- a/lib/mongoid/extensions/array.rb +++ b/lib/mongoid/extensions/array.rb @@ -2,7 +2,6 @@ module Mongoid module Extensions - # Adds type-casting behavior to Array class. module Array @@ -23,9 +22,11 @@ def __evolve_object_id__ # [ 1, 2, 3 ].__find_args__ # # @return [ Array ] The array of args. + # @deprecated def __find_args__ flat_map(&:__find_args__).uniq(&:to_s) end + Mongoid.deprecate(self, :__find_args__) # Mongoize the array into an array of object ids. # @@ -50,27 +51,7 @@ def __mongoize_object_id__ # configured default time zone corresponding to date/time components # in this array. 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 + ::Time.zone.local(*self) end # Is the array a set of multiple arguments in a method? @@ -79,9 +60,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. @@ -130,6 +113,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) @@ -137,6 +121,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. @@ -170,7 +155,5 @@ def resizable? end end -Array.include Mongoid::Extensions::Array +Array.include(Mongoid::Extensions::Array) Array.extend(Mongoid::Extensions::Array::ClassMethods) - -Mongoid.deprecate(Array, :blank_criteria) diff --git a/lib/mongoid/extensions/big_decimal.rb b/lib/mongoid/extensions/big_decimal.rb index e10a82302..363e32fa3 100644 --- a/lib/mongoid/extensions/big_decimal.rb +++ b/lib/mongoid/extensions/big_decimal.rb @@ -2,9 +2,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. # @@ -12,9 +19,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. @@ -38,7 +47,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. @@ -88,5 +96,4 @@ def mongoize(object) end end -BigDecimal.include Mongoid::Extensions::BigDecimal -BigDecimal.extend(Mongoid::Extensions::BigDecimal::ClassMethods) +BigDecimal.include(Mongoid::Extensions::BigDecimal) diff --git a/lib/mongoid/extensions/date.rb b/lib/mongoid/extensions/date.rb index 22f36367e..6dc80bde6 100644 --- a/lib/mongoid/extensions/date.rb +++ b/lib/mongoid/extensions/date.rb @@ -16,7 +16,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/false_class.rb b/lib/mongoid/extensions/false_class.rb index 2c37753f4..5b2366137 100644 --- a/lib/mongoid/extensions/false_class.rb +++ b/lib/mongoid/extensions/false_class.rb @@ -2,19 +2,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? # diff --git a/lib/mongoid/extensions/float.rb b/lib/mongoid/extensions/float.rb index 6a6672128..be3ef4b0e 100644 --- a/lib/mongoid/extensions/float.rb +++ b/lib/mongoid/extensions/float.rb @@ -13,7 +13,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/hash.rb b/lib/mongoid/extensions/hash.rb index ea0862381..9f2eec5d0 100644 --- a/lib/mongoid/extensions/hash.rb +++ b/lib/mongoid/extensions/hash.rb @@ -31,75 +31,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. - def __consolidate__(klass) - each_pair.with_object({}) do |(key, value), consolidated| - if key.start_with?('$') - value = value.each_with_object({}) do |(key2, value2), hash| - key2 = klass.database_field_name(key2) - hash[key2] = key == '$rename' ? value2.to_s : mongoize_for(key, klass, key2, value2) - end - consolidated[key] ||= {} - consolidated[key].update(value) - else - consolidated['$set'] ||= {} - consolidated['$set'].update(key => mongoize_for(key, klass, key, value)) - end - 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 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_method :blank_criteria?, :_mongoid_unsatisfiable_criteria? + def __consolidate__(klass) + Mongoid::AtomicUpdatePreparer.prepare(self, klass) + end + Mongoid.deprecate(self, :__consolidate__) # Deletes an id value from the hash. # @@ -107,9 +52,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. @@ -118,32 +65,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 - - # 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 + Mongoid.deprecate(self, :extract_id) # Turn the object from the ruby type we deal with to a Mongo friendly # type. @@ -174,41 +100,11 @@ def resizable? # { klass: Band, where: { name: "Depeche Mode" }.to_criteria # # @return [ Mongoid::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 - end - - private - - # 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? && !value.is_a?(Array) - val = val.first - end - val - else - value - end + Criteria.from_hash(self) end + Mongoid.deprecate(self, :to_criteria) module ClassMethods @@ -246,7 +142,5 @@ def resizable? end end -Hash.include Mongoid::Extensions::Hash +Hash.include(Mongoid::Extensions::Hash) Hash.extend(Mongoid::Extensions::Hash::ClassMethods) - -Mongoid.deprecate(Hash, :blank_criteria) diff --git a/lib/mongoid/extensions/integer.rb b/lib/mongoid/extensions/integer.rb index 9b6f2b2fa..0a59382a0 100644 --- a/lib/mongoid/extensions/integer.rb +++ b/lib/mongoid/extensions/integer.rb @@ -13,7 +13,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? @@ -32,9 +32,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/nil_class.rb b/lib/mongoid/extensions/nil_class.rb index d23623fd7..da4eff4db 100644 --- a/lib/mongoid/extensions/nil_class.rb +++ b/lib/mongoid/extensions/nil_class.rb @@ -2,19 +2,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. # diff --git a/lib/mongoid/extensions/object.rb b/lib/mongoid/extensions/object.rb index 14a99f9e7..91e9c42d6 100644 --- a/lib/mongoid/extensions/object.rb +++ b/lib/mongoid/extensions/object.rb @@ -2,9 +2,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. # @@ -23,9 +25,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. # @@ -49,9 +53,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. # @@ -59,9 +65,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. # @@ -69,23 +77,11 @@ def __sortable__ # 1.__to_inc__ # # @return [ Object ] The object. + # @deprecated def __to_inc__ self end - - # 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, :__to_inc__) # Do or do not, there is no try. -- Yoda. # @@ -97,9 +93,11 @@ def blank_criteria? # # @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. # @@ -135,9 +133,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? # @@ -196,12 +196,13 @@ 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 # contstraint. # @@ -212,11 +213,13 @@ 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. # @@ -247,7 +250,4 @@ def mongoize(object) end end -Object.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/extensions/range.rb b/lib/mongoid/extensions/range.rb index dd2fa7061..6f04cbab6 100644 --- a/lib/mongoid/extensions/range.rb +++ b/lib/mongoid/extensions/range.rb @@ -2,9 +2,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. # @@ -12,9 +14,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. @@ -38,7 +42,6 @@ def resizable? end module ClassMethods - # Convert the object from its mongo friendly ruby type to this type. # # @example Demongoize the object. @@ -104,5 +107,4 @@ def __mongoize_range__(object) end end -Range.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 13219c360..298979331 100644 --- a/lib/mongoid/extensions/set.rb +++ b/lib/mongoid/extensions/set.rb @@ -5,7 +5,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. # @@ -17,8 +16,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/lib/mongoid/extensions/string.rb b/lib/mongoid/extensions/string.rb index d35849824..b0d1a13a7 100644 --- a/lib/mongoid/extensions/string.rb +++ b/lib/mongoid/extensions/string.rb @@ -7,8 +7,11 @@ 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. # # @example Evolve the string. @@ -37,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.configured == ::Time - 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. @@ -68,9 +69,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. @@ -131,9 +134,11 @@ def before_type_cast? # 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 diff --git a/lib/mongoid/extensions/symbol.rb b/lib/mongoid/extensions/symbol.rb index e0da88777..667c730c8 100644 --- a/lib/mongoid/extensions/symbol.rb +++ b/lib/mongoid/extensions/symbol.rb @@ -12,9 +12,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/lib/mongoid/extensions/time.rb b/lib/mongoid/extensions/time.rb index 1fd34bb09..3c88c0fe1 100644 --- a/lib/mongoid/extensions/time.rb +++ b/lib/mongoid/extensions/time.rb @@ -29,19 +29,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/extensions/true_class.rb b/lib/mongoid/extensions/true_class.rb index 05f09679c..2c826a5ea 100644 --- a/lib/mongoid/extensions/true_class.rb +++ b/lib/mongoid/extensions/true_class.rb @@ -2,19 +2,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? # diff --git a/lib/mongoid/fields/foreign_key.rb b/lib/mongoid/fields/foreign_key.rb index aaa27d019..9b6f145fc 100644 --- a/lib/mongoid/fields/foreign_key.rb +++ b/lib/mongoid/fields/foreign_key.rb @@ -13,7 +13,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 [ Mongoid::Document ] document The document to add to. # @param [ String ] name The name of the field. @@ -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) @@ -94,7 +94,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 @@ -123,6 +123,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/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 8c020dc0d..d598c37bc 100644 --- a/lib/mongoid/interceptable.rb +++ b/lib/mongoid/interceptable.rb @@ -48,7 +48,9 @@ module Interceptable # @api private define_model_callbacks :persist_parent - define_model_callbacks :commit, :rollback, only: :after + define_callbacks :commit, :rollback, + only: :after, + scope: %i[kind name] attr_accessor :before_callback_halted end @@ -152,7 +154,29 @@ def run_callbacks(kind, with_children: true, skip_if: nil, &block) # # @api private def _mongoid_run_child_callbacks(kind, children: nil, &block) - child, *tail = (children || cascadable_children(kind)) + 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? block&.call @@ -160,11 +184,75 @@ 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 ||= 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| # rubocop:disable Style/HashEachMethods + 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. @@ -318,7 +406,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 = chain.compile + sequence = compile_callbacks(chain) sequence.invoke_before(env) env.value = !env.halted sequence.invoke_after(env) @@ -328,5 +416,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/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/persistable/creatable.rb b/lib/mongoid/persistable/creatable.rb index 248cbdd32..ad9f3e893 100644 --- a/lib/mongoid/persistable/creatable.rb +++ b/lib/mongoid/persistable/creatable.rb @@ -85,6 +85,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/persistable/incrementable.rb b/lib/mongoid/persistable/incrementable.rb index 0c6bec538..b44bbed4f 100644 --- a/lib/mongoid/persistable/incrementable.rb +++ b/lib/mongoid/persistable/incrementable.rb @@ -20,7 +20,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 ce63af63a..9849672d5 100644 --- a/lib/mongoid/persistable/multipliable.rb +++ b/lib/mongoid/persistable/multipliable.rb @@ -20,7 +20,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? diff --git a/lib/mongoid/persistable/updatable.rb b/lib/mongoid/persistable/updatable.rb index fd2bc6dcf..20a2d4b61 100644 --- a/lib/mongoid/persistable/updatable.rb +++ b/lib/mongoid/persistable/updatable.rb @@ -130,6 +130,9 @@ 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 diff --git a/lib/mongoid/persistence_context.rb b/lib/mongoid/persistence_context.rb index 5b3556d4d..dcf74066e 100644 --- a/lib/mongoid/persistence_context.rb +++ b/lib/mongoid/persistence_context.rb @@ -10,7 +10,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. @@ -45,6 +45,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.instance_of?((@object.is_a?(Class) ? @object : @object.class)) + else + raise ArgumentError.new('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. @@ -111,7 +131,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. @@ -143,6 +163,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) @@ -175,7 +207,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/lib/mongoid/railties/database.rake b/lib/mongoid/railties/database.rake index 5aeb4c989..d49ce8cc5 100644 --- a/lib/mongoid/railties/database.rake +++ b/lib/mongoid/railties/database.rake @@ -74,11 +74,21 @@ 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' end + 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') desc 'Shard collections with shard keys specified in Mongoid models' task shard_collections: :'mongoid:shard_collections' diff --git a/lib/mongoid/reloadable.rb b/lib/mongoid/reloadable.rb index 47e37bec0..871338fd0 100644 --- a/lib/mongoid/reloadable.rb +++ b/lib/mongoid/reloadable.rb @@ -93,23 +93,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) - segments = atomic_position.split('.').map { |part| Utils.maybe_integer(part) } - attributes.dig(*segments) - end end end diff --git a/lib/mongoid/search_indexable.rb b/lib/mongoid/search_indexable.rb new file mode 100644 index 000000000..bc6764174 --- /dev/null +++ b/lib/mongoid/search_indexable.rb @@ -0,0 +1,170 @@ +# 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 + 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) + 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 d1cf30143..fa6057d04 100644 --- a/lib/mongoid/tasks/database.rake +++ b/lib/mongoid/tasks/database.rake @@ -18,6 +18,12 @@ namespace :db do Mongoid::Tasks::Database.create_indexes end + desc 'Create search indexes specified in Mongoid models' + task create_search_indexes: %i[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: %i[environment load_models] do Mongoid::Tasks::Database.remove_undefined_indexes @@ -28,6 +34,11 @@ namespace :db do Mongoid::Tasks::Database.remove_indexes end + desc 'Remove search indexes specified in Mongoid models' + task remove_search_indexes: %i[environment load_models] do + Mongoid::Tasks::Database.remove_search_indexes + end + desc 'Shard collections with shard keys specified in Mongoid models' task shard_collections: %i[environment load_models] do Mongoid::Tasks::Database.shard_collections diff --git a/lib/mongoid/tasks/database.rb b/lib/mongoid/tasks/database.rb index 596d51cde..fff8805a5 100644 --- a/lib/mongoid/tasks/database.rb +++ b/lib/mongoid/tasks/database.rb @@ -25,6 +25,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 StandardError + logger.error "error while creating collection for #{model}" + raise end end @@ -53,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. # @@ -127,6 +150,18 @@ def remove_indexes(models = ::Mongoid.models) end 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, @@ -203,6 +238,30 @@ 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? + logger.info("MONGOID: Search indexes on #{model} are READY") + else + print '.' # rubocop:disable Rails/Output + $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/tasks/encryption.rake b/lib/mongoid/tasks/encryption.rake index ca60c0257..f79fd6f8e 100644 --- a/lib/mongoid/tasks/encryption.rake +++ b/lib/mongoid/tasks/encryption.rake @@ -2,26 +2,45 @@ require 'optparse' +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 - parser.parse!(parser.order!(ARGV) {}) # rubocop:disable Lint/EmptyBlock + options = parse_data_key_options result = Mongoid::Tasks::Encryption.create_data_key( client_name: options[:client_name], diff --git a/lib/mongoid/timestamps/created.rb b/lib/mongoid/timestamps/created.rb index 5b198178c..0eb4c7299 100644 --- a/lib/mongoid/timestamps/created.rb +++ b/lib/mongoid/timestamps/created.rb @@ -23,9 +23,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 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/touchable.rb b/lib/mongoid/touchable.rb index 433b68430..5cb1ca8b1 100644 --- a/lib/mongoid/touchable.rb +++ b/lib/mongoid/touchable.rb @@ -51,7 +51,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/lib/mongoid/utils.rb b/lib/mongoid/utils.rb index 958473f35..5b2c39923 100644 --- a/lib/mongoid/utils.rb +++ b/lib/mongoid/utils.rb @@ -24,20 +24,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 @@ -53,5 +39,16 @@ def maybe_integer(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/lib/mongoid/validatable.rb b/lib/mongoid/validatable.rb index 96d6fd9b8..c95a520ed 100644 --- a/lib/mongoid/validatable.rb +++ b/lib/mongoid/validatable.rb @@ -67,7 +67,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/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/lib/rails/generators/mongoid/config/templates/mongoid.yml b/lib/rails/generators/mongoid/config/templates/mongoid.yml index 9b4a17843..8cbb62d58 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 @@ -41,20 +41,20 @@ development: # roles: # - 'dbOwner' - # Change the default authentication mechanism. Valid options are: - # :scram, :scram256, :mongodb_x509, :gssapi, :aws, and :plain. - # Default on MongoDB is :scram, which will use "SCRAM-SHA-256" if available, - # otherwise fallback to "SCRAM-SHA-1"; :scram256 will always use "SCRAM-SHA-256". + # 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: :scram256 + # 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 + # 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 @@ -95,6 +95,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 @@ -115,17 +120,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: diff --git a/mongoid.gemspec b/mongoid.gemspec index 4c9ed2dc0..cf9a0cf10 100644 --- a/mongoid.gemspec +++ b/mongoid.gemspec @@ -26,11 +26,12 @@ Gem::Specification.new do |s| } 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', ['>= 6.0', '< 7.1', '!= 7.0.0']) - s.add_dependency('mongo', ['>= 2.18.0', '< 3.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']) # The ruby2_keywords gem normalizes Ruby 2.7's arg delegation. diff --git a/spec/integration/app_spec.rb b/spec/integration/app_spec.rb index fddd886a9..43299db48 100644 --- a/spec/integration/app_spec.rb +++ b/spec/integration/app_spec.rb @@ -302,7 +302,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 gemfile_lines << if rails_version == 'master' "gem 'rails', git: 'https://github.com/rails/rails'\n" diff --git a/spec/integration/callbacks_spec.rb b/spec/integration/callbacks_spec.rb index ebf4c856b..665b17401 100644 --- a/spec/integration/callbacks_spec.rb +++ b/spec/integration/callbacks_spec.rb @@ -557,6 +557,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) { [] } @@ -581,4 +582,24 @@ def will_save_change_to_attribute_values_before expect(logger).to eq(%i[embedded_twice embedded_once root]) end end + + context 'cascade callbacks' do + min_ruby_version '3.0' + require_mri + + 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! }.to_not raise_error(SystemStackError) + end + end end diff --git a/spec/integration/criteria/raw_value_spec.rb b/spec/integration/criteria/raw_value_spec.rb index 2ab4f8914..f6826febc 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 - let(:now_utc) { Time.utc(2020, 1, 1, 16, 0, 0, 0) } let(:today) { Date.new(2020, 1, 1) } diff --git a/spec/mongoid/association/embedded/embeds_many/proxy_spec.rb b/spec/mongoid/association/embedded/embeds_many/proxy_spec.rb index d9814825e..cc353976f 100644 --- a/spec/mongoid/association/embedded/embeds_many/proxy_spec.rb +++ b/spec/mongoid/association/embedded/embeds_many/proxy_spec.rb @@ -2309,9 +2309,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 @@ -2323,6 +2351,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 9f667ff93..b7265d873 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 @@ -2536,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/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/attributes/embedded_spec.rb b/spec/mongoid/attributes/embedded_spec.rb new file mode 100644 index 000000000..6416f4d75 --- /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/clients/options_spec.rb b/spec/mongoid/clients/options_spec.rb index 5a0e045b7..b8090472f 100644 --- a/spec/mongoid/clients/options_spec.rb +++ b/spec/mongoid/clients/options_spec.rb @@ -137,7 +137,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) expect(cluster_during).to be cluster_before end @@ -329,7 +330,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 @@ -424,7 +425,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/clients/transactions_spec.rb b/spec/mongoid/clients/transactions_spec.rb index 56f996713..6d1933892 100644 --- a/spec/mongoid/clients/transactions_spec.rb +++ b/spec/mongoid/clients/transactions_spec.rb @@ -683,49 +683,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!(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!(name: 'Foma Kiniaev') end end @@ -733,61 +818,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!(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 b85695ea2..8eb8bd7cf 100644 --- a/spec/mongoid/clients/transactions_spec_models.rb +++ b/spec/mongoid/clients/transactions_spec_models.rb @@ -51,6 +51,51 @@ 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 diff --git a/spec/mongoid/config/defaults_spec.rb b/spec/mongoid/config/defaults_spec.rb index ca86bd483..2b9624672 100644 --- a/spec/mongoid/config/defaults_spec.rb +++ b/spec/mongoid/config/defaults_spec.rb @@ -25,12 +25,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 @@ -80,12 +82,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/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/config_spec.rb b/spec/mongoid/config_spec.rb index 9f2e12450..38b7b1abf 100644 --- a/spec/mongoid/config_spec.rb +++ b/spec/mongoid/config_spec.rb @@ -327,6 +327,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/contextual/mongo_spec.rb b/spec/mongoid/contextual/mongo_spec.rb index 6634c9e2a..07ee7d411 100644 --- a/spec/mongoid/contextual/mongo_spec.rb +++ b/spec/mongoid/contextual/mongo_spec.rb @@ -157,6 +157,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 @@ -3673,16 +3683,50 @@ 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(%w[electronic pop]) + end + + it 'updates the last matching document' do + expect(new_order.reload.genres).to eq(%w[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 diff --git a/spec/mongoid/criteria/findable_spec.rb b/spec/mongoid/criteria/findable_spec.rb index c17d44239..527e75e41 100644 --- a/spec/mongoid/criteria/findable_spec.rb +++ b/spec/mongoid/criteria/findable_spec.rb @@ -259,6 +259,195 @@ 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 do + found + 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 + 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 do + found + 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 + 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 do + found + 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 + 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/criteria/queryable/extensions/regexp_raw_spec.rb b/spec/mongoid/criteria/queryable/extensions/regexp_raw_spec.rb index de16e88af..69c68c5b5 100644 --- a/spec/mongoid/criteria/queryable/extensions/regexp_raw_spec.rb +++ b/spec/mongoid/criteria/queryable/extensions/regexp_raw_spec.rb @@ -77,15 +77,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 e2c69762a..ccc7fc744 100644 --- a/spec/mongoid/criteria/queryable/extensions/regexp_spec.rb +++ b/spec/mongoid/criteria/queryable/extensions/regexp_spec.rb @@ -77,15 +77,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 b54d5ad57..0bddb4383 100644 --- a/spec/mongoid/criteria/queryable/extensions/string_spec.rb +++ b/spec/mongoid/criteria/queryable/extensions/string_spec.rb @@ -181,48 +181,53 @@ end describe '#__expr_part__' do + subject(:specified) { 'field'.__expr_part__(value) } - let(:specified) do - 'field'.__expr_part__(10) - end + let(:value) { 10 } - it 'returns the string with the value' do + it 'returns the expression with the value' do expect(specified).to eq({ 'field' => 10 }) end - context 'with a regexp' do - - let(:specified) do - 'field'.__expr_part__(/test/) - end + context 'with a Regexp' do + let(:value) { /test/ } - it 'returns the symbol with the value' do + it 'returns the expression with the value' do expect(specified).to eq({ 'field' => /test/ }) end + 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 + 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) - end - - it 'returns the string with the value negated' do + it 'returns the expression 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 + 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 @@ -230,31 +235,26 @@ end 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 + it 'returns the regexp' do + expect(evolved).to eq(/\A[123]/) end + end - let(:evolved) do - described_class.evolve(regex) - end + context 'when provided a BSON::Regexp::Raw' do + let(:object) { BSON::Regexp::Raw.new('^[123]') } - it 'returns the regex' do - expect(evolved).to eq(regex) + it 'returns the BSON::Regexp::Raw' do + expect(evolved).to eq(BSON::Regexp::Raw.new('^[123]')) end end context 'when provided an object' do - - let(:object) do - 1234 - end - - let(:evolved) do - described_class.evolve(object) - end + let(:object) { 1234 } it 'returns the object as a string' do expect(evolved).to eq('1234') diff --git a/spec/mongoid/criteria/queryable/extensions/symbol_spec.rb b/spec/mongoid/criteria/queryable/extensions/symbol_spec.rb index 974fab2d1..1a508c1f9 100644 --- a/spec/mongoid/criteria/queryable/extensions/symbol_spec.rb +++ b/spec/mongoid/criteria/queryable/extensions/symbol_spec.rb @@ -10,6 +10,10 @@ described_class.add_key(:fubar, :union, '$fu', '$bar', &:to_s) end + after do + described_class.undef_method(:fubar) + end + let(:fubar) do :testing.fubar end diff --git a/spec/mongoid/criteria_spec.rb b/spec/mongoid/criteria_spec.rb index 777c7319f..df4622640 100644 --- a/spec/mongoid/criteria_spec.rb +++ b/spec/mongoid/criteria_spec.rb @@ -1425,51 +1425,68 @@ end describe '#merge!' do + subject(:merged) { criteria.merge!(other) } - let(:band) do - Band.new - end + let(:band) { Band.new } + let(:criteria) { Band.scoped.where(name: 'Depeche Mode').asc(:name) } + let(:association) { Band.relations['records'] } - let(:criteria) do - Band.scoped.where(name: 'Depeche Mode').asc(:name) - end + context 'when merging a Criteria' do + let(:other) do + { klass: Band, includes: [:records] } + end - let(:mergeable) do - Band.includes(:records).tap do |crit| - crit.documents = [band] + it 'merges the selector' do + expect(merged.selector).to eq({ 'name' => 'Depeche Mode' }) end - end - let(:association) do - Band.relations['records'] - end + it 'merges the options' do + expect(merged.options).to eq({ sort: { 'name' => 1 } }) + end - let(:merged) do - criteria.merge!(mergeable) - end + it 'merges the scoping options' do + expect(merged.scoping_options).to eq([nil, nil]) + end - it 'merges the selector' do - expect(merged.selector).to eq({ 'name' => 'Depeche Mode' }) - end + it 'merges the inclusions' do + expect(merged.inclusions).to eq([association]) + end - it 'merges the options' do - expect(merged.options).to eq({ sort: { 'name' => 1 } }) + it 'returns the same criteria' do + expect(merged).to equal(criteria) + end end - it 'merges the documents' do - expect(merged.documents).to eq([band]) - end + context 'when merging a Hash' do + let(:other) do + Band.includes(:records).tap do |crit| + crit.documents = [band] + end + end - it 'merges the scoping options' do - expect(merged.scoping_options).to eq([nil, nil]) - end + it 'merges the selector' do + expect(merged.selector).to eq({ 'name' => 'Depeche Mode' }) + end - it 'merges the inclusions' do - expect(merged.inclusions).to eq([association]) - end + it 'merges the options' do + expect(merged.options).to eq({ sort: { 'name' => 1 } }) + 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 @@ -3381,11 +3398,11 @@ def self.ages 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 @@ -3624,4 +3641,44 @@ def self.ages 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(described_class) + 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(described_class) + 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 diff --git a/spec/mongoid/document_persistence_context_spec.rb b/spec/mongoid/document_persistence_context_spec.rb index 6c30bec41..baf2e2fe6 100644 --- a/spec/mongoid/document_persistence_context_spec.rb +++ b/spec/mongoid/document_persistence_context_spec.rb @@ -29,4 +29,80 @@ expect(Person.collection_name).to eq(: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 }.to_not 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(Mongoid::Errors::DocumentNotFound, /Document\(s\) not found for class Person with id\(s\)/) + 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 diff --git a/spec/mongoid/errors/mongoid_error_spec.rb b/spec/mongoid/errors/mongoid_error_spec.rb index 8aac79605..0cb68afb0 100644 --- a/spec/mongoid/errors/mongoid_error_spec.rb +++ b/spec/mongoid/errors/mongoid_error_spec.rb @@ -9,28 +9,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(:t).with("mongoid.errors.messages.#{key}", **{}).and_return(name) - end - - %w[message summary resolution].each do |name| - expect(I18n).to receive(:t) - .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(:t).with("mongoid.errors.messages.#{key}", {}).and_return(name) - end - - %w[message summary resolution].each do |name| - expect(I18n).to receive(:t) - .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 + + %w[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) diff --git a/spec/mongoid/extensions/array_spec.rb b/spec/mongoid/extensions/array_spec.rb index 6f4adadd8..7099cf97b 100644 --- a/spec/mongoid/extensions/array_spec.rb +++ b/spec/mongoid/extensions/array_spec.rb @@ -202,156 +202,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 @@ -377,34 +227,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 @@ -532,78 +354,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 diff --git a/spec/mongoid/extensions/false_class_spec.rb b/spec/mongoid/extensions/false_class_spec.rb index 9894b08dd..a73b1acb8 100644 --- a/spec/mongoid/extensions/false_class_spec.rb +++ b/spec/mongoid/extensions/false_class_spec.rb @@ -4,13 +4,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/hash_spec.rb b/spec/mongoid/extensions/hash_spec.rb index 0f2d91ee4..94d7a887e 100644 --- a/spec/mongoid/extensions/hash_spec.rb +++ b/spec/mongoid/extensions/hash_spec.rb @@ -162,127 +162,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 - - 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 'retrieves 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 'retrieves the falsey nested value under the provided key' do - expect(nested).to be false - end - end - - context 'and the value is nil' do - let(:hash) do - { '100' => { 0 => "Please don't return this value!" } } - end - - it 'retrieves the nil nested value under the provided key' do - expect(nested).to be_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 'retrieves 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 'returns nil' do - expect(nested).to be_nil - end - end - describe '.demongoize' do let(:hash) do @@ -414,107 +293,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 d63abef2a..3855117d0 100644 --- a/spec/mongoid/extensions/object_spec.rb +++ b/spec/mongoid/extensions/object_spec.rb @@ -15,13 +15,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 @@ -29,97 +22,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 @@ -127,13 +29,6 @@ end end - describe '#__sortable__' do - - it 'returns self' do - expect(object.__sortable__).to eq(object) - end - end - describe '.demongoize' do let(:object) do @@ -145,45 +40,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 - %w[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 @@ -213,24 +69,6 @@ end end - describe '#you_must' do - - context 'when the object is frozen' do - - let(:person) do - Person.new.tap(&: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 @@ -278,11 +116,4 @@ expect(object.numeric?).to be(false) end end - - describe '#blank_criteria?' do - - it 'is false' do - expect(object.blank_criteria?).to be false - end - end end diff --git a/spec/mongoid/extensions/range_spec.rb b/spec/mongoid/extensions/range_spec.rb index 1c74e04d7..600d1112c 100644 --- a/spec/mongoid/extensions/range_spec.rb +++ b/spec/mongoid/extensions/range_spec.rb @@ -4,17 +4,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) } diff --git a/spec/mongoid/extensions/string_spec.rb b/spec/mongoid/extensions/string_spec.rb index 69fe70ebd..bc5736156 100644 --- a/spec/mongoid/extensions/string_spec.rb +++ b/spec/mongoid/extensions/string_spec.rb @@ -171,37 +171,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 - %i[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 9d8fa1063..1d8531367 100644 --- a/spec/mongoid/extensions/symbol_spec.rb +++ b/spec/mongoid/extensions/symbol_spec.rb @@ -4,37 +4,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 - %i[mongoize demongoize].each do |method| describe '.mongoize' do diff --git a/spec/mongoid/extensions/true_class_spec.rb b/spec/mongoid/extensions/true_class_spec.rb index 05c4a2922..41855443d 100644 --- a/spec/mongoid/extensions/true_class_spec.rb +++ b/spec/mongoid/extensions/true_class_spec.rb @@ -4,13 +4,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 diff --git a/spec/mongoid/fields/foreign_key_spec.rb b/spec/mongoid/fields/foreign_key_spec.rb index 8bfefd97c..e5a269b6c 100644 --- a/spec/mongoid/fields/foreign_key_spec.rb +++ b/spec/mongoid/fields/foreign_key_spec.rb @@ -497,180 +497,343 @@ end describe '#mongoize' do + subject(:mongoized) { field.mongoize(object) } - context 'when the type is array' do + let(:field) do + described_class.new( + :vals, + type: type, + default: [], + identity: true, + association: association, + overwrite: true + ) + end + let(:association) { Game.relations['person'] } + + context 'when type is Array' do + let(:type) { Array } + + context 'when the object is a BSON::ObjectId' do + let(:object) { BSON::ObjectId.new } - context 'when the array is object ids' do + 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 - let(:association) do - Game.relations['person'] + 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 - let(:field) do - described_class.new( - :vals, - type: Array, - default: [], - identity: true, - association: association, + 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 + + it 'returns the same instance' do + expect(mongoized).to equal(object) + end + end + + context 'when the object is a Set' do + let(:object) { Set['blah'] } + + it 'returns the object id in an array' do + expect(mongoized).to eq(%w[blah]) + end + 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 - 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 'when 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 'when 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) { nil } + + 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 diff --git a/spec/mongoid/findable_spec.rb b/spec/mongoid/findable_spec.rb index abaafb217..6998fe9ad 100644 --- a/spec/mongoid/findable_spec.rb +++ b/spec/mongoid/findable_spec.rb @@ -903,7 +903,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/mongoid/interceptable_spec.rb b/spec/mongoid/interceptable_spec.rb index d24df6741..1453ffb42 100644 --- a/spec/mongoid/interceptable_spec.rb +++ b/spec/mongoid/interceptable_spec.rb @@ -589,6 +589,7 @@ class TestClass 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 @@ -598,6 +599,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 member' do expect(note).to receive(:update_saved).twice @@ -1787,40 +1789,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 @@ -1883,89 +1925,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 @@ -2045,59 +2178,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 @@ -2374,4 +2562,25 @@ class TestClass end.to_not raise_error 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 + + 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 + + parent.save! + end + end + 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 new file mode 100644 index 000000000..883be9c41 --- /dev/null +++ b/spec/mongoid/monkey_patches_spec.rb @@ -0,0 +1,208 @@ +# 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 + ], + BigDecimal => %i[ + __evolve_date__ + __evolve_time__ + ], + 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 diff --git a/spec/mongoid/railties/bson_object_id_serializer_spec.rb b/spec/mongoid/railties/bson_object_id_serializer_spec.rb index 94a934cb4..f4dca0b95 100644 --- a/spec/mongoid/railties/bson_object_id_serializer_spec.rb +++ b/spec/mongoid/railties/bson_object_id_serializer_spec.rb @@ -4,8 +4,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/mongoid/search_indexable_spec.rb b/spec/mongoid/search_indexable_spec.rb new file mode 100644 index 000000000..d86db9fd0 --- /dev/null +++ b/spec/mongoid/search_indexable_spec.rb @@ -0,0 +1,143 @@ +# frozen_string_literal: true + +require 'spec_helper' + +class SearchIndexHelper + attr_reader :model + + def initialize(model) + @model = model + model.collection.drop + model.collection.create + 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. + 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.new('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 + +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.pluck('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.pluck('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.pluck('name')) + end + + it 'removes the indexes' do + expect(model.search_indexes).to be_empty + end + end + end +end diff --git a/spec/mongoid/tasks/database_rake_spec.rb b/spec/mongoid/tasks/database_rake_spec.rb index 4d3b0d241..8b2fa03d4 100644 --- a/spec/mongoid/tasks/database_rake_spec.rb +++ b/spec/mongoid/tasks/database_rake_spec.rb @@ -10,9 +10,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 @@ -32,6 +30,34 @@ end end + shared_examples_for 'create_search_indexes' do + [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 + + 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 @@ -204,6 +230,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' @@ -288,6 +334,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 e312f8ee5..fb5304a7d 100644 --- a/spec/mongoid/tasks/database_spec.rb +++ b/spec/mongoid/tasks/database_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -describe 'Mongoid::Tasks::Database' do +describe Mongoid::Tasks::Database do before(:all) do module DatabaseSpec @@ -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 @@ -206,14 +206,60 @@ 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 + 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 }) + + described_class.create_search_indexes([searchable_model_spy], wait: true) + end + end + + context 'when wait is false' do + it 'invokes only create_search_indexes' do + 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 + it 'calls remove_search_indexes on all non-embedded models' do + models.each do |model| + expect(model).to receive(:remove_search_indexes) unless model.embedded? + end + + described_class.remove_search_indexes(models) + end + end + 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 @@ -242,13 +288,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 @@ -261,8 +307,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 @@ -286,8 +332,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/mongoid/validatable/uniqueness_spec.rb b/spec/mongoid/validatable/uniqueness_spec.rb index d57d8d0a2..ba1112e2c 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 12c06b0bb..9314992f4 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -46,10 +46,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) @@ -66,15 +69,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, + 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: { @@ -115,17 +118,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 -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 + ensure + client.close + end end RSpec.configure do |config| 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? diff --git a/spec/support/spec_config.rb b/spec/support/spec_config.rb index 11b520dcd..599850fb8 100644 --- a/spec/support/spec_config.rb +++ b/spec/support/spec_config.rb @@ -18,7 +18,7 @@ def initialize @uri_str = DEFAULT_MONGODB_URI end - @uri = Mongo::URI.new(@uri_str) + @uri = Mongo::URI.get(@uri_str) end attr_reader :uri_str, :uri @@ -55,6 +55,10 @@ def ci? !!ENV['CI'] end + def atlas? + !!ENV['ATLAS_URI'] + end + def rails_version ENV['RAILS'].presence || '6.1' end