mongoid 9.0.10 → 9.0.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: fefd49681b6f78b222f201fbaea83af9f1599464034e52a73117c0ae94e201f2
4
- data.tar.gz: d86f966109921d2a0f1a4ce0b78d81e0d722123c1d420f0f695385ca43687626
3
+ metadata.gz: b6976a4808058209fda8554306590eeb5fd44759d8c228d02bca544acbe5c200
4
+ data.tar.gz: e0dcd20719682c2a425d9ed17c1459376b6956311f9ec439c74843a4cefb8502
5
5
  SHA512:
6
- metadata.gz: '09799846ca2a44c7dc4d7aff76fba0f9fdcbe38c5bfb001dccd08eb467800f8fac675cecfa06be94eb76ca35f6f1b57b64557ed171e9c0040bcbe0a701d7827f'
7
- data.tar.gz: bc0a741e2e26a84e84238f1461a26a29ae13584d9bd050dffc1ccd04f70e4e6a7fa63b20e7dcda8137ce3c37d383a5ae916b0d72ff950d4f330e66e0635ea89c
6
+ metadata.gz: 64ee6ee13a326ed9fae0e6b6dc08fa28ba0608a75342993b873023e4f87076160f6a8997e3edd53a273da4716c92d5aabc6b49f0931a489d2074d92f8de912d7
7
+ data.tar.gz: 491ecfb05581a08609bc41aedd39600f143a15f681405400c813402be95e11ed24d4be7363ac8079da9bc022d7d5b22ad3c53e534d41ee66a10977751a44b954
data/README.md CHANGED
@@ -21,9 +21,9 @@ Compatibility
21
21
 
22
22
  Mongoid supports and is tested against:
23
23
 
24
- - MRI 2.7 - 3.2
25
- - JRuby 9.4
26
- - MongoDB server 3.6 - 7.0
24
+ - MRI 2.7 - 4.0
25
+ - JRuby 9.4 and 10.0
26
+ - MongoDB server 3.6 - 8.2
27
27
 
28
28
  Issues
29
29
  ------
data/Rakefile CHANGED
@@ -68,6 +68,37 @@ RSpec::Core::RakeTask.new('spec:progress') do |spec|
68
68
  spec.pattern = "spec/**/*_spec.rb"
69
69
  end
70
70
 
71
+ RUBOCOPABLE = %w[ examples gemfiles perf lib spec mongoid.gemspec Gemfile Rakefile upload-api-docs ].freeze
72
+
73
+ desc 'Run rubocop'
74
+ task rubocop: %w[ rubocop:run ]
75
+
76
+ namespace :rubocop do
77
+ desc 'Run rubocop on the codebase'
78
+ task :run do
79
+ Bundler.with_unbundled_env do
80
+ sh 'bundle', 'exec', 'rubocop', *RUBOCOPABLE, verbose: false
81
+ end
82
+ end
83
+
84
+ desc 'Add a git pre-commit hook that runs rubocop'
85
+ task :install_hook do
86
+ hook_path = File.join('.git', 'hooks', 'pre-commit')
87
+ hook_script = <<~HOOK
88
+ #!/usr/bin/env bash
89
+ set -e
90
+
91
+ echo "Running rubocop..."
92
+ rake rubocop
93
+ HOOK
94
+
95
+ File.write(hook_path, hook_script)
96
+ FileUtils.chmod('+x', hook_path)
97
+
98
+ puts "Git pre-commit hook installed at #{hook_path}."
99
+ end
100
+ end
101
+
71
102
  desc 'Build and validate the evergreen config'
72
103
  task eg: %w[ eg:build eg:validate ]
73
104
 
@@ -184,11 +184,17 @@ module Mongoid
184
184
  else
185
185
  update_document(doc, attrs)
186
186
  end
187
- else
187
+ elsif association.embedded?
188
+ raise Errors::DocumentNotFound.new(association.klass, id)
189
+ elsif association.is_a?(Association::Referenced::HasAndBelongsToMany) || Mongoid.allow_reparenting_via_nested_attributes?
190
+ Mongoid::Warnings.warn_reparenting_via_nested_attributes if Mongoid.allow_reparenting_via_nested_attributes?
191
+
188
192
  # push existing document to association
189
193
  doc = association.klass.unscoped.find(converted)
190
194
  update_document(doc, attrs)
191
195
  existing.push(doc) unless destroyable?(attrs)
196
+ else
197
+ raise Errors::DocumentNotFound.new(association.klass, { _id: id, association.foreign_key => parent.id })
192
198
  end
193
199
 
194
200
  parent.children_may_have_changed!
@@ -32,13 +32,24 @@ module Mongoid
32
32
  Threaded.exit_autosave(self)
