Merge: Functional api
[nit.git] / lib / mongodb / queries.nit
1 # This file is part of NIT ( http://www.nitlanguage.org ).
2 #
3 # Copyright 2016 Alexandre Terrasa <alexandre@moz-code.org>.
4 #
5 # Licensed under the Apache License, Version 2.0 (the "License");
6 # you may not use this file except in compliance with the License.
7 # You may obtain a copy of the License at
8 #
9 # http://www.apache.org/licenses/LICENSE-2.0
10 #
11 # Unless required by applicable law or agreed to in writing, software
12 # distributed under the License is distributed on an "AS IS" BASIS,
13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 # See the License for the specific language governing permissions and
15 # limitations under the License.
16
17 # Mongo queries framework
18 #
19 # The `queries` framework is used to build Mongo queries as JsonObject with
20 # a fluent interface.
21 #
22 # Using the `queries` framework we can get from this:
23 #
24 # ~~~nitish
25 # var exists = new JsonObject
26 # exists["$exists"] = true
27 #
28 # var query = new JsonObject
29 # query["login"] = "Morriar"
30 # query["email"] = exists
31 #
32 # collection.find(query)
33 # ~~~
34 #
35 # To this:
36 #
37 # ~~~nitish
38 # collection.find((new MongoMatch).eq("login", "Morriar").exists("email", true))
39 # ~~~
40 #
41 # The framework provides three classes used to map the MongoDB query API:
42 # * `MongoMatch` the base query that can be used with most Mongo services
43 # * `MongoPipeline` the array of queries that is expected by `MongoCollection::aggregate`
44 # * `MongoGroup` the group query for a `MongoPipeline`
45 #
46 # More on this features can be found in the official MongoDB documentation:
47 # https://docs.mongodb.com/manual/reference/operator/
48 module queries
49
50 import mongodb
51
52 # A basic match query
53 #
54 # `MongoMatch` is used with most of the Mongo services like `find`, `find_all`,
55 # `remove` etc.
56 #
57 # Building a query can be done with the fluent interface:
58 #
59 # ~~~
60 # var query = (new MongoMatch).
61 # eq("login", "Morriar").
62 # gt("age", 18).
63 # exists("email", true).
64 # is_in("status", [1, 2, 3, 4])
65 # ~~~
66 #
67 # Fore more help on how to use the query operators of MongoDB please
68 # refer to the official MongoDB documentation:
69 # https://docs.mongodb.com/manual/reference/operator/query/
70 class MongoMatch
71 super JsonObject
72
73 # Define a custom operaton for `field`
74 #
75 # If no `field` is specified, append the operator the the root object:
76 # ~~~json
77 # {$<name>: <value>}
78 # ~~~
79 #
80 # Else, append the operator to the field:
81 # ~~~json
82 # {field: {$<name>: <value>} }
83 # ~~~
84 fun op(name: String, field: nullable String, value: nullable Serializable): MongoMatch do
85 if field != null then
86 var q = new JsonObject
87 q["${name}"] = value
88 self[field] = q
89 else
90 self["${name}"] = value
91 end
92 return self
93 end
94
95 # Match documents where `field` equals `value`
96 #
97 # https://docs.mongodb.com/manual/reference/operator/query/eq/#op._S_eq
98 #
99 # ~~~json
100 # {field: {$eq: value} }
101 # ~~~
102 fun eq(field: String, value: nullable Serializable): MongoMatch do
103 self[field] = value
104 return self
105 end
106
107 # Match documents where `field` not equals `value`
108 #
109 # https://docs.mongodb.com/manual/reference/operator/query/ne/#op._S_ne
110 #
111 # ~~~json
112 # {field: {$ne: value} }
113 # ~~~
114 fun ne(field: String, value: nullable Serializable): MongoMatch do
115 op("ne", field, value)
116 return self
117 end
118
119 # Match documents where `field` is greater than `value`
120 #
121 # https://docs.mongodb.com/manual/reference/operator/query/gt/#op._S_gt
122 #
123 # ~~~json
124 # {field: {$gt: value} }
125 # ~~~
126 fun gt(field: String, value: nullable Serializable): MongoMatch do
127 op("gt", field, value)
128 return self
129 end
130
131 # Match documents where `field` is greater or equal to `value`
132 #
133 # https://docs.mongodb.com/manual/reference/operator/query/gte/#op._S_gte
134 #
135 # ~~~json
136 # {field: {$gte: value} }
137 # ~~~
138 fun gte(field: String, value: nullable Serializable): MongoMatch do
139 op("gte", field, value)
140 return self
141 end
142
143 # Match documents where `field` is less than `value`
144 #
145 # https://docs.mongodb.com/manual/reference/operator/query/lt/#op._S_lt
146 #
147 # ~~~json
148 # {field: {$lt: value} }
149 # ~~~
150 fun lt(field: String, value: nullable Serializable): MongoMatch do
151 op("lt", field, value)
152 return self
153 end
154
155 # Match documents where `field` is less or equal to `value`
156 #
157 # https://docs.mongodb.com/manual/reference/operator/query/lte/
158 #
159 # ~~~json
160 # {field: {$lte: value} }
161 # ~~~
162 fun lte(field: String, value: nullable Serializable): MongoMatch do
163 op("lte", field, value)
164 return self
165 end
166
167 # Match documents where `field` exists or not
168 #
169 # https://docs.mongodb.com/manual/reference/operator/query/exists/#op._S_exists
170 #
171 # ~~~json
172 # {field: {$exists: boolean} }
173 # ~~~
174 #
175 # When `exists` is true, `$exists` matches the documents that contain the
176 # field, including documents where the field value is null.
177 # If <boolean> is false, the query returns only the documents that do not
178 # contain the field.
179 fun exists(field: String, exists: Bool): MongoMatch do
180 op("exists", field, exists)
181 return self
182 end
183
184 # Match documents where `field` matches `pattern`
185 #
186 # To read more about the available options, see:
187 # https://docs.mongodb.com/manual/reference/operator/query/regex/#op._S_regex
188 #
189 # ~~~json
190 # {field: {$regex: 'pattern', $options: '<options>'} }
191 # ~~~
192 #
193 # Provides regular expression capabilities for pattern matching strings in queries.
194 # MongoDB uses Perl compatible regular expressions (i.e. "PCRE" ).
195 fun regex(field: String, pattern: String, options: nullable String): MongoMatch do
196 var q = new JsonObject
197 q["$regex"] = pattern
198 if options != null then q["$options"] = options
199 self[field] = q
200 return self
201 end
202
203 # Match documents where `field` is in `values`
204 #
205 # https://docs.mongodb.com/manual/reference/operator/query/in/
206 #
207 # ~~~json
208 # { field: { $in: [<value1>, <value2>, ... <valueN> ] } }
209 # ~~~
210 #
211 # `$in` selects the documents where the value of a field equals any value
212 # in the specified array.
213 fun is_in(field: String, values: Array[nullable Serializable]): MongoMatch do
214 op("in", field, new JsonArray.from(values))
215 return self
216 end
217
218 # Match documents where `field` is not in `values`
219 #
220 # https://docs.mongodb.com/manual/reference/operator/query/nin/
221 #
222 # ~~~json
223 # { field: { $nin: [<value1>, <value2>, ... <valueN> ] } }
224 # ~~~
225 #
226 # `$nin` selects the documents where:
227 # * the field value is not in the specified array or
228 # * the field does not exist.
229 fun is_nin(field: String, values: Array[nullable Serializable]): MongoMatch do
230 op("nin", field, new JsonArray.from(values))
231 return self
232 end
233
234 # Logical `or`
235 #
236 # https://docs.mongodb.com/manual/reference/operator/query/or/#op._S_or
237 #
238 # The `$or` operator performs a logical OR operation on an array of two or
239 # more `expressions` and selects the documents that satisfy at least one of
240 # the `expressions`.
241 #
242 # The `$or` has the following syntax:
243 #
244 # ~~~json
245 # { field: { $or: [ { <expression1> }, { <expression2> }, ... , { <expressionN> } ] } }
246 # ~~~
247 fun lor(field: nullable String, expressions: Array[Serializable]): MongoMatch do
248 op("or", field, new JsonArray.from(expressions))
249 return self
250 end
251
252 # Logical `and`
253 #
254 # https://docs.mongodb.com/manual/reference/operator/query/and/#op._S_and
255 #
256 # The `$and` operator performs a logical AND operation on an array of two or
257 # more `expressions` and selects the documents that satisfy all of the `expressions`.
258 #
259 # The `$and` has the following syntax:
260 #
261 # ~~~json
262 # { field: { $and: [ { <expression1> }, { <expression2> }, ... , { <expressionN> } ] } }
263 # ~~~
264 fun land(field: nullable String, expressions: Array[Serializable]): MongoMatch do
265 op("and", field, new JsonArray.from(expressions))
266 return self
267 end
268
269 # Logical `not`
270 #
271 # https://docs.mongodb.com/manual/reference/operator/query/not/#op._S_not
272 #
273 # `$not` performs a logical NOT operation on the specified `expression` and
274 # selects the documents that do not match the `expression`.
275 # This includes documents that do not contain the field.
276 #
277 # The $not has the following syntax:
278 #
279 # ~~~json
280 # { field: { $not: { <expression> } } }
281 # ~~~
282 fun lnot(field: nullable String, expression: Serializable): MongoMatch do
283 op("not", field, expression)
284 return self
285 end
286
287 # Logical `nor`
288 #
289 # https://docs.mongodb.com/manual/reference/operator/query/nor/#op._S_nor
290 #
291 # `$nor` performs a logical NOR operation on an array of one or more query
292 # expression and selects the documents that fail all the query expressions
293 # in the array.
294 #
295 # The $nor has the following syntax:
296 #
297 # ~~~json
298 # { field: { $nor: [ { <expression1> }, { <expression2> }, ... , { <expressionN> } ] } }
299 # ~~~
300 fun lnor(field: nullable String, expressions: Array[Serializable]): MongoMatch do
301 op("nor", field, new JsonArray.from(expressions))
302 return self
303 end
304
305 # Array contains all
306 #
307 # https://docs.mongodb.com/manual/reference/operator/query/all/#op._S_all
308 #
309 # `$all` selects the documents where the value of a field is an array that
310 # contains all the specified elements.
311 #
312 # ~~~json
313 # { field: { $all: [ <value1>, <value2>, ... ] } }
314 # ~~~
315 fun all(field: nullable String, values: Array[Serializable]): MongoMatch do
316 op("all", field, new JsonArray.from(values))
317 return self
318 end
319
320 # Array element match
321 #
322 # https://docs.mongodb.com/manual/reference/operator/query/elemMatch/#op._S_elemMatch
323 #
324 # `$elemMatch` matches documents that contain an array field with at least
325 # one element that matches all the specified query criteria.
326 #
327 # ~~~json
328 # { field: { $elemMatch: <query> } }
329 # ~~~
330 fun elem_match(field: nullable String, query: Serializable): MongoMatch do
331 op("elemMatch", field, query)
332 return self
333 end
334
335 # Array size match
336 #
337 # https://docs.mongodb.com/manual/reference/operator/query/size/#op._S_size
338 #
339 # `$size` matches any array with the number of elements specified by the argument
340 #
341 # ~~~json
342 # { field: { $size: <size> } }
343 # ~~~
344 fun size(field: nullable String, size: Int): MongoMatch do
345 op("size", field, size)
346 return self
347 end
348 end
349
350 # Mongo pipelines are arrays of aggregation stages
351 #
352 # With the `MongoCollection::aggregate` method, pipeline stages appear in a array.
353 # Documents pass through the stages in sequence.
354 #
355 # ~~~json
356 # db.collection.aggregate( [ { <stage> }, ... ] )
357 # ~~~
358 #
359 # The MongoPipeline fluent interface can be used to bluid a pipeline:
360 # ~~~
361 # var pipeline = (new MongoPipeline).
362 # match((new MongoMatch).eq("game", "nit")).
363 # group((new MongoGroup("$game._id")).sum("nitcoins", "$game.nitcoins")).
364 # sort((new MongoMatch).eq("nitcoins", -1)).
365 # limit(10)
366 # ~~~
367 #
368 # The pipeline can then be used in an aggregation query:
369 # ~~~nitish
370 # collection.aggregate(pipeline)
371 # ~~~
372 #
373 # For more information read about MongoDB pipeline operators from the MongoDB
374 # official documentation: https://docs.mongodb.com/manual/reference/operator/aggregation/
375 class MongoPipeline
376 super JsonArray
377
378 # Add a stage to the pipeline
379 #
380 # https://docs.mongodb.com/manual/reference/operator/aggregation/#stage-operators
381 #
382 # Each stage is registered as:
383 # ~~~json
384 # { $<stage>: <json> }
385 # ~~~
386 fun add_stage(stage: String, json: Serializable): MongoPipeline do
387 var obj = new JsonObject
388 obj["${stage}"] = json
389 add obj
390 return self
391 end
392
393 # Apply projection
394 #
395 # https://docs.mongodb.com/manual/reference/operator/aggregation/project/#pipe._S_project
396 #
397 # Passes along the documents with only the specified fields to the next stage
398 # in the pipeline.
399 #
400 # ~~~json
401 # { $project: { <specifications> } }
402 # ~~~
403 #
404 # The specified fields can be existing fields from the input documents or
405 # newly computed fields.
406 fun project(projection: JsonObject): MongoPipeline do return add_stage("project", projection)
407
408 # Apply match
409 #
410 # https://docs.mongodb.com/manual/reference/operator/aggregation/match/
411 #
412 # Filters the documents to pass only the documents that match the specified
413 # condition(s) to the next pipeline stage.
414 #
415 # ~~~json
416 # { $match: { <query> } }
417 # ~~~
418 fun match(query: MongoMatch): MongoPipeline do return add_stage("match", query)
419
420 # Apply sort
421 #
422 # https://docs.mongodb.com/manual/reference/operator/aggregation/sort/
423 #
424 # Sorts all input documents and returns them to the pipeline in sorted order.
425 #
426 # ~~~json
427 # { $sort: { <projection> } }
428 # ~~~
429 fun sort(projection: JsonObject): MongoPipeline do return add_stage("sort", projection)
430
431 # Apply skip
432 #
433 # https://docs.mongodb.com/manual/reference/operator/aggregation/skip/
434 #
435 # Skips over the specified number of documents that pass into the stage and
436 # passes the remaining documents to the next stage in the pipeline.
437 #
438 # ~~~json
439 # { $skip: { <number> } }
440 # ~~~
441 #
442 # If `number == null` then no skip stage is generated
443 fun skip(number: nullable Int): MongoPipeline do
444 if number == null then return self
445 return add_stage("skip", number)
446 end
447
448 # Apply limit
449 #
450 # https://docs.mongodb.com/manual/reference/operator/aggregation/limit/
451 #
452 # Limits the number of documents passed to the next stage in the pipeline.
453 #
454 # ~~~json
455 # { $limit: { <number> } }
456 # ~~~
457 #
458 # If `number == null` then no limit stage is generated
459 fun limit(number: nullable Int): MongoPipeline do
460 if number == null then return self
461 return add_stage("limit", number)
462 end
463
464 # Apply group
465 #
466 # https://docs.mongodb.com/manual/reference/operator/aggregation/group/
467 #
468 # Groups documents by some specified expression and outputs to the next stage
469 # a document for each distinct grouping.
470 #
471 # The output documents contain an `_id` field which contains the distinct
472 # group by key.
473 #
474 # The output documents can also contain computed fields that hold the values
475 # of some accumulator expression grouped by the `$group`'s `_id` field.
476 # `$group` does not order its output documents.
477 #
478 # ~~~json
479 # { $group: { <group> } }
480 # ~~~
481 fun group(group: MongoGroup): MongoPipeline do return add_stage("group", group)
482
483 # Apply unwind
484 #
485 # https://docs.mongodb.com/manual/reference/operator/aggregation/unwind/
486 #
487 # Deconstructs an array field from the input documents to output a document
488 # for each element.
489 # Each output document is the input document with the value of the array
490 # field replaced by the element.
491 #
492 # ~~~json
493 # { $unwind: <field path> }
494 # ~~~
495 fun unwind(path: String): MongoPipeline do return add_stage("unwind", path)
496 end
497
498 # Mongo pipeline group stage
499 #
500 # https://docs.mongodb.com/manual/reference/operator/aggregation/group/#pipe._S_group
501 #
502 # Groups documents by some specified expression and outputs to the next stage a
503 # document for each distinct grouping.
504 #
505 # ~~~
506 # var group = (new MongoGroup("$game._id")).sum("nitcoins", "$game.nitcoins")
507 #
508 # var pipeline = (new MongoPipeline).group(group)
509 # ~~~
510 #
511 # The output documents contain an `_id` field which contains the distinct group by key.
512 # The output documents can also contain computed fields that hold the values of
513 # some accumulator expression grouped by the `$group`‘s `_id` field.
514 # `$group` does not order its output documents.
515 #
516 # The `$group` stage has the following prototype form:
517 #
518 # ~~~json
519 # { $group: { _id: <expression>, <field1>: { <accumulator1> : <expression1> }, ... } }
520 # ~~~
521 #
522 # The `_id` field is mandatory; however, you can specify an `_id` value of null
523 # to calculate accumulated values for all the input documents as a whole.
524 #
525 # The remaining computed fields are optional and computed using the `<accumulator>`
526 # operators.
527 class MongoGroup
528 super JsonObject
529
530 # Group `_id`
531 #
532 # See `MongoGroup::group`.
533 var id: String
534
535 init do self["_id"] = id
536
537 # Add an accumulator
538 #
539 # Each accumulator is registered as:
540 # ~~~json
541 # <field>: { <accumulator> : <expression> }
542 # ~~~
543 private fun acc(name: String, field: String, expression: nullable Serializable): MongoGroup do
544 var q = new JsonObject
545 q["${name}"] = expression
546 self[field] = q
547 return self
548 end
549
550 # Calculates and returns the sum of numeric values
551 #
552 # https://docs.mongodb.com/manual/reference/operator/aggregation/sum/#grp._S_sum
553 #
554 # ~~~json
555 # { $sum: <expression> }
556 # ~~~
557 #
558 # `$sum` ignores non-numeric values.
559 fun sum(field: String, expression: Serializable): MongoGroup do
560 return acc("sum", field, expression)
561 end
562
563 # Returns the average value of the numeric values
564 #
565 # https://docs.mongodb.com/manual/reference/operator/aggregation/avg/
566 #
567 # ~~~json
568 # { $avg: <expression> }
569 # ~~~
570 #
571 # `$avg` ignores non-numeric values.
572 fun avg(field: String, expression: Serializable): MongoGroup do
573 return acc("avg", field, expression)
574 end
575
576 # Returns the maximum value
577 #
578 # https://docs.mongodb.com/manual/reference/operator/aggregation/max/
579 #
580 # ~~~json
581 # { $max: <expression> }
582 # ~~~
583 #
584 # `$max` compares both value and type, using the specified BSON comparison
585 # order for values of different types.
586 fun max(field: String, expression: Serializable): MongoGroup do
587 return acc("max", field, expression)
588 end
589
590 # Returns the minimum value
591 #
592 # https://docs.mongodb.com/manual/reference/operator/aggregation/min/
593 #
594 # ~~~json
595 # { $min: <expression> }
596 # ~~~
597 #
598 # `$min` compares both value and type, using the specified BSON comparison
599 # order for values of different types.
600 fun min(field: String, expression: Serializable): MongoGroup do
601 return acc("min", field, expression)
602 end
603
604 # Return the first value
605 #
606 # https://docs.mongodb.com/manual/reference/operator/aggregation/first/
607 #
608 # ~~~json
609 # { $first: <expression> }
610 # ~~~
611 #
612 # Returns the value that results from applying an expression to the first
613 # document in a group of documents that share the same group by key.
614 #
615 # Only meaningful when documents are in a defined order.
616 fun first(field: String, expression: Serializable): MongoGroup do
617 return acc("first", field, expression)
618 end
619
620 # Return the last value
621 #
622 # https://docs.mongodb.com/manual/reference/operator/aggregation/last/
623 #
624 # ~~~json
625 # { $last: <expression> }
626 # ~~~
627 #
628 # Returns the value that results from applying an expression to the last
629 # document in a group of documents that share the same group by key.
630 #
631 # Only meaningful when documents are in a defined order.
632 fun last(field: String, expression: Serializable): MongoGroup do
633 return acc("last", field, expression)
634 end
635
636 # Push to an array
637 #
638 # https://docs.mongodb.com/manual/reference/operator/aggregation/push/
639 #
640 # ~~~json
641 # { $push: <expression> }
642 # ~~~
643 #
644 # Returns an array of all values that result from applying an expression to
645 # each document in a group of documents that share the same group by key.
646 fun push(field: String, expr: Serializable): MongoGroup do
647 return acc("push", field, expr)
648 end
649
650 # Push to a unique array
651 #
652 # https://docs.mongodb.com/manual/reference/operator/aggregation/addToSet/
653 #
654 # ~~~json
655 # { $addToSet: <expression> }
656 # ~~~
657 #
658 # Returns an array of all unique values that results from applying an
659 # expression to each document in a group of documents that share the same
660 # group by key.
661 #
662 # Order of the elements in the output array is unspecified.
663 fun addToSet(field: String, expr: Serializable): MongoGroup do
664 return acc("addToSet", field, expr)
665 end
666 end