(#6664 / #8011 / #6879) - fix nested $or inside $and
diff --git a/packages/node_modules/pouchdb-selector-core/src/utils.js b/packages/node_modules/pouchdb-selector-core/src/utils.js index adfa56d..cdfcbc9 100644 --- a/packages/node_modules/pouchdb-selector-core/src/utils.js +++ b/packages/node_modules/pouchdb-selector-core/src/utils.js
@@ -69,6 +69,7 @@ // $and: [{$gt: 'a'}, {$gt: 'b'}], then it's collapsed into // just {$gt: 'b'} var res = {}; + var first = {$or: true, $nor: true}; selectors.forEach(function (selector) { Object.keys(selector).forEach(function (field) { @@ -78,11 +79,32 @@ } if (isCombinationalField(field)) { + // or, nor if (matcher instanceof Array) { - res[field] = matcher.map(function (m) { - return mergeAndedSelectors([m]); + if (first[field]) { + first[field] = false; + res[field] = matcher; + return; + } + + var entries = []; + res[field].forEach(function (existing) { + Object.keys(matcher).forEach(function (key) { + var m = matcher[key]; + var longest = Math.max(Object.keys(existing).length, Object.keys(m).length); + var merged = mergeAndedSelectors([existing, m]); + if (Object.keys(merged).length <= longest) { + // we have a situation like: (a :{$eq :1} || ...) && (a {$eq: 2} || ...) + // merging would produce a $eq 2 when actually we shouldn't ever match against these merged conditions + // merged should always contain more values to be valid + return; + } + entries.push(merged); + }); }); + res[field] = entries; } else { + // not res[field] = mergeAndedSelectors([matcher]); } } else {
diff --git a/tests/find/test-suite-1/test.or.js b/tests/find/test-suite-1/test.or.js index 735360f..c23cacb 100644 --- a/tests/find/test-suite-1/test.or.js +++ b/tests/find/test-suite-1/test.or.js
@@ -121,6 +121,282 @@ ]); }); }); + describe("nested $or inside $and", function () { + it('equal length $ors', function () { + var db = context.db; + var index = { + "index": { + "fields": ["field.a"] + } + }; + var selector = { + $and: [ + { + $or: [ + {a: 1}, + {b: 2} + ] + }, + { + $or: [ + {a: 3}, + {b: 4} + ] + } + ] + }; + return db.createIndex(index).then(function () { + return db.bulkDocs([ + {_id: '1', a: 1, b: 2}, + {_id: '2', a: 1, b: 4}, + {_id: '3', a: 3, b: 2}, + {_id: '4', a: 3, b: 4}, + ]); + }).then(function () { + return db.find({ + selector, + fields: ["_id"] + }).then(function (resp) { + resp.docs.should.deep.equal([{_id: '2'}, {_id: '3'}]); + }); + }).then(function () { + if (db.adapter === "local") { + return db.explain({ + selector, + fields: ["_id"] + }).then(function (resp) { + resp.selector.should.deep.equal({ + "$or": [ + {"a": {"$eq": 1}, "b": {"$eq": 4}}, + {"a": {"$eq": 3}, "b": {"$eq": 2}} + ] + }); + }); + } + }); + }); + it('first $or length less than second', function () { + var db = context.db; + var index = { + "index": { + "fields": ["field.a"] + } + }; + + var selector = { + $and: [ + { + $or: [ + {c: 2}, + ] + }, + { + $or: [ + {a: 1}, + {b: 1}, + ] + }, + ] + }; + return db.createIndex(index).then(function () { + return db.bulkDocs([ + {_id: '1', a: 1, b: 2, c: 2}, + {_id: '2', a: 2, b: 2, c: 2}, + {_id: '3', a: 2, b: 1, c: 2}, + {_id: '4', a: 2, b: 2, c: 2}, + ]); + }).then(function () { + return db.find({ + selector, + fields: ["_id"] + }).then(function (resp) { + resp.docs.should.deep.equal([{_id: '1'}, {_id: '3'}]); + }); + }).then(function () { + if (db.adapter === "local") { + return db.explain({ + selector, + fields: ["_id"] + }).then(function (resp) { + console.log(resp.selector); + resp.selector.should.deep.equal({ + "$or": [ + {"a": {"$eq": 1}, "c": {"$eq": 2}}, + {"b": {"$eq": 1}, "c": {"$eq": 2}} + ] + }); + }); + } + }); + }); + it('second $or length less than first', function () { + var db = context.db; + var index = { + "index": { + "fields": ["field.a"] + } + }; + + var selector = { + $and: [ + { + $or: [ + {a: 1}, + {b: 1}, + ] + }, + { + $or: [ + {c: 2}, + ] + }, + ] + }; + return db.createIndex(index).then(function () { + return db.bulkDocs([ + {_id: '1', a: 1, b: 2, c: 2}, + {_id: '2', a: 2, b: 2, c: 2}, + {_id: '3', a: 2, b: 1, c: 2}, + {_id: '4', a: 2, b: 2, c: 2}, + ]); + }).then(function () { + return db.find({ + selector, + fields: ["_id"] + }).then(function (resp) { + resp.docs.should.deep.equal([{_id: '1'}, {_id: '3'}]); + }); + }).then(function () { + if (db.adapter === "local") { + return db.explain({ + selector, + fields: ["_id"] + }).then(function (resp) { + resp.selector.should.deep.equal({ + "$or": [ + {"a": {"$eq": 1}, "c": {"$eq": 2}}, + {"b": {"$eq": 1}, "c": {"$eq": 2}} + ] + }); + }); + } + }); + }); + it('should do complex queries', function () { + var db = context.db; + var index = { + "index": { + "fields": ["field.a"] + } + }; + + var selector = { + $or: [ + { + $and: [ + { + $or: [ + { + $or: [ + { + $and: [ + {due: {important: true}}, + {tags: {$all: ["home"]}}, + ] + }, + // implicit and + {due: {date: "soon", important:false}}, + ] + }, + {due: {date: "tomorrow"}}, + ] + }, + { + $or: [ + {assigned: "me"}, + {assigned: "other1"}, + ] + }, + {include: true} + ] + } + ] + }; + return db.createIndex(index).then(function () { + return db.bulkDocs([ + {_id: '1', include:true, due: {repeating: true, date: "friday"}, tags: ["home"], assigned: "other2"}, + {_id: '2', include:true, due: {repeating: true, date: "friday", important: true}, tags: ["home"], assigned: "other1"}, + {_id: '3', include:true, due: {repeating: false, date: "soon", important: false}, tags: ["home"], assigned: "me"}, + {_id: '4', include:true, due: {repeating: false, date: "tuesday"}, tags: ["home"], assigned: "me"}, + {_id: '5', include:true, due: {repeating: true, date: "friday", important: true}, tags: ["work"], assigned: "me"}, + {_id: '6', include:true, due: {repeating: false, date: "tomorrow"}, tags: ["health"], assigned: "me"}, + {_id: '7', include:false, due: {repeating: false, date: "tomorrow"}, tags: ["health"], assigned: "me"}, + ]); + }).then(function () { + return db.find({ + selector, + fields: ["_id"] + }).then(function (resp) { + resp.docs.should.deep.equal([{_id: '2'}, {_id: '3'}, {_id: '6'}]); + }); + }).then(function () { + if (db.adapter === "local") { + return db.explain({ + selector, + fields: ["_id"] + }).then(function (resp) { + console.log(JSON.stringify(resp.selector, null, "\t")); + + resp.selector.should.deep.equal({ + $or: [ + { + $or: [ + { + due: {important: true}, + tags: {$all: ["home"]}, + }, + { + due: { + date: "soon", + important: false + } + }, + ], + assigned: {$eq: "me"}, + }, + { + $or: [ + { + due: {important: true}, + tags: {$all: ["home"]}, + }, + { + due: { + date: "soon", + important: false + } + }, + ], + assigned: {$eq: "other1"}, + }, + { + due: {date: "tomorrow"}, + assigned: {$eq: "me"}, + }, + { + due: {date: "tomorrow"}, + assigned: {$eq: "other1"}, + }, + ], + include: { + $eq: true, + }, + }); + }); + } + }); + }); + }); }); });