Permalink
Browse files

Refactored the logic that parses queries to enable compound queries t…

…o be performed on `$elemMatch` queries. Queries are now parsed in there entirety before any query is actually run.
  • Loading branch information...
davidgtonge committed Sep 22, 2012
1 parent d13e2b7 commit c0ef2d1e035b0601b8d609bd19fcc4400e46ba40
Showing with 80 additions and 47 deletions.
  1. +16 −0 README.md
  2. +42 −47 src/backbone-query.coffee
  3. +22 −0 test/bq-test.coffee
View
@@ -279,6 +279,22 @@ Posts.query({
```
All of the operators above can be performed on `$elemMatch` queries, e.g. `$all`, `$size` or `$lt`.
+`$elemMatch` queries also accept compound operators, for example this query searches for all posts that
+have at least one comment without the word "really" and with the word "totally".
+```js
+Posts.query({
+ comments: {
+ $elemMatch: {
+ $not: {
+ text: /really/i
+ },
+ $and: {
+ text: /totally/i
+ }
+ }
+ }
+});
+```
### $computed
View
@@ -55,7 +55,7 @@ May be freely distributed according to MIT license.
{id:$gte:10}
]
###
- parseQuery = (rawQuery) ->
+ parseSubQuery = (rawQuery) ->
if _.isArray(rawQuery)
queryArray = rawQuery
@@ -75,7 +75,7 @@ May be freely distributed according to MIT license.
# If the query paramater is an object then extract the key and value
when "object"
if key in ["$and", "$or", "$nor", "$not"]
- o.value = queryParam
+ o.value = parseSubQuery queryParam
o.type = key
o.key = null
else
@@ -88,7 +88,7 @@ May be freely distributed according to MIT license.
o.value = parseQuery value
when "$computed"
q = makeObj(key,value)
- o.value = parseQuery q
+ o.value = parseSubQuery q
else
o.value = value
@@ -129,7 +129,7 @@ May be freely distributed according to MIT license.
performQuery = (type, value, attr, model, key) ->
switch type
when "$equal"
- # If the attrubute is an array then search for the query value in the array the same as Mongo
+ # If the attribute is an array then search for the query value in the array the same as Mongo
if _(attr).isArray() then value in attr else attr is value
when "$oEqual" then _(attr).isEqual value
when "$contains" then value in attr
@@ -149,23 +149,22 @@ May be freely distributed according to MIT license.
when "$likeI" then attr.toLowerCase().indexOf(value.toLowerCase()) isnt -1
when "$regex" then value.test attr
when "$cb" then value.call model, attr
- when "$elemMatch" then iterator attr, value, false, detect, "elemMatch"
- when "$relationMatch" then iterator attr.models, value, false, detect, "relationMatch"
+ when "$elemMatch" then (runQuery(attr,value,"elemMatch")).length > 0
+ when "$relationMatch" then (runQuery(attr.models,value,"relationMatch")).length > 0
when "$computed" then iterator [model], value, false, detect, "computed"
when "$and", "$or", "$nor", "$not"
(processQuery[type]([model], value)).length is 1
else false
# The main iterator that actually applies the query
- iterator = (models, query, andOr, filterFunction, subQuery = false) ->
- parsedQuery = if subQuery then query else parseQuery query
+ iterator = (models, query, andOr, filterFunction, itemType = false) ->
# The collections filter or reject method is used to iterate through each model in the collection
filterFunction models, (model) ->
# For each model in the collection, iterate through the supplied queries
- for q in parsedQuery
+ for q in query
# Retrieve the attribute value from the model
- attr = switch subQuery
+ attr = switch itemType
when "elemMatch" then model[q.key]
when "computed" then model[q.key]()
else model.get(q.key)
@@ -185,42 +184,19 @@ May be freely distributed according to MIT license.
# An object with or, and, nor and not methods
processQuery =
- $and: (models, query) -> iterator models, query, false, filter
- $or: (models, query) -> iterator models, query, true, filter
- $nor: (models, query) -> iterator models, query, true, reject
- $not: (models, query) -> iterator models, query, false, reject
+ $and: (models, query, itemType) -> iterator models, query, false, filter, itemType
+ $or: (models, query, itemType) -> iterator models, query, true, filter, itemType
+ $nor: (models, query, itemType) -> iterator models, query, true, reject, itemType
+ $not: (models, query, itemType) -> iterator models, query, false, reject, itemType
-
- # This method attempts to retrieve the result from the cache.
- # If no match is found in the cache, then the query is run and
- # the results are saved in the cache
- getCache = (collection, query, options) ->
- # Convert the query to a string to use as a key in the cache
- queryString = JSON.stringify query
- # Create cache if doesn't exist
- cache = collection._queryCache ?= {}
- # Retrieve cached results
- models = cache[queryString]
- # If no results are retrieved then use the get_models method and cache the result
- unless models
- models = getSortedModels collection, query, options
- cache[queryString] = models
- # Return the results
- models
-
- # This method get the unsorted results
- getModels = (collection, query) ->
-
- # Iterate through the query keys to check for any of the compound methods
- # The resulting array will have "$and" and "$not" first as it is better to use these
- # operators first when performing a compound query as they are likely to return less results
+ parseQuery = (query) ->
queryKeys = _(query).keys()
compoundKeys = ["$and", "$not", "$or", "$nor"]
compoundQuery = _.intersection compoundKeys, queryKeys
+ # If no compound methods are found then use the "and" iterator
if compoundQuery.length is 0
- # If no compound methods are found then use the "and" iterator
- processQuery.$and collection.models, query
+ return [{type:"$and", parsedQuery:parseSubQuery(query)}]
else
# Detect if there is an implicit $and compundQuery operator
if compoundQuery.length isnt queryKeys.length
@@ -231,19 +207,38 @@ May be freely distributed according to MIT license.
for own key, val of query when key not in compoundKeys
query.$and[key] = val
delete query[key]
+ return (for type in compoundQuery
+ {type, parsedQuery:parseSubQuery(query[type])})
- # Iterate through the compound methods using underscore reduce
- # The reduce iterator takes an array of models, performs the query and returns
- # the matched models for the next query
- reduceIterator = (memo, queryType) ->
- processQuery[queryType] memo, query[queryType]
+ runQuery = (items, query, itemType) ->
+ query = parseQuery(query) unless itemType
+ reduceIterator = (memo, queryItem) ->
+ processQuery[queryItem.type] memo, queryItem.parsedQuery, itemType
+ _.reduce query, reduceIterator, items
+
+
+ # This method attempts to retrieve the result from the cache.
+ # If no match is found in the cache, then the query is run and
+ # the results are saved in the cache
+ getCache = (collection, query, options) ->
+ # Convert the query to a string to use as a key in the cache
+ queryString = JSON.stringify query
+ # Create cache if doesn't exist
+ cache = collection._queryCache ?= {}
+ # Retrieve cached results
+ models = cache[queryString]
+ # If no results are retrieved then use the get_models method and cache the result
+ unless models
+ models = getSortedModels collection, query, options
+ cache[queryString] = models
+ # Return the results
+ models
- _.reduce compoundQuery, reduceIterator, collection.models
# Gets the results and optionally sorts them
getSortedModels = (collection, query, options) ->
- models = getModels collection, query
+ models = runQuery(collection.models, query)
if options.sortBy then models = sortModels models, options
models
View
@@ -447,6 +447,28 @@ describe "Backbone Query Tests", ->
assert.equal result.length, 1
assert.equal result[0].get("name"), "test"
+ it "$elemMatch - compound queries", ->
+ a = new QueryCollection [
+ {title: "Home", comments:[
+ {text:"I like this post"}
+ {text:"I love this post"}
+ {text:"I hate this post"}
+ ]}
+ {title: "About", comments:[
+ {text:"I like this page"}
+ {text:"I love this page"}
+ {text:"I really like this page"}
+ ]}
+ ]
+
+ result = a.query
+ comments:
+ $elemMatch:
+ $not:
+ text:/page/
+
+ assert.equal result.length, 1
+
# Test from RobW - https://github.com/Rob--W
it "Explicit $and combined with matching $or must return the correct number of items", ->

0 comments on commit c0ef2d1

Please sign in to comment.