mongodb: fixes crash when the return of `mongoc_cursor_current` was freed
[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 recover_with(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 private class MongoObjectId
166
167 var native: BSONObjectId
168
169 # The unique ID as an MongoDB Object ID string.
170 fun id: String do return native.id
171
172 # Internal JSON representation of this Object ID.
173 #
174 # Something like `{"$oid": "5578e5dcf344225cc2378051"}`.
175 fun to_json: JsonObject do
176 var obj = new JsonObject
177 obj["$oid"] = id
178 return obj
179 end
180
181 # Formatted as `ObjectId("5578e5dcf344225cc2378051")`
182 redef fun to_s do return "ObjectId({id})"
183 end
184
185 # The MongoClient is used to connect to the mongo server and send queries.
186 #
187 # Usage:
188 #
189 # ~~~
190 # var uri = "mongodb://localhost:27017/"
191 # var client = new MongoClient(uri)
192 # assert client.server_uri == uri
193 # ~~~
194 class MongoClient
195 super FinalizableOnce
196
197 # Server URI.
198 var server_uri: String
199
200 private var native: NativeMongoClient is noinit
201
202 init do native = new NativeMongoClient(server_uri.to_cstring)
203
204 # Gets server data.
205 #
206 # Returns `null` if an error occured. See `last_error`.
207 #
208 # ~~~
209 # var client = new MongoClient("mongodb://localhost:27017/")
210 # assert client.server_status["process"] == "mongod"
211 # ~~~
212 fun server_status: nullable JsonObject do
213 var nbson = native.server_status
214 if nbson == null then return null
215 var bson = new BSON(nbson)
216 var res = new JsonObject.from_bson(bson)
217 return res
218 end
219
220 # Lists available database names.
221 #
222 # ~~~
223 # var client = new MongoClient("mongodb://localhost:27017/")
224 # var db = client.database("test")
225 # db.collection("test").insert(new JsonObject)
226 # assert client.database_names.has("test")
227 # ~~~
228 fun database_names: Array[String] do
229 var res = new Array[String]
230 var nas = native.database_names
231 if nas == null then return res
232 var i = 0
233 var name = nas[i]
234 while not name.address_is_null do
235 res.add name.to_s_with_copy
236 name.free
237 i += 1
238 name = nas[i]
239 end
240 return res
241 end
242
243 # Loads or creates a database from its `name`.
244 #
245 # Database are automatically created on the MongoDB server upon insertion of
246 # the first document into a collection.
247 # There is no need to create a database manually.
248 #
249 # ~~~
250 # var client = new MongoClient("mongodb://localhost:27017/")
251 # assert client.database("test").name == "test"
252 # ~~~
253 fun database(name: String): MongoDb do return new MongoDb(self, name)
254
255 # Close the connexion and destroy the instance.
256 #
257 # The reference should not be used beyond this point!
258 fun close do finalize_once
259
260 redef fun finalize_once do native.destroy
261
262 # Last error raised by mongoc.
263 fun last_error: nullable MongoError do
264 var last_error = sys.last_mongoc_error
265 if last_error == null then return null
266 return new MongoError(last_error)
267 end
268
269 # Last auto generated id.
270 private fun last_id: nullable MongoObjectId do
271 var last_id = sys.last_mongoc_id
272 if last_id == null then return null
273 return new MongoObjectId(last_id)
274 end
275
276 # Set the last generated id or `null` to unset once used.
277 private fun last_id=(id: nullable MongoObjectId) do
278 if id == null then
279 sys.last_mongoc_id = null
280 else
281 sys.last_mongoc_id = id.native
282 end
283 end
284 end
285
286 # A MongoDb database.
287 #
288 # Database are automatically created on the MongoDB server upon insertion of the
289 # first document into a collection.
290 # There is no need to create a database manually.
291 class MongoDb
292 super FinalizableOnce
293
294 # `MongoClient` used to load this database.
295 var client: MongoClient
296
297 # The database name.
298 var name: String
299
300 private var native: NativeMongoDb is noinit
301
302 init do native = new NativeMongoDb(client.native, name.to_cstring)
303
304 # Lists available collection names.
305 #
306 # Returns `null` if an error occured. See `Sys::last_mongoc_error`.
307 #
308 # ~~~
309 # var client = new MongoClient("mongodb://localhost:27017/")
310 # var db = client.database("test")
311 # db.collection("test").insert(new JsonObject)
312 # assert db.collection_names.has("test")
313 # ~~~
314 fun collection_names: Array[String] do
315 var res = new Array[String]
316 var nas = native.collection_names
317 if nas == null then return res
318 var i = 0
319 var name = nas[i]
320 while not name.address_is_null do
321 res.add name.to_s_with_copy
322 name.free
323 i += 1
324 name = nas[i]
325 end
326 return res
327 end
328
329 # Loads or creates a collection by its `name`.
330 #
331 # ~~~
332 # var client = new MongoClient("mongodb://localhost:27017/")
333 # var db = client.database("test")
334 # var col = db.collection("test")
335 # assert col.name == "test"
336 # ~~~
337 fun collection(name: String): MongoCollection do
338 return new MongoCollection(self, name)
339 end
340
341 # Checks if a collection named `name` exists.
342 #
343 # ~~~
344 # var client = new MongoClient("mongodb://localhost:27017/")
345 # var db = client.database("test")
346 # assert not db.has_collection("qwerty")
347 # ~~~
348 fun has_collection(name: String): Bool do
349 # TODO handle error
350 return native.has_collection(name.to_cstring)
351 end
352
353 # Drop `self`, returns false if an error occured.
354 fun drop: Bool do return native.drop
355
356 redef fun finalize_once do native.destroy
357 end
358
359 # A Mongo collection.
360 #
361 # Collections are automatically created on the MongoDB server upon insertion of
362 # the first document.
363 # There is no need to create a database manually.
364 class MongoCollection
365 super FinalizableOnce
366
367 # Database that collection belongs to.
368 var database: MongoDb
369
370 # Name of this collection.
371 var name: String
372
373 private var native: NativeMongoCollection is noinit
374
375 # Loads a collection.
376 #
377 # Call `MongoDb::collection` instead.
378 init do
379 native = new NativeMongoCollection(
380 database.client.native,
381 database.name.to_cstring,
382 name.to_cstring)
383 end
384
385 # Set the autogenerated last id if the `doc` does not contain one already.
386 private fun set_id(doc: JsonObject) do
387 var last_id = database.client.last_id
388 if last_id != null then
389 doc["_id"] = last_id.to_json
390 database.client.last_id = null
391 end
392 end
393
394 # Inserts a new document in the collection.
395 #
396 # If no _id element is found in document, then a new one be generated locally
397 # and added to the document.
398 #
399 # Returns `false` if an error occured. See `Sys::last_mongoc_error`.
400 #
401 # ~~~
402 # var client = new MongoClient("mongodb://localhost:27017/")
403 # var col = client.database("test").collection("test")
404 # var doc = new JsonObject
405 # doc["foo"] = 10
406 # doc["bar"] = "bar"
407 # doc["baz"] = new JsonArray
408 # assert col.insert(doc)
409 # assert doc.has_key("_id")
410 # ~~~
411 fun insert(doc: JsonObject): Bool do
412 var res = native.insert(doc.to_bson.native)
413 if res then set_id(doc)
414 return res
415 end
416
417 # Inserts multiple documents in the collection.
418 #
419 # See `insert`.
420 fun insert_all(docs: Collection[JsonObject]): Bool do
421 var res = true
422 for doc in docs do res = insert(doc) and res
423 return res
424 end
425
426 # Saves a new document in the collection.
427 #
428 # If the document has an `_id` field it will be updated.
429 # Otherwise it will be inserted.
430 #
431 # Returns `false` if an error occured. See `Sys::last_mongoc_error`.
432 #
433 # ~~~
434 # var client = new MongoClient("mongodb://localhost:27017/")
435 # var col = client.database("test").collection("test")
436 #
437 # var doc = new JsonObject
438 # doc["foo"] = 10
439 # doc["bar"] = "bar"
440 # doc["baz"] = new JsonArray
441 #
442 # assert col.save(doc) # will be inserted
443 # assert doc.has_key("_id")
444 #
445 # var id = doc["_id"]
446 # assert col.save(doc) # will be updated
447 # assert doc["_id"] == id
448 # ~~~
449 fun save(doc: JsonObject): Bool do
450 var bson = doc.to_bson
451 var nat = bson.native
452 var res = native.save(nat)
453 if res then set_id(doc)
454 assert nat != self #FIXME used to avoid GC crashes
455 assert bson != self #FIXME used to avoid GC crashes
456 return res
457 end
458
459 # Removes the first document that matches `selector`.
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 sel = new JsonObject
467 # sel["foo"] = 10
468 # assert col.remove(sel)
469 # ~~~
470 fun remove(selector: JsonObject): Bool do
471 return native.remove(selector.to_bson.native)
472 end
473
474 # Removes all the document that match `selector`.
475 #
476 # See `remove`.
477 fun remove_all(selector: JsonObject): Bool do
478 return native.remove_all(selector.to_bson.native)
479 end
480
481 # Updates a document already existing in the collection.
482 #
483 # No upsert is done, see `save` instead.
484 #
485 # ~~~
486 # var client = new MongoClient("mongodb://localhost:27017/")
487 # var col = client.database("test").collection("test")
488 # var sel = new JsonObject
489 # sel["foo"] = 10
490 # var upd = new JsonObject
491 # upd["bar"] = "BAR"
492 # assert col.update(sel, upd)
493 # ~~~
494 fun update(selector: JsonObject, update: JsonObject): Bool do
495 return native.update(
496 selector.to_bson.native,
497 update.to_bson.native)
498 end
499
500 # Updates all documents matching the `selector`.
501 #
502 # See `update`.
503 fun update_all(selector: JsonObject, update: JsonObject): Bool do
504 return native.update_all(
505 selector.to_bson.native,
506 update.to_bson.native)
507 end
508
509 # Counts the document matching `query`.
510 #
511 # Returns `-1` if an error occured. See `Sys::last_mongoc_error`.
512 #
513 # ~~~
514 # var client = new MongoClient("mongodb://localhost:27017/")
515 # var col = client.database("test").collection("test")
516 # var query = new JsonObject
517 # query["foo"] = 10
518 # assert col.count(query) > 0
519 # ~~~
520 fun count(query: JsonObject): Int do
521 return native.count(query.to_bson.native)
522 end
523
524 # Finds the first document that matches `query`.
525 #
526 # Returns `null` if an error occured. See `Sys::last_mongoc_error`.
527 #
528 # ~~~
529 # var client = new MongoClient("mongodb://localhost:27017/")
530 # var col = client.database("test").collection("test")
531 # var query = new JsonObject
532 # query["foo"] = 10
533 # var doc = col.find(query)
534 # assert doc["foo"] == 10
535 # ~~~
536 fun find(query: JsonObject): nullable JsonObject do
537 var q = new NativeBSON.from_json_string(query.to_json.to_cstring)
538 var c = native.find(q)
539 q.destroy
540 if c == null then return null
541 var cursor = new MongoCursor(c)
542 if not cursor.is_ok then
543 return null
544 end
545 var item = cursor.item
546 assert cursor != self
547 return item
548 end
549
550 # Finds all the documents matching the `query`.
551 #
552 # ~~~
553 # var client = new MongoClient("mongodb://localhost:27017/")
554 # var col = client.database("test").collection("test")
555 # var query = new JsonObject
556 # query["foo"] = 10
557 # assert col.find_all(query).length > 0
558 # ~~~
559 fun find_all(query: JsonObject): Array[JsonObject] do
560 var res = new Array[JsonObject]
561 var c = native.find(query.to_bson.native)
562 if c == null then return res
563 var cursor = new MongoCursor(c)
564 for item in cursor do res.add item
565 return res
566 end
567
568 # Retrieves statistics about the collection.
569 #
570 # Returns `null` if an error occured. See `Sys::last_mongoc_error`.
571 #
572 # ~~~
573 # var client = new MongoClient("mongodb://localhost:27017/")
574 # var col = client.database("test").collection("test")
575 # assert col.stats["ns"] == "test.test"
576 # ~~~
577 fun stats: nullable JsonObject do
578 var bson = native.stats
579 if bson == null then return null
580 return new JsonObject.from_bson(new BSON(bson))
581 end
582
583 # Drops `self`, returns false if an error occured.
584 fun drop: Bool do return native.drop
585
586 # Moves `self` to another `database`.
587 #
588 # The database will also be updated internally so it is safe to continue using
589 # this collection after the move.
590 # Additional operations will occur on moved collection.
591 fun move(database: MongoDb): Bool do
592 self.database = database
593 return native.rename(database.name.to_cstring, name.to_cstring)
594 end
595
596 # Renames `self`.
597 #
598 # The name of the collection will also be updated internally so it is safe
599 # to continue using this collection after the rename.
600 # Additional operations will occur on renamed collection.
601 fun rename(name: String): Bool do
602 self.name = name
603 return native.rename(database.name.to_cstring, name.to_cstring)
604 end
605
606 redef fun finalize_once do native.destroy
607 end
608
609 # A MongoDB query cursor.
610 #
611 # It wraps up the wire protocol negotation required to initiate a query and
612 # retreive an unknown number of documents.
613 class MongoCursor
614 super FinalizableOnce
615 super Iterator[JsonObject]
616
617 private var native: NativeMongoCursor
618
619 init do next
620
621 redef fun is_ok do return native.more
622
623 redef fun next do native.next
624
625 redef fun item do
626 return new JsonObject.from_bson(new BSON(native.current))
627 end
628
629 redef fun finalize_once do native.destroy
630 end