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