mongodb: allow client to set `skip` and `limit` when calling `find`
[nit.git] / lib / mongodb / mongodb.nit
1 # This file is part of NIT ( http://www.nitlanguage.org ).
2 #
3 # Copyright 2015 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 # MongoDB Nit Driver.
18 #
19 # This is actually a wrapper around the [MongoDB C Driver](http://api.mongodb.org/c/1.1.4/index.html).
20 #
21 # Usage:
22 #
23 # ~~~
24 # # Opens the connexion with the Mongo server.
25 # var client = new MongoClient("mongodb://localhost:27017/")
26 #
27 # # Retrieve a collection.
28 # var col = client.database("test").collection("test")
29 #
30 # # Insert a document in the collection.
31 # var doc = new JsonObject
32 # doc["foo"] = 10
33 # doc["bar"] = "bar"
34 # doc["baz"] = new JsonArray
35 # assert col.insert(doc)
36 #
37 # # Retrieve a document from the collection.
38 # var query = new JsonObject
39 # query["foo"] = 10
40 # var res = col.find(query)
41 # assert res["bar"] == "bar"
42 # ~~~
43 module mongodb
44
45 import json
46 private import native_mongodb
47
48 in "C header" `{
49 #include <mongoc.h>
50 `}
51
52 # Everything inside MongoDB is manipulated as BSON Objects.
53 #
54 # See:
55 # * [Binary JSON spec](http://bsonspec.org/)
56 # * [Libbson](http://api.mongodb.org/libbson/1.1.4/)#
57 private class BSON
58 super Finalizable
59
60 # Native instance pointer.
61 var native: NativeBSON
62
63 # Is the native instance valid?
64 #
65 # This is set to false if the `native` is destroyed.
66 var is_alive = true
67
68 # Returns a new BSON object initialized from the content of `json`.
69 #
70 # ~~~
71 # intrude import mongodb
72 # var obj = new JsonObject
73 # obj["age"] = 10
74 # obj["name"] = "Rick"
75 # obj["ELS"] = new JsonArray
76 # var bson = new BSON.from_json(obj)
77 # assert bson.to_s == """{ "age" : 10, "name" : "Rick", "ELS" : [ ] }"""
78 # ~~~
79 init from_json(json: JsonObject) do
80 init(new NativeBSON.from_json_string(json.to_json.to_cstring))
81 end
82
83 # Returns a new BSON object parsed from `json_string`.
84 #
85 # If `json_string` is not a valid JSON string, this initializer returns NULL.
86 #
87 # ~~~
88 # intrude import mongodb
89 # var str = """{ "age" : 10, "name" : "Rick", "ELS" : [ ] }"""
90 # var bson = new BSON.from_json_string(str)
91 # assert bson.to_s == str
92 # ~~~
93 init from_json_string(json_string: String) do
94 init(new NativeBSON.from_json_string(json_string.to_cstring))
95 end
96
97 redef fun to_s do
98 assert is_alive
99 var ns = native.to_native_string
100 var res = ns.to_s_with_copy
101 ns.free # manual free of gc allocated NativeString
102 return res
103 end
104
105 # Returns a new JsonObject from `self`.
106 #
107 # ~~~
108 # intrude import mongodb
109 # var str = """{ "age" : 10, "name" : "Rick", "ELS" : [ ] }"""
110 # var bson = new BSON.from_json_string(str)
111 # var json = bson.to_json
112 # assert json["age"] == 10
113 # assert json["name"] == "Rick"
114 # assert json["ELS"].as(JsonArray).is_empty
115 # ~~~
116 fun to_json: JsonObject do
117 assert is_alive
118 var json = to_s.parse_json
119 if json isa JsonParseError then
120 print to_s
121 print json.message
122 sys.exit 1
123 end
124 return json.as(JsonObject)
125 end
126
127 redef fun finalize do
128 if is_alive then
129 native.destroy
130 is_alive = false
131 end
132 end
133 end
134
135 redef class JsonObject
136 # Inits `self` from a BSON object.
137 private init from_bson(bson: BSON) do recover_with(bson.to_json)
138
139 # Returns a new BSON object from `self`.
140 private fun to_bson: BSON do return new BSON.from_json(self)
141 end
142
143 # An error returned by the mongoc client.
144 #
145 # Within the client, if a method returns `false` or `null` it's more likely that
146 # an error occured during the execution.
147 #
148 # See `MongoClient::last_error`.
149 class MongoError
150
151 private var native: BSONError
152
153 # Is the native instance valid?
154 #
155 # This is set to false if the `native` is destroyed.
156 private var is_alive = true
157
158 # Logical domain within a library that created the error.
159 fun domain: Int do
160 assert is_alive
161 return native.domain
162 end
163
164 # Domain specific error code.
165 fun code: Int do
166 assert is_alive
167 return native.code
168 end
169
170 # Human readable error message.
171 fun message: String do
172 assert is_alive
173 var ns = native.message
174 var res = ns.to_s_with_copy
175 ns.free
176 return res
177 end
178
179 redef fun to_s do return "{message} (code: {code})"
180 end
181
182 # MongoDB Object ID representation.
183 #
184 # For ObjectIDs, MongoDB uses the `ObjectId("hash")` notation.
185 # This notation is replicated by the `to_s` service.
186 #
187 # Since the MongoDB notation is not JSON complient, the mongoc wrapper uses
188 # a JSON based notation like `{"$oid": "hash"}`.
189 # This is the notation returned by the `to_json` service.
190 private class MongoObjectId
191
192 var native: BSONObjectId
193
194 # The unique ID as an MongoDB Object ID string.
195 fun id: String do return native.id
196
197 # Internal JSON representation of this Object ID.
198 #
199 # Something like `{"$oid": "5578e5dcf344225cc2378051"}`.
200 fun to_json: JsonObject do
201 var obj = new JsonObject
202 obj["$oid"] = id
203 return obj
204 end
205
206 # Formatted as `ObjectId("5578e5dcf344225cc2378051")`
207 redef fun to_s do return "ObjectId({id})"
208 end
209
210 # The MongoClient is used to connect to the mongo server and send queries.
211 #
212 # Usage:
213 #
214 # ~~~
215 # var uri = "mongodb://localhost:27017/"
216 # var client = new MongoClient(uri)
217 # assert client.server_uri == uri
218 # ~~~
219 class MongoClient
220 super Finalizable
221
222 # Server URI.
223 var server_uri: String
224
225 private var native: NativeMongoClient is noinit
226
227 # Is the native instance valid?
228 #
229 # This is set to false if the `native` is destroyed.
230 private var is_alive = true
231
232 init do
233 native = new NativeMongoClient(server_uri.to_cstring)
234 end
235
236 # Gets server data.
237 #
238 # Returns `null` if an error occured. See `last_error`.
239 #
240 # ~~~
241 # var client = new MongoClient("mongodb://localhost:27017/")
242 # assert client.server_status["process"] == "mongod"
243 # ~~~
244 fun server_status: nullable JsonObject do
245 assert is_alive
246 var nbson = native.server_status
247 if nbson == null then return null
248 var bson = new BSON(nbson)
249 var res = new JsonObject.from_bson(bson)
250 return res
251 end
252
253 # Lists available database names.
254 #
255 # ~~~
256 # var client = new MongoClient("mongodb://localhost:27017/")
257 # var db = client.database("test")
258 # db.collection("test").insert(new JsonObject)
259 # assert client.database_names.has("test")
260 # ~~~
261 fun database_names: Array[String] do
262 assert is_alive
263 var res = new Array[String]
264 var nas = native.database_names
265 if nas == null then return res
266 var i = 0
267 var name = nas[i]
268 while not name.address_is_null do
269 res.add name.to_s_with_copy
270 name.free
271 i += 1
272 name = nas[i]
273 end
274 return res
275 end
276
277 # Loads or creates a database from its `name`.
278 #
279 # Database are automatically created on the MongoDB server upon insertion of
280 # the first document into a collection.
281 # There is no need to create a database manually.
282 #
283 # ~~~
284 # var client = new MongoClient("mongodb://localhost:27017/")
285 # assert client.database("test").name == "test"
286 # ~~~
287 fun database(name: String): MongoDb do
288 assert is_alive
289 return new MongoDb(self, name)
290 end
291
292 # Close the connexion and destroy the instance.
293 #
294 # The reference should not be used beyond this point!
295 fun close do
296 assert is_alive
297 finalize
298 end
299
300 redef fun finalize do
301 if is_alive then
302 native.destroy
303 is_alive = false
304 end
305 end
306
307 # Last error raised by mongoc.
308 fun last_error: nullable MongoError do
309 var last_error = sys.last_mongoc_error
310 if last_error == null then return null
311 return new MongoError(last_error)
312 end
313
314 # Last auto generated id.
315 private fun last_id: nullable MongoObjectId do
316 var last_id = sys.last_mongoc_id
317 if last_id == null then return null
318 return new MongoObjectId(last_id)
319 end
320
321 # Set the last generated id or `null` to unset once used.
322 private fun last_id=(id: nullable MongoObjectId) do
323 if id == null then
324 sys.last_mongoc_id = null
325 else
326 sys.last_mongoc_id = id.native
327 end
328 end
329 end
330
331 # A MongoDb database.
332 #
333 # Database are automatically created on the MongoDB server upon insertion of the
334 # first document into a collection.
335 # There is no need to create a database manually.
336 class MongoDb
337 super Finalizable
338
339 # `MongoClient` used to load this database.
340 var client: MongoClient
341
342 # The database name.
343 var name: String
344
345 private var native: NativeMongoDb is noinit
346
347 # Is the native instance valid?
348 #
349 # This is set to false if the `native` is destroyed.
350 private var is_alive = true
351
352 init do
353 native = new NativeMongoDb(client.native, name.to_cstring)
354 end
355
356 # Lists available collection names.
357 #
358 # Returns `null` if an error occured. See `Sys::last_mongoc_error`.
359 #
360 # ~~~
361 # var client = new MongoClient("mongodb://localhost:27017/")
362 # var db = client.database("test")
363 # db.collection("test").insert(new JsonObject)
364 # assert db.collection_names.has("test")
365 # ~~~
366 fun collection_names: Array[String] do
367 assert is_alive
368 var res = new Array[String]
369 var nas = native.collection_names
370 if nas == null then return res
371 var i = 0
372 var name = nas[i]
373 while not name.address_is_null do
374 res.add name.to_s_with_copy
375 name.free
376 i += 1
377 name = nas[i]
378 end
379 return res
380 end
381
382 # Loads or creates a collection by its `name`.
383 #
384 # ~~~
385 # var client = new MongoClient("mongodb://localhost:27017/")
386 # var db = client.database("test")
387 # var col = db.collection("test")
388 # assert col.name == "test"
389 # ~~~
390 fun collection(name: String): MongoCollection do
391 assert is_alive
392 return new MongoCollection(self, name)
393 end
394
395 # Checks if a collection named `name` exists.
396 #
397 # ~~~
398 # var client = new MongoClient("mongodb://localhost:27017/")
399 # var db = client.database("test")
400 # assert not db.has_collection("qwerty")
401 # ~~~
402 fun has_collection(name: String): Bool do
403 assert is_alive
404 # TODO handle error
405 return native.has_collection(name.to_cstring)
406 end
407
408 # Drop `self`, returns false if an error occured.
409 fun drop: Bool do
410 assert is_alive
411 return native.drop
412 end
413
414 redef fun finalize do
415 if is_alive then
416 native.destroy
417 is_alive = false
418 end
419 end
420 end
421
422 # A Mongo collection.
423 #
424 # Collections are automatically created on the MongoDB server upon insertion of
425 # the first document.
426 # There is no need to create a database manually.
427 class MongoCollection
428 super Finalizable
429
430 # Database that collection belongs to.
431 var database: MongoDb
432
433 # Name of this collection.
434 var name: String
435
436 private var native: NativeMongoCollection is noinit
437
438 # Is the native instance valid?
439 #
440 # This is set to false if the `native` is destroyed.
441 private var is_alive = true
442
443 # Loads a collection.
444 #
445 # Call `MongoDb::collection` instead.
446 init do
447 native = new NativeMongoCollection(
448 database.client.native,
449 database.name.to_cstring,
450 name.to_cstring)
451 end
452
453 # Set the autogenerated last id if the `doc` does not contain one already.
454 private fun set_id(doc: JsonObject) do
455 var last_id = database.client.last_id
456 if last_id != null then
457 doc["_id"] = last_id.to_json
458 database.client.last_id = null
459 end
460 end
461
462 # Inserts a new document in the collection.
463 #
464 # If no _id element is found in document, then a new one be generated locally
465 # and added to the document.
466 #
467 # Returns `false` if an error occured. See `Sys::last_mongoc_error`.
468 #
469 # ~~~
470 # var client = new MongoClient("mongodb://localhost:27017/")
471 # var col = client.database("test").collection("test")
472 # var doc = new JsonObject
473 # doc["foo"] = 10
474 # doc["bar"] = "bar"
475 # doc["baz"] = new JsonArray
476 # assert col.insert(doc)
477 # assert doc.has_key("_id")
478 # ~~~
479 fun insert(doc: JsonObject): Bool do
480 assert is_alive
481 var res = native.insert(doc.to_bson.native)
482 if res then set_id(doc)
483 return res
484 end
485
486 # Inserts multiple documents in the collection.
487 #
488 # See `insert`.
489 fun insert_all(docs: Collection[JsonObject]): Bool do
490 assert is_alive
491 var res = true
492 for doc in docs do res = insert(doc) and res
493 return res
494 end
495
496 # Saves a new document in the collection.
497 #
498 # If the document has an `_id` field it will be updated.
499 # Otherwise it will be inserted.
500 #
501 # Returns `false` if an error occured. See `Sys::last_mongoc_error`.
502 #
503 # ~~~
504 # var client = new MongoClient("mongodb://localhost:27017/")
505 # var col = client.database("test").collection("test")
506 #
507 # var doc = new JsonObject
508 # doc["foo"] = 10
509 # doc["bar"] = "bar"
510 # doc["baz"] = new JsonArray
511 #
512 # assert col.save(doc) # will be inserted
513 # assert doc.has_key("_id")
514 #
515 # var id = doc["_id"]
516 # assert col.save(doc) # will be updated
517 # assert doc["_id"] == id
518 # ~~~
519 fun save(doc: JsonObject): Bool do
520 assert is_alive
521 var bson = doc.to_bson
522 var nat = bson.native
523 var res = native.save(nat)
524 if res then set_id(doc)
525 assert nat != self #FIXME used to avoid GC crashes
526 assert bson != self #FIXME used to avoid GC crashes
527 return res
528 end
529
530 # Removes the first document that matches `selector`.
531 #
532 # Returns `false` if an error occured. See `Sys::last_mongoc_error`.
533 #
534 # ~~~
535 # var client = new MongoClient("mongodb://localhost:27017/")
536 # var col = client.database("test").collection("test")
537 # var sel = new JsonObject
538 # sel["foo"] = 10
539 # assert col.remove(sel)
540 # ~~~
541 fun remove(selector: JsonObject): Bool do
542 assert is_alive
543 return native.remove(selector.to_bson.native)
544 end
545
546 # Removes all the document that match `selector`.
547 #
548 # See `remove`.
549 fun remove_all(selector: JsonObject): Bool do
550 assert is_alive
551 return native.remove_all(selector.to_bson.native)
552 end
553
554 # Updates a document already existing in the collection.
555 #
556 # No upsert is done, see `save` instead.
557 #
558 # ~~~
559 # var client = new MongoClient("mongodb://localhost:27017/")
560 # var col = client.database("test").collection("test")
561 # var sel = new JsonObject
562 # sel["foo"] = 10
563 # var upd = new JsonObject
564 # upd["bar"] = "BAR"
565 # assert col.update(sel, upd)
566 # ~~~
567 fun update(selector: JsonObject, update: JsonObject): Bool do
568 assert is_alive
569 return native.update(
570 selector.to_bson.native,
571 update.to_bson.native)
572 end
573
574 # Updates all documents matching the `selector`.
575 #
576 # See `update`.
577 fun update_all(selector: JsonObject, update: JsonObject): Bool do
578 return native.update_all(
579 selector.to_bson.native,
580 update.to_bson.native)
581 end
582
583 # Counts the document matching `query`.
584 #
585 # Returns `-1` if an error occured. See `Sys::last_mongoc_error`.
586 #
587 # ~~~
588 # var client = new MongoClient("mongodb://localhost:27017/")
589 # var col = client.database("test").collection("test")
590 # var query = new JsonObject
591 # query["foo"] = 10
592 # assert col.count(query) > 0
593 # ~~~
594 fun count(query: JsonObject): Int do
595 assert is_alive
596 return native.count(query.to_bson.native)
597 end
598
599 # Finds the first document that matches `query`.
600 #
601 # Params:
602 # * `skip` number of documents to skip
603 # * `limit` number of documents to return
604 #
605 # Returns `null` if an error occured. See `Sys::last_mongoc_error`.
606 #
607 # ~~~
608 # var client = new MongoClient("mongodb://localhost:27017/")
609 # var col = client.database("test").collection("test")
610 # var query = new JsonObject
611 # query["foo"] = 10
612 # var doc = col.find(query)
613 # assert doc["foo"] == 10
614 # ~~~
615 fun find(query: JsonObject, skip, limit: nullable Int): nullable JsonObject do
616 assert is_alive
617 var q = new NativeBSON.from_json_string(query.to_json.to_cstring)
618 var s = skip or else 0
619 var l = limit or else 0
620 var c = native.find(q, s, l)
621 q.destroy
622 if c == null then return null
623 var cursor = new MongoCursor(c)
624 if not cursor.is_ok then
625 return null
626 end
627 var item = cursor.item
628 assert cursor != self
629 return item
630 end
631
632 # Finds all the documents matching the `query`.
633 #
634 # Params:
635 # * `skip` number of documents to skip
636 # * `limit` number of documents to return
637 #
638 # ~~~
639 # var client = new MongoClient("mongodb://localhost:27017/")
640 # var col = client.database("test").collection("test")
641 # var query = new JsonObject
642 # query["foo"] = 10
643 # assert col.find_all(query).length > 0
644 # ~~~
645 fun find_all(query: JsonObject, skip, limit: nullable Int): Array[JsonObject] do
646 assert is_alive
647 var s = skip or else 0
648 var l = limit or else 0
649 var res = new Array[JsonObject]
650 var c = native.find(query.to_bson.native, s, l)
651 if c == null then return res
652 var cursor = new MongoCursor(c)
653 for item in cursor do res.add item
654 return res
655 end
656
657 # Retrieves statistics about the collection.
658 #
659 # Returns `null` if an error occured. See `Sys::last_mongoc_error`.
660 #
661 # ~~~
662 # var client = new MongoClient("mongodb://localhost:27017/")
663 # var col = client.database("test").collection("test")
664 # assert col.stats["ns"] == "test.test"
665 # ~~~
666 fun stats: nullable JsonObject do
667 assert is_alive
668 var bson = native.stats
669 if bson == null then return null
670 return new JsonObject.from_bson(new BSON(bson))
671 end
672
673 # Drops `self`, returns false if an error occured.
674 fun drop: Bool do
675 assert is_alive
676 return native.drop
677 end
678
679 # Moves `self` to another `database`.
680 #
681 # The database will also be updated internally so it is safe to continue using
682 # this collection after the move.
683 # Additional operations will occur on moved collection.
684 fun move(database: MongoDb): Bool do
685 assert is_alive
686 self.database = database
687 return native.rename(database.name.to_cstring, name.to_cstring)
688 end
689
690 # Renames `self`.
691 #
692 # The name of the collection will also be updated internally so it is safe
693 # to continue using this collection after the rename.
694 # Additional operations will occur on renamed collection.
695 fun rename(name: String): Bool do
696 assert is_alive
697 self.name = name
698 return native.rename(database.name.to_cstring, name.to_cstring)
699 end
700
701 redef fun finalize do
702 if is_alive then
703 native.destroy
704 is_alive = false
705 end
706 end
707 end
708
709 # A MongoDB query cursor.
710 #
711 # It wraps up the wire protocol negotation required to initiate a query and
712 # retreive an unknown number of documents.
713 class MongoCursor
714 super Finalizable
715 super Iterator[JsonObject]
716
717 private var native: NativeMongoCursor
718
719 # Is the native instance valid?
720 #
721 # This is set to false if the `native` is destroyed.
722 private var is_alive = true
723
724 init do next
725
726 redef fun is_ok do
727 assert is_alive
728 return native.more
729 end
730
731 redef fun next do
732 assert is_alive
733 native.next
734 end
735
736 redef fun item do
737 assert is_alive
738 return new JsonObject.from_bson(new BSON(native.current))
739 end
740
741 redef fun finalize do
742 if is_alive then
743 native.destroy
744 is_alive = false
745 end
746 end
747 end