From f7d991cc87fa3ed32123a7ca910a13a3ebd5c786 Mon Sep 17 00:00:00 2001 From: Avital Oliver Date: Mon, 29 Jul 2013 11:33:19 -0700 Subject: [PATCH] Fix Mongo queries with {$regex: /foo/} The bug manifested as incorrect cursor de-duping, making queries on different regular expressions "de-dup" into one, which, combined with hot code reload caused crazy seeming things such as needing to save twice to see a code change executed. --- packages/mongo-livedata/collection.js | 36 +++++++++++++------ .../mongo-livedata/mongo_livedata_tests.js | 28 ++++++++++----- 2 files changed, 45 insertions(+), 19 deletions(-) diff --git a/packages/mongo-livedata/collection.js b/packages/mongo-livedata/collection.js index 9288a3d710..0984811555 100644 --- a/packages/mongo-livedata/collection.js +++ b/packages/mongo-livedata/collection.js @@ -261,17 +261,11 @@ Meteor.Collection._rewriteSelector = function (selector) { var ret = {}; _.each(selector, function (value, key) { + // Mongo supports both {field: /foo/} and {field: {$regex: /foo/}} if (value instanceof RegExp) { - ret[key] = {$regex: value.source}; - var regexOptions = ''; - // JS RegExp objects support 'i', 'm', and 'g'. Mongo regex $options - // support 'i', 'm', 'x', and 's'. So we support 'i' and 'm' here. - if (value.ignoreCase) - regexOptions += 'i'; - if (value.multiline) - regexOptions += 'm'; - if (regexOptions) - ret[key].$options = regexOptions; + ret[key] = convertRegexpToMongoSelector(value); + } else if (value && value.$regex instanceof RegExp) { + ret[key] = convertRegexpToMongoSelector(value.$regex); } else if (_.contains(['$or','$and','$nor'], key)) { // Translate lower levels of $and/$or/$nor @@ -279,12 +273,32 @@ Meteor.Collection._rewriteSelector = function (selector) { return Meteor.Collection._rewriteSelector(v); }); } - else + else { ret[key] = value; + } }); return ret; }; +// convert a JS RegExp object to a Mongo {$regex: ..., $options: ...} +// selector +var convertRegexpToMongoSelector = function (regexp) { + check(regexp, RegExp); // safety belt + + var selector = {$regex: regexp.source}; + var regexOptions = ''; + // JS RegExp objects support 'i', 'm', and 'g'. Mongo regex $options + // support 'i', 'm', 'x', and 's'. So we support 'i' and 'm' here. + if (regexp.ignoreCase) + regexOptions += 'i'; + if (regexp.multiline) + regexOptions += 'm'; + if (regexOptions) + selector.$options = regexOptions; + + return selector; +}; + var throwIfSelectorIsNotId = function (selector, methodName) { if (!LocalCollection._selectorIsIdPerhapsAsObject(selector)) { throw new Meteor.Error( diff --git a/packages/mongo-livedata/mongo_livedata_tests.js b/packages/mongo-livedata/mongo_livedata_tests.js index d00547dc09..62178eccc0 100644 --- a/packages/mongo-livedata/mongo_livedata_tests.js +++ b/packages/mongo-livedata/mongo_livedata_tests.js @@ -876,8 +876,12 @@ if (Meteor.isServer) { Tinytest.add('mongo-livedata - rewrite selector', function (test) { test.equal(Meteor.Collection._rewriteSelector({x: /^o+B/im}), {x: {$regex: '^o+B', $options: 'im'}}); + test.equal(Meteor.Collection._rewriteSelector({x: {$regex: /^o+B/im}}), + {x: {$regex: '^o+B', $options: 'im'}}); test.equal(Meteor.Collection._rewriteSelector({x: /^o+B/}), {x: {$regex: '^o+B'}}); + test.equal(Meteor.Collection._rewriteSelector({x: {$regex: /^o+B/}}), + {x: {$regex: '^o+B'}}); test.equal(Meteor.Collection._rewriteSelector('foo'), {_id: 'foo'}); @@ -886,13 +890,15 @@ Tinytest.add('mongo-livedata - rewrite selector', function (test) { {'$or': [ {x: /^o/}, {y: /^p/}, - {z: 'q'} + {z: 'q'}, + {w: {$regex: /^r/}} ]} ), {'$or': [ {x: {$regex: '^o'}}, {y: {$regex: '^p'}}, - {z: 'q'} + {z: 'q'}, + {w: {$regex: '^r'}} ]} ); @@ -901,22 +907,28 @@ Tinytest.add('mongo-livedata - rewrite selector', function (test) { {'$or': [ {'$and': [ {x: /^a/i}, - {y: /^b/} + {y: /^b/}, + {z: {$regex: /^c/i}}, + {w: {$regex: '^[abc]', $options: 'i'}} // make sure we don't break vanilla selectors ]}, {'$nor': [ - {s: /^c/}, - {t: /^d/i} + {s: /^d/}, + {t: /^e/i}, + {u: {$regex: /^f/i}} ]} ]} ), {'$or': [ {'$and': [ {x: {$regex: '^a', $options: 'i'}}, - {y: {$regex: '^b'}} + {y: {$regex: '^b'}}, + {z: {$regex: '^c', $options: 'i'}}, + {w: {$regex: '^[abc]', $options: 'i'}} ]}, {'$nor': [ - {s: {$regex: '^c'}}, - {t: {$regex: '^d', $options: 'i'}} + {s: {$regex: '^d'}}, + {t: {$regex: '^e', $options: 'i'}}, + {u: {$regex: '^f', $options: 'i'}} ]} ]} );