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