Allow disabling workaround for since-fixed MongoDB bug (#5617)

* Allow disabling workaround for fixed MongoDB bug

* skipMongoDBServer13732Workaround description fix

* flip test boolean

* Remove CLI flag, use databaseVersion & engine

* Revert "Remove CLI flag, use databaseVersion & engine"

This reverts commit 042d1ba19f636fe0da06074168c6fd5db37ea048.

* clean up
This commit is contained in:
Jack Wearden
2019-06-19 23:30:08 +01:00
committed by Diamond Lewis
parent fcdf2d7947
commit 559096f1c2
9 changed files with 203 additions and 98 deletions

View File

@@ -3,61 +3,118 @@ const validateQuery = DatabaseController._validateQuery;
describe('DatabaseController', function() { describe('DatabaseController', function() {
describe('validateQuery', function() { describe('validateQuery', function() {
it('should restructure simple cases of SERVER-13732', done => { describe('with skipMongoDBServer13732Workaround disabled (the default)', function() {
const query = { it('should restructure simple cases of SERVER-13732', done => {
$or: [{ a: 1 }, { a: 2 }], const query = {
_rperm: { $in: ['a', 'b'] }, $or: [{ a: 1 }, { a: 2 }],
foo: 3, _rperm: { $in: ['a', 'b'] },
}; foo: 3,
validateQuery(query); };
expect(query).toEqual({ validateQuery(query, false);
$or: [ expect(query).toEqual({
{ a: 1, _rperm: { $in: ['a', 'b'] }, foo: 3 }, $or: [
{ a: 2, _rperm: { $in: ['a', 'b'] }, foo: 3 }, { a: 1, _rperm: { $in: ['a', 'b'] }, foo: 3 },
], { a: 2, _rperm: { $in: ['a', 'b'] }, foo: 3 },
}); ],
done(); });
}); done();
it('should not restructure SERVER-13732 queries with $nears', done => {
let query = { $or: [{ a: 1 }, { b: 1 }], c: { $nearSphere: {} } };
validateQuery(query);
expect(query).toEqual({
$or: [{ a: 1 }, { b: 1 }],
c: { $nearSphere: {} },
}); });
query = { $or: [{ a: 1 }, { b: 1 }], c: { $near: {} } }; it('should not restructure SERVER-13732 queries with $nears', done => {
validateQuery(query); let query = { $or: [{ a: 1 }, { b: 1 }], c: { $nearSphere: {} } };
expect(query).toEqual({ $or: [{ a: 1 }, { b: 1 }], c: { $near: {} } }); validateQuery(query, false);
expect(query).toEqual({
done(); $or: [{ a: 1 }, { b: 1 }],
}); c: { $nearSphere: {} },
});
it('should push refactored keys down a tree for SERVER-13732', done => { query = { $or: [{ a: 1 }, { b: 1 }], c: { $near: {} } };
const query = { validateQuery(query, false);
a: 1, expect(query).toEqual({ $or: [{ a: 1 }, { b: 1 }], c: { $near: {} } });
$or: [{ $or: [{ b: 1 }, { b: 2 }] }, { $or: [{ c: 1 }, { c: 2 }] }], done();
};
validateQuery(query);
expect(query).toEqual({
$or: [
{ $or: [{ b: 1, a: 1 }, { b: 2, a: 1 }] },
{ $or: [{ c: 1, a: 1 }, { c: 2, a: 1 }] },
],
}); });
done(); it('should push refactored keys down a tree for SERVER-13732', done => {
const query = {
a: 1,
$or: [{ $or: [{ b: 1 }, { b: 2 }] }, { $or: [{ c: 1 }, { c: 2 }] }],
};
validateQuery(query, false);
expect(query).toEqual({
$or: [
{ $or: [{ b: 1, a: 1 }, { b: 2, a: 1 }] },
{ $or: [{ c: 1, a: 1 }, { c: 2, a: 1 }] },
],
});
done();
});
it('should reject invalid queries', done => {
expect(() => validateQuery({ $or: { a: 1 } }, false)).toThrow();
done();
});
it('should accept valid queries', done => {
expect(() =>
validateQuery({ $or: [{ a: 1 }, { b: 2 }] }, false)
).not.toThrow();
done();
});
}); });
it('should reject invalid queries', done => { describe('with skipMongoDBServer13732Workaround enabled', function() {
expect(() => validateQuery({ $or: { a: 1 } })).toThrow(); it('should not restructure simple cases of SERVER-13732', done => {
done(); const query = {
}); $or: [{ a: 1 }, { a: 2 }],
_rperm: { $in: ['a', 'b'] },
foo: 3,
};
validateQuery(query, true);
expect(query).toEqual({
$or: [{ a: 1 }, { a: 2 }],
_rperm: { $in: ['a', 'b'] },
foo: 3,
});
done();
});
it('should accept valid queries', done => { it('should not restructure SERVER-13732 queries with $nears', done => {
expect(() => validateQuery({ $or: [{ a: 1 }, { b: 2 }] })).not.toThrow(); let query = { $or: [{ a: 1 }, { b: 1 }], c: { $nearSphere: {} } };
done(); validateQuery(query, true);
expect(query).toEqual({
$or: [{ a: 1 }, { b: 1 }],
c: { $nearSphere: {} },
});
query = { $or: [{ a: 1 }, { b: 1 }], c: { $near: {} } };
validateQuery(query, true);
expect(query).toEqual({ $or: [{ a: 1 }, { b: 1 }], c: { $near: {} } });
done();
});
it('should not push refactored keys down a tree for SERVER-13732', done => {
const query = {
a: 1,
$or: [{ $or: [{ b: 1 }, { b: 2 }] }, { $or: [{ c: 1 }, { c: 2 }] }],
};
validateQuery(query, true);
expect(query).toEqual({
a: 1,
$or: [{ $or: [{ b: 1 }, { b: 2 }] }, { $or: [{ c: 1 }, { c: 2 }] }],
});
done();
});
it('should reject invalid queries', done => {
expect(() => validateQuery({ $or: { a: 1 } }, true)).toThrow();
done();
});
it('should accept valid queries', done => {
expect(() =>
validateQuery({ $or: [{ a: 1 }, { b: 2 }] }, true)
).not.toThrow();
done();
});
}); });
}); });
}); });

View File

@@ -818,9 +818,9 @@ export class MongoStorageAdapter implements StorageAdapter {
// Pass objects down to MongoDB...this is more than likely an $exists operator. // Pass objects down to MongoDB...this is more than likely an $exists operator.
returnValue[`_p_${field}`] = pipeline[field]; returnValue[`_p_${field}`] = pipeline[field];
} else { } else {
returnValue[`_p_${field}`] = `${schema.fields[field].targetClass}$${ returnValue[
pipeline[field] `_p_${field}`
}`; ] = `${schema.fields[field].targetClass}$${pipeline[field]}`;
} }
} else if ( } else if (
schema.fields[field] && schema.fields[field] &&

View File

@@ -1396,9 +1396,7 @@ export class PostgresStorageAdapter implements StorageAdapter {
if (Object.keys(query).length === 0) { if (Object.keys(query).length === 0) {
where.pattern = 'TRUE'; where.pattern = 'TRUE';
} }
const qs = `WITH deleted AS (DELETE FROM $1:name WHERE ${ const qs = `WITH deleted AS (DELETE FROM $1:name WHERE ${where.pattern} RETURNING *) SELECT count(*) FROM deleted`;
where.pattern
} RETURNING *) SELECT count(*) FROM deleted`;
debug(qs, values); debug(qs, values);
return this._client return this._client
.one(qs, values, a => +a.count) .one(qs, values, a => +a.count)

View File

@@ -34,7 +34,8 @@ export class Config {
); );
config.database = new DatabaseController( config.database = new DatabaseController(
cacheInfo.databaseController.adapter, cacheInfo.databaseController.adapter,
schemaCache schemaCache,
cacheInfo.skipMongoDBServer13732Workaround
); );
} else { } else {
config[key] = cacheInfo[key]; config[key] = cacheInfo[key];

View File

@@ -69,48 +69,73 @@ const isSpecialQueryKey = key => {
return specialQuerykeys.indexOf(key) >= 0; return specialQuerykeys.indexOf(key) >= 0;
}; };
const validateQuery = (query: any): void => { const validateQuery = (
query: any,
skipMongoDBServer13732Workaround: boolean
): void => {
if (query.ACL) { if (query.ACL) {
throw new Parse.Error(Parse.Error.INVALID_QUERY, 'Cannot query on ACL.'); throw new Parse.Error(Parse.Error.INVALID_QUERY, 'Cannot query on ACL.');
} }
if (query.$or) { if (query.$or) {
if (query.$or instanceof Array) { if (query.$or instanceof Array) {
query.$or.forEach(validateQuery); query.$or.forEach(el =>
validateQuery(el, skipMongoDBServer13732Workaround)
);
/* In MongoDB, $or queries which are not alone at the top level of the if (!skipMongoDBServer13732Workaround) {
* query can not make efficient use of indexes due to a long standing /* In MongoDB 3.2 & 3.4, $or queries which are not alone at the top
* bug known as SERVER-13732. * level of the query can not make efficient use of indexes due to a
* * long standing bug known as SERVER-13732.
* This block restructures queries in which $or is not the sole top *
* level element by moving all other top-level predicates inside every * This bug was fixed in MongoDB version 3.6.
* subdocument of the $or predicate, allowing MongoDB's query planner *
* to make full use of the most relevant indexes. * For versions pre-3.6, the below logic produces a substantial
* * performance improvement inside the database by avoiding the bug.
* EG: {$or: [{a: 1}, {a: 2}], b: 2} *
* Becomes: {$or: [{a: 1, b: 2}, {a: 2, b: 2}]} * For versions 3.6 and above, there is no performance improvement and
* * the logic is unnecessary. Some query patterns are even slowed by
* The only exceptions are $near and $nearSphere operators, which are * the below logic, due to the bug having been fixed and better
* constrained to only 1 operator per query. As a result, these ops * query plans being chosen.
* remain at the top level *
* * When versions before 3.4 are no longer supported by this project,
* https://jira.mongodb.org/browse/SERVER-13732 * this logic, and the accompanying `skipMongoDBServer13732Workaround`
* https://github.com/parse-community/parse-server/issues/3767 * flag, can be removed.
*/ *
Object.keys(query).forEach(key => { * This block restructures queries in which $or is not the sole top
const noCollisions = !query.$or.some(subq => subq.hasOwnProperty(key)); * level element by moving all other top-level predicates inside every
let hasNears = false; * subdocument of the $or predicate, allowing MongoDB's query planner
if (query[key] != null && typeof query[key] == 'object') { * to make full use of the most relevant indexes.
hasNears = '$near' in query[key] || '$nearSphere' in query[key]; *
} * EG: {$or: [{a: 1}, {a: 2}], b: 2}
if (key != '$or' && noCollisions && !hasNears) { * Becomes: {$or: [{a: 1, b: 2}, {a: 2, b: 2}]}
query.$or.forEach(subquery => { *
subquery[key] = query[key]; * The only exceptions are $near and $nearSphere operators, which are
}); * constrained to only 1 operator per query. As a result, these ops
delete query[key]; * remain at the top level
} *
}); * https://jira.mongodb.org/browse/SERVER-13732
query.$or.forEach(validateQuery); * https://github.com/parse-community/parse-server/issues/3767
*/
Object.keys(query).forEach(key => {
const noCollisions = !query.$or.some(subq =>
subq.hasOwnProperty(key)
);
let hasNears = false;
if (query[key] != null && typeof query[key] == 'object') {
hasNears = '$near' in query[key] || '$nearSphere' in query[key];
}
if (key != '$or' && noCollisions && !hasNears) {
query.$or.forEach(subquery => {
subquery[key] = query[key];
});
delete query[key];
}
});
query.$or.forEach(el =>
validateQuery(el, skipMongoDBServer13732Workaround)
);
}
} else { } else {
throw new Parse.Error( throw new Parse.Error(
Parse.Error.INVALID_QUERY, Parse.Error.INVALID_QUERY,
@@ -121,7 +146,9 @@ const validateQuery = (query: any): void => {
if (query.$and) { if (query.$and) {
if (query.$and instanceof Array) { if (query.$and instanceof Array) {
query.$and.forEach(validateQuery); query.$and.forEach(el =>
validateQuery(el, skipMongoDBServer13732Workaround)
);
} else { } else {
throw new Parse.Error( throw new Parse.Error(
Parse.Error.INVALID_QUERY, Parse.Error.INVALID_QUERY,
@@ -132,7 +159,9 @@ const validateQuery = (query: any): void => {
if (query.$nor) { if (query.$nor) {
if (query.$nor instanceof Array && query.$nor.length > 0) { if (query.$nor instanceof Array && query.$nor.length > 0) {
query.$nor.forEach(validateQuery); query.$nor.forEach(el =>
validateQuery(el, skipMongoDBServer13732Workaround)
);
} else { } else {
throw new Parse.Error( throw new Parse.Error(
Parse.Error.INVALID_QUERY, Parse.Error.INVALID_QUERY,
@@ -381,14 +410,20 @@ class DatabaseController {
adapter: StorageAdapter; adapter: StorageAdapter;
schemaCache: any; schemaCache: any;
schemaPromise: ?Promise<SchemaController.SchemaController>; schemaPromise: ?Promise<SchemaController.SchemaController>;
skipMongoDBServer13732Workaround: boolean;
constructor(adapter: StorageAdapter, schemaCache: any) { constructor(
adapter: StorageAdapter,
schemaCache: any,
skipMongoDBServer13732Workaround: boolean
) {
this.adapter = adapter; this.adapter = adapter;
this.schemaCache = schemaCache; this.schemaCache = schemaCache;
// We don't want a mutable this.schema, because then you could have // We don't want a mutable this.schema, because then you could have
// one request that uses different schemas for different parts of // one request that uses different schemas for different parts of
// it. Instead, use loadSchema to get a schema. // it. Instead, use loadSchema to get a schema.
this.schemaPromise = null; this.schemaPromise = null;
this.skipMongoDBServer13732Workaround = skipMongoDBServer13732Workaround;
} }
collectionExists(className: string): Promise<boolean> { collectionExists(className: string): Promise<boolean> {
@@ -524,7 +559,7 @@ class DatabaseController {
if (acl) { if (acl) {
query = addWriteACL(query, acl); query = addWriteACL(query, acl);
} }
validateQuery(query); validateQuery(query, this.skipMongoDBServer13732Workaround);
return schemaController return schemaController
.getOneSchema(className, true) .getOneSchema(className, true)
.catch(error => { .catch(error => {
@@ -798,7 +833,7 @@ class DatabaseController {
if (acl) { if (acl) {
query = addWriteACL(query, acl); query = addWriteACL(query, acl);
} }
validateQuery(query); validateQuery(query, this.skipMongoDBServer13732Workaround);
return schemaController return schemaController
.getOneSchema(className) .getOneSchema(className)
.catch(error => { .catch(error => {
@@ -1197,6 +1232,7 @@ class DatabaseController {
): Promise<any> { ): Promise<any> {
const isMaster = acl === undefined; const isMaster = acl === undefined;
const aclGroup = acl || []; const aclGroup = acl || [];
op = op =
op || op ||
(typeof query.objectId == 'string' && Object.keys(query).length === 1 (typeof query.objectId == 'string' && Object.keys(query).length === 1
@@ -1297,7 +1333,7 @@ class DatabaseController {
query = addReadACL(query, aclGroup); query = addReadACL(query, aclGroup);
} }
} }
validateQuery(query); validateQuery(query, this.skipMongoDBServer13732Workaround);
if (count) { if (count) {
if (!classExists) { if (!classExists) {
return 0; return 0;
@@ -1563,7 +1599,7 @@ class DatabaseController {
]); ]);
} }
static _validateQuery: any => void; static _validateQuery: (any, boolean) => void;
} }
module.exports = DatabaseController; module.exports = DatabaseController;

View File

@@ -147,6 +147,7 @@ export function getDatabaseController(
const { const {
databaseURI, databaseURI,
databaseOptions, databaseOptions,
skipMongoDBServer13732Workaround,
collectionPrefix, collectionPrefix,
schemaCacheTTL, schemaCacheTTL,
enableSingleSchemaCache, enableSingleSchemaCache,
@@ -170,7 +171,8 @@ export function getDatabaseController(
} }
return new DatabaseController( return new DatabaseController(
databaseAdapter, databaseAdapter,
new SchemaCache(cacheController, schemaCacheTTL, enableSingleSchemaCache) new SchemaCache(cacheController, schemaCacheTTL, enableSingleSchemaCache),
skipMongoDBServer13732Workaround
); );
} }

View File

@@ -320,6 +320,12 @@ module.exports.ParseServerOptions = {
help: 'Disables console output', help: 'Disables console output',
action: parsers.booleanParser, action: parsers.booleanParser,
}, },
skipMongoDBServer13732Workaround: {
env: 'PARSE_SKIP_MONGODB_SERVER_13732_WORKAROUND',
help: 'Circumvent Parse workaround for historical MongoDB bug SERVER-13732',
action: parsers.booleanParser,
default: false,
},
startLiveQueryServer: { startLiveQueryServer: {
env: 'PARSE_SERVER_START_LIVE_QUERY_SERVER', env: 'PARSE_SERVER_START_LIVE_QUERY_SERVER',
help: 'Starts the liveQuery server', help: 'Starts the liveQuery server',

View File

@@ -57,6 +57,7 @@
* @property {String} serverURL URL to your parse server with http:// or https://. * @property {String} serverURL URL to your parse server with http:// or https://.
* @property {Number} sessionLength Session duration, in seconds, defaults to 1 year * @property {Number} sessionLength Session duration, in seconds, defaults to 1 year
* @property {Boolean} silent Disables console output * @property {Boolean} silent Disables console output
* @property {Boolean} skipMongoDBServer13732Workaround Circumvent Parse workaround for historical MongoDB bug SERVER-13732
* @property {Boolean} startLiveQueryServer Starts the liveQuery server * @property {Boolean} startLiveQueryServer Starts the liveQuery server
* @property {String[]} userSensitiveFields Personally identifiable information fields in the user table the should be removed for non-authorized users. Deprecated @see protectedFields * @property {String[]} userSensitiveFields Personally identifiable information fields in the user table the should be removed for non-authorized users. Deprecated @see protectedFields
* @property {Boolean} verbose Set the logging to verbose * @property {Boolean} verbose Set the logging to verbose

View File

@@ -58,6 +58,10 @@ export interface ParseServerOptions {
databaseOptions: ?any; databaseOptions: ?any;
/* Adapter module for the database */ /* Adapter module for the database */
databaseAdapter: ?Adapter<StorageAdapter>; databaseAdapter: ?Adapter<StorageAdapter>;
/* Circumvent Parse workaround for historical MongoDB bug SERVER-13732
:ENV: PARSE_SKIP_MONGODB_SERVER_13732_WORKAROUND
:DEFAULT: false */
skipMongoDBServer13732Workaround: ?boolean;
/* Full path to your cloud code main.js */ /* Full path to your cloud code main.js */
cloud: ?string; cloud: ?string;
/* A collection prefix for the classes /* A collection prefix for the classes