33
33
  end
34
34
 
35
- # Check if there is changes for auto-saving
35
+ # Check if there are changes for auto-saving. Returns true if the
36
+ # document is new, changed, or marked for destruction, or if any
37
+ # in-memory referenced child with autosave: true recursively
38
+ # satisfies the same condition.
36
39
  #
37
- # @example Return true if there is changes on self or in
38
- # autosaved associations.
39
- # document.changed_for_autosave?
40
- def changed_for_autosave?(doc)
41
- doc.new_record? || doc.changed? || doc.marked_for_destruction?
40
+ # The seen set prevents infinite recursion when autosave associations
41
+ # form a cycle (e.g. a belongs_to with autosave: true whose target
42
+ # has a has_many with autosave: true pointing back).
43
+ #
44
+ # @param [ Document ] doc The document to check.
45
+ # @param [ Set ] seen Documents already visited (cycle guard).
46
+ #
47
+ # @return [ true | false ] Whether the document needs autosaving.
48
+ def changed_for_autosave?(doc, seen = Set.new)
49
+ return false unless seen.add?(doc)
50
+
51
+ doc.new_record? || doc.changed? || doc.marked_for_destruction? ||
52
+ autosave_children_changed?(doc, seen)
42
53
  end
43
54
 
44
55
  # Define the autosave method on an association's owning class for
@@ -60,6 +71,8 @@ module Mongoid
60
71
  __autosaving__ do
61
72
  if assoc_value = ivar(association.name)
62
73
  Array(assoc_value).each do |doc|
74
+ next unless changed_for_autosave?(doc)
75
+
63
76
  pc = doc.persistence_context? ? doc.persistence_context : persistence_context.for_child(doc)
64
77
  doc.with(pc) do |d|
65
78
  d.save
@@ -72,6 +85,35 @@ module Mongoid
72
85
  klass.after_persist_parent save_method, unless: :autosaved?
73
86
  end
74
87
  end
88
+
89
+ private
90
+
91
+ # Returns true if any in-memory referenced child with autosave: true
92
+ # needs saving.
93
+ #
94
+ # @param [ Document ] doc The document whose children to check.
95
+ # @param [ Set ] seen Cycle guard passed through from changed_for_autosave?.
96
+ #
97
+ # @return [ true | false ]
98
+ def autosave_children_changed?(doc, seen)
99
+ if Mongoid.autosave_saves_unchanged_documents?
100
+ Mongoid::Warnings.warn_autosave_saves_unchanged_documents
101
+ return true
102
+ end
103
+
104
+ doc.class.relations.values.select { |a| a.autosave? && !a.embedded? }.any? do |assoc|
105
+ (assoc_value = doc.ivar(assoc.name)) &&
106
+ in_memory_docs(assoc_value).any? { |child| changed_for_autosave?(child, seen) }
107
+ end
108
+ end
109
+
110
+ # Returns the in-memory documents for an association value without
111
+ # triggering a database load of any unloaded documents. Association
112
+ # proxies expose in_memory for this purpose; a plain document (which
113
+ # belongs_to can store directly in the ivar) is itself in-memory.
114
+ def in_memory_docs(assoc_value)
115
+ assoc_value.respond_to?(:in_memory) ? assoc_value.in_memory : [ assoc_value ]
116
+ end
75
117
  end
76
118
  end
77
119
  end
@@ -18,19 +18,23 @@ module Mongoid
18
18
  case version.to_s
19
19
  when /^[0-7]\./
20
20
  raise ArgumentError, "Version no longer supported: #{version}"
21
- when "8.0"
21
+
22
+ when '8.0'
22
23
  self.legacy_readonly = true
23
24
 
24
- load_defaults "8.1"
25
- when "8.1"
25
+ load_defaults '8.1'
26
+
27
+ when '8.1'
26
28
  self.immutable_ids = false
27
29
  self.legacy_persistence_context_behavior = true
28
30
  self.around_callbacks_for_embeds = true
29
31
  self.prevent_multiple_calls_of_embedded_callbacks = false
30
32
 
31
- load_defaults "9.0"
32
- when "9.0"
33
+ load_defaults '9.0'
34
+
35
+ when '9.0'
33
36
  # All flag defaults currently reflect 9.0 behavior.
37
+
34
38
  else
35
39
  raise ArgumentError, "Unknown version: #{version}"
36
40
  end
@@ -110,6 +110,31 @@ module Mongoid
110
110
  # to `:global_thread_pool`.
111
111
  option :global_executor_concurrency, default: nil
112
112
 
