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