diff --git a/DEPRECATIONS.md b/DEPRECATIONS.md
index 27ccee409e..593be3d3d0 100644
--- a/DEPRECATIONS.md
+++ b/DEPRECATIONS.md
@@ -28,6 +28,7 @@ The following is a list of deprecations, according to the [Deprecation Policy](h
| DEPPS22 | Config option `protectedFieldsTriggerExempt` defaults to `true` | | 9.6.0 (2026) | 10.0.0 (2027) | deprecated | - |
| DEPPS23 | Config option `protectedFieldsSaveResponseExempt` defaults to `false` | | 9.7.0 (2026) | 10.0.0 (2027) | deprecated | - |
| DEPPS24 | Config option `installation.duplicateDeviceTokenActionEnforceAuth` defaults to `true` | [#10451](https://github.com/parse-community/parse-server/pull/10451) | 9.9.0 (2026) | 10.0.0 (2027) | deprecated | - |
+| DEPPS25 | Database option `databaseOptions.explainResultsAsArray` defaults to `true` | [#7442](https://github.com/parse-community/parse-server/issues/7442) | 9.11.0 (2026) | 10.0.0 (2027) | deprecated | - |
[i_deprecation]: ## "The version and date of the deprecation."
[i_change]: ## "The version and date of the planned change."
diff --git a/spec/ParseQuery.spec.js b/spec/ParseQuery.spec.js
index b7c3fe02d7..9a631075cf 100644
--- a/spec/ParseQuery.spec.js
+++ b/spec/ParseQuery.spec.js
@@ -5564,6 +5564,51 @@ describe('Parse.Query testing', () => {
expect(resultWithMasterKey).toBeDefined();
}
);
+
+ it_only_db('mongo')(
+ 'explain results are returned as an array when databaseOptions.explainResultsAsArray is true (#7442)',
+ async () => {
+ await reconfigureServer({
+ databaseAdapter: undefined,
+ databaseURI: 'mongodb://localhost:27017/parse',
+ databaseOptions: {
+ explainResultsAsArray: true,
+ },
+ });
+
+ const obj = new TestObject({ foo: 'bar' });
+ await obj.save();
+
+ const query = new Parse.Query(TestObject);
+ query.explain();
+ const result = await query.find({ useMasterKey: true });
+ expect(Array.isArray(result)).toBe(true);
+ expect(result.length).toBe(1);
+ expect(result[0].executionStats).not.toBeUndefined();
+ }
+ );
+
+ it_only_db('mongo')(
+ 'explain results are a single object by default, consistent with legacy behaviour (#7442)',
+ async () => {
+ await reconfigureServer({
+ databaseAdapter: undefined,
+ databaseURI: 'mongodb://localhost:27017/parse',
+ databaseOptions: {
+ explainResultsAsArray: undefined,
+ },
+ });
+
+ const obj = new TestObject({ foo: 'bar' });
+ await obj.save();
+
+ const query = new Parse.Query(TestObject);
+ query.explain();
+ const result = await query.find({ useMasterKey: true });
+ expect(Array.isArray(result)).toBe(false);
+ expect(result.executionStats).not.toBeUndefined();
+ }
+ );
});
describe('query input type validation', () => {
diff --git a/src/Config.js b/src/Config.js
index 95543c6c6b..b8d363263d 100644
--- a/src/Config.js
+++ b/src/Config.js
@@ -804,6 +804,11 @@ export class Config {
} else if (typeof databaseOptions.allowPublicExplain !== 'boolean') {
throw `Parse Server option 'databaseOptions.allowPublicExplain' must be a boolean.`;
}
+ if (databaseOptions.explainResultsAsArray === undefined) {
+ databaseOptions.explainResultsAsArray = DatabaseOptions.explainResultsAsArray.default;
+ } else if (typeof databaseOptions.explainResultsAsArray !== 'boolean') {
+ throw `Parse Server option 'databaseOptions.explainResultsAsArray' must be a boolean.`;
+ }
}
static validateLiveQueryOptions(liveQuery) {
diff --git a/src/Controllers/DatabaseController.js b/src/Controllers/DatabaseController.js
index d598cacac5..1b64b643ff 100644
--- a/src/Controllers/DatabaseController.js
+++ b/src/Controllers/DatabaseController.js
@@ -1558,7 +1558,16 @@ class DatabaseController {
);
}
} else if (explain) {
- return this.adapter.find(className, schema, query, queryOptions);
+ return this.adapter
+ .find(className, schema, query, queryOptions)
+ .then(results =>
+ // Normalise MongoDB's single explain object to an array, consistent with `find`
+ // and the Postgres adapter (which already returns an array). Opt-in until the
+ // default flips in a future major, see databaseOptions.explainResultsAsArray.
+ this.options.databaseOptions?.explainResultsAsArray && !Array.isArray(results)
+ ? [results]
+ : results
+ );
} else {
return this.adapter
.find(className, schema, query, queryOptions)
diff --git a/src/Deprecator/Deprecations.js b/src/Deprecator/Deprecations.js
index 4d8ffb0195..526b548d64 100644
--- a/src/Deprecator/Deprecations.js
+++ b/src/Deprecator/Deprecations.js
@@ -118,4 +118,9 @@ module.exports = [
changeNewDefault: 'false',
solution: "Set 'allowAggregationForReadOnlyMasterKey' to 'false' to prevent the read-only master key from running aggregation pipelines, which can include write-capable stages (e.g. '$out', '$merge'). Set to 'true' to keep the current behavior where the read-only master key can run aggregation pipelines.",
},
+ {
+ optionKey: 'databaseOptions.explainResultsAsArray',
+ changeNewDefault: 'true',
+ solution: "Set 'databaseOptions.explainResultsAsArray' to 'true' to return MongoDB 'explain' results as an array, consistent with 'find' and the Postgres adapter. Set to 'false' to keep the current behavior where MongoDB returns a single object.",
+ },
];
diff --git a/src/Options/Definitions.js b/src/Options/Definitions.js
index 5b2e6e497a..2568f77eba 100644
--- a/src/Options/Definitions.js
+++ b/src/Options/Definitions.js
@@ -1373,6 +1373,12 @@ module.exports.DatabaseOptions = {
action: parsers.booleanParser,
default: false,
},
+ explainResultsAsArray: {
+ env: 'PARSE_SERVER_DATABASE_EXPLAIN_RESULTS_AS_ARRAY',
+ help: 'Set to `true` to return `Parse.Query.explain` results for MongoDB as an array, consistent with `find` and the Postgres adapter. When `false`, MongoDB returns a single explain object (legacy behaviour), which is inconsistent with `find` and breaks strongly-typed SDKs that expect an array.',
+ action: parsers.booleanParser,
+ default: false,
+ },
forceServerObjectId: {
env: 'PARSE_SERVER_DATABASE_FORCE_SERVER_OBJECT_ID',
help: 'The MongoDB driver option to force server to assign _id values instead of driver.',
diff --git a/src/Options/docs.js b/src/Options/docs.js
index 91d58cbe9b..f7782098a3 100644
--- a/src/Options/docs.js
+++ b/src/Options/docs.js
@@ -326,6 +326,7 @@
* @property {Boolean} directConnection The MongoDB driver option to force a Single topology type with a connection string containing one host.
* @property {Boolean} disableIndexFieldValidation Set to `true` to disable validation of index fields. When disabled, indexes can be created even if the fields do not exist in the schema. This can be useful when creating indexes on fields that will be added later.
* @property {Boolean} enableSchemaHooks Enables database real-time hooks to update single schema cache. Set to `true` if using multiple Parse Servers instances connected to the same database. Failing to do so will cause a schema change to not propagate to all instances and re-syncing will only happen when the instances restart. To use this feature with MongoDB, a replica set cluster with [change stream](https://docs.mongodb.com/manual/changeStreams/#availability) support is required.
+ * @property {Boolean} explainResultsAsArray Set to `true` to return `Parse.Query.explain` results for MongoDB as an array, consistent with `find` and the Postgres adapter. When `false`, MongoDB returns a single explain object (legacy behaviour), which is inconsistent with `find` and breaks strongly-typed SDKs that expect an array.
* @property {Boolean} forceServerObjectId The MongoDB driver option to force server to assign _id values instead of driver.
* @property {Number} heartbeatFrequencyMS The MongoDB driver option to specify the frequency in milliseconds at which the driver checks the state of the MongoDB deployment.
* @property {Boolean} loadBalanced The MongoDB driver option to instruct the driver it is connecting to a load balancer fronting a mongos like service.
diff --git a/src/Options/index.js b/src/Options/index.js
index d8f56defca..b32c0bc8e0 100644
--- a/src/Options/index.js
+++ b/src/Options/index.js
@@ -908,6 +908,9 @@ export interface DatabaseOptions {
/* Set to `true` to allow `Parse.Query.explain` without master key.
⚠️ Enabling this option may expose sensitive query performance data to unauthorized users and could potentially be exploited for malicious purposes.
:DEFAULT: false */
allowPublicExplain: ?boolean;
+ /* Set to `true` to return `Parse.Query.explain` results for MongoDB as an array, consistent with `find` and the Postgres adapter. When `false`, MongoDB returns a single explain object (legacy behaviour), which is inconsistent with `find` and breaks strongly-typed SDKs that expect an array.
+ :DEFAULT: false */
+ explainResultsAsArray: ?boolean;
/* An array of MongoDB client event configurations to enable logging of specific events. */
logClientEvents: ?(LogClientEvent[]);
/* Custom metadata to append to database client connections for identifying Parse Server instances in database logs. If set, this metadata will be visible in database logs during connection handshakes. This can help with debugging and monitoring in deployments with multiple database clients. Set `name` to identify your application (e.g., 'MyApp') and `version` to your application's version. Leave undefined (default) to disable this feature and avoid the additional data transfer overhead. */
diff --git a/src/defaults.js b/src/defaults.js
index b7d05f1550..59e0149275 100644
--- a/src/defaults.js
+++ b/src/defaults.js
@@ -58,6 +58,7 @@ export const ParseServerDatabaseOptions = [
'createIndexUserUsernameCaseInsensitive',
'disableIndexFieldValidation',
'enableSchemaHooks',
+ 'explainResultsAsArray',
'logClientEvents',
'maxTimeMS',
'schemaCacheTtl',