@@ -69,48 +69,73 @@ const isSpecialQueryKey = key => {
69
69
return specialQuerykeys . indexOf ( key ) >= 0 ;
70
70
} ;
71
71
72
- const validateQuery = ( query : any ) : void => {
72
+ const validateQuery = (
73
+ query : any ,
74
+ skipMongoDBServer13732Workaround : boolean
75
+ ) : void => {
73
76
if ( query . ACL ) {
74
77
throw new Parse . Error ( Parse . Error . INVALID_QUERY , 'Cannot query on ACL.' ) ;
75
78
}
76
79
77
80
if ( query . $or ) {
78
81
if ( query . $or instanceof Array ) {
79
- query . $or . forEach ( validateQuery ) ;
80
-
81
- /* In MongoDB, $or queries which are not alone at the top level of the
82
- * query can not make efficient use of indexes due to a long standing
83
- * bug known as SERVER-13732.
84
- *
85
- * This block restructures queries in which $or is not the sole top
86
- * level element by moving all other top-level predicates inside every
87
- * subdocument of the $or predicate, allowing MongoDB's query planner
88
- * to make full use of the most relevant indexes.
89
- *
90
- * EG: {$or: [{a: 1}, {a: 2}], b: 2}
91
- * Becomes: {$or: [{a: 1, b: 2}, {a: 2, b: 2}]}
92
- *
93
- * The only exceptions are $near and $nearSphere operators, which are
94
- * constrained to only 1 operator per query. As a result, these ops
95
- * remain at the top level
96
- *
97
- * https://jira.mongodb.org/browse/SERVER-13732
98
- * https://github.com/parse-community/parse-server/issues/3767
99
- */
100
- Object . keys ( query ) . forEach ( key => {
101
- const noCollisions = ! query . $or . some ( subq => subq . hasOwnProperty ( key ) ) ;
102
- let hasNears = false ;
103
- if ( query [ key ] != null && typeof query [ key ] == 'object' ) {
104
- hasNears = '$near' in query [ key ] || '$nearSphere' in query [ key ] ;
105
- }
106
- if ( key != '$or' && noCollisions && ! hasNears ) {
107
- query . $or . forEach ( subquery => {
108
- subquery [ key ] = query [ key ] ;
109
- } ) ;
110
- delete query [ key ] ;
111
- }
112
- } ) ;
113
- query . $or . forEach ( validateQuery ) ;
82
+ query . $or . forEach ( el =>
83
+ validateQuery ( el , skipMongoDBServer13732Workaround )
84
+ ) ;
85
+
86
+ if ( ! skipMongoDBServer13732Workaround ) {
87
+ /* In MongoDB 3.2 & 3.4, $or queries which are not alone at the top
88
+ * level of the query can not make efficient use of indexes due to a
89
+ * long standing bug known as SERVER-13732.
90
+ *
91
+ * This bug was fixed in MongoDB version 3.6.
92
+ *
93
+ * For versions pre-3.6, the below logic produces a substantial
94
+ * performance improvement inside the database by avoiding the bug.
95
+ *
96
+ * For versions 3.6 and above, there is no performance improvement and
97
+ * the logic is unnecessary. Some query patterns are even slowed by
98
+ * the below logic, due to the bug having been fixed and better
99
+ * query plans being chosen.
100
+ *
101
+ * When versions before 3.4 are no longer supported by this project,
102
+ * this logic, and the accompanying `skipMongoDBServer13732Workaround`
103
+ * flag, can be removed.
104
+ *
105
+ * This block restructures queries in which $or is not the sole top
106
+ * level element by moving all other top-level predicates inside every
107
+ * subdocument of the $or predicate, allowing MongoDB's query planner
108
+ * to make full use of the most relevant indexes.
109
+ *
110
+ * EG: {$or: [{a: 1}, {a: 2}], b: 2}
111
+ * Becomes: {$or: [{a: 1, b: 2}, {a: 2, b: 2}]}
112
+ *
113
+ * The only exceptions are $near and $nearSphere operators, which are
114
+ * constrained to only 1 operator per query. As a result, these ops
115
+ * remain at the top level
116
+ *
117
+ * https://jira.mongodb.org/browse/SERVER-13732
118
+ * https://github.com/parse-community/parse-server/issues/3767
119
+ */
120
+ Object . keys ( query ) . forEach ( key => {
121
+ const noCollisions = ! query . $or . some ( subq =>
122
+ subq . hasOwnProperty ( key )
123
+ ) ;
124
+ let hasNears = false ;
125
+ if ( query [ key ] != null && typeof query [ key ] == 'object' ) {
126
+ hasNears = '$near' in query [ key ] || '$nearSphere' in query [ key ] ;
127
+ }
128
+ if ( key != '$or' && noCollisions && ! hasNears ) {
129
+ query . $or . forEach ( subquery => {
130
+ subquery [ key ] = query [ key ] ;
131
+ } ) ;
132
+ delete query [ key ] ;
133
+ }
134
+ } ) ;
135
+ query . $or . forEach ( el =>
136
+ validateQuery ( el , skipMongoDBServer13732Workaround )
137
+ ) ;
138
+ }
114
139
} else {
115
140
throw new Parse . Error (
116
141
Parse . Error . INVALID_QUERY ,
@@ -121,7 +146,9 @@ const validateQuery = (query: any): void => {
121
146
122
147
if ( query . $and ) {
123
148
if ( query . $and instanceof Array ) {
124
- query . $and . forEach ( validateQuery ) ;
149
+ query . $and . forEach ( el =>
150
+ validateQuery ( el , skipMongoDBServer13732Workaround)
151
+ ) ;
125
152
} else {
126
153
throw new Parse . Error (
127
154
Parse . Error . INVALID_QUERY ,
@@ -132,7 +159,9 @@ const validateQuery = (query: any): void => {
132
159
133
160
if ( query . $nor ) {
134
161
if ( query . $nor instanceof Array && query . $nor . length > 0 ) {
135
- query . $nor . forEach ( validateQuery ) ;
162
+ query . $nor . forEach ( el =>
163
+ validateQuery ( el , skipMongoDBServer13732Workaround )
164
+ ) ;
136
165
} else {
137
166
throw new Parse . Error (
138
167
Parse . Error . INVALID_QUERY ,
@@ -381,14 +410,20 @@ class DatabaseController {
381
410
adapter : StorageAdapter ;
382
411
schemaCache : any ;
383
412
schemaPromise : ?Promise < SchemaController . SchemaController > ;
413
+ skipMongoDBServer13732Workaround : boolean ;
384
414
385
- constructor ( adapter : StorageAdapter , schemaCache : any ) {
415
+ constructor (
416
+ adapter : StorageAdapter ,
417
+ schemaCache : any ,
418
+ skipMongoDBServer13732Workaround : boolean
419
+ ) {
386
420
this . adapter = adapter ;
387
421
this . schemaCache = schemaCache ;
388
422
// We don't want a mutable this.schema, because then you could have
389
423
// one request that uses different schemas for different parts of
390
424
// it. Instead, use loadSchema to get a schema.
391
425
this . schemaPromise = null ;
426
+ this . skipMongoDBServer13732Workaround = skipMongoDBServer13732Workaround ;
392
427
}
393
428
394
429
collectionExists ( className : string ) : Promise < boolean > {
@@ -524,7 +559,7 @@ class DatabaseController {
524
559
if ( acl ) {
525
560
query = addWriteACL ( query , acl ) ;
526
561
}
527
- validateQuery ( query ) ;
562
+ validateQuery ( query , this . skipMongoDBServer13732Workaround ) ;
528
563
return schemaController
529
564
. getOneSchema ( className , true )
530
565
. catch ( error => {
@@ -798,7 +833,7 @@ class DatabaseController {
798
833
if ( acl ) {
799
834
query = addWriteACL ( query , acl ) ;
800
835
}
801
- validateQuery ( query ) ;
836
+ validateQuery ( query , this . skipMongoDBServer13732Workaround ) ;
802
837
return schemaController
803
838
. getOneSchema ( className )
804
839
. catch ( error => {
@@ -1197,6 +1232,7 @@ class DatabaseController {
1197
1232
) : Promise < any > {
1198
1233
const isMaster = acl === undefined ;
1199
1234
const aclGroup = acl || [ ] ;
1235
+
1200
1236
op =
1201
1237
op ||
1202
1238
( typeof query . objectId == 'string' && Object . keys ( query ) . length === 1
@@ -1297,7 +1333,7 @@ class DatabaseController {
1297
1333
query = addReadACL ( query , aclGroup ) ;
1298
1334
}
1299
1335
}
1300
- validateQuery ( query ) ;
1336
+ validateQuery ( query , this . skipMongoDBServer13732Workaround ) ;
1301
1337
if ( count ) {
1302
1338
if ( ! classExists ) {
1303
1339
return 0 ;
@@ -1563,7 +1599,7 @@ class DatabaseController {
1563
1599
] ) ;
1564
1600
}
1565
1601
1566
- static _validateQuery : any => void ;
1602
+ static _validateQuery : ( any , boolean ) => void ;
1567
1603
}
1568
1604
1569
1605
module . exports = DatabaseController ;
0 commit comments