From daaeed8df396b9fa9611cf41dbea3814668a603f Mon Sep 17 00:00:00 2001 From: Antoine Cormouls Date: Sat, 26 Oct 2024 09:34:37 +0200 Subject: [PATCH 1/5] fix: mongodb-runner usage and default version to 6.0.2 --- .gitignore | 2 ++ package.json | 10 +++++----- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/.gitignore b/.gitignore index e4e19156c2..7c65ffb638 100644 --- a/.gitignore +++ b/.gitignore @@ -59,3 +59,5 @@ lib/ # Redis Dump dump.rdb + +_mongodb_runner \ No newline at end of file diff --git a/package.json b/package.json index 26d9d591d2..1f0aab11a2 100644 --- a/package.json +++ b/package.json @@ -127,16 +127,16 @@ "test:mongodb:6.0.2": "npm run test:mongodb --dbversion=6.0.2", "test:mongodb:7.0.1": "npm run test:mongodb --dbversion=7.0.1", "test:postgres:testonly": "cross-env PARSE_SERVER_TEST_DB=postgres PARSE_SERVER_TEST_DATABASE_URI=postgres://postgres:password@localhost:5432/parse_server_postgres_adapter_test_database npm run testonly", - "pretest": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=5.3.2} MONGODB_TOPOLOGY=${MONGODB_TOPOLOGY:=standalone} mongodb-runner start -t ${MONGODB_TOPOLOGY} --version ${MONGODB_VERSION} -- --port 27017", - "testonly": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=5.3.2} MONGODB_TOPOLOGY=${MONGODB_TOPOLOGY:=standalone} TESTING=1 jasmine", + "pretest": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=6.0.2} MONGODB_TOPOLOGY=${MONGODB_TOPOLOGY:=standalone} mongodb-runner start -t ${MONGODB_TOPOLOGY} --version ${MONGODB_VERSION} --runnerDir ./_mongodb_runner -- --port 27017", + "testonly": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=6.0.2} MONGODB_TOPOLOGY=${MONGODB_TOPOLOGY:=standalone} TESTING=1 jasmine", "test": "npm run testonly", - "posttest": "cross-env mongodb-runner stop --all", - "coverage": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=5.3.2} MONGODB_TOPOLOGY=${MONGODB_TOPOLOGY:=standalone} TESTING=1 nyc jasmine", + "posttest": "cross-env mongodb-runner stop --all --runnerDir ./_mongodb_runner", + "coverage": "cross-env MONGODB_VERSION=${MONGODB_VERSION:=6.0.2} MONGODB_TOPOLOGY=${MONGODB_TOPOLOGY:=standalone} TESTING=1 nyc jasmine", "start": "node ./bin/parse-server", "prettier": "prettier --write {src,spec}/{**/*,*}.js", "prepare": "npm run build", "postinstall": "node -p 'require(\"./postinstall.js\")()'", - "madge:circular": "node_modules/.bin/madge ./src --circular" + "madge:circular": "node_modules/.bin/madge ./src --circular", }, "engines": { "node": ">=18.0.0 <19.0.0 || >=19.0.0 <20.0.0 || >=20.0.0 <21.0.0 || >=22.0.0 <23.0.0" From 5811465aaa3189a64bb548b99e1e49fb39fc8481 Mon Sep 17 00:00:00 2001 From: Antoine Cormouls Date: Sat, 26 Oct 2024 09:42:14 +0200 Subject: [PATCH 2/5] fix: json --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 1f0aab11a2..77fa2ff18e 100644 --- a/package.json +++ b/package.json @@ -136,7 +136,7 @@ "prettier": "prettier --write {src,spec}/{**/*,*}.js", "prepare": "npm run build", "postinstall": "node -p 'require(\"./postinstall.js\")()'", - "madge:circular": "node_modules/.bin/madge ./src --circular", + "madge:circular": "node_modules/.bin/madge ./src --circular" }, "engines": { "node": ">=18.0.0 <19.0.0 || >=19.0.0 <20.0.0 || >=20.0.0 <21.0.0 || >=22.0.0 <23.0.0" From 6e89fd16769bfa9efb3869aae86c1e034281a88b Mon Sep 17 00:00:00 2001 From: Antoine Cormouls Date: Mon, 28 Oct 2024 19:28:02 +0100 Subject: [PATCH 3/5] feat: parallel include --- src/RestQuery.js | 61 ++++++++++++++++++++++++++++++++---------------- 1 file changed, 41 insertions(+), 20 deletions(-) diff --git a/src/RestQuery.js b/src/RestQuery.js index 621700984b..dd1992e56b 100644 --- a/src/RestQuery.js +++ b/src/RestQuery.js @@ -852,31 +852,54 @@ _UnsafeRestQuery.prototype.handleExcludeKeys = function () { }; // Augments this.response with data at the paths provided in this.include. -_UnsafeRestQuery.prototype.handleInclude = function () { +_UnsafeRestQuery.prototype.handleInclude = async function () { if (this.include.length == 0) { return; } - var pathResponse = includePath( - this.config, - this.auth, - this.response, - this.include[0], - this.context, - this.restOptions - ); - if (pathResponse.then) { - return pathResponse.then(newResponse => { - this.response = newResponse; - this.include = this.include.slice(1); - return this.handleInclude(); + const indexedResults = this.response.results.reduce((indexed, result, i) => { + indexed[result.objectId] = i; + return indexed; + }, {}); + + // Build the execution tree + const executionTree = {} + this.include.forEach(path => { + let current = executionTree; + path.forEach((node) => { + if (!current[node]) { + current[node] = { + path, + children: {} + }; + } + current = current[node].children }); - } else if (this.include.length > 0) { - this.include = this.include.slice(1); - return this.handleInclude(); + }); + + const recursiveExecutionTree = async (treeNode) => { + const { path, children } = treeNode; + const pathResponse = includePath( + this.config, + this.auth, + this.response, + path, + this.context, + this.restOptions, + this, + ); + if (pathResponse.then) { + const newResponse = await pathResponse + newResponse.results.forEach(newObject => { + // We hydrate the root of each result with sub results + this.response.results[indexedResults[newObject.objectId]][path[0]] = newObject[path[0]]; + }) + } + return Promise.all(Object.values(children).map(recursiveExecutionTree)); } - return pathResponse; + await Promise.all(Object.values(executionTree).map(recursiveExecutionTree)); + this.include = [] }; //Returns a promise of a processed set of results @@ -1013,7 +1036,6 @@ function includePath(config, auth, response, path, context, restOptions = {}) { } else if (restOptions.readPreference) { includeRestOptions.readPreference = restOptions.readPreference; } - const queryPromises = Object.keys(pointersHash).map(async className => { const objectIds = Array.from(pointersHash[className]); let where; @@ -1052,7 +1074,6 @@ function includePath(config, auth, response, path, context, restOptions = {}) { } return replace; }, {}); - var resp = { results: replacePointers(response.results, path, replace), }; From 8b99dc5ed70c41bc1a3f63f8d1f8de21162d3870 Mon Sep 17 00:00:00 2001 From: Antoine Cormouls Date: Sat, 8 Nov 2025 20:01:03 +0100 Subject: [PATCH 4/5] feat: add test to battle test include --- spec/RestQuery.spec.js | 82 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) diff --git a/spec/RestQuery.spec.js b/spec/RestQuery.spec.js index 6fe3c0fa18..7b676da1ea 100644 --- a/spec/RestQuery.spec.js +++ b/spec/RestQuery.spec.js @@ -386,6 +386,88 @@ describe('rest query', () => { } ); }); + + it('battle test parallel include with 100 nested includes', async () => { + const RootObject = Parse.Object.extend('RootObject'); + const Level1Object = Parse.Object.extend('Level1Object'); + const Level2Object = Parse.Object.extend('Level2Object'); + + // Create 100 level2 objects (10 per level1 object) + const level2Objects = []; + for (let i = 0; i < 100; i++) { + const level2 = new Level2Object({ + index: i, + value: `level2_${i}`, + }); + level2Objects.push(level2); + } + await Parse.Object.saveAll(level2Objects); + + // Create 10 level1 objects, each with 10 pointers to level2 objects + const level1Objects = []; + for (let i = 0; i < 10; i++) { + const level1 = new Level1Object({ + index: i, + value: `level1_${i}`, + }); + // Set 10 pointer fields (level2_0 through level2_9) + for (let j = 0; j < 10; j++) { + level1.set(`level2_${j}`, level2Objects[i * 10 + j]); + } + level1Objects.push(level1); + } + await Parse.Object.saveAll(level1Objects); + + // Create 1 root object with 10 pointers to level1 objects + const rootObject = new RootObject({ + value: 'root', + }); + for (let i = 0; i < 10; i++) { + rootObject.set(`level1_${i}`, level1Objects[i]); + } + await rootObject.save(); + + // Build include paths: level1_0 through level1_9, and level1_0.level2_0 through level1_9.level2_9 + const includePaths = []; + for (let i = 0; i < 10; i++) { + includePaths.push(`level1_${i}`); + for (let j = 0; j < 10; j++) { + includePaths.push(`level1_${i}.level2_${j}`); + } + } + + // Query with all includes + const query = new Parse.Query(RootObject); + query.equalTo('objectId', rootObject.id); + for (const path of includePaths) { + query.include(path); + } + console.time('query.find'); + const results = await query.find(); + console.timeEnd('query.find'); + expect(results.length).toBe(1); + + const result = results[0]; + expect(result.id).toBe(rootObject.id); + + // Verify all 10 level1 objects are included + for (let i = 0; i < 10; i++) { + const level1Field = result.get(`level1_${i}`); + expect(level1Field).toBeDefined(); + expect(level1Field instanceof Parse.Object).toBe(true); + expect(level1Field.get('index')).toBe(i); + expect(level1Field.get('value')).toBe(`level1_${i}`); + + // Verify all 10 level2 objects are included for each level1 object + for (let j = 0; j < 10; j++) { + const level2Field = level1Field.get(`level2_${j}`); + expect(level2Field).toBeDefined(); + expect(level2Field instanceof Parse.Object).toBe(true); + expect(level2Field.get('index')).toBe(i * 10 + j); + expect(level2Field.get('value')).toBe(`level2_${i * 10 + j}`); + } + } + }); }); describe('RestQuery.each', () => { From 87339eb30a5039751cb8461f634f1ef5740378e7 Mon Sep 17 00:00:00 2001 From: Manuel Trezza <5673677+mtrezza@users.noreply.github.com> Date: Sun, 9 Nov 2025 02:17:58 +0100 Subject: [PATCH 5/5] add perf test --- benchmark/performance.js | 50 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) diff --git a/benchmark/performance.js b/benchmark/performance.js index 7021ed35b3..55033916ab 100644 --- a/benchmark/performance.js +++ b/benchmark/performance.js @@ -293,6 +293,52 @@ async function benchmarkUserLogin() { }, Math.floor(ITERATIONS / 10)); // Fewer iterations for user operations } +/** + * Benchmark: Query with Include (Parallel Include Pointers) + */ +async function benchmarkQueryWithInclude() { + // Setup: Create nested object hierarchy + const Level2Class = Parse.Object.extend('Level2'); + const Level1Class = Parse.Object.extend('Level1'); + const RootClass = Parse.Object.extend('Root'); + + // Create 10 Level2 objects + const level2Objects = []; + for (let i = 0; i < 10; i++) { + const obj = new Level2Class(); + obj.set('name', `level2-${i}`); + obj.set('value', i); + level2Objects.push(obj); + } + await Parse.Object.saveAll(level2Objects); + + // Create 10 Level1 objects, each pointing to a Level2 object + const level1Objects = []; + for (let i = 0; i < 10; i++) { + const obj = new Level1Class(); + obj.set('name', `level1-${i}`); + obj.set('level2', level2Objects[i % level2Objects.length]); + level1Objects.push(obj); + } + await Parse.Object.saveAll(level1Objects); + + // Create 10 Root objects, each pointing to a Level1 object + const rootObjects = []; + for (let i = 0; i < 10; i++) { + const obj = new RootClass(); + obj.set('name', `root-${i}`); + obj.set('level1', level1Objects[i % level1Objects.length]); + rootObjects.push(obj); + } + await Parse.Object.saveAll(rootObjects); + + return measureOperation('Query with Include (2 levels)', async () => { + const query = new Parse.Query('Root'); + query.include('level1.level2'); + await query.find(); + }, Math.floor(ITERATIONS / 10)); // Fewer iterations for complex queries +} + /** * Run all benchmarks */ @@ -341,6 +387,10 @@ async function runBenchmarks() { await cleanupDatabase(); results.push(await benchmarkUserLogin()); + console.log('Running Query with Include benchmark...'); + await cleanupDatabase(); + results.push(await benchmarkQueryWithInclude()); + // Output results in github-action-benchmark format (stdout) console.log(JSON.stringify(results, null, 2));