113
+ # When this flag is true, it will be possible to change the parent of a
114
+ # record in a "has_many" association by passing the child record's id in the
115
+ # nested attributes for another parent record.
116
+ #
117
+ # When this flag is false, attempting to change the parent of a record in a
118
+ # "has-many" association via nested attributes will raise an error.
119
+ #
120
+ # The default is `true`. Note that allowing reparenting via nested attributes
121
+ # is a potential security risk, since it could allow a malicious user to move
122
+ # records that they do not own to a parent record that they do own.
123
+ #
124
+ # This option will default to `false` in Mongoid 9.1, and will be removed
125
+ # in Mongoid 10.
126
+ option :allow_reparenting_via_nested_attributes, default: true
127
+
128
+ # When this flag is true, any documents in associations with `autosave: true`
129
+ # will be saved even if they have not been changed. When this flag is false,
130
+ # only autosaved documents that have been changed will be saved. The default
131
+ # is `true`.
132
+ #
133
+ # This option will default to `false` in Mongoid 9.1, and will be removed
134
+ # in Mongoid 10, with the only behavior at that point being as if this
135
+ # option were set to `false`.
136
+ option :autosave_saves_unchanged_documents, default: true
137
+
113
138
  # When this flag is false, a document will become read-only only once the
114
139
  # #readonly! method is called, and an error will be raised on attempting
115
140
  # to save or update such documents, instead of just on delete. When this
@@ -5,5 +5,5 @@ module Mongoid
5
5
  #
6
6
  # Note that this file is automatically updated via `rake candidate:create`.
7
7
  # Manual changes to this file will be overwritten by that rake task.
8
- VERSION = '9.0.10'
8
+ VERSION = '9.0.11'
9
9
  end
@@ -33,5 +33,9 @@ module Mongoid
33
33
  warning :symbol_type_deprecated, 'The BSON Symbol type is deprecated by MongoDB. Please use String or StringifiedSymbol field types instead of the Symbol field type.'
34
34
  warning :legacy_readonly, 'The readonly! method will only mark the document readonly when the legacy_readonly feature flag is switched off.'
35
35
  warning :mutable_ids, 'Ignoring updates to immutable attribute `_id`. Please set Mongoid::Config.immutable_ids to true and update your code so that `_id` is never updated.'
36
+ warning :reparenting_via_nested_attributes, 'Reparenting documents via nested attributes is insecure and is deprecated. Set Mongoid.allow_reparenting_via_nested_attributes to false and update your code to avoid reparenting documents via nested attributes.'
37
+ warning :autosave_saves_unchanged_documents, "Autosave associations are currently configured to save documents even if they haven't changed. " \
38
+ 'This legacy behavior is deprecated. Set Mongoid.autosave_saves_unchanged_documents to false to ' \
39
+ 'skip saving unchanged documents in autosave associations.'
36
40
  end
37
41
  end
@@ -37,37 +37,34 @@ describe 'Mongoid application tests' do
37
37
  FileUtils.mkdir_p(TMP_BASE)
38
38
  end
39
39
 
40
- context 'demo application' do
40
+ context 'generated application' do
41
41
  context 'sinatra' do
42
42
  it 'runs' do
43
- skip 'https://jira.mongodb.org/browse/MONGOID-5826'
44
-
45
- clone_application(
46
- 'https://github.com/mongoid/mongoid-demo',
47
- subdir: 'sinatra-minimal',
48
- ) do
49
-
43
+ create_sinatra_app('mongoid-sinatra-test') do
50
44
  # JRuby needs a long timeout
51
45
  start_app(%w(bundle exec ruby app.rb), 4567, 40) do |port|
52
46
  uri = URI.parse('http://localhost:4567/posts')
53
47
  resp = JSON.parse(uri.open.read)
54
48
 
55
49
  resp.should == []
56
-
57
50
  end
58
51
  end
59
52
  end
60
53
  end
61
54
 
62
55
  context 'rails-api' do
63
- it 'runs' do
64
- skip 'https://jira.mongodb.org/browse/MONGOID-5826'
65
-
66
- clone_application(
67
- 'https://github.com/mongoid/mongoid-demo',
68
- subdir: 'rails-api',
69
- ) do
56
+ before(:all) do
57
+ # Rails 6.0/6.1 have Logger issues on Ruby 3.1+
58
+ # Rails < 7.1 have concurrent-ruby issues on Ruby 4.0+
59
+ if RUBY_VERSION >= '4.0' && SpecConfig.instance.rails_version < '7.1'
60
+ skip 'Rails < 7.1 is not compatible with Ruby 4.0+. Set RAILS=7.1 or higher.'
61
+ elsif RUBY_VERSION >= '3.1' && SpecConfig.instance.rails_version < '6.1'
62
+ skip 'Rails < 6.1 is not compatible with Ruby 3.1+. Set RAILS=6.1 or higher.'
63
+ end
64
+ end
70
65
 
