mongodb: Fixed failing test for `aggregate` method.
[nit.git] / lib / mongodb / mongodb.nit
index 78f2fb8..7db69e7 100644 (file)
 #
 # ~~~
 # # Opens the connexion with the Mongo server.
-# var client = new MongoClient("mongodb://localhost:27017/")
+# var client = new MongoClient("mongodb://mongo:27017/")
+#
+# # Select the database.
+# var db_suffix = "NIT_TESTING_ID".environ
+# var db_name = "test_{db_suffix}"
+# var db = client.database(db_name)
 #
 # # Retrieve a collection.
-# var col = client.database("test").collection("test")
+# var col = db.collection("test")
 #
 # # Insert a document in the collection.
 # var doc = new JsonObject
@@ -42,6 +47,7 @@
 # ~~~
 module mongodb
 
+import json::static
 import json
 private import native_mongodb
 
@@ -55,16 +61,11 @@ in "C header" `{
 # * [Binary JSON spec](http://bsonspec.org/)
 # * [Libbson](http://api.mongodb.org/libbson/1.1.4/)#
 private class BSON
-       super Finalizable
+       super FinalizableOnce
 
        # Native instance pointer.
        var native: NativeBSON
 
-       # Is the native instance valid?
-       #
-       # This is set to false if the `native` is destroyed.
-       var is_alive = true
-
        # Returns a new BSON object initialized from the content of `json`.
        #
        # ~~~
@@ -95,10 +96,9 @@ private class BSON
        end
 
        redef fun to_s do
-               assert is_alive
-               var ns = native.to_native_string
-               var res = ns.to_s_with_copy
-               ns.free # manual free of gc allocated NativeString
+               var ns = native.to_c_string
+               var res = ns.to_s
+               ns.free # manual free of gc allocated CString
                return res
        end
 
@@ -114,27 +114,20 @@ private class BSON
        # assert json["ELS"].as(JsonArray).is_empty
        # ~~~
        fun to_json: JsonObject do
-               assert is_alive
                var json = to_s.parse_json
                if json isa JsonParseError then
-                       print to_s
                        print json.message
                        sys.exit 1
                end
                return json.as(JsonObject)
        end
 
-       redef fun finalize do
-               if is_alive then
-                       native.destroy
-                       is_alive = false
-               end
-       end
+       redef fun finalize_once do native.destroy
 end
 
 redef class JsonObject
        # Inits `self` from a BSON object.
-       private init from_bson(bson: BSON) do recover_with(bson.to_json)
+       private init from_bson(bson: BSON) do add_all(bson.to_json)
 
        # Returns a new BSON object from `self`.
        private fun to_bson: BSON do return new BSON.from_json(self)
@@ -150,28 +143,16 @@ class MongoError
 
        private var native: BSONError
 
-       # Is the native instance valid?
-       #
-       # This is set to false if the `native` is destroyed.
-       private var is_alive = true
-
        # Logical domain within a library that created the error.
-       fun domain: Int do
-               assert is_alive
-               return native.domain
-       end
+       fun domain: Int do return native.domain
 
        # Domain specific error code.
-       fun code: Int do
-               assert is_alive
-               return native.code
-       end
+       fun code: Int do return native.code
 
        # Human readable error message.
        fun message: String do
-               assert is_alive
                var ns = native.message
-               var res = ns.to_s_with_copy
+               var res = ns.to_s
                ns.free
                return res
        end
@@ -187,9 +168,13 @@ end
 # Since the MongoDB notation is not JSON complient, the mongoc wrapper uses
 # a JSON based notation like `{"$oid": "hash"}`.
 # This is the notation returned by the `to_json` service.
-private class MongoObjectId
+class MongoObjectId
+
+       private var native: BSONObjectId = new BSONObjectId
 
-       var native: BSONObjectId
+       private init with_native(native: BSONObjectId) do
+               self.native = native
+       end
 
        # The unique ID as an MongoDB Object ID string.
        fun id: String do return native.id
@@ -212,37 +197,29 @@ end
 # Usage:
 #
 # ~~~
-# var uri = "mongodb://localhost:27017/"
+# var uri = "mongodb://mongo:27017/"
 # var client = new MongoClient(uri)
 # assert client.server_uri == uri
 # ~~~
 class MongoClient
-       super Finalizable
+       super FinalizableOnce
 
        # Server URI.
        var server_uri: String
 
        private var native: NativeMongoClient is noinit
 
-       # Is the native instance valid?
-       #
-       # This is set to false if the `native` is destroyed.
-       private var is_alive = true
-
-       init do
-               native = new NativeMongoClient(server_uri.to_cstring)
-       end
+       init do native = new NativeMongoClient(server_uri.to_cstring)
 
        # Gets server data.
        #
        # Returns `null` if an error occured. See `last_error`.
        #
        # ~~~
-       # var client = new MongoClient("mongodb://localhost:27017/")
+       # var client = new MongoClient("mongodb://mongo:27017/")
        # assert client.server_status["process"] == "mongod"
        # ~~~
        fun server_status: nullable JsonObject do
-               assert is_alive
                var nbson = native.server_status
                if nbson == null then return null
                var bson = new BSON(nbson)
@@ -253,20 +230,21 @@ class MongoClient
        # Lists available database names.
        #
        # ~~~
-       # var client = new MongoClient("mongodb://localhost:27017/")
-       # var db = client.database("test")
+       # var client = new MongoClient("mongodb://mongo:27017/")
+       # var db_suffix = "NIT_TESTING_ID".environ
+       # var db_name = "test_{db_suffix}"
+       # var db = client.database(db_name)
        # db.collection("test").insert(new JsonObject)
-       # assert client.database_names.has("test")
+       # assert client.database_names.has(db_name)
        # ~~~
        fun database_names: Array[String] do
-               assert is_alive
                var res = new Array[String]
                var nas = native.database_names
                if nas == null then return res
                var i = 0
                var name = nas[i]
                while not name.address_is_null do
-                       res.add name.to_s_with_copy
+                       res.add name.to_s
                        name.free
                        i += 1
                        name = nas[i]
@@ -281,28 +259,20 @@ class MongoClient
        # There is no need to create a database manually.
        #
        # ~~~
-       # var client = new MongoClient("mongodb://localhost:27017/")
-       # assert client.database("test").name == "test"
+       # var client = new MongoClient("mongodb://mongo:27017/")
+       # var db_suffix = "NIT_TESTING_ID".environ
+       # var db_name = "test_{db_suffix}"
+       # var db = client.database(db_name)
+       # assert db.name == db_name
        # ~~~
-       fun database(name: String): MongoDb do
-               assert is_alive
-               return new MongoDb(self, name)
-       end
+       fun database(name: String): MongoDb do return new MongoDb(self, name)
 
        # Close the connexion and destroy the instance.
        #
        # The reference should not be used beyond this point!
-       fun close do
-               assert is_alive
-               finalize
-       end
+       fun close do finalize_once
 
-       redef fun finalize do
-               if is_alive then
-                       native.destroy
-                       is_alive = false
-               end
-       end
+       redef fun finalize_once do native.destroy
 
        # Last error raised by mongoc.
        fun last_error: nullable MongoError do
@@ -315,7 +285,7 @@ class MongoClient
        private fun last_id: nullable MongoObjectId do
                var last_id = sys.last_mongoc_id
                if last_id == null then return null
-               return new MongoObjectId(last_id)
+               return new MongoObjectId.with_native(last_id)
        end
 
        # Set the last generated id or `null` to unset once used.
@@ -334,7 +304,7 @@ end
 # first document into a collection.
 # There is no need to create a database manually.
 class MongoDb
-       super Finalizable
+       super FinalizableOnce
 
        # `MongoClient` used to load this database.
        var client: MongoClient
@@ -344,34 +314,28 @@ class MongoDb
 
        private var native: NativeMongoDb is noinit
 
-       # Is the native instance valid?
-       #
-       # This is set to false if the `native` is destroyed.
-       private var is_alive = true
-
-       init do
-               native = new NativeMongoDb(client.native, name.to_cstring)
-       end
+       init do native = new NativeMongoDb(client.native, name.to_cstring)
 
        # Lists available collection names.
        #
        # Returns `null` if an error occured. See `Sys::last_mongoc_error`.
        #
        # ~~~
-       # var client = new MongoClient("mongodb://localhost:27017/")
-       # var db = client.database("test")
+       # var client = new MongoClient("mongodb://mongo:27017/")
+       # var db_suffix = "NIT_TESTING_ID".environ
+       # var db_name = "test_{db_suffix}"
+       # var db = client.database(db_name)
        # db.collection("test").insert(new JsonObject)
        # assert db.collection_names.has("test")
        # ~~~
        fun collection_names: Array[String] do
-               assert is_alive
                var res = new Array[String]
                var nas = native.collection_names
                if nas == null then return res
                var i = 0
                var name = nas[i]
                while not name.address_is_null do
-                       res.add name.to_s_with_copy
+                       res.add name.to_s
                        name.free
                        i += 1
                        name = nas[i]
@@ -382,41 +346,35 @@ class MongoDb
        # Loads or creates a collection by its `name`.
        #
        # ~~~
-       # var client = new MongoClient("mongodb://localhost:27017/")
-       # var db = client.database("test")
+       # var client = new MongoClient("mongodb://mongo:27017/")
+       # var db_suffix = "NIT_TESTING_ID".environ
+       # var db_name = "test_{db_suffix}"
+       # var db = client.database(db_name)
        # var col = db.collection("test")
        # assert col.name == "test"
        # ~~~
        fun collection(name: String): MongoCollection do
-               assert is_alive
                return new MongoCollection(self, name)
        end
 
        # Checks if a collection named `name` exists.
        #
        # ~~~
-       # var client = new MongoClient("mongodb://localhost:27017/")
-       # var db = client.database("test")
+       # var client = new MongoClient("mongodb://mongo:27017/")
+       # var db_suffix = "NIT_TESTING_ID".environ
+       # var db_name = "test_{db_suffix}"
+       # var db = client.database(db_name)
        # assert not db.has_collection("qwerty")
        # ~~~
        fun has_collection(name: String): Bool do
-               assert is_alive
                # TODO handle error
                return native.has_collection(name.to_cstring)
        end
 
        # Drop `self`, returns false if an error occured.
-       fun drop: Bool do
-               assert is_alive
-               return native.drop
-       end
+       fun drop: Bool do return native.drop
 
-       redef fun finalize do
-               if is_alive then
-                       native.destroy
-                       is_alive = false
-               end
-       end
+       redef fun finalize_once do native.destroy
 end
 
 # A Mongo collection.
@@ -425,7 +383,7 @@ end
 # the first document.
 # There is no need to create a database manually.
 class MongoCollection
-       super Finalizable
+       super FinalizableOnce
 
        # Database that collection belongs to.
        var database: MongoDb
@@ -435,11 +393,6 @@ class MongoCollection
 
        private var native: NativeMongoCollection is noinit
 
-       # Is the native instance valid?
-       #
-       # This is set to false if the `native` is destroyed.
-       private var is_alive = true
-
        # Loads a collection.
        #
        # Call `MongoDb::collection` instead.
@@ -467,8 +420,11 @@ class MongoCollection
        # Returns `false` if an error occured. See `Sys::last_mongoc_error`.
        #
        # ~~~
-       # var client = new MongoClient("mongodb://localhost:27017/")
-       # var col = client.database("test").collection("test")
+       # var client = new MongoClient("mongodb://mongo:27017/")
+       # var db_suffix = "NIT_TESTING_ID".environ
+       # var db_name = "test_{db_suffix}"
+       # var db = client.database(db_name)
+       # var col = db.collection("test")
        # var doc = new JsonObject
        # doc["foo"] = 10
        # doc["bar"] = "bar"
@@ -477,7 +433,6 @@ class MongoCollection
        # assert doc.has_key("_id")
        # ~~~
        fun insert(doc: JsonObject): Bool do
-               assert is_alive
                var res = native.insert(doc.to_bson.native)
                if res then set_id(doc)
                return res
@@ -487,7 +442,6 @@ class MongoCollection
        #
        # See `insert`.
        fun insert_all(docs: Collection[JsonObject]): Bool do
-               assert is_alive
                var res = true
                for doc in docs do res = insert(doc) and res
                return res
@@ -501,8 +455,11 @@ class MongoCollection
        # Returns `false` if an error occured. See `Sys::last_mongoc_error`.
        #
        # ~~~
-       # var client = new MongoClient("mongodb://localhost:27017/")
-       # var col = client.database("test").collection("test")
+       # var client = new MongoClient("mongodb://mongo:27017/")
+       # var db_suffix = "NIT_TESTING_ID".environ
+       # var db_name = "test_{db_suffix}"
+       # var db = client.database(db_name)
+       # var col = db.collection("test")
        #
        # var doc = new JsonObject
        # doc["foo"] = 10
@@ -517,7 +474,6 @@ class MongoCollection
        # assert doc["_id"] == id
        # ~~~
        fun save(doc: JsonObject): Bool do
-               assert is_alive
                var bson = doc.to_bson
                var nat = bson.native
                var res = native.save(nat)
@@ -532,14 +488,16 @@ class MongoCollection
        # Returns `false` if an error occured. See `Sys::last_mongoc_error`.
        #
        # ~~~
-       # var client = new MongoClient("mongodb://localhost:27017/")
-       # var col = client.database("test").collection("test")
+       # var client = new MongoClient("mongodb://mongo:27017/")
+       # var db_suffix = "NIT_TESTING_ID".environ
+       # var db_name = "test_{db_suffix}"
+       # var db = client.database(db_name)
+       # var col = db.collection("test")
        # var sel = new JsonObject
        # sel["foo"] = 10
        # assert col.remove(sel)
        # ~~~
        fun remove(selector: JsonObject): Bool do
-               assert is_alive
                return native.remove(selector.to_bson.native)
        end
 
@@ -547,7 +505,6 @@ class MongoCollection
        #
        # See `remove`.
        fun remove_all(selector: JsonObject): Bool do
-               assert is_alive
                return native.remove_all(selector.to_bson.native)
        end
 
@@ -556,8 +513,11 @@ class MongoCollection
        # No upsert is done, see `save` instead.
        #
        # ~~~
-       # var client = new MongoClient("mongodb://localhost:27017/")
-       # var col = client.database("test").collection("test")
+       # var client = new MongoClient("mongodb://mongo:27017/")
+       # var db_suffix = "NIT_TESTING_ID".environ
+       # var db_name = "test_{db_suffix}"
+       # var db = client.database(db_name)
+       # var col = db.collection("test")
        # var sel = new JsonObject
        # sel["foo"] = 10
        # var upd = new JsonObject
@@ -565,7 +525,6 @@ class MongoCollection
        # assert col.update(sel, upd)
        # ~~~
        fun update(selector: JsonObject, update: JsonObject): Bool do
-               assert is_alive
                return native.update(
                        selector.to_bson.native,
                        update.to_bson.native)
@@ -585,33 +544,43 @@ class MongoCollection
        # Returns `-1` if an error occured. See `Sys::last_mongoc_error`.
        #
        # ~~~
-       # var client = new MongoClient("mongodb://localhost:27017/")
-       # var col = client.database("test").collection("test")
+       # var client = new MongoClient("mongodb://mongo:27017/")
+       # var db_suffix = "NIT_TESTING_ID".environ
+       # var db_name = "test_{db_suffix}"
+       # var db = client.database(db_name)
+       # var col = db.collection("test")
        # var query = new JsonObject
        # query["foo"] = 10
        # assert col.count(query) > 0
        # ~~~
        fun count(query: JsonObject): Int do
-               assert is_alive
                return native.count(query.to_bson.native)
        end
 
        # Finds the first document that matches `query`.
        #
+       # Params:
+       # * `skip` number of documents to skip
+       # * `limit` number of documents to return
+       #
        # Returns `null` if an error occured. See `Sys::last_mongoc_error`.
        #
        # ~~~
-       # var client = new MongoClient("mongodb://localhost:27017/")
-       # var col = client.database("test").collection("test")
+       # var client = new MongoClient("mongodb://mongo:27017/")
+       # var db_suffix = "NIT_TESTING_ID".environ
+       # var db_name = "test_{db_suffix}"
+       # var db = client.database(db_name)
+       # var col = db.collection("test")
        # var query = new JsonObject
        # query["foo"] = 10
        # var doc = col.find(query)
        # assert doc["foo"] == 10
        # ~~~
-       fun find(query: JsonObject): nullable JsonObject do
-               assert is_alive
+       fun find(query: JsonObject, skip, limit: nullable Int): nullable JsonObject do
                var q = new NativeBSON.from_json_string(query.to_json.to_cstring)
-               var c = native.find(q)
+               var s = skip or else 0
+               var l = limit or else 0
+               var c = native.find(q, s, l)
                q.destroy
                if c == null then return null
                var cursor = new MongoCursor(c)
@@ -625,20 +594,70 @@ class MongoCollection
 
        # Finds all the documents matching the `query`.
        #
+       # Params:
+       # * `skip` number of documents to skip
+       # * `limit` number of documents to return
+       #
        # ~~~
-       # var client = new MongoClient("mongodb://localhost:27017/")
-       # var col = client.database("test").collection("test")
+       # var client = new MongoClient("mongodb://mongo:27017/")
+       # var db_suffix = "NIT_TESTING_ID".environ
+       # var db_name = "test_{db_suffix}"
+       # var db = client.database(db_name)
+       # var col = db.collection("test")
        # var query = new JsonObject
        # query["foo"] = 10
        # assert col.find_all(query).length > 0
        # ~~~
-       fun find_all(query: JsonObject): Array[JsonObject] do
-               assert is_alive
+       fun find_all(query: JsonObject, skip, limit: nullable Int): Array[JsonObject] do
+               var s = skip or else 0
+               var l = limit or else 0
                var res = new Array[JsonObject]
-               var c = native.find(query.to_bson.native)
+               var c = native.find(query.to_bson.native, s, l)
                if c == null then return res
                var cursor = new MongoCursor(c)
-               for item in cursor do res.add item
+               while cursor.is_ok do
+                       res.add cursor.item
+                       cursor.next
+               end
+               return res
+       end
+
+       # Applies an aggregation `pipeline` over the collection.
+       #
+       # ~~~
+       # var client = new MongoClient("mongodb://mongo:27017/")
+       # var db_suffix = "NIT_TESTING_ID".environ
+       # var db_name = "test_{db_suffix}"
+       # var db = client.database(db_name)
+       # var col = db.collection("test_aggregate")
+       #
+       # col.drop
+       #
+       # col.insert("""{ "cust_id": "A123", "amount": 500, "status": "A"}""".parse_json.as(JsonObject))
+       # col.insert("""{ "cust_id": "A123", "amount": 250, "status": "A"}""".parse_json.as(JsonObject))
+       # col.insert("""{ "cust_id": "B212", "amount": 200, "status": "A"}""".parse_json.as(JsonObject))
+       # col.insert("""{ "cust_id": "A123", "amount": 300, "status": "D"}""".parse_json.as(JsonObject))
+       #
+       # var res = col.aggregate("""[
+       #       { "$match": { "status": "A" } },
+       #       { "$group": { "_id": "$cust_id", "total": { "$sum": "$amount" } } },
+        #       { "$sort" : { "_id": 1 } }
+       # ]""".parse_json.as(JsonArray))
+       #
+        # assert res[0].to_json == """{"_id":"A123","total":750}"""
+       # assert res[1].to_json == """{"_id":"B212","total":200}"""
+       # ~~~
+       fun aggregate(pipeline: JsonArray): Array[JsonObject] do
+               var q = new JsonObject
+               q["pipeline"] = pipeline
+               var res = new Array[JsonObject]
+               var c = native.aggregate(q.to_bson.native)
+               if c == null then return res
+               var cursor = new MongoCursor(c)
+               while cursor.is_ok do
+                       res.add cursor.item
+                       cursor.next
+               end
                return res
        end
 
@@ -647,22 +666,21 @@ class MongoCollection
        # Returns `null` if an error occured. See `Sys::last_mongoc_error`.
        #
        # ~~~
-       # var client = new MongoClient("mongodb://localhost:27017/")
-       # var col = client.database("test").collection("test")
-       # assert col.stats["ns"] == "test.test"
+       # var client = new MongoClient("mongodb://mongo:27017/")
+       # var db_suffix = "NIT_TESTING_ID".environ
+       # var db_name = "test_{db_suffix}"
+       # var db = client.database(db_name)
+       # var col = db.collection("test")
+       # assert col.stats["ns"] == "{db_name}.test"
        # ~~~
        fun stats: nullable JsonObject do
-               assert is_alive
                var bson = native.stats
                if bson == null then return null
                return new JsonObject.from_bson(new BSON(bson))
        end
 
        # Drops `self`, returns false if an error occured.
-       fun drop: Bool do
-               assert is_alive
-               return native.drop
-       end
+       fun drop: Bool do return native.drop
 
        # Moves `self` to another `database`.
        #
@@ -670,7 +688,6 @@ class MongoCollection
        # this collection after the move.
        # Additional operations will occur on moved collection.
        fun move(database: MongoDb): Bool do
-               assert is_alive
                self.database = database
                return native.rename(database.name.to_cstring, name.to_cstring)
        end
@@ -681,17 +698,11 @@ class MongoCollection
        # to continue using this collection after the rename.
        # Additional operations will occur on renamed collection.
        fun rename(name: String): Bool do
-               assert is_alive
                self.name = name
                return native.rename(database.name.to_cstring, name.to_cstring)
        end
 
-       redef fun finalize do
-               if is_alive then
-                       native.destroy
-                       is_alive = false
-               end
-       end
+       redef fun finalize_once do native.destroy
 end
 
 # A MongoDB query cursor.
@@ -699,37 +710,20 @@ end
 # It wraps up the wire protocol negotation required to initiate a query and
 # retreive an unknown number of documents.
 class MongoCursor
-       super Finalizable
+       super FinalizableOnce
        super Iterator[JsonObject]
 
        private var native: NativeMongoCursor
 
-       # Is the native instance valid?
-       #
-       # This is set to false if the `native` is destroyed.
-       private var is_alive = true
-
        init do next
 
-       redef fun is_ok do
-               assert is_alive
-               return native.more
-       end
+       redef var is_ok = true
 
-       redef fun next do
-               assert is_alive
-               native.next
-       end
+       redef fun next do is_ok = native.next
 
        redef fun item do
-               assert is_alive
                return new JsonObject.from_bson(new BSON(native.current))
        end
 
-       redef fun finalize do
-               if is_alive then
-                       native.destroy
-                       is_alive = false
-               end
-       end
+       redef fun finalize_once do native.destroy
 end