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