66
+ it 'runs' do
67
+ create_rails_api_app('mongoid-rails-api-test') do
71
68
  # JRuby needs a long timeout
72
69
  start_app(%w(bundle exec rails s), 3000, 50) do |port|
73
70
  uri = URI.parse('http://localhost:3000/posts')
@@ -114,7 +111,7 @@ describe 'Mongoid application tests' do
114
111
 
115
112
  Dir.chdir(TMP_BASE) do
116
113
  FileUtils.rm_rf(name)
117
- check_call(insert_rails_gem_version(%W(rails new #{name} --skip-spring --skip-active-record)), env: clean_env)
114
+ check_call(insert_rails_gem_version(%W(rails new #{name} --skip-spring --skip-active-record)), env: rails_env)
118
115
 
119
116
  Dir.chdir(name) do
120
117
  adjust_rails_defaults
@@ -126,10 +123,149 @@ describe 'Mongoid application tests' do
126
123
  end
127
124
  end
128
125
 
126
+ def create_sinatra_app(name)
127
+ Dir.chdir(TMP_BASE) do
128
+ FileUtils.rm_rf(name)
129
+ FileUtils.mkdir_p(name)
130
+
131
+ Dir.chdir(name) do
132
+ # Create minimal Sinatra app with Post model
133
+ File.open('app.rb', 'w') do |f|
134
+ f.write(<<~RUBY)
135
+ require 'sinatra'
136
+ require 'mongoid'
137
+ require 'json'
138
+
139
+ Mongoid.load!('config/mongoid.yml')
140
+
141
+ class Post
142
+ include Mongoid::Document
143
+ field :title, type: String
144
+ end
145
+
146
+ get '/posts' do
147
+ content_type :json
148
+ Post.all.to_json
149
+ end
150
+ RUBY
151
+ end
152
+
153
+ # Create Gemfile
154
+ File.open('Gemfile', 'w') do |f|
155
+ f.write(<<~RUBY)
156
+ source 'https://rubygems.org'
157
+ gem 'sinatra'
158
+ gem 'rackup'
159
+ gem 'mongoid', path: '#{File.expand_path(BASE)}'
160
+ gem 'puma'
161
+ RUBY
162
+ end
163
+
164
+ FileUtils.mkdir_p('config')
165
+ write_mongoid_yml
166
+ check_call(%w(bundle install), env: clean_env)
167
+
168
+ yield
169
+ end
170
+ end
171
+ end
172
+
173
+ def create_rails_api_app(name)
174
+ install_rails
175
+
176
+ Dir.chdir(TMP_BASE) do
177
+ FileUtils.rm_rf(name)
178
+ check_call(insert_rails_gem_version(%W(rails new #{name} --api --skip-spring --skip-active-record)), env: rails_env)
179
+
180
+ Dir.chdir(name) do
181
+ adjust_rails_defaults
182
+ adjust_app_gemfile
183
+
184
+ # Create Post model
185
+ File.open('app/models/post.rb', 'w') do |f|
186
+ f.write(<<~RUBY)
187
+ class Post
188
+ include Mongoid::Document
189
+ field :title, type: String
190
+ end
191
+ RUBY
192
+ end
193
+
194
+ # Create PostsController
195
+ File.open('app/controllers/posts_controller.rb', 'w') do |f|
196
+ f.write(<<~RUBY)
197
+ class PostsController < ApplicationController
198
+ def index
199
+ render json: Post.all
200
+ end
201
+ end
202
+ RUBY
203
+ end
204
+
205
+ # Add route
206
+ routes_content = File.read('config/routes.rb')
207
+ routes_content.sub!(/Rails\.application\.routes\.draw do\n/,
208
+ "Rails.application.routes.draw do\n resources :posts, only: [:index]\n")
209
+ File.open('config/routes.rb', 'w') { |f| f.write(routes_content) }
210
+
211
+ write_mongoid_yml
212
+ check_call(%w(bundle install), env: clean_env)
213
+
214
+ yield
215
+ end
216
+ end
217
+ end
218
+
219
+ def create_rails_rake_test_app(name)
220
+ install_rails
221
+
222
+ Dir.chdir(TMP_BASE) do
223
+ FileUtils.rm_rf(name)
224
+ check_call(insert_rails_gem_version(%W(rails new #{name} --api --skip-spring --skip-active-record)), env: rails_env)
225
+
226
+ Dir.chdir(name) do
227
+ adjust_rails_defaults
228
+ adjust_app_gemfile
229
+
230
+ # Create Post model with index
231
+ File.open('app/models/post.rb', 'w') do |f|
232
+ f.write(<<~RUBY)
233
+ class Post
234
+ include Mongoid::Document
235
+ include Mongoid::Timestamps
236
+ field :subject, type: String
237
+ field :message, type: String
238
+
239
+ index subject: 1
240
+ end
241
+ RUBY
242
+ end
243
+
244
+ # Create Comment model
245
+ File.open('app/models/comment.rb', 'w') do |f|
246
+ f.write(<<~RUBY)
247
+ class Comment
248
+ include Mongoid::Document
249
+ include Mongoid::Timestamps
250
+ belongs_to :post
251
+ end
252
+ RUBY
253
+ end
254
+
255
+ write_mongoid_yml
256
+ check_call(%w(bundle install), env: clean_env)
257
+ end
258
+ end
259
+ end
260
+
129
261
  context 'new application - rails' do
130
262
  before(:all) do
131
- if SpecConfig.instance.rails_version < '7.1'
132
- skip '`rails new` with rails < 7.1 fails because modern concurrent-ruby removed logger dependency'
263
+ # Rails 6.0/6.1 have Logger issues on Ruby 3.1+
264
+ # Rails < 7.1 have concurrent-ruby issues on Ruby 4.0+
265
+ if RUBY_VERSION >= '4.0' && SpecConfig.instance.rails_version < '7.1'
266
+ skip 'Rails < 7.1 is not compatible with Ruby 4.0+. Set RAILS=7.1 or higher.'
267
+ elsif RUBY_VERSION >= '3.1' && SpecConfig.instance.rails_version < '6.1'
268
+ skip 'Rails < 6.1 is not compatible with Ruby 3.1+. Set RAILS=6.1 or higher.'
133
269
  end
134
270
  end
135
271
 
@@ -186,17 +322,31 @@ describe 'Mongoid application tests' do
186
322
  if (rails_version = SpecConfig.instance.rails_version) == 'master'
187
323
  else
188
324
  check_call(%w(gem list))
325
+
326
+ # Rails 6.0 and 6.1 need logger gem on Ruby 2.7+ due to stdlib changes
327
+ if rails_version.to_f < 7.0 && RUBY_VERSION >= '2.7'
328
+ check_call(%w(gem install logger --no-document))
329
+ end
330
+
189
331
  check_call(%w(gem install rails --no-document --force -v) + ["~> #{rails_version}.0"])
190
332
  end
191
333
  end
192
334
 
193
- context 'local test applications' do
335
+ context 'generated test applications' do
336
+ before(:all) do
337
+ # Rails 6.0/6.1 have Logger issues on Ruby 3.1+
338
+ # Rails < 7.1 have concurrent-ruby issues on Ruby 4.0+
339
+ if RUBY_VERSION >= '4.0' && SpecConfig.instance.rails_version < '7.1'
340
+ skip 'Rails < 7.1 is not compatible with Ruby 4.0+. Set RAILS=7.1 or higher.'
341
+ elsif RUBY_VERSION >= '3.1' && SpecConfig.instance.rails_version < '6.1'
342
+ skip 'Rails < 6.1 is not compatible with Ruby 3.1+. Set RAILS=6.1 or higher.'
343
+ end
344
+ end
345
+
194
346
  let(:client) { Mongoid.default_client }
195
347
 
196
348
  describe 'create_indexes rake task' do
197
349
 
198
- APP_PATH = File.join(File.dirname(__FILE__), '../../test-apps/rails-api')
199
-
200
350
  %w(development production).each do |rails_env|
201
351
  context "in #{rails_env}" do
202
352
 
@@ -207,20 +357,11 @@ describe 'Mongoid application tests' do
207
357
  clean_env.merge(RAILS_ENV: rails_env, AUTOLOADER: autoloader)
208
358
  end
209
359
 
360
+ let(:app_name) { "mongoid-rake-test-#{rails_env}-#{autoloader}" }
361
+ let(:app_path) { File.join(TMP_BASE, app_name) }
362
+
210
363
  before do
211
- Dir.chdir(APP_PATH) do
212
- remove_bundler_req
213
-
214
- if BSON::Environment.jruby?
215
- # Remove existing Gemfile.lock - see
216
- # https://github.com/rubygems/rubygems/issues/3231
217
- require 'fileutils'
218
- FileUtils.rm_f('Gemfile.lock')
219
- end
220
-
221
- check_call(%w(bundle install), env: env)
222
- write_mongoid_yml
223
- end
364
+ create_rails_rake_test_app(app_name)
224
365
 
225
366
  client['posts'].drop
226
367
  client['posts'].create
@@ -233,7 +374,7 @@ describe 'Mongoid application tests' do
233
374
  index.should be nil
234
375
 
235
376
  check_call(%w(bundle exec rake db:mongoid:create_indexes -t),
236
- cwd: APP_PATH, env: env)
377
+ cwd: app_path, env: env)
237
378
 
238
379
  index = client['posts'].indexes.detect do |index|
239
380
  index['key'] == {'subject' => 1}
@@ -317,6 +458,12 @@ describe 'Mongoid application tests' do
317
458
  line =~ /mongoid/
318
459
  end
319
460
  gemfile_lines << "gem 'mongoid', path: '#{File.expand_path(BASE)}'\n"
461
+
462
+ # Rails 6.0 and 6.1 need logger gem on Ruby 2.7+ due to stdlib changes
463
+ if rails_version && rails_version.to_f < 7.0 && RUBY_VERSION >= '2.7'
464
+ gemfile_lines << "gem 'logger'\n"
465
+ end
466
+
320
467
  if rails_version
321
468
  gemfile_lines.delete_if do |line|
322
469
  line =~ /gem ['"]rails['"]/
@@ -376,6 +523,17 @@ describe 'Mongoid application tests' do
376
523
  @clean_env ||= Hash[ENV.keys.grep(/BUNDLE|RUBYOPT/).map { |k| [k, nil ] }]
377
524
  end
378
525
 
526
+ def rails_env
527
+ # For Rails 6.0/6.1 on Ruby 2.7+, we need to require logger
528
+ # to fix "uninitialized constant ActiveSupport::LoggerThreadSafeLevel::Logger"
529
+ env = clean_env.dup
530
+ rails_version = SpecConfig.instance.rails_version
531
+ if rails_version && rails_version.to_f < 7.0 && RUBY_VERSION >= '2.7'
532
+ env['RUBYOPT'] = '-rlogger'
533
+ end
534
+ env
535
+ end
536
+
379
537
  def wait_for_port(port, timeout, process)
380
538
  deadline = Mongoid::Utils.monotonic_time + timeout
381
539
  loop do
@@ -5,6 +5,53 @@ require "spec_helper"
5
5
  require_relative './referenced/has_many_models'
6
6
  require_relative './referenced/has_one_models'
7
7
 
8
+ # Models for the MONGOID-5751 regression test: after_save callbacks must not
9
+ # fire for pre-existing, unchanged documents when autosave: true cascades a
10
+ # save from a parent to its children.
11
+ module AutoSaveMONGOID5751
12
+ class Table
13
+ include Mongoid::Document
14
+
15
+ has_many :rows, autosave: true, class_name: 'AutoSaveMONGOID5751::Row',
16
+ inverse_of: :table
17
+
18
+ class << self
19
+ attr_accessor :after_save_count
20
+ end
21
+ self.after_save_count = 0
22
+
23
+ after_save { self.class.after_save_count += 1 }
24
+ end
25
+
26
+ class Row
27
+ include Mongoid::Document
28
+
29
+ belongs_to :table, class_name: 'AutoSaveMONGOID5751::Table', inverse_of: :rows
30
+ has_many :cells, autosave: true, class_name: 'AutoSaveMONGOID5751::Cell',
31
+ inverse_of: :row
32
+
33
+ class << self
34
+ attr_accessor :after_save_count
35
+ end
36
+ self.after_save_count = 0
37
+
38
+ after_save { self.class.after_save_count += 1 }
39
+ end
40
+
41
+ class Cell
42
+ include Mongoid::Document
43
+
44
+ belongs_to :row, class_name: 'AutoSaveMONGOID5751::Row', inverse_of: :cells
45
+
46
+ class << self
47
+ attr_accessor :after_save_count
48
+ end
49
+ self.after_save_count = 0
50
+
51
+ after_save { self.class.after_save_count += 1 }
52
+ end
53
+ end
54
+
8
55
  describe Mongoid::Association::Referenced::AutoSave do
9
56
 
10
57
  describe ".auto_save" do
@@ -399,6 +446,57 @@ describe Mongoid::Association::Referenced::AutoSave do
399
446
  expect(harvest.reload.season).to eq('Fall')
400
447
  end
401
448
  end
449
+
450
+ # Regression test for MONGOID-5751: after_save must not fire for
451
+ # pre-existing, unchanged documents that are merely loaded into memory
452
+ # as a side-effect of the autosave traversal.
453
+ context 'when a parent with existing children has a new child added' do
454
+ before do
455
+ # Persist a table with 3 pre-existing rows, each with 3 cells.
456
+ table = AutoSaveMONGOID5751::Table.create!
457
+ 3.times do
458
+ row = table.rows.create!
459
+ 3.times { row.cells.create! }
460
+ end
461
+
462
+ # Reset counters so only the saves triggered by the call below are
463
+ # measured.
464
+ AutoSaveMONGOID5751::Table.after_save_count = 0
465
+ AutoSaveMONGOID5751::Row.after_save_count = 0
466
+ AutoSaveMONGOID5751::Cell.after_save_count = 0
467
+
468
+ # Reload the table fresh from the database, then append exactly one
469
+ # new row (with one new cell) and persist.
470
+ reloaded = AutoSaveMONGOID5751::Table.find(table.id)
471
+ new_row = reloaded.rows.build
472
+ new_row.cells.build
473
+ reloaded.save!
474
+ end
475
+
476
+ it 'fires after_save once for the parent table' do
477
+ expect(AutoSaveMONGOID5751::Table.after_save_count).to eq(1)
478
+ end
479
+
480
+ it 'fires after_save only for the newly added cell' do
481
+ expect(AutoSaveMONGOID5751::Cell.after_save_count).to eq(1)
482
+ end
483
+
484
+ context 'when autosave_saves_unchanged_documents is true' do
485
+ config_override :autosave_saves_unchanged_documents, true
486
+
487
+ it 'fires after_save for all new and pre-existing rows' do
488
+ expect(AutoSaveMONGOID5751::Row.after_save_count).to eq(4)
489
+ end
490
+ end
491
+
492
+ context 'when autosave_saves_unchanged_documents is false' do
493
+ config_override :autosave_saves_unchanged_documents, false
494
+
495
+ it 'fires after_save only for the newly added row, not for pre-existing rows' do
496
+ expect(AutoSaveMONGOID5751::Row.after_save_count).to eq(1)
497
+ end
498
+ end
499
+ end
402
500
  end
403
501
  end
404
502
  end
@@ -164,24 +164,47 @@ describe Mongoid::Attributes::Nested do
164
164
  end
165
165
  end
166
166
 
167
- context "when the relation is a references many" do
168
-
167
+ context 'when the relation is a has-many' do
169
168
  before do
170
169
  Person.send(:undef_method, :posts_attributes=)
171
170
  Person.accepts_nested_attributes_for :posts
172
171
  end
173
172
 
174
- let(:person) do
175
- Person.new(posts_attributes: { "1" => { title: "First" }})
173
+ context 'when adding a new document to a relation' do
174
+ let(:person) do
175
+ Person.new(posts_attributes: { '1' => { title: 'First' } })
176
+ end
177
+
178
+ it 'sets the nested attributes' do
179
+ expect(person.posts.first.title).to eq('First')
180
+ end
176
181
  end
177
182
 
178
- it "sets the nested attributes" do
179
- expect(person.posts.first.title).to eq("First")
183
+ context 'when adding an existing document to a relation' do
184
+ let(:person1) { Person.create! }
185
+ let(:post) { person1.posts.create!(title: 'Sample Post') }
186
+
187
+ let(:person2) { Person.create!(posts_attributes: { '0' => { id: post.id, title: 'Reparented!' } }) }
188
+
189
+ context 'when allow_reparenting_via_nested_attributes is false' do
190
+ config_override :allow_reparenting_via_nested_attributes, false
191
+
192
+ it 'raises a document not found error' do
193
+ expect { person2 }.to raise_error(Mongoid::Errors::DocumentNotFound)
194
+ end
195
+ end
196
+
197
+ context 'when allow_reparenting_via_nested_attributes is true' do
198
+ config_override :allow_reparenting_via_nested_attributes, true
199
+
200
+ it 'sets the nested attributes' do
201
+ expect(person2.posts.map(&:title)).to eq([ 'Reparented!' ])
202
+ end
203
+ end
180
204
  end
181
205
  end
182
206
 
183
- context "when the relation is a references and referenced in many" do
184
-
207
+ context 'when the relation is a has-and-belongs-to-many' do
185
208
  before do
186
209
  Person.send(:undef_method, :preferences_attributes=)
187
210
  Person.accepts_nested_attributes_for :preferences
@@ -1355,10 +1378,11 @@ describe Mongoid::Attributes::Nested do
1355
1378
  context "when the ids do not match" do
1356
1379
 
1357
1380
  it "raises an error" do
1358
- expect {
1381
+ expect do
1359
1382
  person.addresses_attributes =
1360
- { "foo" => { "id" => "test", "street" => "Test" } }
1361
- }.to raise_error(Mongoid::Errors::DocumentNotFound, /Document\(s\) not found for class Address with id\(s\)/)
1383
+ { 'foo' => { 'id' => 'test', 'street' => 'Test' } }
1384
+ end.to raise_error(Mongoid::Errors::DocumentNotFound,
1385
+ /Document\(s\) not found for class Address/)
1362
1386
  end
1363
1387
  end
1364
1388
  end
@@ -3012,12 +3036,12 @@ describe Mongoid::Attributes::Nested do
3012
3036
  end
3013
3037
 
3014
3038
  it "raises a document not found error" do
3015
- expect {
3039
+ expect do
3016
3040
  person.posts_attributes =
3017
- { "0" =>
3018
- { "id" => BSON::ObjectId.new.to_s, "title" => "Rogue" }
3019
- }
3020
- }.to raise_error(Mongoid::Errors::DocumentNotFound, /Document\(s\) not found for class Post with id\(s\)/)
3041
+ { '0' =>
3042
+ { 'id' => BSON::ObjectId.new.to_s, 'title' => 'Rogue' } }
3043
+ end.to raise_error(Mongoid::Errors::DocumentNotFound,
3044
+ /Document\(s\) not found for class Post/)
3021
3045
  end
3022
3046
  end
3023
3047
  end
@@ -3048,10 +3072,11 @@ describe Mongoid::Attributes::Nested do
3048
3072
  context "when the ids do not match" do
3049
3073
 
3050
3074
  it "raises an error" do
3051
- expect {
3075
+ expect do
3052
3076
  person.posts_attributes =
3053
- { "foo" => { "id" => "test", "title" => "Test" } }
3054
- }.to raise_error(Mongoid::Errors::DocumentNotFound, /Document\(s\) not found for class Post with id\(s\)/)
3077
+ { 'foo' => { 'id' => 'test', 'title' => 'Test' } }
3078
+ end.to raise_error(Mongoid::Errors::DocumentNotFound,
3079
+ /Document\(s\) not found for class Post/)
3055
3080
  end
3056
3081
  end
3057
3082
  end
@@ -3765,10 +3790,11 @@ describe Mongoid::Attributes::Nested do
3765
3790
  context "when the ids do not match" do
3766
3791
 
3767
3792
  it "raises an error" do
3768
- expect {
3793
+ expect do
3769
3794
  person.preferences_attributes =
3770
- { "foo" => { "id" => "test", "name" => "Test" } }
3771
- }.to raise_error(Mongoid::Errors::DocumentNotFound, /Document\(s\) not found for class Preference with id\(s\)/)
3795
+ { 'foo' => { 'id' => 'test', 'name' => 'Test' } }
3796
+ end.to raise_error(Mongoid::Errors::DocumentNotFound,
3797
+ /Document\(s\) not found for class Preference/)
3772
3798
  end
3773
3799
  end
3774
3800
  end
@@ -64,9 +64,16 @@ describe Mongoid::Errors::DocumentNotFound do
64
64
  end
65
65
 
66
66
  it "contains the problem in the message" do
67
- expect(error.message).to include(
68
- "Document not found for class Person with attributes {:name=>\"syd\"}."
69
- )
67
+ # Ruby 3.4+ changed Hash#inspect format from {:name=>"syd"} to {name: "syd"}
68
+ if RUBY_VERSION >= '3.4'
69
+ expect(error.message).to include(
70
+ "Document not found for class Person with attributes {name: \"syd\"}."
71
+ )
72
+ else
73
+ expect(error.message).to include(
74
+ "Document not found for class Person with attributes {:name=>\"syd\"}."
75
+ )
76
+ end
70
77
  end
71
78
 
72
79
  it "contains the summary in the message" do
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mongoid
3
3
  version: !ruby/object:Gem::Version
4
- version: 9.0.10
4
+ version: 9.0.11
5
5
  platform: ruby
6
6
  authors:
7
7
  - The MongoDB Ruby Team
@@ -75,6 +75,20 @@ dependencies:
75
75
  - - "<"
76
76
  - !ruby/object:Gem::Version
77
77
  version: '2.0'
78
+ - !ruby/object:Gem::Dependency
79
+ name: ostruct
80
+ requirement: !ruby/object:Gem::Requirement
81
+ requirements:
82
+ - - ">="
83
+ - !ruby/object:Gem::Version
84
+ version: '0'
85
+ type: :runtime
86
+ prerelease: false
87
+ version_requirements: !ruby/object:Gem::Requirement
88
+ requirements:
89
+ - - ">="
90
+ - !ruby/object:Gem::Version
91
+ version: '0'
78
92
  - !ruby/object:Gem::Dependency
79
93
  name: bson
80
94
  requirement: !ruby/object:Gem::Requirement
@@ -1231,7 +1245,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
1231
1245
  - !ruby/object:Gem::Version
1232
1246
  version: 1.3.6
1233
1247
  requirements: []
1234
- rubygems_version: 4.0.4
1248
+ rubygems_version: 4.0.10
1235
1249
  specification_version: 4
1236
1250
  summary: Elegant Persistence in Ruby for MongoDB.
1237
1251
  test_files: