├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── VERSION ├── lib ├── mongo.ex ├── mongo_collection.ex ├── mongo_cursor.ex ├── mongo_db.ex ├── mongo_find.ex ├── mongo_helpers.ex ├── mongo_request.ex ├── mongo_response.ex └── mongo_server.ex ├── mix.exs ├── mix.lock └── test ├── mongo_aggr_test.exs ├── mongo_crud_test.exs ├── mongo_cursor_test.exs ├── mongo_server_test.exs ├── mongo_test.exs └── test_helper.exs /.gitignore: -------------------------------------------------------------------------------- 1 | .eunit 2 | deps 3 | *.o 4 | *.beam 5 | *.plt 6 | erl_crash.dump 7 | _build 8 | doc 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: elixir 2 | services: 3 | - mongodb 4 | notifications: 5 | email: 6 | - jerp@checkiz.com 7 | elixir: 8 | - 1.0.2 9 | - 1.1.1 10 | otp_release: 11 | - 18.1 12 | sudo: false 13 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # v0.5.4 2 | * erlang v18 and elixir 1.1 3 | * :erlang.now/0 is depracated 4 | # v0.5.3 5 | * fix travis 6 | * fix getLastError 7 | * fix unsupported Enumerable protocol functions according to Elixir documentation 8 | # v0.5.2 9 | * fix `erlang:now()` depracated, replace with `erlang:system_time(micro_seconds)` 10 | # issue 19 and 28: retreive documents of last batch when not full 11 | * use of bson v0.4.4 12 | # v0.5.1 13 | * fix small bug for big batch 14 | * use of bson v0.4.3 15 | # v0.5.0 16 | * complete rework of the API 17 | * drop the Mongodb-like notation in favor of a more Elixir syntax 18 | * no more use of records 19 | * implements Enumerable protocol for: 20 | * %Mongo.Find{}: to retrieve all docs of a query 21 | * %Mongo.Response{}: to retrieve all docs of a particular batch (specific use) 22 | * %Mongo.Cursoer{}: to retrive all batches (specific use) 23 | 24 | in this version you write this: 25 | 26 | ```elixir 27 | coll = Mongo.connect! |> Mongo.db("test") |> Mongo.Db.collection("anycoll") 28 | ``` 29 | rather than: 30 | 31 | ```elixir 32 | coll = Mongo.connect!.db("test").collection("anycoll") 33 | ``` 34 | 35 | see https://github.com/checkiz/elixir-mongo/issues/11 36 | 37 | # v0.4.0 38 | * compatible with Elixir v1.0.0 39 | * elixir-bson v0.4.0 40 | 41 | # v0.3.1 42 | * compatible with Elixir v0.15.1 43 | 44 | # v0.3 45 | * compatible with Elixir v0.14.1 46 | 47 | # v0.2 48 | 49 | * Enhancements 50 | * Mongo.Cursor: module to interact with MongoDB cursors 51 | * Authentication 52 | * Allows to mode to get message back from MongoDB 53 | * passive: the drivers controls when to fetch responses (for sync calls) 54 | * active: MongoDB sends message back directly (allows assync calls) 55 | * getLastError, getPrevError 56 | 57 | * Bug fixes 58 | 59 | * Deprecations 60 | 61 | * Backwards incompatible changes 62 | * A major revamp of the API was necessary 63 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 checkiz 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | elixir-mongo 2 | ============ 3 | [![Build Status](https://travis-ci.org/checkiz/elixir-mongo.png?branch=master)](https://travis-ci.org/checkiz/elixir-mongo) [![Hex Version](https://img.shields.io/hexpm/v/mongo.svg)](https://hex.pm/packages/mongo) 4 | [![Hex Downloads](https://img.shields.io/hexpm/dt/bson.svg)](https://hex.pm/packages/mongo) 5 | 6 | A [MongoDB](http://www.mongodb.org) driver in Elixir. 7 | 8 | API entirely reviewed, see [CHANGELOG.md](https://github.com/checkiz/elixir-mongo/blob/master/CHANGELOG.md) 9 | 10 | ### Connecting 11 | 12 | Example preparing access to the `anycoll` collection in the `test` db : 13 | ```elixir 14 | # Connect the mongo server (by default port 27017 at 127.0.0.1) 15 | mongo = Mongo.connect! 16 | # Select the db to access 17 | db = mongo |> Mongo.db("test") 18 | # Select the db to access 19 | anycoll = db |> Mongo.Db.collection("anycoll") 20 | ``` 21 | 22 | ### Wrappers for CRUD operations 23 | 24 | Examples accessing the `anycoll` collection via CRUD operations see `Mongo.Find` 25 | 26 | 27 | ### Wrappers for Aggregate operations 28 | 29 | Example of aggregate operation applied to the `anycoll` collection see `Mongo.Collection` 30 | 31 | ### Other commands 32 | 33 | ```elixir 34 | # Authenticate against the db 35 | db |> Mongo.auth("testuser", "123")` 36 | # Retrieve the last error 37 | db |> Mongo.getLastError 38 | ``` 39 | 40 | ### Documentation 41 | 42 | - [documentation](http://checkiz.github.io/elixir-mongo) 43 | 44 | ### Dependencies 45 | 46 | - MongoDB needs a Bson encoder/decoder, this project uses the elixir-bson encoder/decoder. See [elixir-bson source repo](https://github.com/checkiz/elixir-bson) and its 47 | [documentation](http://checkiz.github.io/elixir-bson) 48 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 0.5.4 2 | -------------------------------------------------------------------------------- /lib/mongo.ex: -------------------------------------------------------------------------------- 1 | defmodule Mongo do 2 | use Mongo.Helpers 3 | @moduledoc """ 4 | [MongoDB](http://www.mongodb.org) driver in Elixir 5 | 6 | See [elixir-mongo source repo](https://github.com/checkiz/elixir-mongo) 7 | """ 8 | 9 | @doc """ 10 | Connects to a Mongo Database Server, see `Mongo.Server.connect/0` 11 | """ 12 | defdelegate connect, to: Mongo.Server 13 | 14 | @doc """ 15 | Connects to a Mongo Database Server, see `Mongo.Server.connect/2` 16 | """ 17 | defdelegate connect(host, port), to: Mongo.Server 18 | 19 | @doc """ 20 | Connects to a Mongo Database Server, see `Mongo.Server.connect/1` 21 | """ 22 | defdelegate connect(opts), to: Mongo.Server 23 | defbang connect 24 | defbang connect(opts) 25 | defbang connect(host, port) 26 | 27 | @doc """ 28 | Returns a db struct `%Mongo.Db{}, see `Mongo.Server.new/2`` 29 | """ 30 | defdelegate db(mongo, name), to: Mongo.Db, as: :new 31 | 32 | @doc """ 33 | Helper function that assigns radom ids to a list of documents when `:_id` is missing 34 | 35 | see `Mongo.Server.assign_id/2` 36 | """ 37 | defdelegate assign_id(docs), to: Mongo.Server 38 | 39 | @doc """ 40 | Helper function that assigns radom ids (with prefix) to a list of documents when `:_id` is missing 41 | 42 | see `Mongo.Server.assign_id/2` 43 | """ 44 | defdelegate assign_id(docs, mongo), to: Mongo.Server 45 | 46 | defmodule Error, do: defstruct([msg: nil, acc: []]) 47 | 48 | defmodule Bang do 49 | defexception [:message, :stack] 50 | def exception(message) when is_bitstring(message), do: %Bang{message: message} 51 | def exception(msg: msg, acc: acc), do: %Bang{message: inspect(msg), stack: acc} 52 | end 53 | 54 | end 55 | -------------------------------------------------------------------------------- /lib/mongo_collection.ex: -------------------------------------------------------------------------------- 1 | defmodule Mongo.Collection do 2 | @moduledoc """ 3 | Module holding operations that can be performed on a collection (find, count...) 4 | 5 | Usage: 6 | 7 | iex> Mongo.Helpers.test_collection("anycoll") |> Mongo.Collection.count 8 | {:ok, 6} 9 | 10 | `count()` or `count!()` 11 | 12 | The first returns `{:ok, value}`, the second returns simply `value` when the call is sucessful. 13 | In case of error, the first returns `%Mongo.Error{}` the second raises a `Mongo.Bang` exception. 14 | 15 | iex> collection = Mongo.Helpers.test_collection("anycoll") 16 | ...> {:ok, 6} = collection |> Mongo.Collection.count 17 | ...> 6 === collection |> Mongo.Collection.count! 18 | true 19 | 20 | iex> collection = Mongo.Helpers.test_collection("anycoll") 21 | ...> {:ok, 2} = collection |> Mongo.Collection.count(a: ['$in': [1,3]]) 22 | ...> %Mongo.Error{} = collection |> Mongo.Collection.count(a: ['$in': 1]) # $in should take a list, so this triggers an error 23 | ...> collection |> Mongo.Collection.count!(a: ['$in': 1]) 24 | ** (Mongo.Bang) :"cmd error" 25 | 26 | """ 27 | use Mongo.Helpers 28 | alias Mongo.Server 29 | alias Mongo.Db 30 | alias Mongo.Request 31 | 32 | defstruct [ 33 | name: nil, 34 | db: nil, 35 | opts: %{} ] 36 | 37 | @def_reduce "function(k, vs){return Array.sum(vs)}" 38 | 39 | @doc """ 40 | New collection 41 | """ 42 | def new(db, name), do: %__MODULE__{db: db, name: name, opts: Db.coll_opts(db)} 43 | 44 | @doc """ 45 | Creates a `%Mongo.Find{}` for a given collection, query and projection 46 | 47 | See `Mongo.Find` for details. 48 | 49 | """ 50 | def find(collection, criteria \\ %{}, projection \\ %{}) do 51 | Mongo.Find.new(collection, criteria, projection) 52 | end 53 | 54 | @doc """ 55 | Insert one document into the collection returns the document it received. 56 | 57 | iex> collection = Mongo.connect! |> Mongo.db("test") |> Mongo.Db.collection("anycoll") 58 | ...> %{a: 23} |> Mongo.Collection.insert_one(collection) |> elem(1) 59 | %{a: 23} 60 | 61 | """ 62 | def insert_one(doc, collection) when is_map(doc) do 63 | case insert([doc], collection) do 64 | {:ok, docs} -> {:ok, docs |> hd} 65 | error -> error 66 | end 67 | end 68 | defbang insert_one(doc, collection) 69 | 70 | @doc """ 71 | Insert a list of documents into the collection 72 | 73 | iex> collection = Mongo.connect! |> Mongo.db("test") |> Mongo.Db.collection("anycoll") 74 | ...> [%{a: 23}, %{a: 24, b: 1}] |> Mongo.Collection.insert(collection) |> elem(1) 75 | [%{a: 23}, %{a: 24, b: 1}] 76 | 77 | You can chain it with `Mongo.assign_id/1` when you need ids for further processing. If you don't Mongodb will assign ids automatically. 78 | 79 | iex> collection = Mongo.connect! |> Mongo.db("test") |> Mongo.Db.collection("anycoll") 80 | ...> [%{a: 23}, %{a: 24, b: 1}] |> Mongo.assign_id |> Mongo.Collection.insert(collection) |> elem(1) |> Enum.at(0) |> Map.has_key?(:"_id") 81 | true 82 | 83 | `Mongo.Collection.insert` returns the list of documents it received. 84 | """ 85 | def insert(docs, collection) do 86 | Server.send( 87 | collection.db.mongo, 88 | Request.insert(collection, docs)) 89 | case collection.opts[:wc] do 90 | nil -> {:ok, docs} 91 | :safe -> case collection.db |> Mongo.Db.getLastError do 92 | :ok -> {:ok, docs} 93 | error -> error 94 | end 95 | end 96 | end 97 | defbang insert(docs, collection) 98 | 99 | @doc """ 100 | Modifies an existing document or documents in the collection 101 | 102 | iex> collection = Mongo.connect! |> Mongo.db("test") |> Mongo.Db.collection("anycoll") 103 | ...> collection |> Mongo.Collection.update(%{a: 456}, %{a: 123, b: 789}) 104 | :ok 105 | 106 | """ 107 | def update(collection, query, update, upsert \\ false, multi \\ false) 108 | def update(collection, query, update, upsert, multi) do 109 | Server.send( 110 | collection.db.mongo, 111 | Request.update(collection, query, update, upsert, multi)) 112 | case collection.opts[:wc] do 113 | nil -> :ok 114 | :safe -> collection.db |> Mongo.Db.getLastError 115 | end 116 | end 117 | 118 | @doc """ 119 | Removes an existing document or documents in the collection (see db.collection.remove) 120 | 121 | iex> collection = Mongo.connect! |> Mongo.db("test") |> Mongo.Db.collection("anycoll") 122 | ...> collection |> Mongo.Collection.delete(%{b: 789}) 123 | :ok 124 | 125 | """ 126 | def delete(collection, query, justOne \\ false) 127 | def delete(collection, query, justOne) do 128 | Server.send( 129 | collection.db.mongo, 130 | Request.delete(collection, query, justOne)) 131 | case collection.opts[:wc] do 132 | nil -> :ok 133 | :safe -> collection.db |> Mongo.Db.getLastError 134 | end 135 | end 136 | 137 | @doc """ 138 | Count documents in the collection 139 | 140 | If `query` is not specify, it counts all document collection. 141 | `skip_limit` is a map that specify Mongodb otions skip and limit 142 | 143 | """ 144 | def count(collection, query \\ %{}, skip_limit \\ %{}) 145 | def count(collection, query, skip_limit) do 146 | skip_limit = Map.take(skip_limit, [:skip, :limit]) 147 | case Mongo.Db.cmd_sync(collection.db, %{count: collection.name}, 148 | Map.merge(skip_limit, %{query: query})) do 149 | {:ok, resp} -> 150 | case resp |> Mongo.Response.count do 151 | {:ok, n} -> {:ok, n |> trunc} 152 | # _error -> {:ok, -1} 153 | error -> error 154 | end 155 | error -> error 156 | end 157 | end 158 | defbang count(collection) 159 | defbang count(collection, query) 160 | defbang count(collection, query, skip_limit) 161 | 162 | @doc """ 163 | Finds the distinct values for a specified field across a single collection (see db.collection.distinct) 164 | 165 | 166 | iex> collection = Mongo.connect! |> Mongo.db("test") |> Mongo.Db.collection("anycoll") 167 | ...> collection |> Mongo.Collection.distinct!("value", %{value: %{"$lt": 3}}) 168 | [0, 1] 169 | 170 | """ 171 | def distinct(collection, key, query \\ %{}) 172 | def distinct(collection, key, query) do 173 | case Mongo.Db.cmd_sync(collection.db, %{distinct: collection.name}, %{key: key, query: query}) do 174 | {:ok, resp} -> Mongo.Response.distinct(resp) 175 | error -> error 176 | end 177 | end 178 | defbang distinct(key, collection) 179 | defbang distinct(key, query, collection) 180 | 181 | @doc """ 182 | Provides a wrapper around the mapReduce command 183 | 184 | Returns `:ok` or an array of documents (with option `:inline` active - set by default). 185 | 186 | iex> collection = Mongo.connect! |> Mongo.db("test") |> Mongo.Db.collection("anycoll") 187 | ...> Mongo.Collection.mr!(collection, "function(d){emit(this._id, this.value*2)}", "function(k, vs){return Array.sum(vs)}") |> is_list 188 | true 189 | 190 | %{_id: Bson.ObjectId.from_string("542aa3fab9742bc0d5eaa12d"), value: 0.0} 191 | 192 | iex> collection = Mongo.connect! |> Mongo.db("test") |> Mongo.Db.collection("anycoll") 193 | ...> Mongo.Collection.mr!(collection, "function(d){emit('z', 3*this.value)}", "function(k, vs){return Array.sum(vs)}", "mrcoll") 194 | :ok 195 | 196 | """ 197 | def mr(collection, map, reduce \\ @def_reduce, out \\ %{inline: true}, params \\ %{}) 198 | def mr(collection, map, reduce, out, params) do 199 | params = Map.take(params, [:limit, :finalize, :scope, :jsMode, :verbose]) 200 | case Mongo.Db.cmd_sync(collection.db, %{mapReduce: collection.name}, Map.merge(params, %{map: map, reduce: reduce, out: out})) do 201 | {:ok, resp} -> Mongo.Response.mr resp 202 | error -> error 203 | end 204 | end 205 | defbang mr(map, collection) 206 | defbang mr(map, reduce, collection) 207 | defbang mr(map, reduce, out, collection) 208 | defbang mr(map, reduce, out, more, collection) 209 | 210 | @doc """ 211 | Groups documents in the collection by the specified key 212 | 213 | iex> collection = Mongo.connect! |> Mongo.db("test") |> Mongo.Db.collection("anycoll") 214 | ...> collection |> Mongo.Collection.group!(%{a: true}) |> is_list 215 | true 216 | 217 | [%{a: 0.0}, %{a: 1.0}, %{a: 2.0}, ...] 218 | 219 | """ 220 | def group(collection, key, reduce \\ @def_reduce, initial \\ %{}, params \\ %{}) 221 | def group(collection, key, reduce, initial, params) do 222 | params = Map.take(params, [:'$keyf', :cond, :finalize]) 223 | if params[:keyf], do: params = Map.put_new(:'$keyf', params[:keyf]) 224 | case Mongo.Db.cmd_sync(collection.db, %{group: Map.merge(params, %{ns: collection.name, key: key, '$reduce': reduce, initial: initial})}) do 225 | {:ok, resp} -> Mongo.Response.group resp 226 | error -> error 227 | end 228 | end 229 | defbang group(key, collection) 230 | defbang group(key, reduce, collection) 231 | defbang group(key, reduce, initial, collection) 232 | defbang group(key, reduce, initial, params, collection) 233 | 234 | @doc """ 235 | Drops the collection 236 | 237 | returns `:ok` or a string containing the error message 238 | """ 239 | def drop(collection) do 240 | case Db.cmd_sync(collection.db, %{drop: collection.name}) do 241 | {:ok, resp} -> Mongo.Response.success resp 242 | error -> error 243 | end 244 | end 245 | defbang drop(collection) 246 | 247 | @doc """ 248 | Calculates aggregate values for the data in the collection (see db.collection.aggregate) 249 | 250 | iex> collection = Mongo.Helpers.test_collection("anycoll") 251 | ...> collection |> Mongo.Collection.aggregate([ 252 | ...> %{'$skip': 1}, 253 | ...> %{'$limit': 5}, 254 | ...> %{'$project': %{'_id': false, value: true}} ]) 255 | [%{value: 1}, %{value: 1}, %{value: 1}, %{value: 1}, %{value: 3}] 256 | 257 | """ 258 | def aggregate(collection, pipeline) do 259 | case Mongo.Db.cmd_sync(collection.db, %{aggregate: collection.name}, %{pipeline: pipeline} ) do 260 | {:ok, resp} -> Mongo.Response.aggregate resp 261 | error -> error 262 | end 263 | end 264 | defbang aggregate(pipeline, collection) 265 | 266 | @doc """ 267 | Adds options to the collection overwriting database options 268 | 269 | new_opts must be a map with zero or more pairs represeting one of these options: 270 | 271 | * read: `:awaitdata`, `:nocursortimeout`, `:slaveok`, `:tailablecursor` 272 | * write concern: `:wc` 273 | * socket: `:mode`, `:timeout` 274 | """ 275 | def opts(collection, new_opts) do 276 | %__MODULE__{collection| opts: Map.merge(collection.opts, new_opts)} 277 | end 278 | 279 | @doc """ 280 | Gets read default options 281 | """ 282 | def read_opts(collection) do 283 | Map.take(collection.opts, [:awaitdata, :nocursortimeout, :slaveok, :tailablecursor, :mode, :timeout]) 284 | end 285 | 286 | @doc """ 287 | Gets write default options 288 | """ 289 | def write_opts(collection) do 290 | Map.take(collection.opts, [:wc, :mode, :timeout]) 291 | end 292 | 293 | @doc """ 294 | Creates an index for the collection 295 | """ 296 | def createIndex(collection, name, key, unique \\ false, options \\ %{}) do 297 | system_indexes = new(collection.db, "system.indexes") 298 | %{name: name, ns: collection.db.name <> "." <> collection.name, key: key, unique: unique} 299 | |> Map.merge(options) 300 | |> insert_one(system_indexes) 301 | end 302 | 303 | @doc """ 304 | Gets a list of All Indexes 305 | """ 306 | def getIndexes(collection) do 307 | new(collection.db, "system.indexes") 308 | |> find(%{ns: collection.db.name <> "." <> collection.name}) 309 | |> Enum.to_list 310 | end 311 | 312 | @doc """ 313 | Remove a Specific Index 314 | col = Mongo.connect! |> Mongo.db("AF_VortexShort_2358") |> Mongo.Db.collection("test.test") 315 | col |> Mongo.Collection.dropIndex(%{time: 1}) 316 | """ 317 | def dropIndex(collection, key) do 318 | Server.send(collection.db.mongo, 319 | Request.cmd(collection.db.name, %{deleteIndexes: collection.name}, %{index: key})) 320 | end 321 | 322 | @doc """ 323 | Remove All Indexes 324 | """ 325 | def dropIndexes(collection) do 326 | dropIndex(collection, "*") 327 | end 328 | 329 | end 330 | -------------------------------------------------------------------------------- /lib/mongo_cursor.ex: -------------------------------------------------------------------------------- 1 | defmodule Mongo.Cursor do 2 | require Record 3 | @moduledoc """ 4 | Manages MongoDB [cursors](http://docs.mongodb.org/manual/core/cursors/). 5 | 6 | Cursors are returned by a find see `Mongo.Collection.find/3`. 7 | """ 8 | use Mongo.Helpers 9 | 10 | defstruct [ 11 | collection: nil, 12 | response: nil, 13 | batchSize: 0, 14 | exhausted: nil] 15 | 16 | def next_batch(%Mongo.Cursor{exhausted: true}), do: nil 17 | def next_batch(cursor) do 18 | mongo = cursor.collection.db.mongo 19 | case Mongo.Server.send(mongo, Mongo.Request.get_more(cursor.collection, cursor.batchSize, cursor.response.cursorID)) do 20 | {:ok, _reqid} -> 21 | case Mongo.Server.response(mongo) do 22 | {:ok, response} -> %Mongo.Cursor{cursor| response: response, exhausted: response.cursorID == 0} 23 | error -> error 24 | end 25 | error -> error 26 | end 27 | end 28 | 29 | def exec(collection, query, batchSize \\ 0) do 30 | mongo = collection.db.mongo 31 | case Mongo.Server.send(mongo, query) do 32 | {:ok, _reqid} -> 33 | case Mongo.Server.response(mongo) do 34 | {:ok, initialResponse} -> 35 | %Mongo.Cursor{ collection: collection, 36 | response: initialResponse, 37 | exhausted: initialResponse.cursorID == 0, 38 | batchSize: batchSize} 39 | %Mongo.Error{}=error -> error 40 | # {:error, msg} -> %Mongo.Error{msg: msg} 41 | end 42 | error -> error 43 | end 44 | end 45 | 46 | defimpl Enumerable do 47 | 48 | @doc """ 49 | Reduce documents in the buffer into a value 50 | """ 51 | def reduce(cursor, acc, reducer) 52 | def reduce(cursor, {:cont, acc}, reducer) do 53 | case reducer.(cursor.response, acc) do 54 | {:cont, acc} -> 55 | if cursor.exhausted do 56 | {:done, acc} 57 | else 58 | case Mongo.Cursor.next_batch(cursor) do 59 | %Mongo.Cursor{exhausted: true, response: %Mongo.Response{nbdoc: 0}} -> 60 | {:done, acc} 61 | %Mongo.Cursor{exhausted: true, response: response} -> 62 | {:cont, acc} = reducer.(response, acc) 63 | {:done, acc} 64 | %Mongo.Cursor{}=cursor -> reduce(cursor, {:cont, acc}, reducer) 65 | error -> {:halted, %Mongo.Error{error| acc: [cursor | error.acc]}} 66 | end 67 | end 68 | reduced -> reduce(cursor, reduced, reducer) 69 | end 70 | end 71 | def reduce(_, {:halt, acc}, _reducer), do: {:halted, acc} 72 | def reduce(cursor, {:suspend, acc}, reducer), do: {:suspended, acc, &reduce(cursor, &1, reducer)} 73 | 74 | 75 | @doc false 76 | #Not implemented use `Mongo.Collection.count/1` 77 | def count(_cursor), do: {:error, __MODULE__} 78 | 79 | @doc false 80 | #Not implemented 81 | def member?(_, _cursor), do: {:error, __MODULE__} 82 | end 83 | 84 | end 85 | -------------------------------------------------------------------------------- /lib/mongo_db.ex: -------------------------------------------------------------------------------- 1 | defmodule Mongo.Db do 2 | @moduledoc """ 3 | Module holding operations that can be performed on MongoDB databases 4 | """ 5 | defstruct [ 6 | name: nil, 7 | mongo: nil, 8 | auth: nil, 9 | opts: %{} ] 10 | 11 | use Mongo.Helpers 12 | 13 | alias Mongo.Server 14 | 15 | @doc """ 16 | Creates `%Mongo.Db{}` with default options 17 | """ 18 | def new(mongo, name), do: %Mongo.Db{mongo: mongo, name: name, opts: Server.db_opts(mongo)} 19 | 20 | @doc """ 21 | Authenticates a user to a database 22 | 23 | Expects a DB struct, a user and a password returns `{:ok, db}` or `%Mongo.Error{}` 24 | """ 25 | def auth(db, username, password) do 26 | %Mongo.Db{db| auth: {username, hash(username <> ":mongo:" <> password)}} |> auth 27 | end 28 | defbang auth(username, password, db) 29 | 30 | @doc """ 31 | Check authentication 32 | 33 | returns true if authentication was performed and succesful 34 | """ 35 | def auth?(db) 36 | def auth?(%Mongo.Db{auth: nil}), do: false 37 | def auth?(_), do: true 38 | 39 | @doc false 40 | # Authenticates a user to a database (or do it again after failure) 41 | def auth(%Mongo.Db{auth: nil}=db), do: {:ok, db} 42 | def auth(%Mongo.Db{auth: {username, hash_password}}=db) do 43 | nonce = getnonce(db) 44 | case Mongo.Request.cmd(db, %{authenticate: 1}, %{nonce: nonce, user: username, key: hash(nonce <> username <> hash_password)}) 45 | |> Server.call do 46 | {:ok, resp} -> 47 | case resp.success do 48 | :ok ->{:ok, db} 49 | error -> error 50 | end 51 | error -> error 52 | end 53 | end 54 | 55 | @doc """ 56 | Returns a collection struct 57 | """ 58 | defdelegate collection(db, name), to: Mongo.Collection, as: :new 59 | 60 | @doc """ 61 | Executes a db command requesting imediate response 62 | """ 63 | def cmd_sync(db, command, cmd_args \\ %{}) do 64 | case cmd(db, command, cmd_args) do 65 | {:ok, _reqid} -> Server.response(db.mongo) 66 | error -> error 67 | end 68 | end 69 | 70 | @doc """ 71 | Executes a db command 72 | 73 | Before using this check `Mongo.Collection`, `Mongo.Db` or `Mongo.Server` 74 | for commands already implemented by these modules 75 | """ 76 | def cmd(db, cmd, cmd_args \\ %{}) do 77 | Server.send(db.mongo, Mongo.Request.cmd(db.name, cmd, cmd_args)) 78 | end 79 | defbang cmd(db, command) 80 | 81 | # creates a md5 hash in hex with loawercase 82 | defp hash(data) do 83 | :crypto.hash(:md5, data) |> binary_to_hex 84 | end 85 | 86 | # creates an hex string from binary 87 | defp binary_to_hex(bin) do 88 | for << <> <- bin >>, into: <<>> do 89 | <> 90 | end |> String.downcase 91 | end 92 | 93 | # get `nonce` token from server 94 | defp getnonce(db) do 95 | case cmd_sync(db, %{getnonce: true}) do 96 | {:ok, resp} -> resp |> Mongo.Response.getnonce 97 | error -> error 98 | end 99 | end 100 | 101 | @doc """ 102 | Returns the error status of the preceding operation. 103 | """ 104 | def getLastError(db) do 105 | case cmd_sync(db, %{getlasterror: true}) do 106 | {:ok, resp} -> resp |> Mongo.Response.error 107 | error -> error 108 | end 109 | end 110 | defbang getLastError(db) 111 | 112 | @doc """ 113 | Returns the previous error status of the preceding operation(s). 114 | """ 115 | def getPrevError(db) do 116 | case cmd_sync(db, %{getPrevError: true}) do 117 | {:ok, resp} -> resp |> Mongo.Response.error 118 | error -> error 119 | end 120 | end 121 | defbang getPrevError(db) 122 | 123 | @doc """ 124 | Resets error 125 | """ 126 | def resetError(db) do 127 | case cmd(db, %{resetError: true}) do 128 | {:ok, _} -> :ok 129 | error -> error 130 | end 131 | end 132 | defbang resetError(db) 133 | 134 | @doc """ 135 | Kill a cursor of the db 136 | """ 137 | def kill_cursor(db, cursorID) do 138 | Mongo.Request.kill_cursor(cursorID) |> Server.send(db.mongo) 139 | end 140 | 141 | @doc """ 142 | Adds options to the database overwriting mongo server connection options 143 | 144 | new_opts must be a map with zero or more of the following keys: 145 | 146 | * read: `:awaitdata`, `:nocursortimeout`, `:slaveok`, `:tailablecursor` 147 | * write concern: `:wc` 148 | * socket: `:mode`, `:timeout` 149 | """ 150 | def opts(db, new_opts) do 151 | %Mongo.Db{db| opts: Map.merge(db.opts, new_opts)} 152 | end 153 | 154 | @doc """ 155 | Gets collection default options 156 | """ 157 | def coll_opts(db) do 158 | Map.take(db.opts, [:awaitdata, :nocursortimeout, :slaveok, :tailablecursor, :wc]) 159 | end 160 | 161 | end 162 | -------------------------------------------------------------------------------- /lib/mongo_find.ex: -------------------------------------------------------------------------------- 1 | defmodule Mongo.Find do 2 | @moduledoc """ 3 | Find operation on MongoDB 4 | """ 5 | use Mongo.Helpers 6 | 7 | defstruct [ 8 | mongo: nil, 9 | collection: nil, 10 | selector: %{}, 11 | projector: %{}, 12 | batchSize: 0, 13 | skip: 0, 14 | opts: %{}, 15 | mods: %{}] 16 | 17 | @doc """ 18 | Creates a new find operation. 19 | 20 | Not to be used directly, prefer `Mongo.Collection.find/3` 21 | """ 22 | def new(collection, jsString, projector) when is_binary(jsString), do: new(collection, %{'$where': jsString}, projector) 23 | def new(collection, selector, projector) do 24 | %__MODULE__{collection: collection, selector: selector, projector: projector, opts: collection |> Mongo.Collection.read_opts} 25 | end 26 | 27 | @doc """ 28 | Sets where MongoDB begins returning results 29 | 30 | Must be run before executing the query 31 | 32 | iex> Mongo.connect.%@m{"test"}.collection("anycoll").find.skip(1).toArray |> Enum.count 33 | 5 34 | iex> Mongo.connect.%@m{"test"}.collection("anycoll").find.skip(2).toArray |> Enum.count 35 | 4 36 | 37 | """ 38 | def skip(find, skip), do: %__MODULE__{find| skip: skip} 39 | 40 | @doc """ 41 | Executes the query and returns a `%Mongo.Cursor{}` 42 | """ 43 | def exec(find) do 44 | Mongo.Cursor.exec(find.collection, Mongo.Request.query(find), find.batchSize) 45 | end 46 | 47 | @doc """ 48 | Runs the explain operator that provides information on the query plan 49 | """ 50 | def explain(find) do 51 | find |> addSpecial(:'$explain', 1) |> Enum.at(0) 52 | end 53 | 54 | @doc """ 55 | Add hint opperator that forces the query optimizer to use a specific index to fulfill the query 56 | """ 57 | def hint(f, hints) 58 | def hint(f, indexName) when is_atom(indexName), do: f |> addSpecial(:'$hint', indexName) 59 | def hint(f, hints) when is_map(hints), do: f |> addSpecial(:'$hint', hints) 60 | 61 | @doc """ 62 | Sets query options 63 | 64 | Defaults option set is equivalent of calling: 65 | 66 | Find.opts( 67 | awaitdata: false 68 | nocursortimeout: false 69 | slaveok: true 70 | tailablecursor: false) 71 | """ 72 | def opts(find, options), do: %__MODULE__{find| opts: options} 73 | 74 | def addSpecial(find, k, v) do 75 | %__MODULE__{find| mods: Map.put(find.mods, k, v)} 76 | end 77 | 78 | defimpl Enumerable do 79 | 80 | @doc """ 81 | Executes the query and reduce retrieved documents into a value 82 | """ 83 | def reduce(find, acc, reducer) do 84 | case Mongo.Find.exec(find) do 85 | %Mongo.Cursor{}=cursor -> 86 | case Enumerable.reduce(cursor, {:cont, acc}, 87 | fn(response, acc)-> 88 | case Enumerable.reduce(response, acc, reducer) do 89 | {:done, acc} -> {:cont, {:cont, acc}} 90 | {:halted, acc} -> {:halt, acc} 91 | {:suspended, acc} -> {:suspend, acc} 92 | error -> {:halt, error} 93 | end 94 | end) do 95 | {:done, {:cont, acc}} -> {:done, acc} 96 | other -> other 97 | end 98 | error -> error 99 | end 100 | end 101 | 102 | @doc """ 103 | Counts number of documents to be retreived 104 | """ 105 | def count(find) do 106 | case Mongo.Collection.count(find.collection, find.selector, Map.take(find, [:skip, :limit])) do 107 | %Mongo.Error{} -> -1 108 | n -> n 109 | end 110 | end 111 | 112 | @doc """ 113 | Not implemented 114 | """ 115 | def member?(_, _), do: {:error, __MODULE__} 116 | end 117 | end 118 | -------------------------------------------------------------------------------- /lib/mongo_helpers.ex: -------------------------------------------------------------------------------- 1 | defmodule Mongo.Helpers do 2 | @moduledoc "Helper macros" 3 | defmacro __using__(_opts) do 4 | quote do 5 | import Mongo.Helpers 6 | end 7 | end 8 | 9 | @doc """ 10 | Helps defining a function like `count!` calling function `count` 11 | 12 | When `count` returns `{:ok, value}`, `count!` returns `value` 13 | 14 | When `count` returns `{:error, reason}`, `count!` raises an exception 15 | """ 16 | defmacro defbang({ name, _, args }) do 17 | unless args |> is_list do 18 | args = [] 19 | end 20 | 21 | {:__block__, [], quoted} = 22 | quote bind_quoted: [name: Macro.escape(name), args: Macro.escape(args)] do 23 | def unquote(to_string(name) <> "!" |> String.to_atom)(unquote_splicing(args)) do 24 | case unquote(name)(unquote_splicing(args)) do 25 | :ok -> :ok 26 | nil -> nil 27 | { :ok, result } -> result 28 | { :error, reason } -> raise Mongo.Bang, msg: reason, acc: unquote(args) 29 | %{msg: msg, acc: acc}=err -> raise Mongo.Bang, msg: msg, acc: acc 30 | end 31 | end 32 | end 33 | {:__block__, [], [{:@, [context: Mongo.Helpers, import: Kernel], [{:doc, [], ["See "<>to_string(name)<>"/"<>to_string(args |> length)]}]}|quoted]} 34 | end 35 | 36 | @doc """ 37 | Feeds sample data into a collection of database `test` 38 | """ 39 | def test_collection(collname) do 40 | mongo = Mongo.connect! 41 | db = Mongo.db(mongo, "test") 42 | collection = Mongo.Db.collection(db, collname) 43 | Mongo.Collection.drop collection 44 | [ 45 | %{a: 0, value: 0}, 46 | %{a: 1, value: 1}, 47 | %{a: 2, value: 1}, 48 | %{a: 3, value: 1}, 49 | %{a: 4, value: 1}, 50 | %{a: 5, value: 3} ] |> Mongo.Collection.insert(collection) 51 | collection 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /lib/mongo_request.ex: -------------------------------------------------------------------------------- 1 | defmodule Mongo.Request do 2 | @moduledoc """ 3 | Defines, encodes and sends MongoDB operations to the server 4 | """ 5 | 6 | defstruct [ 7 | requestID: nil, 8 | payload: nil] 9 | 10 | 11 | @update <<0xd1, 0x07, 0, 0>> # 2001 update document 12 | @insert <<0xd2, 0x07, 0, 0>> # 2002 insert new document 13 | @get_more <<0xd5, 0x07, 0, 0>> # 2005 Get more data from a query. See Cursors 14 | @delete <<0xd6, 0x07, 0, 0>> # 2006 Delete documents 15 | @kill_cursor <<0xd7, 0x07, 0, 0>> # 2007 Tell database client is done with a cursor 16 | 17 | @query <<0xd4, 0x07, 0, 0>> # 2004 query a collection 18 | @query_opts <<0b00000100::8>> # default query options, equvalent to `cursor.set_opts(slaveok: true)` 19 | 20 | @doc """ 21 | Builds a query message 22 | 23 | * collection: collection 24 | * selector: selection criteria (Map or nil) 25 | * projector: fields (Map or nil) 26 | """ 27 | def query(find) do 28 | selector = if find.mods == %{}, do: find.selector, else: Map.put(find.mods, :'$query', find.selector) 29 | @query <> (Enum.reduce(find.opts, @query_opts, &queryopt_red/2)) <> <<0::24>> <> 30 | find.collection.db.name <> "." <> find.collection.name <> <<0::8>> <> 31 | <> <> 32 | <> <> 33 | Bson.encode(selector) <> 34 | Bson.encode(find.projector) 35 | end 36 | 37 | @doc """ 38 | Builds a database command message composed of the command tag and its arguments. 39 | """ 40 | def cmd(dbname, cmd, cmd_args \\ %{}) do 41 | @query <> @query_opts <> <<0::24>> <> # [slaveok: true] 42 | dbname <> ".$cmd" <> 43 | <<0::40, 255, 255, 255, 255>> <> # skip(0), batchSize(-1) 44 | document(cmd, cmd_args) 45 | end 46 | 47 | @doc """ 48 | Builds an insert command message 49 | """ 50 | def insert(collection, docs) do 51 | docs |> Enum.reduce( 52 | @insert <> <<0::32>> <> 53 | collection.db.name <> "." <> collection.name <> <<0::8>>, 54 | fn(doc, acc) -> acc <> Bson.encode(doc) end) 55 | end 56 | 57 | @doc """ 58 | Builds an update command message 59 | """ 60 | def update(collection, selector, update, upsert, multi) do 61 | @update <> <<0::32>> <> 62 | collection.db.name <> "." <> collection.name <> <<0::8>> <> 63 | <<0::6, (bit(multi))::1, (bit(upsert))::1, 0::24>> <> 64 | (document(selector) ) <> 65 | (document(update)) 66 | end 67 | # transforms `true` and `false` to bits 68 | defp bit(false), do: 0 69 | defp bit(true), do: 1 70 | 71 | @doc """ 72 | Builds a delete command message 73 | """ 74 | def delete(collection, selector, justOne) do 75 | @delete <> <<0::32>> <> 76 | collection.db.name <> "." <> collection.name <> <<0::8>> <> 77 | <<0::7, (bit(justOne))::1, 0::24>> <> 78 | document(selector) 79 | end 80 | 81 | @doc """ 82 | Builds a kill_cursor command message 83 | """ 84 | def kill_cursor(cursorid) do 85 | @kill_cursor <> <<0::32>> <> 86 | <<1::32-little-signed>> <> 87 | <> 88 | end 89 | 90 | @doc """ 91 | Builds a get_more command message 92 | """ 93 | def get_more(collection, batchsize, cursorid) do 94 | @get_more <> <<0::32>> <> 95 | collection.db.name <> "." <> collection.name <> <<0::8>> <> 96 | <> <> 97 | <> 98 | end 99 | 100 | # transform a document into bson 101 | defp document(command), do: Bson.encode(command) 102 | defp document(command, command_args), do: Bson.Encoder.document(Enum.to_list(command) ++ Enum.to_list(command_args)) 103 | 104 | use Bitwise 105 | # Operates one option 106 | defp queryopt_red({opt, true}, bits), do: bits ||| queryopt(opt) 107 | defp queryopt_red({opt, false}, bits), do: bits &&& ~~~queryopt(opt) 108 | defp queryopt_red(_, bits), do: bits 109 | # Identifies the bit that is switched by an option when it is set to `true` 110 | defp queryopt(:awaitdata), do: 0b00100000 111 | defp queryopt(:nocursortimeout), do: 0b00010000 112 | defp queryopt(:slaveok), do: 0b00000100 113 | defp queryopt(:tailablecursor), do: 0b00000010 114 | defp queryopt(_), do: 0b00000000 115 | 116 | end 117 | -------------------------------------------------------------------------------- /lib/mongo_response.ex: -------------------------------------------------------------------------------- 1 | defmodule Mongo.Response do 2 | @moduledoc """ 3 | Receives, decode and parse MongoDB response from the server 4 | """ 5 | defstruct [ 6 | cursorID: nil, 7 | startingFrom: nil, 8 | nbdoc: nil, 9 | buffer: nil, 10 | requestID: nil, 11 | decoder: nil] 12 | 13 | @msg <<1, 0, 0, 0>> # 1 Opcode OP_REPLY : Reply to a client request 14 | 15 | defimpl Enumerable, for: Mongo.Response do 16 | 17 | @doc """ 18 | Reduce documents in the buffer into a value 19 | """ 20 | def reduce(resp, acc, reducer) 21 | def reduce(%Mongo.Response{buffer: buffer, nbdoc: nbdoc, decoder: decoder}, acc, reducer), do: do_reduce(buffer, nbdoc, decoder, acc, reducer) 22 | 23 | defp do_reduce(_, _, _, {:halt, acc}, _fun), do: {:halted, acc} 24 | defp do_reduce(buffer, nbdoc, decoder, {:suspend, acc}, reducer), do: {:suspended, acc, &do_reduce(buffer, nbdoc, decoder, &1, reducer)} 25 | defp do_reduce(_, 0, _, {:cont, acc}, _reducer), do: {:done, acc} 26 | defp do_reduce(buffer, nbdoc, decoder, {:cont, acc}, reducer) do 27 | case decoder.(buffer) do 28 | {:cont, doc, rest} -> do_reduce(rest, nbdoc-1, decoder, reducer.(doc, acc), reducer) 29 | other -> other 30 | end 31 | end 32 | 33 | @doc """ 34 | Retreives number of documents in the buffer 35 | """ 36 | def count(%Mongo.Response{nbdoc: nbdoc}), do: {:ok, nbdoc} 37 | 38 | @doc """ 39 | Checks whether a document is part of the buffer 40 | """ 41 | def member?(resp, doc) 42 | def member?(%Mongo.Response{buffer: buffer}, doc), do: is_member(buffer, doc) 43 | 44 | defp is_member(buffer, doc), do: is_member(buffer, doc, byte_size(doc)) 45 | # size are identical, check content 46 | defp is_member(buffer, _doc, docsize) when byte_size(buffer) < docsize, do: false 47 | defp is_member(<>=buffer, doc, docsize) do 48 | case :erlang.split_binary(buffer, docsize) do 49 | {^doc, _} -> {:ok, true} 50 | {_, tail} -> is_member(tail, doc) 51 | end 52 | end 53 | # size different, skip to next doc 54 | defp is_member(<>=buffer, doc, docsize) do 55 | is_member(:erlang.split_binary(buffer, size)|>elem(1), doc, docsize) 56 | end 57 | end 58 | 59 | @doc """ 60 | Parses a response message 61 | 62 | If the message is partial, this method makes shure the response is complete by fetching additional messages 63 | """ 64 | def new( 65 | <<_::32, # total message size, including this 66 | _::32, # identifier for this message 67 | requestID::size(32)-signed-little, # requestID from the original request 68 | @msg::binary, # Opcode OP_REPLY 69 | _::6, queryFailure::1, cursorNotFound::1, _::24, # bit vector representing response flags 70 | cursorID::size(64)-signed-little, # cursor id if client needs to do get more's 71 | startingFrom::size(32)-signed-little, # where in the cursor this reply is starting 72 | numberReturned::size(32)-signed-little, # number of documents in the reply 73 | buffer::bitstring>>, # buffer of Bson documents 74 | decoder \\ &(Mongo.Response.bson_decode(&1))) do 75 | cond do 76 | cursorNotFound>0 -> 77 | %Mongo.Error{msg: :"cursor not found"} 78 | queryFailure>0 -> 79 | if numberReturned>0 do 80 | %Mongo.Error{ 81 | msg: :"query failure", 82 | acc: %Mongo.Response{buffer: buffer, nbdoc: numberReturned, decoder: decoder}|>Enum.to_list} 83 | else 84 | %Mongo.Error{msg: :"query failure"} 85 | end 86 | true -> {:ok, %Mongo.Response{ 87 | cursorID: cursorID, 88 | startingFrom: startingFrom, 89 | nbdoc: numberReturned, 90 | buffer: buffer, 91 | requestID: requestID, 92 | decoder: decoder }} 93 | end 94 | end 95 | 96 | @doc """ 97 | Decodes a command response 98 | 99 | Returns `{:ok, doc}` or transfers the error message 100 | """ 101 | def cmd(%Mongo.Response{nbdoc: 1, buffer: buffer}) do 102 | case buffer |> Bson.decode do 103 | nil -> %Mongo.Error{msg: :"no document received"} 104 | %{ok: ok}=doc when ok>0 -> {:ok, doc} 105 | errdoc -> %Mongo.Error{msg: :"cmd error", acc: errdoc} 106 | end 107 | end 108 | 109 | @doc """ 110 | Decodes a count respsonse 111 | 112 | Returns `{:ok, n}` or transfers the error message 113 | """ 114 | def count(response) do 115 | case cmd(response) do 116 | {:ok, doc} -> {:ok, doc[:n]} 117 | error -> error 118 | end 119 | end 120 | 121 | @doc """ 122 | Decodes a success respsonse 123 | 124 | Returns `:ok` or transfers the error message 125 | """ 126 | def success(response) do 127 | case cmd(response) do 128 | {:ok, _} -> :ok 129 | error -> error 130 | end 131 | end 132 | 133 | @doc """ 134 | Decodes a distinct respsonse 135 | 136 | Returns `{:ok, values}` or transfers the error message 137 | """ 138 | def distinct(response) do 139 | case cmd(response) do 140 | {:ok, doc} -> {:ok, doc[:values]} 141 | error -> error 142 | end 143 | end 144 | 145 | @doc """ 146 | Decodes a map-reduce respsonse 147 | 148 | Returns `{:ok, results}` (inline) or `:ok` or transfers the error message 149 | """ 150 | def mr(response) do 151 | case cmd(response) do 152 | {:ok, doc} -> 153 | case doc[:results] do 154 | nil -> :ok 155 | results -> {:ok, results} 156 | end 157 | error -> error 158 | end 159 | end 160 | 161 | @doc """ 162 | Decodes a group respsonse 163 | 164 | Returns `{:ok, retval}` or transfers the error message 165 | """ 166 | def group(response) do 167 | case cmd(response) do 168 | {:ok, doc} -> {:ok, doc[:retval]} 169 | error -> error 170 | end 171 | end 172 | 173 | @doc """ 174 | Decodes an aggregate respsonse 175 | 176 | Returns `{:ok, result}` or transfers the error message 177 | """ 178 | def aggregate(response) do 179 | case cmd(response) do 180 | {:ok, doc} -> doc[:result] 181 | error -> error 182 | end 183 | end 184 | @doc """ 185 | Decodes a getnonce respsonse 186 | 187 | Returns `{:ok, nonce}` or transfers the error message 188 | """ 189 | def getnonce(response) do 190 | case cmd(response) do 191 | {:ok, doc} -> doc[:nonce] 192 | error -> error 193 | end 194 | end 195 | @doc """ 196 | Decodes an error respsonse 197 | 198 | Returns `{:ok, nonce}` or transfers the error message 199 | """ 200 | def error(response) do 201 | case cmd(response) do 202 | {:ok, doc} -> 203 | case doc[:err] do 204 | nil -> :ok 205 | _ -> {:error, doc} 206 | end 207 | error -> error 208 | end 209 | end 210 | 211 | @doc """ 212 | Helper fuction to decode the first document of a bson buffer 213 | """ 214 | def bson_decode(buffer, opts \\ %Bson.Decoder{}) do 215 | case Bson.Decoder.document(buffer, opts) do 216 | %Bson.Decoder.Error{}=error -> {:halt, %Mongo.Error{msg: :bson, acc: [error]}} 217 | {doc, rest} -> {:cont, doc, rest} 218 | end 219 | end 220 | 221 | @doc """ 222 | Helper fuction to split buffer into documents in binary format 223 | """ 224 | def bson_no_decoding(<>=buffer) do 225 | {doc, rest} = :erlang.split_binary(buffer, size) 226 | {:cont, doc, rest} 227 | end 228 | end 229 | -------------------------------------------------------------------------------- /lib/mongo_server.ex: -------------------------------------------------------------------------------- 1 | defmodule Mongo.Server do 2 | import Kernel, except: [send: 2] 3 | @moduledoc """ 4 | Manage the connection to a mongodb server 5 | """ 6 | defstruct [ 7 | host: nil, 8 | port: nil, 9 | mode: false, 10 | timeout: nil, 11 | opts: %{}, 12 | id_prefix: nil, 13 | socket: nil ] 14 | 15 | @port 27017 16 | @mode :passive 17 | @host "127.0.0.1" 18 | @timeout 6000 19 | 20 | use Mongo.Helpers 21 | 22 | @doc """ 23 | connects to local mongodb server by defaults to {"127.0.0.1", 27017} 24 | 25 | This can be overwritten by the environment variable `:host`, ie: 26 | 27 | ```erlang 28 | [ 29 | {mongo, 30 | [ 31 | {host, {"127.0.0.1", 27017}} 32 | ]} 33 | ]. 34 | ``` 35 | """ 36 | def connect do 37 | connect %{} 38 | end 39 | 40 | @doc """ 41 | connects to a mongodb server 42 | """ 43 | def connect(host, port) when is_binary(host) and is_integer(port) do 44 | connect %{host: host, port: port} 45 | end 46 | 47 | @doc """ 48 | connects to a mongodb server specifying options 49 | 50 | Opts must be a Map 51 | """ 52 | def connect(opts) when is_map(opts) do 53 | opts = default_env(opts) 54 | host = Map.get(opts, :host, @host) 55 | tcp_connect %Mongo.Server{ 56 | host: case host do 57 | host when is_binary(host) -> String.to_char_list(host) 58 | host -> host 59 | end, 60 | port: Map.get(opts, :port, @port), 61 | mode: Map.get(opts, :mode, @mode), 62 | timeout: Map.get(opts, :timeout, @timeout), 63 | id_prefix: mongo_prefix} 64 | end 65 | 66 | @doc false 67 | def tcp_connect(mongo) do 68 | case :gen_tcp.connect(mongo.host, mongo.port, tcp_options(mongo), mongo.timeout) do 69 | {:ok, socket} -> 70 | {:ok, %Mongo.Server{mongo| socket: socket}} 71 | error -> error 72 | end 73 | end 74 | 75 | defp tcp_recv(mongo) do 76 | :gen_tcp.recv(mongo.socket, 0, mongo.timeout) 77 | end 78 | 79 | @doc """ 80 | Retreives a repsonce from the MongoDB server (only for passive mode) 81 | """ 82 | def response(mongo, decoder \\ &(Mongo.Response.bson_decode(&1))) do 83 | case tcp_recv(mongo) do 84 | {:ok, <> = message} -> 85 | complete(mongo, messageLength, message) |> Mongo.Response.new(decoder) 86 | {:error, msg} -> %Mongo.Error{msg: msg} 87 | end 88 | end 89 | 90 | @doc """ 91 | Sends a message to MongoDB 92 | """ 93 | def send(mongo, payload, reqid \\ gen_reqid) 94 | def send(%Mongo.Server{socket: socket, mode: :passive}, payload, reqid) do 95 | do_send(socket, payload, reqid) 96 | end 97 | def send(%Mongo.Server{socket: socket, mode: :active}, payload, reqid) do 98 | :inet.setopts(socket, active: :once) 99 | do_send(socket, payload, reqid) 100 | end 101 | # sends the message to the socket, returns request {:ok, reqid} 102 | defp do_send(socket, payload, reqid) do 103 | case :gen_tcp.send(socket, payload |> message(reqid)) do 104 | :ok -> {:ok, reqid} 105 | error -> error 106 | end 107 | end 108 | 109 | @doc false 110 | # preprares for a one-time async request 111 | def async(%Mongo.Server{mode: :passive}=mongo) do 112 | :inet.setopts(mongo.socket, active: :once) 113 | end 114 | 115 | @doc """ 116 | Sends a command message requesting imediate response 117 | """ 118 | def cmd_sync(mongo, command) do 119 | case cmd(mongo, command) do 120 | {:ok, _reqid} -> response(mongo) 121 | error -> error 122 | end 123 | end 124 | 125 | @doc """ 126 | Executes an admin command to the server 127 | 128 | """ 129 | def cmd(mongo, cmd) do 130 | send(mongo, Mongo.Request.cmd("admin", cmd)) 131 | end 132 | 133 | @doc """ 134 | Pings the server 135 | 136 | iex> Mongo.connect! |> Mongo.Server.ping 137 | :ok 138 | 139 | """ 140 | def ping(mongo) do 141 | case cmd_sync(mongo, %{ping: true}) do 142 | {:ok, resp} -> Mongo.Response.success(resp) 143 | error -> error 144 | end 145 | end 146 | 147 | @doc """ 148 | Returns true if connection mode is active 149 | """ 150 | def active?(mongo), do: mongo.mode == :active 151 | 152 | @doc """ 153 | Closes the connection 154 | """ 155 | def close(mongo) do 156 | :gen_tcp.close(mongo.socket) 157 | end 158 | 159 | defp default_env(opts) do 160 | case :application.get_env(:mongo, :host) do 161 | {:ok, {host, port}} -> 162 | opts |> Map.put_new(:host, host) |> Map.put_new(:port, port) 163 | _ -> opts 164 | end 165 | end 166 | 167 | # makes sure response is complete 168 | defp complete(_mongo, expected_length, buffer) when byte_size(buffer) == expected_length, do: buffer 169 | defp complete(_mongo, expected_length, buffer) when byte_size(buffer) > expected_length, do: binary_part(buffer, 0, expected_length) 170 | defp complete(mongo, expected_length, buffer) do 171 | case tcp_recv(mongo) do 172 | {:ok, mess} -> complete(mongo, expected_length, buffer <> mess) 173 | end 174 | end 175 | 176 | # Convert TCP options to `:inet.setopts` compatible arguments. 177 | defp tcp_options(m) do 178 | args = options(m) 179 | 180 | # default to binary 181 | args = [:binary | args] 182 | 183 | args 184 | end 185 | # default server options 186 | defp options(mongo) do 187 | [ active: false, 188 | send_timeout: mongo.timeout, 189 | send_timeout_close: true ] 190 | end 191 | 192 | defp mongo_prefix do 193 | case :inet.gethostname do 194 | {:ok, hostname} -> 195 | <> = :crypto.hash(:md5, (hostname ++ :os.getpid) |> to_string) 196 | prefix 197 | _ -> :crypto.rand_uniform(0, 65535) 198 | end 199 | end 200 | @doc false 201 | def prefix(%Mongo.Server{id_prefix: prefix}) do 202 | for << <> <- <> >>, into: <<>> do 203 | <> 204 | end |> String.downcase 205 | end 206 | 207 | @doc """ 208 | Adds options to an existing mongo server connection 209 | 210 | new_opts must be a map with zero or more of the following keys: 211 | 212 | * read: `:awaitdata`, `:nocursortimeout`, `:slaveok`, `:tailablecursor` 213 | * write concern: `:wc` 214 | * socket: `:mode`, `:timeout` 215 | """ 216 | def opts(mongo, new_opts) do 217 | %Mongo.Server{mongo| opts: Map.merge(mongo.opts, new_opts)} 218 | end 219 | 220 | @doc """ 221 | Gets mongo connection default options 222 | """ 223 | def db_opts(mongo) do 224 | Map.take(mongo.opts, [:awaitdata, :nocursortimeout, :slaveok, :tailablecursor, :wc]) #, :mode, :timeout]) 225 | |> Map.put(:mode, mongo.mode) |> Map.put(:timeout, mongo.timeout) 226 | end 227 | 228 | use Bitwise, only_operators: true 229 | @doc """ 230 | Assigns radom ids to a list of documents when `:_id` is missing 231 | 232 | iex> [%{a: 1}] |> Mongo.Server.assign_id |> Enum.at(0) |> Map.keys 233 | [:"_id", :a] 234 | 235 | #a prefix to ids can be set manually like this 236 | iex> prefix = case [%{a: 1}] |> Mongo.Server.assign_id(256*256-1) |> Enum.at(0) |> Map.get(:"_id") do 237 | ...> %Bson.ObjectId{oid: <>} -> prefix 238 | ...> error -> error 239 | ...> end 240 | ...> prefix 241 | 256*256-1 242 | 243 | #by default prefix are set at connection time and remains identical for the entire connection 244 | iex> mongo = Mongo.connect! 245 | ...> prefix = case [%{a: 1}] |> Mongo.Server.assign_id(mongo) |> Enum.at(0) |> Map.get(:"_id") do 246 | ...> %Bson.ObjectId{oid: <>} -> prefix 247 | ...> error -> error 248 | ...> end 249 | ...> prefix == mongo.id_prefix 250 | true 251 | 252 | """ 253 | def assign_id(docs, client_prefix \\ gen_client_prefix) 254 | def assign_id(docs, client_prefix) do 255 | client_prefix = check_client_prefix(client_prefix) 256 | Enum.map_reduce( 257 | docs, 258 | {client_prefix, gen_trans_prefix, :crypto.rand_uniform(0, 4294967295)}, 259 | fn(doc, id) -> { Map.put(doc, :'_id', %Bson.ObjectId{oid: to_oid(id)}), next_id(id) } end) 260 | |> elem(0) 261 | end 262 | 263 | # returns a 2 bites prefix integer 264 | defp check_client_prefix(%Mongo.Server{id_prefix: prefix}) when is_integer(prefix), do: prefix 265 | defp check_client_prefix(prefix) when is_integer(prefix), do: prefix 266 | defp check_client_prefix(_), do: gen_client_prefix 267 | # generates a 2 bites prefix integer 268 | defp gen_client_prefix, do: :crypto.rand_uniform(0, 65535) 269 | # returns a 6 bites prefix integer 270 | # :erlang.system_time/1 271 | Kernel.if Keyword.get(:erlang.module_info, :exports) |> Enum.any?(fn({:system_time, 1}) -> true; (_) -> false end) do 272 | defp gen_trans_prefix do 273 | :erlang.system_time(:micro_seconds) &&& 281474976710655 274 | end 275 | else 276 | defp gen_trans_prefix do 277 | {gs, s, ms} = :erlang.now 278 | (gs * 1000000000000 + s * 1000000 + ms) &&& 281474976710655 279 | end 280 | end 281 | 282 | # from a 3 integer tuple to ObjectID 283 | defp to_oid({client_prefix, trans_prefix, suffix}), do: <> 284 | # Selects next ID 285 | defp next_id({client_prefix, trans_prefix, suffix}), do: {client_prefix, trans_prefix, suffix+1} 286 | 287 | # add request ID to a payload message 288 | defp message(payload, reqid) 289 | defp message(payload, reqid) do 290 | <<(byte_size(payload) + 12)::size(32)-little>> <> reqid <> <<0::32>> <> <> 291 | end 292 | # generates a request Id when not provided (makes sure it is a positive integer) 293 | defp gen_reqid() do 294 | <> = :crypto.rand_bytes(4) 295 | <> 296 | end 297 | 298 | end 299 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Mongo.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [ app: :mongo, 6 | name: "mongo", 7 | version: "0.5.4", 8 | elixir: "~> 1.0 or ~> 1.1", 9 | source_url: "https://github.com/checkiz/elixir-mongo", 10 | description: "MongoDB driver for Elixir", 11 | deps: deps(Mix.env), 12 | package: package, 13 | docs: &docs/0 ] 14 | end 15 | 16 | # Configuration for the OTP application 17 | def application do 18 | [ 19 | applications: [], 20 | env: [host: {"127.0.0.1", 27017}] 21 | ] 22 | end 23 | 24 | # Returns the list of dependencies for prod 25 | defp deps(:prod) do 26 | [ 27 | bson: "~> 0.4.4" 28 | ] 29 | end 30 | 31 | # Returns the list of dependencies for docs 32 | defp deps(:docs) do 33 | deps(:prod) ++ 34 | [ 35 | {:ex_doc, ">= 0.0.0" }, 36 | {:earmark, ">= 0.0.0"} 37 | ] 38 | end 39 | defp deps(_), do: deps(:prod) 40 | 41 | defp docs do 42 | [ #readme: false, 43 | #main: "README", 44 | source_ref: System.cmd("git", ["rev-parse", "--verify", "--quiet", "HEAD"])|>elem(0) ] 45 | end 46 | 47 | defp package do 48 | [ maintainers: ["jerp"], 49 | licenses: ["MIT"], 50 | links: %{ 51 | "GitHub" => "https://github.com/checkiz/elixir-mongo", 52 | "Documentation" => "http://hexdocs.pm/mongo" 53 | } ] 54 | end 55 | 56 | end 57 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{"bson": {:hex, :bson, "0.4.4"}, 2 | "earmark": {:hex, :earmark, "0.1.15"}, 3 | "ex_doc": {:hex, :ex_doc, "0.7.2"}} 4 | -------------------------------------------------------------------------------- /test/mongo_aggr_test.exs: -------------------------------------------------------------------------------- 1 | Code.require_file "test_helper.exs", __DIR__ 2 | 3 | defmodule Mongo.Aggr.Test do 4 | use ExUnit.Case, async: false 5 | 6 | # In order to run the tests a mongodb server must be listening locally on the default port 7 | setup do 8 | mongo = Mongo.connect! 9 | db = Mongo.db(mongo, "test") 10 | anycoll = Mongo.Db.collection(db, "coll_aggr") 11 | Mongo.Collection.drop anycoll 12 | [ 13 | %{a: 0, value: 0}, 14 | %{a: 1, value: 1}, 15 | %{a: 2, value: 1}, 16 | %{a: 3, value: 1}, 17 | %{a: 4, value: 1}, 18 | %{a: 5, value: 3} ] |> Mongo.Collection.insert(anycoll) 19 | { :ok, mongo: mongo, db: db, anycoll: anycoll } 20 | end 21 | 22 | test "count", ctx do 23 | if true do 24 | assert ctx[:anycoll] |> Mongo.Collection.count!(%{value: %{'$gt': 0}}) == 5 25 | end 26 | end 27 | 28 | test "distinct", ctx do 29 | if true do 30 | assert ctx[:anycoll] |> Mongo.Collection.distinct!("value", %{value: %{"$lt": 3}}) 31 | |> is_list 32 | end 33 | end 34 | 35 | test "mapreduce", ctx do 36 | if true do 37 | anycoll = ctx[:anycoll] 38 | assert Mongo.Collection.mr!(anycoll, "function(d){emit(this._id, this.value*2)}", "function(k, vs){return Array.sum(vs)}") |> is_list 39 | assert :ok == Mongo.Collection.mr!(anycoll, "function(d){emit('z', 3*this.value)}", "function(k, vs){return Array.sum(vs)}", "anycoll2") 40 | end 41 | end 42 | 43 | test "group", ctx do 44 | if true do 45 | assert ctx[:anycoll] |> Mongo.Collection.group!(%{a: true}) |> is_list 46 | end 47 | end 48 | 49 | test "aggregate", ctx do 50 | if true do 51 | assert [%{value: 1}|_] = ctx[:anycoll] |> Mongo.Collection.aggregate([ 52 | %{'$skip': 1}, 53 | %{'$limit': 5}, 54 | %{'$project': %{'_id': false, value: true}} 55 | ]) 56 | end 57 | end 58 | 59 | test "error count", ctx do 60 | if true do 61 | assert %Mongo.Error{} = ctx[:anycoll] |> Mongo.Collection.count(%{value: %{'$in': 0}}) 62 | end 63 | end 64 | 65 | end 66 | -------------------------------------------------------------------------------- /test/mongo_crud_test.exs: -------------------------------------------------------------------------------- 1 | Code.require_file "test_helper.exs", __DIR__ 2 | 3 | defmodule Mongo.Crud.Test do 4 | use ExUnit.Case, async: false 5 | 6 | # In order to run the tests a mongodb server must be listening locally on the default port 7 | setup do 8 | mongo = Mongo.connect! 9 | db = Mongo.db(mongo, "test") 10 | anycoll = Mongo.Db.collection(db, "coll_crud") 11 | Mongo.Collection.drop anycoll 12 | [ 13 | %{a: 0, value: 0}, 14 | %{a: 1, value: 1}, 15 | %{a: 2, value: 1}, 16 | %{a: 3, value: 1}, 17 | %{a: 4, value: 1}, 18 | %{a: 5, value: 3} ] |> Mongo.Collection.insert(anycoll) 19 | { :ok, mongo: mongo, db: db, anycoll: anycoll } 20 | end 21 | 22 | test "find", ctx do 23 | if true do 24 | anycoll = ctx[:anycoll] 25 | # count without retreiving 26 | assert anycoll |> Mongo.Collection.find |> Enum.count == 6 27 | # retreive all docs then count 28 | assert anycoll |> Mongo.Collection.find |> Enum.to_list |> Enum.count == 6 29 | # retreive all but one doc then count 30 | assert anycoll |> Mongo.Collection.find |> Mongo.Find.skip(1) |> Enum.to_list |> Enum.count == 5 31 | end 32 | end 33 | 34 | test "find where", ctx do 35 | if true do 36 | assert ctx[:anycoll] |> Mongo.Collection.find("obj.value == 0") |> Enum.count == 1 37 | assert ctx[:anycoll] |> Mongo.Collection.find("obj.value == 0") |> Enum.to_list |> Enum.count == 1 38 | end 39 | end 40 | 41 | test "insert", ctx do 42 | anycoll = ctx[:anycoll] 43 | if true do 44 | assert %{a: 23} |> Mongo.Collection.insert_one!(anycoll) == %{a: 23} 45 | assert [%{a: 23}, %{a: 24, b: 1}] |> Mongo.Collection.insert!(anycoll) |> is_list 46 | end 47 | if true do 48 | assert %{'_id': 2, a: 456} |> Mongo.Collection.insert_one!(anycoll) |> is_map 49 | assert ctx[:db] |> Mongo.Db.getLastError == :ok 50 | end 51 | end 52 | 53 | test "update", ctx do 54 | if true do 55 | ctx[:anycoll] |> Mongo.Collection.update(%{a: 456}, %{a: 123, b: 789}) 56 | assert ctx[:db] |> Mongo.Db.getLastError == :ok 57 | end 58 | end 59 | 60 | test "delete", ctx do 61 | if true do 62 | ctx[:anycoll] |> Mongo.Collection.delete(%{b: 789}) 63 | assert ctx[:db] |> Mongo.Db.getLastError == :ok 64 | end 65 | end 66 | 67 | test "objid", ctx do 68 | if true do 69 | anycoll = ctx[:anycoll] 70 | assert [%{a: -23}, %{a: -24, b: 1}] |> Mongo.Server.assign_id(ctx[:mongo]) |> Mongo.Collection.insert!(anycoll) |> is_list 71 | end 72 | end 73 | 74 | test "bang find", ctx do 75 | if true do 76 | assert %Mongo.Error{} = ctx[:anycoll] |> Mongo.Collection.find(%{value: %{'$in': 0}}) |> Mongo.Find.exec 77 | end 78 | end 79 | 80 | test "insert error", ctx do 81 | anycoll = ctx[:anycoll] 82 | if true do 83 | %{_id: 1, a: 31} |> Mongo.Collection.insert_one!(anycoll) 84 | %{_id: 1, a: 32} |> Mongo.Collection.insert_one!(anycoll) 85 | assert {:error, _} = ctx[:db] |> Mongo.Db.getLastError 86 | end 87 | end 88 | 89 | end 90 | -------------------------------------------------------------------------------- /test/mongo_cursor_test.exs: -------------------------------------------------------------------------------- 1 | Code.require_file "test_helper.exs", __DIR__ 2 | 3 | defmodule Mongo.Cursor.Test do 4 | use ExUnit.Case, async: false 5 | 6 | # In order to run the tests a mongodb server must be listening locally on the default port 7 | setup do 8 | mongo = Mongo.connect! 9 | db = Mongo.db(mongo, "test") 10 | anycoll = Mongo.Db.collection(db, "coll_cursor") 11 | Mongo.Collection.drop anycoll 12 | [ 13 | %{a: 0, value: 0}, 14 | %{a: 1, value: 1}, 15 | %{a: 2, value: 1}, 16 | %{a: 3, value: 1}, 17 | %{a: 4, value: 1}, 18 | %{a: 5, value: 3} ] |> Mongo.Collection.insert(anycoll) 19 | { :ok, mongo: mongo, db: db, anycoll: anycoll } 20 | end 21 | 22 | test "batchSize", ctx do 23 | assert ctx[:anycoll] |> Mongo.Collection.find |> Map.put(:batchSize, 2) |> Enum.to_list |> Enum.count == 6 24 | end 25 | 26 | test "batchArray", ctx do 27 | assert ctx[:anycoll] |> Mongo.Collection.find |> Map.put(:batchSize, 2) |> Mongo.Find.exec |> Enum.to_list |> Enum.count == 3 28 | end 29 | 30 | test "explain", ctx do 31 | assert ctx[:anycoll] |> Mongo.Collection.find |> Mongo.Find.explain |> Map.has_key?(:cursor) 32 | end 33 | 34 | test "find hint", ctx do 35 | ctx[:anycoll] |> Mongo.Collection.createIndex("tst_value", %{value: true}) 36 | assert "BtreeCursor tst_value" == ctx[:anycoll] |> Mongo.Collection.find |> Mongo.Find.hint(%{value: true}) |> Mongo.Find.explain |> Map.get(:cursor) 37 | end 38 | 39 | test "Correct count is returned if more than 100 items are queried with no batch size specified or batchSize zero", ctx do 40 | anycoll = ctx[:anycoll] 41 | 42 | Mongo.Collection.drop anycoll 43 | 44 | items = 1..110 |> Enum.map fn r -> %{a: r, value: r} end 45 | 46 | Mongo.Collection.insert(items, anycoll) 47 | 48 | assert ctx[:anycoll] |> Mongo.Collection.find |> Enum.to_list |> Enum.count == 110 49 | assert ctx[:anycoll] |> Mongo.Collection.find |> Map.put(:batchSize, 109) |> Enum.to_list |> Enum.count == 110 50 | assert ctx[:anycoll] |> Mongo.Collection.find |> Map.put(:batchSize, 0) |> Enum.to_list |> Enum.count == 110 51 | 52 | end 53 | 54 | end 55 | -------------------------------------------------------------------------------- /test/mongo_server_test.exs: -------------------------------------------------------------------------------- 1 | Code.require_file "test_helper.exs", __DIR__ 2 | 3 | defmodule Mongo.Server.Test do 4 | use ExUnit.Case, async: false 5 | 6 | test "ping" do 7 | assert :ok == Mongo.connect! |> Mongo.Server.ping 8 | case Mongo.connect %{port: 27017, timeout: 0} do 9 | {:ok, localhost} -> 10 | assert %Mongo.Error{msg: :timeout} == localhost |> Mongo.Server.ping 11 | _ -> 12 | {:error, :no_localhost_80} 13 | end 14 | end 15 | 16 | test "active mode" do 17 | mongo = Mongo.connect! %{mode: :active} 18 | ping_cmd = Mongo.Request.cmd("admin", %{ping: true}) 19 | Mongo.Server.send(mongo, ping_cmd) 20 | receive do 21 | {:tcp, _, m} -> 22 | assert :ok == (case Mongo.Response.new(m) do 23 | {:ok, resp } -> Mongo.Response.success(resp) 24 | error -> error 25 | end) 26 | end 27 | end 28 | 29 | test "async request" do 30 | mongo = Mongo.connect! 31 | ping_cmd = Mongo.Request.cmd("admin", %{ping: true}) 32 | Mongo.Server.async(mongo) 33 | Mongo.Server.send(mongo, ping_cmd) 34 | receive do 35 | {:tcp, _, m} -> 36 | assert :ok == (case Mongo.Response.new(m) do 37 | {:ok, resp } -> Mongo.Response.success(resp) 38 | error -> error 39 | end) 40 | end 41 | end 42 | 43 | test "async ping" do 44 | me = self() 45 | spawn_link( 46 | fn() -> 47 | mongo = Mongo.connect! %{mode: :active} 48 | ping_cmd = Mongo.Request.cmd("admin", %{ping: true}) 49 | Mongo.Server.send(mongo, ping_cmd) 50 | receive do 51 | {:tcp, _, m} -> 52 | send(me, case Mongo.Response.new(m) do 53 | {:ok, resp } -> Mongo.Response.success(resp) 54 | error -> error 55 | end) 56 | end 57 | end) 58 | assert_receive :ok 59 | end 60 | 61 | test "def connection" do 62 | assert {:ok, {_, _}} = :application.get_env(:mongo, :host) 63 | end 64 | 65 | test "chunked messages" do 66 | db = Mongo.connect! |> Mongo.db("test") 67 | chunked_test = Mongo.Db.collection(db, "chunked_test") 68 | chunked_test |> Mongo.Collection.drop 69 | 1..5000 |> Enum.map(&(%{a: &1, value: "this should be long enough"})) |> Mongo.Collection.insert(chunked_test) 70 | assert 5000 == chunked_test |> Mongo.Collection.find() |> Enum.count 71 | end 72 | 73 | # # test "write concern" do 74 | # # db = Mongo.connect!.db("test") 75 | # # db2 = Mongo.connect!.db("test") 76 | # # chunked_test = db.collection("wc_test") 77 | # # chunked_test.drop 78 | # # 1..50000 |> Enum.map(&(%{a: &1, value: "this should be long enough"})) |> chunked_test.insert 79 | # # assert 50000 > db2.collection("wc_test").find().toArray |> Enum.count 80 | # # # assert :ok = db.getLastError 81 | # # # assert 5000 == Mongo.connect!.db("test").collection("chunked_test").find().toArray |> Enum.count 82 | # # end 83 | 84 | test "timout recv" do 85 | db = Mongo.connect! |> Mongo.db("test") 86 | timout_test = Mongo.Db.collection(db, "timout_test") 87 | timout_test |> Mongo.Collection.drop 88 | 1..5000 |> 89 | Enum.map(&(%{a: &1, value: "this should be long enough"})) 90 | |> Mongo.Collection.insert(timout_test) 91 | assert :ok = Mongo.Db.getLastError(db) 92 | db = Mongo.connect!(%{timeout: 1}) |> Mongo.db("test") 93 | timout_test = Mongo.Db.collection(db, "timout_test") 94 | assert %Mongo.Error{msg: :timeout} == timout_test |> Mongo.Collection.find("obj.a == 1 || obj.a == 49000") |> Mongo.Find.exec 95 | end 96 | 97 | end 98 | -------------------------------------------------------------------------------- /test/mongo_test.exs: -------------------------------------------------------------------------------- 1 | Code.require_file "test_helper.exs", __DIR__ 2 | 3 | defmodule Mongo.Test do 4 | use ExUnit.Case, async: false 5 | 6 | doctest Mongo 7 | doctest Mongo.Server 8 | doctest Mongo.Collection 9 | 10 | end 11 | -------------------------------------------------------------------------------- /test/test_helper.exs: -------------------------------------------------------------------------------- 1 | ExUnit.start 2 | 3 | defmodule TestAll do 4 | def now(yes_no) do 5 | yes_no || false 6 | end 7 | 8 | end 9 | --------------------------------------------------------------------------------