Skip to content

Commit 022a856

Browse files
github-actions[bot]mtrezza
authored andcommitted
feat: add MongoDB 5.1 compatibility (#7682)
1 parent 94e27ef commit 022a856

File tree

5 files changed

+195
-22
lines changed

5 files changed

+195
-22
lines changed

Diff for: .github/workflows/ci.yml

+5
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,11 @@ jobs:
101101
strategy:
102102
matrix:
103103
include:
104+
- name: MongoDB 5.1, ReplicaSet, WiredTiger
105+
MONGODB_VERSION: 5.1.0
106+
MONGODB_TOPOLOGY: replicaset
107+
MONGODB_STORAGE_ENGINE: wiredTiger
108+
NODE_VERSION: 14.18.1
104109
- name: MongoDB 5.0, ReplicaSet, WiredTiger
105110
MONGODB_VERSION: 5.0.3
106111
MONGODB_TOPOLOGY: replicaset

Diff for: README.md

+12-11
Original file line numberDiff line numberDiff line change
@@ -112,8 +112,8 @@ Before you start make sure you have installed:
112112
#### Node.js
113113
Parse Server is continuously tested with the most recent releases of Node.js to ensure compatibility. We follow the [Node.js Long Term Support plan](https://github.com/nodejs/Release) and only test against versions that are officially supported and have not reached their end-of-life date.
114114

115-
| Version | Latest Version | End-of-Life | Compatible |
116-
|------------|----------------|-------------|---------------|
115+
| Version | Latest Version | End-of-Life | Compatible |
116+
|------------|----------------|-------------|--------------|
117117
| Node.js 12 | 12.22.7 | April 2022 | ✅ Yes |
118118
| Node.js 14 | 14.18.1 | April 2023 | ✅ Yes |
119119
| Node.js 16 | 16.13.0 | April 2024 | ✅ Yes |
@@ -124,20 +124,21 @@ Parse Server is continuously tested with the most recent releases of MongoDB to
124124

125125
| Version | Latest Version | End-of-Life | Compatible |
126126
|-------------|----------------|--------------|------------|
127-
| MongoDB 4.0 | 4.0.27 | April 2022 | ✅ Yes |
128-
| MongoDB 4.2 | 4.2.17 | TBD | ✅ Yes |
129-
| MongoDB 4.4 | 4.4.10 | TBD | ✅ Yes |
130-
| MongoDB 5.0 | 5.0.3 | January 2024 | ✅ Yes |
131-
127+
| MongoDB 4.0 | 4.0.27 | April 2022 | ✅ Yes |
128+
| MongoDB 4.2 | 4.2.17 | TBD | ✅ Yes |
129+
| MongoDB 4.4 | 4.4.10 | TBD | ✅ Yes |
130+
| MongoDB 5.0 | 5.0.3 | January 2024 | ✅ Yes |
131+
| MongoDB 5.1 | 5.1.0 | January 2024 | ✅ Yes |
132+
132133
#### PostgreSQL
133134
Parse Server is continuously tested with the most recent releases of PostgreSQL and PostGIS to ensure compatibility, using [PostGIS docker images](https://registry.hub.docker.com/r/postgis/postgis/tags?page=1&ordering=last_updated). We follow the [PostgreSQL support schedule](https://www.postgresql.org/support/versioning) and [PostGIS support schedule](https://www.postgis.net/eol_policy/) and only test against versions that are officially supported and have not reached their end-of-life date. Due to the extensive PostgreSQL support duration of 5 years, Parse Server drops support if a version is older than 3.5 years and a newer version has been available for at least 2.5 years.
134135

135136
| Version | PostGIS Version | End-of-Life | Parse Server Support End | Compatible |
136137
|-------------|-----------------|---------------|--------------------------|------------|
137-
| Postgres 11 | 3.0, 3.1, 3.2 | November 2023 | April 2022 | ✅ Yes |
138-
| Postgres 12 | 3.2 | November 2024 | April 2023 | ✅ Yes |
139-
| Postgres 13 | 3.2 | November 2025 | April 2024 | ✅ Yes |
140-
| Postgres 14 | 3.2 | November 2026 | April 2025 | ✅ Yes |
138+
| Postgres 11 | 3.0, 3.1, 3.2 | November 2023 | April 2022 | ✅ Yes |
139+
| Postgres 12 | 3.2 | November 2024 | April 2023 | ✅ Yes |
140+
| Postgres 13 | 3.2 | November 2025 | April 2024 | ✅ Yes |
141+
| Postgres 14 | 3.2 | November 2026 | April 2025 | ✅ Yes |
141142

142143
### Locally
143144
```bash

Diff for: package.json

+5-4
Original file line numberDiff line numberDiff line change
@@ -120,12 +120,13 @@
120120
"test:mongodb:4.2.17": "npm run test:mongodb --dbversion=4.2.17",
121121
"test:mongodb:4.4.10": "npm run test:mongodb --dbversion=4.4.10",
122122
"test:mongodb:5.0.5": "npm run test:mongodb --dbversion=5.0.5",
123+
"test:mongodb:5.1.0": "npm run test:mongodb --dbversion=5.1.0",
123124
"posttest:mongodb": "mongodb-runner stop",
124-
"pretest": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=5.0.5} MONGODB_TOPOLOGY=${MONGODB_TOPOLOGY:=standalone} MONGODB_STORAGE_ENGINE=${MONGODB_STORAGE_ENGINE:=wiredTiger} mongodb-runner start",
125-
"testonly": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=5.0.5} MONGODB_TOPOLOGY=${MONGODB_TOPOLOGY:=standalone} MONGODB_STORAGE_ENGINE=${MONGODB_STORAGE_ENGINE:=wiredTiger} TESTING=1 jasmine",
125+
"pretest": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=5.1.0} MONGODB_TOPOLOGY=${MONGODB_TOPOLOGY:=standalone} MONGODB_STORAGE_ENGINE=${MONGODB_STORAGE_ENGINE:=wiredTiger} mongodb-runner start",
126+
"testonly": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=5.1.0} MONGODB_TOPOLOGY=${MONGODB_TOPOLOGY:=standalone} MONGODB_STORAGE_ENGINE=${MONGODB_STORAGE_ENGINE:=wiredTiger} TESTING=1 jasmine",
126127
"test": "npm run testonly",
127-
"posttest": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=5.0.5} MONGODB_TOPOLOGY=${MONGODB_TOPOLOGY:=standalone} MONGODB_STORAGE_ENGINE=${MONGODB_STORAGE_ENGINE:=wiredTiger} mongodb-runner stop",
128-
"coverage": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=5.0.5} MONGODB_TOPOLOGY=${MONGODB_TOPOLOGY:=standalone} MONGODB_STORAGE_ENGINE=${MONGODB_STORAGE_ENGINE:=wiredTiger} TESTING=1 nyc jasmine",
128+
"posttest": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=5.1.0} MONGODB_TOPOLOGY=${MONGODB_TOPOLOGY:=standalone} MONGODB_STORAGE_ENGINE=${MONGODB_STORAGE_ENGINE:=wiredTiger} mongodb-runner stop",
129+
"coverage": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=5.1.0} MONGODB_TOPOLOGY=${MONGODB_TOPOLOGY:=standalone} MONGODB_STORAGE_ENGINE=${MONGODB_STORAGE_ENGINE:=wiredTiger} TESTING=1 nyc jasmine",
129130
"start": "node ./bin/parse-server",
130131
"prettier": "prettier --write {src,spec}/{**/*,*}.js",
131132
"prepare": "npm run build",

Diff for: spec/MongoStorageAdapter.spec.js

+35-1
Original file line numberDiff line numberDiff line change
@@ -308,7 +308,7 @@ describe_only_db('mongo')('MongoStorageAdapter', () => {
308308
await expectAsync(adapter.getClass('UnknownClass')).toBeRejectedWith(undefined);
309309
});
310310

311-
it('should use index for caseInsensitive query', async () => {
311+
it_only_mongodb_version('<5.1')('should use index for caseInsensitive query', async () => {
312312
const user = new Parse.User();
313313
user.set('username', 'Bugs');
314314
user.set('password', 'Bunny');
@@ -342,6 +342,40 @@ describe_only_db('mongo')('MongoStorageAdapter', () => {
342342
expect(postIndexPlan.executionStats.executionStages.stage).toBe('FETCH');
343343
});
344344

345+
it_only_mongodb_version('>=5.1')('should use index for caseInsensitive query', async () => {
346+
const user = new Parse.User();
347+
user.set('username', 'Bugs');
348+
user.set('password', 'Bunny');
349+
await user.signUp();
350+
351+
const database = Config.get(Parse.applicationId).database;
352+
await database.adapter.dropAllIndexes('_User');
353+
354+
const preIndexPlan = await database.find(
355+
'_User',
356+
{ username: 'bugs' },
357+
{ caseInsensitive: true, explain: true }
358+
);
359+
360+
const schema = await new Parse.Schema('_User').get();
361+
362+
await database.adapter.ensureIndex(
363+
'_User',
364+
schema,
365+
['username'],
366+
'case_insensitive_username',
367+
true
368+
);
369+
370+
const postIndexPlan = await database.find(
371+
'_User',
372+
{ username: 'bugs' },
373+
{ caseInsensitive: true, explain: true }
374+
);
375+
expect(preIndexPlan.queryPlanner.winningPlan.queryPlan.stage).toBe('COLLSCAN');
376+
expect(postIndexPlan.queryPlanner.winningPlan.queryPlan.stage).toBe('FETCH');
377+
});
378+
345379
it('should delete field without index', async () => {
346380
const database = Config.get(Parse.applicationId).database;
347381
const obj = new Parse.Object('MyObject');

Diff for: spec/ParseQuery.hint.spec.js

+138-6
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ describe_only_db('mongo')('Parse.Query hint', () => {
2727
await TestUtils.destroyAllDataPermanently(false);
2828
});
2929

30-
it('query find with hint string', async () => {
30+
it_only_mongodb_version('<5.1')('query find with hint string', async () => {
3131
const object = new TestObject();
3232
await object.save();
3333

@@ -39,7 +39,18 @@ describe_only_db('mongo')('Parse.Query hint', () => {
3939
expect(explain.queryPlanner.winningPlan.inputStage.indexName).toBe('_id_');
4040
});
4141

42-
it('query find with hint object', async () => {
42+
it_only_mongodb_version('>=5.1')('query find with hint string', async () => {
43+
const object = new TestObject();
44+
await object.save();
45+
46+
const collection = await config.database.adapter._adaptiveCollection('TestObject');
47+
const explain = await collection._rawFind({ _id: object.id }, { hint: '_id_', explain: true });
48+
expect(explain.queryPlanner.winningPlan.queryPlan.stage).toBe('FETCH');
49+
expect(explain.queryPlanner.winningPlan.queryPlan.inputStage.stage).toBe('IXSCAN');
50+
expect(explain.queryPlanner.winningPlan.queryPlan.inputStage.indexName).toBe('_id_');
51+
});
52+
53+
it_only_mongodb_version('<5.1')('query find with hint object', async () => {
4354
const object = new TestObject();
4455
await object.save();
4556

@@ -53,6 +64,20 @@ describe_only_db('mongo')('Parse.Query hint', () => {
5364
});
5465
});
5566

67+
it_only_mongodb_version('>=5.1')('query find with hint object', async () => {
68+
const object = new TestObject();
69+
await object.save();
70+
71+
const collection = await config.database.adapter._adaptiveCollection('TestObject');
72+
const explain = await collection._rawFind(
73+
{ _id: object.id },
74+
{ hint: { _id: 1 }, explain: true }
75+
);
76+
expect(explain.queryPlanner.winningPlan.queryPlan.stage).toBe('FETCH');
77+
expect(explain.queryPlanner.winningPlan.queryPlan.inputStage.stage).toBe('IXSCAN');
78+
expect(explain.queryPlanner.winningPlan.queryPlan.inputStage.keyPattern).toEqual({ _id: 1 });
79+
});
80+
5681
it_only_mongodb_version('<4.4')('query aggregate with hint string', async () => {
5782
const object = new TestObject({ foo: 'bar' });
5883
await object.save();
@@ -73,7 +98,7 @@ describe_only_db('mongo')('Parse.Query hint', () => {
7398
expect(queryPlanner.winningPlan.inputStage.indexName).toBe('_id_');
7499
});
75100

76-
it_only_mongodb_version('>=4.4')('query aggregate with hint string', async () => {
101+
it_only_mongodb_version('>=4.4<5.1')('query aggregate with hint string', async () => {
77102
const object = new TestObject({ foo: 'bar' });
78103
await object.save();
79104

@@ -97,6 +122,30 @@ describe_only_db('mongo')('Parse.Query hint', () => {
97122
expect(queryPlanner.winningPlan.inputStage.inputStage.indexName).toBe('_id_');
98123
});
99124

125+
it_only_mongodb_version('>=5.1')('query aggregate with hint string', async () => {
126+
const object = new TestObject({ foo: 'bar' });
127+
await object.save();
128+
129+
const collection = await config.database.adapter._adaptiveCollection('TestObject');
130+
let result = await collection.aggregate([{ $group: { _id: '$foo' } }], {
131+
explain: true,
132+
});
133+
let { queryPlanner } = result[0].stages[0].$cursor;
134+
expect(queryPlanner.winningPlan.queryPlan.stage).toBe('PROJECTION_SIMPLE');
135+
expect(queryPlanner.winningPlan.queryPlan.inputStage.stage).toBe('COLLSCAN');
136+
expect(queryPlanner.winningPlan.queryPlan.inputStage.inputStage).toBeUndefined();
137+
138+
result = await collection.aggregate([{ $group: { _id: '$foo' } }], {
139+
hint: '_id_',
140+
explain: true,
141+
});
142+
queryPlanner = result[0].stages[0].$cursor.queryPlanner;
143+
expect(queryPlanner.winningPlan.queryPlan.stage).toBe('PROJECTION_SIMPLE');
144+
expect(queryPlanner.winningPlan.queryPlan.inputStage.stage).toBe('FETCH');
145+
expect(queryPlanner.winningPlan.queryPlan.inputStage.inputStage.stage).toBe('IXSCAN');
146+
expect(queryPlanner.winningPlan.queryPlan.inputStage.inputStage.indexName).toBe('_id_');
147+
});
148+
100149
it_only_mongodb_version('<4.4')('query aggregate with hint object', async () => {
101150
const object = new TestObject({ foo: 'bar' });
102151
await object.save();
@@ -117,7 +166,7 @@ describe_only_db('mongo')('Parse.Query hint', () => {
117166
expect(queryPlanner.winningPlan.inputStage.keyPattern).toEqual({ _id: 1 });
118167
});
119168

120-
it_only_mongodb_version('>=4.4')('query aggregate with hint object', async () => {
169+
it_only_mongodb_version('>=4.4<5.1')('query aggregate with hint object', async () => {
121170
const object = new TestObject({ foo: 'bar' });
122171
await object.save();
123172

@@ -142,7 +191,32 @@ describe_only_db('mongo')('Parse.Query hint', () => {
142191
expect(queryPlanner.winningPlan.inputStage.inputStage.keyPattern).toEqual({ _id: 1 });
143192
});
144193

145-
it('query find with hint (rest)', async () => {
194+
it_only_mongodb_version('>=5.1')('query aggregate with hint object', async () => {
195+
const object = new TestObject({ foo: 'bar' });
196+
await object.save();
197+
198+
const collection = await config.database.adapter._adaptiveCollection('TestObject');
199+
let result = await collection.aggregate([{ $group: { _id: '$foo' } }], {
200+
explain: true,
201+
});
202+
let { queryPlanner } = result[0].stages[0].$cursor;
203+
expect(queryPlanner.winningPlan.queryPlan.stage).toBe('PROJECTION_SIMPLE');
204+
expect(queryPlanner.winningPlan.queryPlan.inputStage.stage).toBe('COLLSCAN');
205+
expect(queryPlanner.winningPlan.queryPlan.inputStage.inputStage).toBeUndefined();
206+
207+
result = await collection.aggregate([{ $group: { _id: '$foo' } }], {
208+
hint: { _id: 1 },
209+
explain: true,
210+
});
211+
queryPlanner = result[0].stages[0].$cursor.queryPlanner;
212+
expect(queryPlanner.winningPlan.queryPlan.stage).toBe('PROJECTION_SIMPLE');
213+
expect(queryPlanner.winningPlan.queryPlan.inputStage.stage).toBe('FETCH');
214+
expect(queryPlanner.winningPlan.queryPlan.inputStage.inputStage.stage).toBe('IXSCAN');
215+
expect(queryPlanner.winningPlan.queryPlan.inputStage.inputStage.indexName).toBe('_id_');
216+
expect(queryPlanner.winningPlan.queryPlan.inputStage.inputStage.keyPattern).toEqual({ _id: 1 });
217+
});
218+
219+
it_only_mongodb_version('<5.1')('query find with hint (rest)', async () => {
146220
const object = new TestObject();
147221
await object.save();
148222
let options = Object.assign({}, masterKeyOptions, {
@@ -167,6 +241,31 @@ describe_only_db('mongo')('Parse.Query hint', () => {
167241
expect(explain.queryPlanner.winningPlan.inputStage.inputStage.indexName).toBe('_id_');
168242
});
169243

244+
it_only_mongodb_version('>=5.1')('query find with hint (rest)', async () => {
245+
const object = new TestObject();
246+
await object.save();
247+
let options = Object.assign({}, masterKeyOptions, {
248+
url: Parse.serverURL + '/classes/TestObject',
249+
qs: {
250+
explain: true,
251+
},
252+
});
253+
let response = await request(options);
254+
let explain = response.data.results;
255+
expect(explain.queryPlanner.winningPlan.queryPlan.inputStage.stage).toBe('COLLSCAN');
256+
257+
options = Object.assign({}, masterKeyOptions, {
258+
url: Parse.serverURL + '/classes/TestObject',
259+
qs: {
260+
explain: true,
261+
hint: '_id_',
262+
},
263+
});
264+
response = await request(options);
265+
explain = response.data.results;
266+
expect(explain.queryPlanner.winningPlan.queryPlan.inputStage.inputStage.indexName).toBe('_id_');
267+
});
268+
170269
it_only_mongodb_version('<4.4')('query aggregate with hint (rest)', async () => {
171270
const object = new TestObject({ foo: 'bar' });
172271
await object.save();
@@ -194,7 +293,7 @@ describe_only_db('mongo')('Parse.Query hint', () => {
194293
expect(queryPlanner.winningPlan.inputStage.keyPattern).toEqual({ _id: 1 });
195294
});
196295

197-
it_only_mongodb_version('>=4.4')('query aggregate with hint (rest)', async () => {
296+
it_only_mongodb_version('>=4.4<5.1')('query aggregate with hint (rest)', async () => {
198297
const object = new TestObject({ foo: 'bar' });
199298
await object.save();
200299
let options = Object.assign({}, masterKeyOptions, {
@@ -226,4 +325,37 @@ describe_only_db('mongo')('Parse.Query hint', () => {
226325
expect(queryPlanner.winningPlan.inputStage.inputStage.indexName).toBe('_id_');
227326
expect(queryPlanner.winningPlan.inputStage.inputStage.keyPattern).toEqual({ _id: 1 });
228327
});
328+
329+
it_only_mongodb_version('>=5.1')('query aggregate with hint (rest)', async () => {
330+
const object = new TestObject({ foo: 'bar' });
331+
await object.save();
332+
let options = Object.assign({}, masterKeyOptions, {
333+
url: Parse.serverURL + '/aggregate/TestObject',
334+
qs: {
335+
explain: true,
336+
group: JSON.stringify({ objectId: '$foo' }),
337+
},
338+
});
339+
let response = await request(options);
340+
let { queryPlanner } = response.data.results[0].stages[0].$cursor;
341+
expect(queryPlanner.winningPlan.queryPlan.stage).toBe('PROJECTION_SIMPLE');
342+
expect(queryPlanner.winningPlan.queryPlan.inputStage.stage).toBe('COLLSCAN');
343+
expect(queryPlanner.winningPlan.queryPlan.inputStage.inputStage).toBeUndefined();
344+
345+
options = Object.assign({}, masterKeyOptions, {
346+
url: Parse.serverURL + '/aggregate/TestObject',
347+
qs: {
348+
explain: true,
349+
hint: '_id_',
350+
group: JSON.stringify({ objectId: '$foo' }),
351+
},
352+
});
353+
response = await request(options);
354+
queryPlanner = response.data.results[0].stages[0].$cursor.queryPlanner;
355+
expect(queryPlanner.winningPlan.queryPlan.stage).toBe('PROJECTION_SIMPLE');
356+
expect(queryPlanner.winningPlan.queryPlan.inputStage.stage).toBe('FETCH');
357+
expect(queryPlanner.winningPlan.queryPlan.inputStage.inputStage.stage).toBe('IXSCAN');
358+
expect(queryPlanner.winningPlan.queryPlan.inputStage.inputStage.indexName).toBe('_id_');
359+
expect(queryPlanner.winningPlan.queryPlan.inputStage.inputStage.keyPattern).toEqual({ _id: 1 });
360+
});
229361
});

0 commit comments

Comments
 (0)