lib/mongo: introduce MongoMatch::regex
[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 Jsonable): 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 Jsonable): 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 Jsonable): 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 Jsonable): 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 Jsonable): 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 Jsonable): 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 Jsonable): 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 Jsonable]): 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 Jsonable]): MongoMatch do
230 op("$nin", field, new JsonArray.from(values))
231 return self
232 end
233 end
234
235 # Mongo pipelines are arrays of aggregation stages
236 #
237 # With the `MongoCollection::aggregate` method, pipeline stages appear in a array.
238 # Documents pass through the stages in sequence.
239 #
240 # ~~~json
241 # db.collection.aggregate( [ { <stage> }, ... ] )
242 # ~~~
243 #
244 # The MongoPipeline fluent interface can be used to bluid a pipeline:
245 # ~~~
246 # var pipeline = (new MongoPipeline).
247 # match((new MongoMatch).eq("game", "nit")).
248 # group((new MongoGroup("$game._id")).sum("nitcoins", "$game.nitcoins")).
249 # sort((new MongoMatch).eq("nitcoins", -1)).
250 # limit(10)
251 # ~~~
252 #
253 # The pipeline can then be used in an aggregation query:
254 # ~~~nitish
255 # collection.aggregate(pipeline)
256 # ~~~
257 #
258 # For more information read about MongoDB pipeline operators from the MongoDB
259 # official documentation: https://docs.mongodb.com/manual/reference/operator/aggregation/
260 class MongoPipeline
261 super JsonArray
262
263 # Add a stage to the pipeline
264 #
265 # https://docs.mongodb.com/manual/reference/operator/aggregation/#stage-operators
266 #
267 # Each stage is registered as:
268 # ~~~json
269 # { $<stage>: <json> }
270 # ~~~
271 fun add_stage(stage: String, json: Jsonable): MongoPipeline do
272 var obj = new JsonObject
273 obj["${stage}"] = json
274 add obj
275 return self
276 end
277
278 # Apply projection
279 #
280 # https://docs.mongodb.com/manual/reference/operator/aggregation/project/#pipe._S_project
281 #
282 # Passes along the documents with only the specified fields to the next stage
283 # in the pipeline.
284 #
285 # ~~~json
286 # { $project: { <specifications> } }
287 # ~~~
288 #
289 # The specified fields can be existing fields from the input documents or
290 # newly computed fields.
291 fun project(projection: JsonObject): MongoPipeline do return add_stage("project", projection)
292
293 # Apply match
294 #
295 # https://docs.mongodb.com/manual/reference/operator/aggregation/match/
296 #
297 # Filters the documents to pass only the documents that match the specified
298 # condition(s) to the next pipeline stage.
299 #
300 # ~~~json
301 # { $match: { <query> } }
302 # ~~~
303 fun match(query: MongoMatch): MongoPipeline do return add_stage("match", query)
304
305 # Apply sort
306 #
307 # https://docs.mongodb.com/manual/reference/operator/aggregation/sort/
308 #
309 # Sorts all input documents and returns them to the pipeline in sorted order.
310 #
311 # ~~~json
312 # { $sort: { <projection> } }
313 # ~~~
314 fun sort(projection: JsonObject): MongoPipeline do return add_stage("sort", projection)
315
316 # Apply skip
317 #
318 # https://docs.mongodb.com/manual/reference/operator/aggregation/skip/
319 #
320 # Skips over the specified number of documents that pass into the stage and
321 # passes the remaining documents to the next stage in the pipeline.
322 #
323 # ~~~json
324 # { $skip: { <number> } }
325 # ~~~
326 #
327 # If `number == null` then no skip stage is generated
328 fun skip(number: nullable Int): MongoPipeline do
329 if number == null then return self
330 return add_stage("skip", number)
331 end
332
333 # Apply limit
334 #
335 # https://docs.mongodb.com/manual/reference/operator/aggregation/limit/
336 #
337 # Limits the number of documents passed to the next stage in the pipeline.
338 #
339 # ~~~json
340 # { $limit: { <number> } }
341 # ~~~
342 #
343 # If `number == null` then no limit stage is generated
344 fun limit(number: nullable Int): MongoPipeline do
345 if number == null then return self
346 return add_stage("limit", number)
347 end
348
349 # Apply group
350 #
351 # https://docs.mongodb.com/manual/reference/operator/aggregation/group/
352 #
353 # Groups documents by some specified expression and outputs to the next stage
354 # a document for each distinct grouping.
355 #
356 # The output documents contain an `_id` field which contains the distinct
357 # group by key.
358 #
359 # The output documents can also contain computed fields that hold the values
360 # of some accumulator expression grouped by the `$group`'s `_id` field.
361 # `$group` does not order its output documents.
362 #
363 # ~~~json
364 # { $group: { <group> } }
365 # ~~~
366 fun group(group: MongoGroup): MongoPipeline do return add_stage("group", group)
367 end
368
369 # Mongo pipeline group stage
370 #
371 # https://docs.mongodb.com/manual/reference/operator/aggregation/group/#pipe._S_group
372 #
373 # Groups documents by some specified expression and outputs to the next stage a
374 # document for each distinct grouping.
375 #
376 # ~~~
377 # var group = (new MongoGroup("$game._id")).sum("nitcoins", "$game.nitcoins")
378 #
379 # var pipeline = (new MongoPipeline).group(group)
380 # ~~~
381 #
382 # The output documents contain an `_id` field which contains the distinct group by key.
383 # The output documents can also contain computed fields that hold the values of
384 # some accumulator expression grouped by the `$group`‘s `_id` field.
385 # `$group` does not order its output documents.
386 #
387 # The `$group` stage has the following prototype form:
388 #
389 # ~~~json
390 # { $group: { _id: <expression>, <field1>: { <accumulator1> : <expression1> }, ... } }
391 # ~~~
392 #
393 # The `_id` field is mandatory; however, you can specify an `_id` value of null
394 # to calculate accumulated values for all the input documents as a whole.
395 #
396 # The remaining computed fields are optional and computed using the `<accumulator>`
397 # operators.
398 class MongoGroup
399 super JsonObject
400
401 # Group `_id`
402 #
403 # See `MongoGroup::group`.
404 var id: String
405
406 init do self["_id"] = id
407
408 # Add an accumulator
409 #
410 # Each accumulator is registered as:
411 # ~~~json
412 # <field>: { <accumulator> : <expression> }
413 # ~~~
414 private fun acc(name: String, field: String, expression: nullable Jsonable): MongoGroup do
415 var q = new JsonObject
416 q["${name}"] = expression
417 self[field] = q
418 return self
419 end
420
421 # Calculates and returns the sum of numeric values
422 #
423 # https://docs.mongodb.com/manual/reference/operator/aggregation/sum/#grp._S_sum
424 #
425 # ~~~json
426 # { $sum: <expression> }
427 # ~~~
428 #
429 # `$sum` ignores non-numeric values.
430 fun sum(field: String, expression: Jsonable): MongoGroup do
431 return acc("sum", field, expression)
432 end
433
434 # Returns the average value of the numeric values
435 #
436 # https://docs.mongodb.com/manual/reference/operator/aggregation/avg/
437 #
438 # ~~~json
439 # { $avg: <expression> }
440 # ~~~
441 #
442 # `$avg` ignores non-numeric values.
443 fun avg(field: String, expression: Jsonable): MongoGroup do
444 return acc("avg", field, expression)
445 end
446
447 # Returns the maximum value
448 #
449 # https://docs.mongodb.com/manual/reference/operator/aggregation/max/
450 #
451 # ~~~json
452 # { $max: <expression> }
453 # ~~~
454 #
455 # `$max` compares both value and type, using the specified BSON comparison
456 # order for values of different types.
457 fun max(field: String, expression: Jsonable): MongoGroup do
458 return acc("max", field, expression)
459 end
460
461 # Returns the minimum value
462 #
463 # https://docs.mongodb.com/manual/reference/operator/aggregation/min/
464 #
465 # ~~~json
466 # { $min: <expression> }
467 # ~~~
468 #
469 # `$min` compares both value and type, using the specified BSON comparison
470 # order for values of different types.
471 fun min(field: String, expression: Jsonable): MongoGroup do
472 return acc("min", field, expression)
473 end
474
475 # Return the first value
476 #
477 # https://docs.mongodb.com/manual/reference/operator/aggregation/first/
478 #
479 # ~~~json
480 # { $first: <expression> }
481 # ~~~
482 #
483 # Returns the value that results from applying an expression to the first
484 # document in a group of documents that share the same group by key.
485 #
486 # Only meaningful when documents are in a defined order.
487 fun first(field: String, expression: Jsonable): MongoGroup do
488 return acc("first", field, expression)
489 end
490
491 # Return the last value
492 #
493 # https://docs.mongodb.com/manual/reference/operator/aggregation/last/
494 #
495 # ~~~json
496 # { $last: <expression> }
497 # ~~~
498 #
499 # Returns the value that results from applying an expression to the last
500 # document in a group of documents that share the same group by key.
501 #
502 # Only meaningful when documents are in a defined order.
503 fun last(field: String, expression: Jsonable): MongoGroup do
504 return acc("last", field, expression)
505 end
506
507 # Push to an array
508 #
509 # https://docs.mongodb.com/manual/reference/operator/aggregation/push/
510 #
511 # ~~~json
512 # { $push: <expression> }
513 # ~~~
514 #
515 # Returns an array of all values that result from applying an expression to
516 # each document in a group of documents that share the same group by key.
517 fun push(field: String, expr: Jsonable): MongoGroup do
518 return acc("push", field, expr)
519 end
520
521 # Push to a unique array
522 #
523 # https://docs.mongodb.com/manual/reference/operator/aggregation/addToSet/
524 #
525 # ~~~json
526 # { $addToSet: <expression> }
527 # ~~~
528 #
529 # Returns an array of all unique values that results from applying an
530 # expression to each document in a group of documents that share the same
531 # group by key.
532 #
533 # Order of the elements in the output array is unspecified.
534 fun addToSet(field: String, expr: Jsonable): MongoGroup do
535 return acc("addToSet", field, expr)
536 end
537 end