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