Repository: Automattic/mongoose (JavaScript)

Issue:
Changing a schema path using `schema.path` significantly affects performance
### Prerequisites

- [x] I have written a descriptive issue title
- [x] I have searched existing issues to ensure the bug has not already been reported


### Mongoose version

8.10.0

### Node.js version

v18.19.0

### MongoDB server version

8.0.4

### Typescript version (if applicable)

_No response_

### Description

When using `schema.path` to add a SingleNested type to a preexisting schema, adds the new SingleNested children to the `childSchemas` schema property, regarding of whether it was already in the array. This affects `save()` operations on large documents, with deeply nested documents. 

Multiple calls to the `path` method make the issue exponentially worse.

We've found that calling `_gatherChildSchemas()` is an effective workaround and fixes the issue.

### Steps to Reproduce

```
const mongoose = require('mongoose');
const crypto   = require('crypto');

const RecursiveSchema = new mongoose.Schema({
  data: String,
});

const s = [ RecursiveSchema ];
RecursiveSchema.path('nested', s);

// If you comment the following line the issue goes away
RecursiveSchema.path('nested', s);

// If you uncomment the following lines the issue gets even worse
//RecursiveSchema.path('nested', s);
//RecursiveSchema.path('nested', s);

// Uncoment for a workaround
//RecursiveSchema._gatherChildSchemas();

const generateRecursiveDocument = (depth, curr = 0) => {
  return {
    name: `Document of depth ${curr}`,
    nested: depth > 0 ? new Array(3).fill().map(() => generateRecursiveDocument(depth-1, curr+1)) : [],
    data: crypto.randomBytes(128).toString('hex'),
  };
}

const main = async () => {
  console.log(mongoose.version)
  await mongoose.connect('mongodb://localhost/mongoose-test');
  const model = new mongoose.model('Recursive', RecursiveSchema);
  const data = generateRecursiveDocument(6);
  const doc = new model(data)
  console.time('Save time');
  await doc.save();
  console.timeEnd('Save time');
};

main().then(process.exit);
```

### Expected Behavior

The save operation should not take more than 100ms, which is the case when you only call the `path` function once.


Discussion:
Hello @vkarpov15, 

Thanks for taking a look into this performance issue. We've found a very similar related issue and reported it on #15255

Thanks!



Patch:
diff --git a/lib/schema.js b/lib/schema.js
index 0204c6cc9c4..f7528f6b4b4 100644
--- a/lib/schema.js
+++ b/lib/schema.js
@@ -1118,6 +1118,9 @@ Schema.prototype.path = function(path, obj) {
   this.paths[path] = this.interpretAsType(path, obj, this.options);
   const schemaType = this.paths[path];
 
+  // If overwriting an existing path, make sure to clear the childSchemas
+  this.childSchemas = this.childSchemas.filter(childSchema => childSchema.path !== path);
+
   if (schemaType.$isSchemaMap) {
     // Maps can have arbitrary keys, so `$*` is internal shorthand for "any key"
     // The '$' is to imply this path should never be stored in MongoDB so we


Test patch:
diff --git a/test/schema.test.js b/test/schema.test.js
index 416502b03a5..52a1b265781 100644
--- a/test/schema.test.js
+++ b/test/schema.test.js
@@ -3889,4 +3889,34 @@ describe('schema', function() {
       assert.throws(() => schema.toJSONSchema(), /unsupported SchemaType to JSON Schema: Mixed/);
     });
   });
+
+  it('path() clears existing child schemas (gh-15253)', async function() {
+    const RecursiveSchema = new mongoose.Schema({
+      data: String
+    });
+
+    const s = [RecursiveSchema];
+    RecursiveSchema.path('nested', s);
+    assert.strictEqual(RecursiveSchema.childSchemas.length, 1);
+    RecursiveSchema.path('nested', s);
+    assert.strictEqual(RecursiveSchema.childSchemas.length, 1);
+    RecursiveSchema.path('nested', s);
+    assert.strictEqual(RecursiveSchema.childSchemas.length, 1);
+    RecursiveSchema.path('nested', s);
+    assert.strictEqual(RecursiveSchema.childSchemas.length, 1);
+
+    const generateRecursiveDocument = (depth, curr = 0) => {
+      return {
+        name: `Document of depth ${curr}`,
+        nested: depth > 0 ? new Array(3).fill().map(() => generateRecursiveDocument(depth - 1, curr + 1)) : [],
+        data: Math.random()
+      };
+    };
+
+    const TestModel = db.model('Test', RecursiveSchema);
+    const data = generateRecursiveDocument(6);
+    const doc = new TestModel(data);
+    await doc.save();
+
+  });
 });