├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── VERSION ├── lib ├── mongo.ex ├── mongo_auth.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_collection_test.exs ├── mongo_crud_test.exs ├── mongo_cursor_test.exs ├── mongo_db_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 | docs 9 | .elixir_ls 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: erlang 2 | services: 3 | - mongodb 4 | notifications: 5 | email: 6 | - zztczcx@gmail.com 7 | otp_release: 8 | - 17.1 9 | before_install: 10 | - wget http://s3.hex.pm/builds/elixir/v1.0.0.zip 11 | - unzip -d elixir v1.0.0.zip 12 | before_script: 13 | - sleep 15 14 | - export PATH=`pwd`/elixir/bin:$PATH 15 | - mix local.hex --force 16 | - mix deps.get 17 | script: "MIX_ENV=test mix test" 18 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # v0.5.0 2 | * complete rework of the API 3 | * drop the Mongodb-like notation in favor of a more Elixir syntax 4 | * no more use of records 5 | * implements Enumerable protocol for: 6 | * %Mongo.Find{}: to retrieve all docs of a query 7 | * %Mongo.Response{}: to retrieve all docs of a particular batch (specific use) 8 | * %Mongo.Cursoer{}: to retrive all batches (specific use) 9 | 10 | in this version you write this: 11 | 12 | ```elixir 13 | coll = Mongo.connect! |> Mongo.db("test") |> Mongo.Db.collection("anycoll") 14 | ``` 15 | rather than: 16 | 17 | ```elixir 18 | coll = Mongo.connect!.db("test").collection("anycoll") 19 | ``` 20 | 21 | see https://github.com/checkiz/elixir-mongo/issues/11 22 | 23 | # v0.4.0 24 | * compatible with Elixir v1.0.0 25 | * elixir-bson v0.4.0 26 | 27 | # v0.3.1 28 | * compatible with Elixir v0.15.1 29 | 30 | # v0.3 31 | * compatible with Elixir v0.14.1 32 | 33 | # v0.2 34 | 35 | * Enhancements 36 | * Mongo.Cursor: module to interact with MongoDB cursors 37 | * Authentication 38 | * Allows to mode to get message back from MongoDB 39 | * passive: the drivers controls when to fetch responses (for sync calls) 40 | * active: MongoDB sends message back directly (allows assync calls) 41 | * getLastError, getPrevError 42 | 43 | * Bug fixes 44 | 45 | * Deprecations 46 | 47 | * Backwards incompatible changes 48 | * A major revamp of the API was necessary 49 | -------------------------------------------------------------------------------- /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/zztczcx/elixir-mongo.png?branch=master)](https://travis-ci.org/zztczcx/elixir-mongo) 4 | [![Hex Version](https://img.shields.io/hexpm/v/mongo.svg)](https://hex.pm/packages/mongo) 5 | [![Hex Downloads](https://img.shields.io/hexpm/dt/bson.svg)](https://hex.pm/packages/mongo) 6 | 7 | A [MongoDB](http://www.mongodb.org) driver in Elixir. 8 | 9 | API entirely reviewed, see [CHANGELOG.md](https://github.com/checkiz/elixir-mongo/blob/master/CHANGELOG.md) 10 | 11 | ### Connecting 12 | 13 | Example preparing access to the `anycoll` collection in the `test` db : 14 | ```elixir 15 | # Connect the mongo server (by default port 27017 at 127.0.0.1) 16 | mongo = Mongo.connect! 17 | # Select the db to access 18 | db = mongo |> Mongo.db("test") 19 | # Select the db to access 20 | anycoll = db |> Mongo.Db.collection("anycoll") 21 | ``` 22 | 23 | ### Wrappers for CRUD operations 24 | 25 | Examples accessing the `anycoll` collection via CRUD operations see `Mongo.Find` 26 | 27 | 28 | ### Wrappers for Aggregate operations 29 | 30 | Example of aggregate operation applied to the `anycoll` collection see `Mongo.Collection` 31 | 32 | ### Other commands 33 | 34 | ```elixir 35 | # Authenticate against the db 36 | db |> Mongo.auth("testuser", "123")` 37 | # Retrieve the last error 38 | db |> Mongo.getLastError 39 | ``` 40 | 41 | ### Documentation 42 | 43 | - [documentation](http://checkiz.github.io/elixir-mongo) 44 | 45 | ### Dependencies 46 | 47 | - MongoDB needs a Bson encoder/decoder, this project uses the elixir-bson encoder/decoder. See [elixir-cbson source repo](https://github.com/sean-lin/elixir-cbson) 48 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 0.5.0 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, :raw] 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, raw: msg} 52 | end 53 | 54 | end 55 | -------------------------------------------------------------------------------- /lib/mongo_auth.ex: -------------------------------------------------------------------------------- 1 | defmodule Mongo.Auth do 2 | def auth(opts, mongo) do 3 | user = opts[:username] 4 | passwd = opts[:password] 5 | mod = mechanism(mongo) 6 | mod.auth(user, passwd, mongo) 7 | end 8 | 9 | @doc false 10 | defp mechanism(%Mongo.Server{wire_version: version}) when version >= 3, do: Mongo.Auth.SCRAM 11 | defp mechanism(_), do: Mongo.Auth.CR 12 | end 13 | 14 | 15 | defmodule Mongo.Auth.CR do 16 | def auth(username, password, mongo) do 17 | nonce = getnonce(mongo) 18 | hash_password = hash(username <> ":mongo:" <> password) 19 | 20 | Mongo.Server.cmd_sync(mongo, 21 | %{authenticate: 1, nonce: nonce, user: username, 22 | key: hash(nonce <> username <> hash_password)}) 23 | |> case do 24 | {:ok, resp} -> 25 | case Mongo.Response.success(resp) do 26 | :ok -> {:ok, mongo} 27 | error -> error 28 | end 29 | error -> error 30 | end 31 | end 32 | 33 | # get `nonce` token from server 34 | defp getnonce(mongo) do 35 | case Mongo.Server.cmd_sync(mongo, %{getnonce: true}) do 36 | {:ok, resp} -> resp |> Mongo.Response.getnonce 37 | error -> error 38 | end 39 | end 40 | 41 | # creates a md5 hash in hex with loawercase 42 | defp hash(data) do 43 | :crypto.hash(:md5, data) |> binary_to_hex 44 | end 45 | 46 | # creates an hex string from binary 47 | defp binary_to_hex(bin) do 48 | for << <> <- bin >>, into: <<>> do 49 | <> 50 | end |> String.downcase 51 | end 52 | end 53 | 54 | defmodule Mongo.Auth.SCRAM do 55 | use Bitwise 56 | 57 | def auth(username, password, mongo) do 58 | nonce = nonce() 59 | first_bare = first_bare(username, nonce) 60 | payload = first_message(first_bare) 61 | message = [saslStart: 1, mechanism: "SCRAM-SHA-1", payload: payload] 62 | 63 | with {:ok, reply} <- command(message, mongo), 64 | {message, signature} = first(reply, first_bare, username, password, nonce), 65 | {:ok, reply} <- command(message, mongo), 66 | message = second(reply, signature), 67 | {:ok, reply} <- command(message, mongo), 68 | :ok <- final(reply) do 69 | {:ok, mongo} 70 | else 71 | {:ok, %{ok: z, errmsg: reason, code: code}} when z == 0 -> 72 | {:error, %Mongo.Error{msg: "auth failed for user:#{username} code: #{code} reason:#{reason}"}} 73 | error -> error 74 | end 75 | end 76 | 77 | 78 | defp command(cmd, mongo) do 79 | case Mongo.Server.cmd_sync(mongo, cmd) do 80 | {:ok, resp} -> 81 | case Mongo.Response.cmd(resp) do 82 | {:ok, %{ok: ok} = reply} when ok == 1 -> {:ok, reply} 83 | error -> error 84 | end 85 | error -> error 86 | end 87 | end 88 | 89 | defp first(%{conversationId: 1, payload: server_payload, done: false}, 90 | first_bare, username, password, client_nonce) do 91 | params = parse_payload(server_payload) 92 | server_nonce = params["r"] 93 | salt = params["s"] |> Base.decode64! 94 | iter = params["i"] |> String.to_integer 95 | pass = digest_password(username, password) 96 | salted_password = hi(pass, salt, iter) 97 | 98 | <<^client_nonce::binary-size(24), _::binary>> = server_nonce 99 | 100 | client_message = "c=biws,r=#{server_nonce}" 101 | auth_message = "#{first_bare},#{server_payload.bin},#{client_message}" 102 | server_signature = generate_signature(salted_password, auth_message) 103 | proof = generate_proof(salted_password, auth_message) 104 | client_final_message = %Bson.Bin{bin: "#{client_message},#{proof}"} 105 | message = [saslContinue: 1, conversationId: 1, payload: client_final_message] 106 | 107 | {message, server_signature} 108 | end 109 | 110 | defp second(%{conversationId: 1, payload: payload, done: false}, signature) do 111 | params = parse_payload(payload) 112 | ^signature = params["v"] |> Base.decode64! 113 | [saslContinue: 1, conversationId: 1, payload: %Bson.Bin{bin: ""}] 114 | end 115 | 116 | defp final(%{conversationId: 1, payload: %Bson.Bin{bin: ""}, done: true}), do: :ok 117 | defp final(_), do: :failed 118 | 119 | defp first_message(first_bare) do 120 | %Bson.Bin{bin: "n,,#{first_bare}"} 121 | end 122 | 123 | defp first_bare(username, nonce) do 124 | "n=#{encode_username(username)},r=#{nonce}" 125 | end 126 | 127 | defp hi(password, salt, iterations) do 128 | Mongo.PBKDF2.generate(password, salt, 129 | iterations: iterations, length: 20, digest: :sha) 130 | end 131 | 132 | defp generate_proof(salted_password, auth_message) do 133 | client_key = :crypto.hmac(:sha, salted_password, "Client Key") 134 | stored_key = :crypto.hash(:sha, client_key) 135 | signature = :crypto.hmac(:sha, stored_key, auth_message) 136 | client_proof = xor_keys(client_key, signature, "") 137 | "p=#{Base.encode64(client_proof)}" 138 | end 139 | 140 | defp generate_signature(salted_password, auth_message) do 141 | server_key = :crypto.hmac(:sha, salted_password, "Server Key") 142 | :crypto.hmac(:sha, server_key, auth_message) 143 | end 144 | 145 | defp xor_keys("", "", result), 146 | do: result 147 | defp xor_keys(<>, <>, result), 148 | do: xor_keys(ra, rb, <>) 149 | 150 | 151 | defp nonce do 152 | :crypto.strong_rand_bytes(18) 153 | |> Base.encode64 154 | end 155 | 156 | defp encode_username(username) do 157 | username 158 | |> String.replace("=", "=3D") 159 | |> String.replace(",", "=2C") 160 | end 161 | 162 | defp parse_payload(%Bson.Bin{subtype: 0, bin: payload}) do 163 | payload 164 | |> String.split(",") 165 | |> Enum.into(%{}, &List.to_tuple(String.split(&1, "=", parts: 2))) 166 | end 167 | 168 | defp digest_password(username, password) do 169 | :crypto.hash(:md5, [username, ":mongo:", password]) 170 | |> Base.encode16(case: :lower) 171 | end 172 | end 173 | 174 | defmodule Mongo.PBKDF2 do 175 | # From https://github.com/elixir-lang/plug/blob/ef616a9db9c87ec392dd8a0949bc52fafcf37005/lib/plug/crypto/key_generator.ex 176 | # with modifications 177 | 178 | @moduledoc """ 179 | `PBKDF2` implements PBKDF2 (Password-Based Key Derivation Function 2), 180 | part of PKCS #5 v2.0 (Password-Based Cryptography Specification). 181 | It can be used to derive a number of keys for various purposes from a given 182 | secret. This lets applications have a single secure secret, but avoid reusing 183 | that key in multiple incompatible contexts. 184 | see http://tools.ietf.org/html/rfc2898#section-5.2 185 | """ 186 | 187 | use Bitwise 188 | @max_length bsl(1, 32) - 1 189 | 190 | @doc """ 191 | Returns a derived key suitable for use. 192 | ## Options 193 | * `:iterations` - defaults to 1000 (increase to at least 2^16 if used for passwords); 194 | * `:length` - a length in octets for the derived key. Defaults to 32; 195 | * `:digest` - an hmac function to use as the pseudo-random function. Defaults to `:sha256`; 196 | """ 197 | def generate(secret, salt, opts \\ []) do 198 | iterations = Keyword.get(opts, :iterations, 1000) 199 | length = Keyword.get(opts, :length, 32) 200 | digest = Keyword.get(opts, :digest, :sha256) 201 | 202 | if length > @max_length do 203 | raise ArgumentError, "length must be less than or equal to #{@max_length}" 204 | else 205 | generate(mac_fun(digest, secret), salt, iterations, length, 1, [], 0) 206 | end 207 | end 208 | 209 | defp generate(_fun, _salt, _iterations, max_length, _block_index, acc, length) 210 | when length >= max_length do 211 | key = acc |> Enum.reverse |> IO.iodata_to_binary 212 | <> = key 213 | bin 214 | end 215 | 216 | defp generate(fun, salt, iterations, max_length, block_index, acc, length) do 217 | initial = fun.(<>) 218 | block = iterate(fun, iterations - 1, initial, initial) 219 | generate(fun, salt, iterations, max_length, block_index + 1, 220 | [block | acc], byte_size(block) + length) 221 | end 222 | 223 | defp iterate(_fun, 0, _prev, acc), do: acc 224 | 225 | defp iterate(fun, iteration, prev, acc) do 226 | next = fun.(prev) 227 | iterate(fun, iteration - 1, next, :crypto.exor(next, acc)) 228 | end 229 | 230 | defp mac_fun(digest, secret) do 231 | &:crypto.hmac(digest, secret, &1) 232 | end 233 | end 234 | -------------------------------------------------------------------------------- /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> _collection = Mongo.Helpers.test_collection("anycoll") 8 | ...> Mongo.Helpers.test_collection("anycoll") |> Mongo.Collection.count 9 | {:ok, 6} 10 | 11 | `count()` or `count!()` 12 | 13 | The first returns `{:ok, value}`, the second returns simply `value` when the call is sucessful. 14 | In case of error, the first returns `%Mongo.Error{}` the second raises a `Mongo.Bang` exception. 15 | 16 | iex> collection = Mongo.Helpers.test_collection("anycoll") 17 | ...> {:ok, 6} = collection |> Mongo.Collection.count 18 | ...> 6 === collection |> Mongo.Collection.count! 19 | true 20 | 21 | iex> collection = Mongo.Helpers.test_collection("anycoll") 22 | ...> {:ok, 2} = collection |> Mongo.Collection.count(a: ['$in': [1,3]]) 23 | ...> %Mongo.Error{} = collection |> Mongo.Collection.count(a: ['$in': 1]) # $in should take a list, so this triggers an error 24 | ...> collection |> Mongo.Collection.count!(a: ['$in': 1]) 25 | ** (Mongo.Bang) :"cmd error" 26 | 27 | """ 28 | use Mongo.Helpers 29 | alias Mongo.Server 30 | alias Mongo.Db 31 | alias Mongo.Request 32 | 33 | defstruct [ 34 | name: nil, 35 | db: nil, 36 | opts: %{} ] 37 | 38 | @def_reduce "function(obj, prev){}" 39 | 40 | @doc """ 41 | New collection 42 | """ 43 | def new(db, name) when is_atom(name), do: new(db, Atom.to_string(name)) 44 | def new(db, name), do: %__MODULE__{db: db, name: name, opts: Db.coll_opts(db)} 45 | 46 | @doc """ 47 | Creates a `%Mongo.Find{}` for a given collection, query and projection 48 | 49 | See `Mongo.Find` for details. 50 | 51 | """ 52 | def find(collection, criteria \\ %{}, projection \\ %{}) do 53 | Mongo.Find.new(collection, criteria, projection) 54 | end 55 | 56 | @doc """ 57 | Insert one document into the collection returns the document it received. 58 | 59 | iex> collection = Mongo.Helpers.test_collection("anycoll") 60 | ...> %{a: 23} |> Mongo.Collection.insert_one(collection) |> elem(1) 61 | %{a: 23} 62 | 63 | """ 64 | def insert_one(doc, collection) when is_map(doc) do 65 | case insert([doc], collection) do 66 | {:ok, docs} -> {:ok, docs |> hd} 67 | error -> error 68 | end 69 | end 70 | defbang insert_one(doc, collection) 71 | 72 | @doc """ 73 | Insert a list of documents into the collection 74 | 75 | iex> collection = Mongo.Helpers.test_collection("anycoll") 76 | ...> [%{a: 23}, %{a: 24, b: 1}] |> Mongo.Collection.insert(collection) |> elem(1) 77 | [%{a: 23}, %{a: 24, b: 1}] 78 | 79 | 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. 80 | 81 | iex> collection = Mongo.Helpers.test_collection("anycoll") 82 | ...> [%{a: 23}, %{a: 24, b: 1}] |> Mongo.assign_id |> Mongo.Collection.insert(collection) |> elem(1) |> Enum.at(0) |> Map.has_key?(:_id) 83 | true 84 | 85 | `Mongo.Collection.insert` returns the list of documents it received. 86 | """ 87 | def insert(docs, collection) do 88 | Server.send( 89 | collection.db.mongo, 90 | Request.insert(collection, docs)) 91 | case collection.opts[:wc] do 92 | nil -> {:ok, docs} 93 | :safe -> case collection.db |> Mongo.Db.getLastError do 94 | {:ok, _doc} -> {:ok, docs} 95 | error -> error 96 | end 97 | end 98 | end 99 | defbang insert(docs, collection) 100 | 101 | @doc """ 102 | Modifies an existing document or documents in the collection 103 | 104 | iex> collection = Mongo.Helpers.test_collection("anycoll") 105 | ...> _ = [%{a: 23}, %{a: 24, b: 1}] |> Mongo.assign_id |> Mongo.Collection.insert(collection) |> elem(1) |> Enum.at(0) |> Map.has_key?(:_id) 106 | ...> collection |> Mongo.Collection.update(%{a: 456}, %{a: 123, b: 789}) 107 | :ok 108 | 109 | """ 110 | def update(collection, query, update, upsert \\ false, multi \\ false) 111 | def update(collection, query, update, upsert, multi) do 112 | Server.send( 113 | collection.db.mongo, 114 | Request.update(collection, query, update, upsert, multi)) 115 | case collection.opts[:wc] do 116 | nil -> :ok 117 | :safe -> 118 | collection.db |> Mongo.Db.getLastError 119 | end 120 | end 121 | 122 | @doc """ 123 | Removes an existing document or documents in the collection (see db.collection.remove) 124 | 125 | iex> collection = Mongo.Helpers.test_collection("anycoll") 126 | ...> _ = [%{a: 23}, %{a: 24, b: 789}] |> Mongo.assign_id |> Mongo.Collection.insert(collection) |> elem(1) |> Enum.at(0) |> Map.has_key?(:_id) 127 | ...> collection |> Mongo.Collection.delete(%{b: 789}) 128 | :ok 129 | 130 | """ 131 | def delete(collection, query, justOne \\ false) 132 | def delete(collection, query, justOne) do 133 | Server.send( 134 | collection.db.mongo, 135 | Request.delete(collection, query, justOne)) 136 | case collection.opts[:wc] do 137 | nil -> :ok 138 | :safe -> collection.db |> Mongo.Db.getLastError 139 | end 140 | end 141 | 142 | @doc """ 143 | Count documents in the collection 144 | 145 | If `query` is not specify, it counts all document collection. 146 | `skip_limit` is a map that specify Mongodb otions skip and limit 147 | 148 | """ 149 | def count(collection, query \\ %{}, skip_limit \\ %{}) 150 | def count(collection, query, skip_limit) do 151 | skip_limit = Map.take(skip_limit, [:skip, :limit]) 152 | case Mongo.Db.cmd_sync(collection.db, %{count: collection.name}, 153 | Map.merge(skip_limit, %{query: query})) do 154 | {:ok, resp} -> 155 | case resp |> Mongo.Response.count do 156 | {:ok, n} -> {:ok, n |> trunc} 157 | # _error -> {:ok, -1} 158 | error -> error 159 | end 160 | error -> error 161 | end 162 | end 163 | defbang count(collection) 164 | defbang count(collection, query) 165 | defbang count(collection, query, skip_limit) 166 | 167 | @doc """ 168 | Finds the distinct values for a specified field across a single collection (see db.collection.distinct) 169 | 170 | 171 | iex> collection = Mongo.Helpers.test_collection("anycoll") 172 | ...> collection |> Mongo.Collection.distinct!("value", %{value: %{"$lt": 3}}) 173 | [0, 1] 174 | 175 | """ 176 | def distinct(collection, key, query \\ %{}) 177 | def distinct(collection, key, query) do 178 | case Mongo.Db.cmd_sync(collection.db, %{distinct: collection.name}, %{key: key, query: query}) do 179 | {:ok, resp} -> Mongo.Response.distinct(resp) 180 | error -> error 181 | end 182 | end 183 | defbang distinct(key, collection) 184 | defbang distinct(key, query, collection) 185 | 186 | @doc """ 187 | Provides a wrapper around the mapReduce command 188 | 189 | Returns `:ok` or an array of documents (with option `:inline` active - set by default). 190 | 191 | iex> collection = Mongo.Helpers.test_collection("anycoll") 192 | ...> Mongo.Collection.mr!(collection, "function(d){emit(this._id, this.value*2)}", "function(k, vs){return Array.sum(vs)}") |> is_list 193 | true 194 | 195 | iex> collection = Mongo.Helpers.test_collection("anycoll") 196 | ...> Mongo.Collection.mr!(collection, "function(d){emit('z', 3*this.value)}", "function(k, vs){return Array.sum(vs)}", "mrcoll") 197 | :ok 198 | 199 | """ 200 | def mr(collection, map, reduce \\ @def_reduce, out \\ %{inline: true}, params \\ %{}) 201 | def mr(collection, map, reduce, out, params) do 202 | params = Map.take(params, [:limit, :finalize, :scope, :jsMode, :verbose]) 203 | case Mongo.Db.cmd_sync(collection.db, %{mapReduce: collection.name}, Map.merge(params, %{map: map, reduce: reduce, out: out})) do 204 | {:ok, resp} -> Mongo.Response.mr resp 205 | error -> error 206 | end 207 | end 208 | defbang mr(map, collection) 209 | defbang mr(map, reduce, collection) 210 | defbang mr(map, reduce, out, collection) 211 | defbang mr(map, reduce, out, more, collection) 212 | 213 | @doc """ 214 | Groups documents in the collection by the specified key 215 | 216 | iex> collection = Mongo.Helpers.test_collection("anycoll") 217 | ...> collection |> Mongo.Collection.group!(%{a: true}) |> is_list 218 | true 219 | 220 | [%{a: 0.0}, %{a: 1.0}, %{a: 2.0}, ...] 221 | 222 | """ 223 | def group(collection, key, reduce \\ @def_reduce, initial \\ %{}, params \\ %{}) 224 | def group(collection, key, reduce, initial, params) do 225 | params = Map.take(params, [:'$keyf', :cond, :finalize]) 226 | params = if params[:keyf] do 227 | Map.put(params, :'$keyf', params[:keyf]) 228 | else 229 | params 230 | end 231 | case Mongo.Db.cmd_sync(collection.db, %{group: Map.merge(params, %{ns: collection.name, key: key, '$reduce': reduce, initial: initial})}) do 232 | {:ok, resp} -> Mongo.Response.group resp 233 | error -> error 234 | end 235 | end 236 | defbang group(key, collection) 237 | defbang group(key, reduce, collection) 238 | defbang group(key, reduce, initial, collection) 239 | defbang group(key, reduce, initial, params, collection) 240 | 241 | @doc """ 242 | Drops the collection 243 | 244 | returns `:ok` or a string containing the error message 245 | """ 246 | def drop(collection) do 247 | case Db.cmd_sync(collection.db, %{drop: collection.name}) do 248 | {:ok, resp} -> Mongo.Response.success resp 249 | error -> error 250 | end 251 | end 252 | defbang drop(collection) 253 | 254 | @doc """ 255 | Calculates aggregate values for the data in the collection (see db.collection.aggregate) 256 | 257 | iex> collection = Mongo.Helpers.test_collection("anycoll") 258 | ...> collection |> Mongo.Collection.aggregate([ 259 | ...> %{'$skip': 1}, 260 | ...> %{'$limit': 5}, 261 | ...> %{'$project': %{_id: false, value: true}} ], %{cursor: %{}}) 262 | [%{value: 1}, %{value: 1}, %{value: 1}, %{value: 1}, %{value: 3}] 263 | 264 | """ 265 | def aggregate(collection, pipeline, options \\ %{}) do 266 | cmd_args = Map.merge(%{pipeline: pipeline}, options) 267 | case Mongo.Db.cmd_sync(collection.db, %{aggregate: collection.name}, cmd_args) do 268 | {:ok, resp} -> Mongo.Response.aggregate(resp) 269 | error -> error 270 | end 271 | end 272 | defbang aggregate(pipeline, collection) 273 | 274 | @doc """ 275 | Adds options to the collection overwriting database options 276 | 277 | new_opts must be a map with zero or more pairs represeting one of these options: 278 | 279 | * read: `:awaitdata`, `:nocursortimeout`, `:slaveok`, `:tailablecursor` 280 | * write concern: `:wc` 281 | * socket: `:mode`, `:timeout` 282 | """ 283 | def opts(collection, new_opts) do 284 | %__MODULE__{collection| opts: Map.merge(collection.opts, new_opts)} 285 | end 286 | 287 | @doc """ 288 | Gets read default options 289 | """ 290 | def read_opts(collection) do 291 | Map.take(collection.opts, [:awaitdata, :nocursortimeout, :slaveok, :tailablecursor, :mode, :timeout]) 292 | end 293 | 294 | @doc """ 295 | Gets write default options 296 | """ 297 | def write_opts(collection) do 298 | Map.take(collection.opts, [:wc, :mode, :timeout]) 299 | end 300 | 301 | @doc """ 302 | Creates an index for the collection 303 | """ 304 | def createIndex(collection, name, key, unique \\ false, options \\ %{}) do 305 | indexes = %{ 306 | key: key, 307 | name: name, 308 | unique: unique, 309 | } |> Map.merge(options) 310 | createIndexes(collection, [indexes]) 311 | end 312 | 313 | def createIndexes(collection, indexes) do 314 | req = %{ 315 | createIndexes: collection.name, 316 | indexes: indexes, 317 | } 318 | case Mongo.Db.cmd_sync(collection.db, req) do 319 | {:ok, %{docs: [resp]}} -> resp 320 | error -> error 321 | end 322 | end 323 | 324 | @doc """ 325 | Gets a list of All Indexes, only work at 3.0 326 | """ 327 | def getIndexes(collection) do 328 | case Mongo.Db.cmd_sync(collection.db, %{listIndexes: collection.name}) do 329 | {:ok, %{docs: [%{ok: 1.0} = resp]}} -> resp.cursor.firstBatch 330 | {:ok, %{docs: [error]}} -> error 331 | error -> error 332 | end 333 | end 334 | 335 | @doc """ 336 | Remove a Specific Index 337 | col = Mongo.connect! |> Mongo.db("AF_VortexShort_2358") |> Mongo.Db.collection("test.test") 338 | col |> Mongo.Collection.dropIndex(%{time: 1}) 339 | """ 340 | def dropIndex(collection, key) do 341 | Server.send(collection.db.mongo, 342 | Request.cmd(collection.db.name, %{deleteIndexes: collection.name}, %{index: key})) 343 | end 344 | 345 | @doc """ 346 | Remove All Indexes 347 | """ 348 | def dropIndexes(collection) do 349 | dropIndex(collection, "*") 350 | end 351 | 352 | end 353 | -------------------------------------------------------------------------------- /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, reqid) 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, reqid) 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, for: Mongo.Cursor 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.docs, 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{}=cursor -> reduce(cursor, {:cont, acc}, reducer) 60 | error -> {:halted, %Mongo.Error{error| acc: [cursor | error.acc]}} 61 | end 62 | end 63 | reduced -> reduce(cursor, reduced, reducer) 64 | end 65 | end 66 | def reduce(_, {:halt, acc}, _reducer), do: {:halted, acc} 67 | def reduce(cursor, {:suspend, acc}, reducer), do: {:suspended, acc, &reduce(cursor, &1, reducer)} 68 | 69 | 70 | @doc false 71 | #Not implemented use `Mongo.Collection.count/1` 72 | def count(_cursor), do: {:ok, -1} 73 | 74 | @doc false 75 | #Not implemented 76 | def member?(_, _cursor), do: {:ok, false} 77 | 78 | @doc false 79 | #Not implemented 80 | #https://hexdocs.pm/elixir/Enumerable.html#slice/1 81 | def slice(_), 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 | 6 | defstruct [ 7 | name: nil, 8 | mongo: nil, 9 | auth: nil, 10 | opts: %{} ] 11 | 12 | use Mongo.Helpers 13 | 14 | alias Mongo.Server 15 | 16 | @doc """ 17 | Creates `%Mongo.Db{}` with default options 18 | """ 19 | def new(mongo, name), do: %Mongo.Db{mongo: mongo, name: name, opts: Server.db_opts(mongo)} 20 | 21 | @doc """ 22 | Returns a collection struct 23 | """ 24 | defdelegate collection(db, name), to: Mongo.Collection, as: :new 25 | 26 | @doc """ 27 | Executes a db command requesting imediate response 28 | """ 29 | def cmd_sync(db, command, cmd_args \\ %{}) do 30 | case cmd(db, command, cmd_args) do 31 | {:ok, reqid} -> Server.response(db.mongo, reqid) 32 | error -> error 33 | end 34 | end 35 | 36 | @doc """ 37 | Executes a db command 38 | 39 | Before using this check `Mongo.Collection`, `Mongo.Db` or `Mongo.Server` 40 | for commands already implemented by these modules 41 | """ 42 | def cmd(db, cmd, cmd_args \\ %{}) do 43 | Server.send(db.mongo, Mongo.Request.cmd(db.name, cmd, cmd_args)) 44 | end 45 | defbang cmd(db, command) 46 | 47 | @doc """ 48 | Returns the error status of the preceding operation. 49 | """ 50 | def getLastError(db) do 51 | case cmd_sync(db, %{getlasterror: true}) do 52 | {:ok, resp} -> resp |> Mongo.Response.error 53 | error -> error 54 | end 55 | end 56 | defbang getLastError(db) 57 | 58 | @doc """ 59 | drop the database 60 | """ 61 | def dropDatabase(db) do 62 | case cmd_sync(db, %{dropDatabase: 1}) do 63 | {:ok, resp} -> resp |> Mongo.Response.error 64 | error -> error 65 | end 66 | end 67 | defbang dropDatabase(db) 68 | 69 | @doc """ 70 | Returns the previous error status of the preceding operation(s). 71 | """ 72 | def getPrevError(db) do 73 | case cmd_sync(db, %{getPrevError: true}) do 74 | {:ok, resp} -> resp |> Mongo.Response.error 75 | error -> error 76 | end 77 | end 78 | defbang getPrevError(db) 79 | 80 | @doc """ 81 | Resets error 82 | """ 83 | def resetError(db) do 84 | case cmd(db, %{resetError: true}) do 85 | {:ok, _} -> :ok 86 | error -> error 87 | end 88 | end 89 | defbang resetError(db) 90 | 91 | @doc """ 92 | Kill a cursor of the db 93 | """ 94 | def kill_cursor(db, cursorID) do 95 | Mongo.Request.kill_cursor(cursorID) |> Server.send(db.mongo) 96 | end 97 | 98 | @doc """ 99 | Adds options to the database overwriting mongo server connection options 100 | 101 | new_opts must be a map with zero or more of the following keys: 102 | 103 | * read: `:awaitdata`, `:nocursortimeout`, `:slaveok`, `:tailablecursor` 104 | * write concern: `:wc` 105 | * socket: `:mode`, `:timeout` 106 | """ 107 | def opts(db, new_opts) do 108 | %Mongo.Db{db| opts: Map.merge(db.opts, new_opts)} 109 | end 110 | 111 | @doc """ 112 | Gets collection default options 113 | """ 114 | def coll_opts(db) do 115 | Map.take(db.opts, [:awaitdata, :nocursortimeout, :slaveok, :tailablecursor, :wc]) 116 | end 117 | 118 | end 119 | -------------------------------------------------------------------------------- /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 | Limits the number of documents to the query. 42 | 43 | Must be run before executing the query 44 | """ 45 | def limit(find, limit) when is_integer(limit), do: %__MODULE__{find| batchSize: -limit} 46 | 47 | @doc """ 48 | Executes the query and returns a `%Mongo.Cursor{}` 49 | """ 50 | def exec(find) do 51 | Mongo.Cursor.exec(find.collection, Mongo.Request.query(find), find.batchSize) 52 | end 53 | 54 | @doc """ 55 | Runs the explain operator that provides information on the query plan 56 | """ 57 | def explain(find) do 58 | find |> addSpecial(:'$explain', 1) |> Enum.at(0) 59 | end 60 | 61 | @doc """ 62 | Add hint opperator that forces the query optimizer to use a specific index to fulfill the query 63 | """ 64 | def hint(f, hints) 65 | def hint(f, indexName) when is_atom(indexName), do: f |> addSpecial(:'$hint', indexName) 66 | def hint(f, hints) when is_map(hints), do: f |> addSpecial(:'$hint', hints) 67 | 68 | def sort(f, opts) when is_map(opts) or is_list(opts), do: f |> addSpecial(:"$orderby", opts) 69 | @doc """ 70 | Sets query options 71 | 72 | Defaults option set is equivalent of calling: 73 | 74 | Find.opts( 75 | awaitdata: false 76 | nocursortimeout: false 77 | slaveok: true 78 | tailablecursor: false) 79 | """ 80 | def opts(find, options), do: %__MODULE__{find| opts: options} 81 | 82 | def addSpecial(find, k, v) do 83 | %__MODULE__{find| mods: Map.put(find.mods, k, v)} 84 | end 85 | 86 | defimpl Enumerable, for: Mongo.Find do 87 | 88 | @doc """ 89 | Executes the query and reduce retrieved documents into a value 90 | """ 91 | def reduce(find, acc, reducer) do 92 | case Mongo.Find.exec(find) do 93 | %Mongo.Cursor{}=cursor -> 94 | case Enumerable.reduce(cursor, {:cont, acc}, 95 | fn(docs, acc)-> 96 | case Enumerable.reduce(docs, acc, reducer) do 97 | {:done, acc} -> {:cont, {:cont, acc}} 98 | {:halted, acc} -> {:halt, acc} 99 | {:suspended, acc} -> {:suspend, acc} 100 | error -> {:halt, error} 101 | end 102 | end) do 103 | {:done, {:cont, acc}} -> {:done, acc} 104 | other -> other 105 | end 106 | error -> 107 | case error do 108 | {:error, msg} -> raise Mongo.Bang, msg: msg, acc: acc 109 | %Mongo.Error{msg: msg, acc: acc} -> raise Mongo.Bang, msg: msg, acc: acc 110 | end 111 | end 112 | end 113 | 114 | @doc """ 115 | Counts number of documents to be retreived 116 | """ 117 | def count(find) do 118 | case Mongo.Collection.count(find.collection, find.selector, Map.take(find, [:skip, :limit])) do 119 | %Mongo.Error{} -> -1 120 | n -> n 121 | end 122 | end 123 | 124 | @doc """ 125 | Not implemented 126 | """ 127 | def member?(_, _), do: :not_implemented 128 | 129 | @doc false 130 | #Not implemented 131 | def slice(_), do: {:error, __MODULE__} 132 | end 133 | end 134 | -------------------------------------------------------------------------------- /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 | args = if is_list(args), do: args, else: [] 18 | {:__block__, [], quoted} = 19 | quote bind_quoted: [name: Macro.escape(name), args: Macro.escape(args)] do 20 | def unquote(to_string(name) <> "!" |> String.to_atom)(unquote_splicing(args)) do 21 | case unquote(name)(unquote_splicing(args)) do 22 | :ok -> :ok 23 | nil -> nil 24 | { :ok, result } -> result 25 | { :error, reason } -> raise Mongo.Bang, msg: reason, acc: unquote(args) 26 | %{msg: msg, acc: acc}=err -> raise Mongo.Bang, msg: msg, acc: acc 27 | end 28 | end 29 | end 30 | {:__block__, [], [{:@, [context: Mongo.Helpers, import: Kernel], [{:doc, [], ["See "<>to_string(name)<>"/"<>to_string(args |> length)]}]}|quoted]} 31 | end 32 | 33 | @doc """ 34 | Feeds sample data into a collection of database `test` 35 | """ 36 | def test_collection(collname) do 37 | mongo = Mongo.connect! 38 | db = Mongo.db(mongo, "test") 39 | collection = Mongo.Db.collection(db, collname) 40 | Mongo.Collection.drop collection 41 | [ 42 | %{a: 0, value: 0}, 43 | %{a: 1, value: 1}, 44 | %{a: 2, value: 1}, 45 | %{a: 3, value: 1}, 46 | %{a: 4, value: 1}, 47 | %{a: 5, value: 3} ] |> Mongo.Collection.insert(collection) 48 | collection 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /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, 0::32>> # 2001 update document 12 | @insert <<0xd2, 0x07, 0, 0, 0::32>> # 2002 insert new document 13 | @get_more <<0xd5, 0x07, 0, 0, 0::32>> # 2005 Get more data from a query. See Cursors 14 | @delete <<0xd6, 0x07, 0, 0, 0::32>> # 2006 Delete documents 15 | @kill_cursor <<0xd7, 0x07, 0, 0, 0::32>> # 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 | [ 30 | @query, (Enum.reduce(find.opts, @query_opts, &queryopt_red/2)), <<0::24>>, 31 | find.collection.db.name, 46, find.collection.name, 32 | <<0, find.skip::32-little-signed, find.batchSize::32-little-signed>>, 33 | Bson.encode(selector), 34 | Bson.encode(find.projector) 35 | ] 36 | end 37 | 38 | @doc """ 39 | Builds a database command message composed of the command tag and its arguments. 40 | """ 41 | def cmd(dbname, cmd, cmd_args \\ %{}) do 42 | [ 43 | @query, @query_opts, <<0::24>>, # [slaveok: true] 44 | dbname, <<".$cmd", 0, 0::32, 255, 255, 255, 255>>, # skip(0), batchSize(-1) 45 | document(cmd, cmd_args) 46 | ] 47 | end 48 | 49 | @doc """ 50 | Builds an insert command message 51 | """ 52 | def insert(collection, docs) do 53 | [ 54 | @insert, collection.db.name, 46, collection.name, <<0::8>>, 55 | Enum.map(docs, fn(doc) -> Bson.encode(doc) end) 56 | ] 57 | end 58 | 59 | @doc """ 60 | Builds an update command message 61 | """ 62 | def update(collection, selector, update, upsert, multi) do 63 | [ 64 | @update, 65 | collection.db.name, 46, collection.name, 66 | <<0::8, 0::6, (bit(multi))::1, (bit(upsert))::1, 0::24>>, 67 | document(selector), document(update) 68 | ] 69 | end 70 | # transforms `true` and `false` to bits 71 | defp bit(false), do: 0 72 | defp bit(true), do: 1 73 | 74 | @doc """ 75 | Builds a delete command message 76 | """ 77 | def delete(collection, selector, justOne) do 78 | [ 79 | @delete, 80 | collection.db.name, 46, collection.name, 81 | <<0, 0::7, (bit(justOne))::1, 0::24>>, 82 | document(selector) 83 | ] 84 | end 85 | 86 | @doc """ 87 | Builds a kill_cursor command message 88 | """ 89 | def kill_cursor(cursorid) do 90 | [ 91 | @kill_cursor, 92 | <<1::32-little-signed, cursorid::64-little-signed>> 93 | ] 94 | end 95 | 96 | @doc """ 97 | Builds a get_more command message 98 | """ 99 | def get_more(collection, batchsize, cursorid) do 100 | [ 101 | @get_more, 102 | collection.db.name, 46, collection.name, 103 | <<0, batchsize::32-little-signed, cursorid::64-little-signed>>, 104 | ] 105 | end 106 | 107 | # transform a document into bson 108 | defp document(command), do: Bson.encode(command) 109 | defp document(command, command_args) do 110 | Bson.encode(Enum.to_list(command) ++ Enum.to_list(command_args)) 111 | end 112 | 113 | use Bitwise 114 | # Operates one option 115 | defp queryopt_red({opt, true}, bits), do: bits ||| queryopt(opt) 116 | defp queryopt_red({opt, false}, bits), do: bits &&& ~~~queryopt(opt) 117 | defp queryopt_red(_, bits), do: bits 118 | # Identifies the bit that is switched by an option when it is set to `true` 119 | defp queryopt(:awaitdata), do: 0b00100000 120 | defp queryopt(:nocursortimeout), do: 0b00010000 121 | defp queryopt(:slaveok), do: 0b00000100 122 | defp queryopt(:tailablecursor), do: 0b00000010 123 | defp queryopt(_), do: 0b00000000 124 | 125 | end 126 | -------------------------------------------------------------------------------- /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 | docs: nil, 10 | requestID: nil] 11 | 12 | @msg <<1, 0, 0, 0>> # 1 Opcode OP_REPLY : Reply to a client request 13 | 14 | @doc """ 15 | Parses a response message 16 | 17 | If the message is partial, this method makes shure the response is complete by fetching additional messages 18 | """ 19 | def new( 20 | <<_::32, # total message size, including this 21 | _::32, # identifier for this message 22 | requestID::binary-size(4), # requestID from the original request 23 | @msg::binary, # Opcode OP_REPLY 24 | _::6, queryFailure::1, cursorNotFound::1, _::24, # bit vector representing response flags 25 | cursorID::size(64)-signed-little, # cursor id if client needs to do get more's 26 | startingFrom::size(32)-signed-little, # where in the cursor this reply is starting 27 | numberReturned::size(32)-signed-little, # number of documents in the reply 28 | buffer::bitstring>>) do # buffer of Bson documents 29 | cond do 30 | cursorNotFound > 0 -> 31 | %Mongo.Error{msg: :"cursor not found"} 32 | queryFailure > 0 -> 33 | if numberReturned > 0 do 34 | case bson_decode_all(buffer) do 35 | %Mongo.Error{} = _error -> 36 | %Mongo.Error{msg: :"query failure"} 37 | docs -> 38 | %Mongo.Error{ msg: :"query failure", acc: docs} 39 | end 40 | else 41 | %Mongo.Error{msg: :"query failure"} 42 | end 43 | true -> 44 | case bson_decode_all(buffer) do 45 | %Mongo.Error{} = error -> error 46 | docs when length(docs) == numberReturned -> {:ok, %Mongo.Response{ 47 | cursorID: cursorID, 48 | startingFrom: startingFrom, 49 | nbdoc: numberReturned, 50 | docs: docs, 51 | requestID: requestID}} 52 | _ -> %Mongo.Error{msg: :"query failure"} 53 | end 54 | end 55 | end 56 | 57 | def new(%Mongo.Error{msg: msg}) do 58 | %Mongo.Error{msg: msg} 59 | end 60 | 61 | @doc """ 62 | Decodes a command response 63 | 64 | Returns `{:ok, doc}` or transfers the error message 65 | """ 66 | def cmd(%Mongo.Response{nbdoc: 1, docs: [doc]}) do 67 | case doc do 68 | %{ok: ok} = doc when ok > 0 -> {:ok, doc} 69 | errdoc -> %Mongo.Error{msg: :"cmd error", acc: errdoc} 70 | end 71 | end 72 | 73 | @doc """ 74 | Decodes a count respsonse 75 | 76 | Returns `{:ok, n}` or transfers the error message 77 | """ 78 | def count(response) do 79 | case cmd(response) do 80 | {:ok, doc} -> {:ok, doc[:n]} 81 | error -> error 82 | end 83 | end 84 | 85 | @doc """ 86 | Decodes a success respsonse 87 | 88 | Returns `:ok` or transfers the error message 89 | """ 90 | def success(response) do 91 | case cmd(response) do 92 | {:ok, _} -> :ok 93 | error -> error 94 | end 95 | end 96 | 97 | @doc """ 98 | Decodes a distinct respsonse 99 | 100 | Returns `{:ok, values}` or transfers the error message 101 | """ 102 | def distinct(response) do 103 | case cmd(response) do 104 | {:ok, doc} -> {:ok, doc[:values]} 105 | error -> error 106 | end 107 | end 108 | 109 | @doc """ 110 | Decodes a map-reduce respsonse 111 | 112 | Returns `{:ok, results}` (inline) or `:ok` or transfers the error message 113 | """ 114 | def mr(response) do 115 | case cmd(response) do 116 | {:ok, doc} -> 117 | case doc[:results] do 118 | nil -> :ok 119 | results -> {:ok, results} 120 | end 121 | error -> error 122 | end 123 | end 124 | 125 | @doc """ 126 | Decodes a group respsonse 127 | 128 | Returns `{:ok, retval}` or transfers the error message 129 | """ 130 | def group(response) do 131 | case cmd(response) do 132 | {:ok, doc} -> {:ok, doc[:retval]} 133 | error -> error 134 | end 135 | end 136 | 137 | @doc """ 138 | Decodes an aggregate respsonse 139 | 140 | Returns `{:ok, result}` or transfers the error message 141 | """ 142 | def aggregate(response) do 143 | case cmd(response) do 144 | {:ok, doc} -> 145 | doc.cursor.firstBatch #TODO: 这个库.. 包括 getIndexes, 就是这么粗糙的只拿第一批, 等我们壮大到 batch 不够用, 再加上... 146 | error -> error 147 | end 148 | end 149 | @doc """ 150 | Decodes a getnonce respsonse 151 | 152 | Returns `{:ok, nonce}` or transfers the error message 153 | """ 154 | def getnonce(response) do 155 | case cmd(response) do 156 | {:ok, doc} -> doc[:nonce] 157 | error -> error 158 | end 159 | end 160 | @doc """ 161 | Decodes an error respsonse 162 | 163 | Returns `{:ok, nonce}` or transfers the error message 164 | """ 165 | def error(response) do 166 | case cmd(response) do 167 | {:ok, doc} -> 168 | case doc[:err] do 169 | nil -> {:ok, doc} 170 | _ -> {:error, doc} 171 | end 172 | error -> error 173 | end 174 | end 175 | 176 | @doc """ 177 | Helper fuction to decode bson buffer 178 | """ 179 | def bson_decode_all(<<>>), do: [] 180 | def bson_decode_all(buffer) do 181 | try do 182 | bson_decode_all(buffer, []) 183 | catch 184 | error -> 185 | %Mongo.Error{msg: :bson_decode_error, acc: [error]} 186 | end 187 | end 188 | 189 | defp bson_decode_all(buffer, acc) do 190 | case Bson.decode(buffer, [:return_atom, :return_trailer]) do 191 | {:has_trailer, doc, rest} -> bson_decode_all(rest, [doc|acc]) 192 | doc -> [doc | acc] |> :lists.reverse 193 | end 194 | end 195 | end 196 | -------------------------------------------------------------------------------- /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 | wire_version: nil, 12 | opts: %{}, 13 | id_prefix: nil, 14 | socket: nil ] 15 | 16 | @port 27017 17 | @mode :passive 18 | @host "127.0.0.1" 19 | @timeout 6000 20 | 21 | use Mongo.Helpers 22 | require Logger 23 | 24 | @doc """ 25 | connects to local mongodb server by defaults to {"127.0.0.1", 27017} 26 | 27 | This can be overwritten by the environment variable `:host`, ie: 28 | 29 | ```erlang 30 | [ 31 | {mongo, 32 | [ 33 | {host, {"127.0.0.1", 27017}} 34 | ]} 35 | ]. 36 | ``` 37 | """ 38 | def connect do 39 | connect %{} 40 | end 41 | 42 | @doc """ 43 | connects to a mongodb server 44 | """ 45 | def connect(host, port) when is_binary(host) and is_integer(port) do 46 | connect %{host: host, port: port} 47 | end 48 | 49 | @doc """ 50 | connects to a mongodb server specifying options 51 | 52 | Opts must be a Map 53 | """ 54 | def connect(opts) when is_map(opts) do 55 | host = Map.get(opts, :host, @host) 56 | mongo_server = %Mongo.Server{ 57 | host: case host do 58 | host when is_binary(host) -> String.to_charlist(host) 59 | host -> host 60 | end, 61 | port: Map.get(opts, :port, @port), 62 | mode: Map.get(opts, :mode, @mode), 63 | timeout: Map.get(opts, :timeout, @timeout), 64 | id_prefix: mongo_prefix()} 65 | 66 | case tcp_connect(mongo_server) do 67 | {:ok, s} -> 68 | with {:ok, s1} <- wire_version(s), 69 | {:ok, s2} <- maybe_auth(opts, s1) do 70 | {:ok, s2} 71 | else 72 | error -> 73 | close(s) 74 | error 75 | end 76 | error -> error 77 | end 78 | end 79 | 80 | @doc false 81 | def tcp_connect(mongo) do 82 | case :gen_tcp.connect(mongo.host, mongo.port, tcp_options(mongo), mongo.timeout) do 83 | {:ok, socket} -> 84 | {:ok, %Mongo.Server{mongo| socket: socket}} 85 | error -> error 86 | end 87 | end 88 | 89 | @doc false 90 | defp wire_version(mongo) do 91 | cmd = %{ismaster: 1} 92 | case cmd_sync(mongo, cmd) do 93 | {:ok, resp} -> 94 | case Mongo.Response.cmd(resp) do 95 | {:ok, %{maxWireVersion: version}} -> {:ok, %{mongo | wire_version: version}} 96 | {:ok, %{ok: ok}} when ok == 1 -> {:ok, %{mongo | wire_version: 0}} 97 | error -> error 98 | end 99 | error -> error 100 | end 101 | end 102 | 103 | @doc false 104 | defp maybe_auth(opts, mongo) do 105 | if opts[:username] != nil and opts[:password] != nil do 106 | Mongo.Auth.auth(opts, mongo) 107 | else 108 | {:ok, mongo} 109 | end 110 | end 111 | 112 | @doc false 113 | defp tcp_recv(mongo) do 114 | :gen_tcp.recv(mongo.socket, 0, mongo.timeout) 115 | end 116 | 117 | @doc """ 118 | Retreives a repsonce from the MongoDB server (only for passive mode) 119 | """ 120 | def response(mongo, req_id) do 121 | case tcp_recv(mongo) do 122 | {:ok, <> = message} -> 123 | case complete(mongo, messageLength, message) |> Mongo.Response.new do 124 | {:ok, response = %Mongo.Response{requestID: res_id}} when res_id != req_id -> 125 | Logger.info("#{__MODULE__} receive unknown package from mongo: #{inspect response}") 126 | response(mongo, req_id) 127 | res -> res 128 | end 129 | {:error, msg} -> %Mongo.Error{msg: msg} 130 | end 131 | end 132 | 133 | @doc """ 134 | Sends a message to MongoDB 135 | """ 136 | def send(mongo, payload, reqid \\ gen_reqid()) 137 | def send(%Mongo.Server{socket: socket, mode: :passive}, payload, reqid) do 138 | do_send(socket, payload, reqid) 139 | end 140 | def send(%Mongo.Server{socket: socket, mode: :active}, payload, reqid) do 141 | :inet.setopts(socket, active: :once) 142 | do_send(socket, payload, reqid) 143 | end 144 | # sends the message to the socket, returns request {:ok, reqid} 145 | defp do_send(socket, payload, reqid) do 146 | case :gen_tcp.send(socket, payload |> message(reqid)) do 147 | :ok -> {:ok, reqid} 148 | error -> raise Mongo.Bang, msg: :network_error, acc: error 149 | end 150 | end 151 | 152 | @doc false 153 | # preprares for a one-time async request 154 | def async(%Mongo.Server{mode: :passive}=mongo) do 155 | :inet.setopts(mongo.socket, active: :once) 156 | end 157 | 158 | @doc """ 159 | Sends a command message requesting imediate response 160 | """ 161 | def cmd_sync(mongo, command) do 162 | case cmd(mongo, command) do 163 | {:ok, reqid} -> 164 | response(mongo, reqid) 165 | error -> error 166 | end 167 | end 168 | 169 | @doc """ 170 | Executes an admin command to the server 171 | 172 | iex> Mongo.connect! # Returns a exception when connection fails 173 | iex> case Mongo.connect do 174 | ...> {:ok, _mongo } -> :ok 175 | ...> error -> error 176 | ...> end 177 | :ok 178 | 179 | """ 180 | def cmd(mongo, cmd) do 181 | send(mongo, Mongo.Request.cmd("admin", cmd)) 182 | end 183 | 184 | @doc """ 185 | Pings the server 186 | 187 | iex> Mongo.connect! |> Mongo.Server.ping 188 | :ok 189 | 190 | """ 191 | def ping(mongo) do 192 | case cmd_sync(mongo, %{ping: true}) do 193 | {:ok, resp} -> Mongo.Response.success(resp) 194 | error -> error 195 | end 196 | end 197 | 198 | @doc """ 199 | Returns true if connection mode is active 200 | """ 201 | def active?(mongo), do: mongo.mode == :active 202 | 203 | @doc """ 204 | Closes the connection 205 | """ 206 | def close(mongo) do 207 | :gen_tcp.close(mongo.socket) 208 | end 209 | 210 | # makes sure response is complete 211 | defp complete(_mongo, expected_length, buffer) when byte_size(buffer) == expected_length, do: buffer 212 | defp complete(mongo, expected_length, buffer) do 213 | case tcp_recv(mongo) do 214 | {:ok, mess} -> complete(mongo, expected_length, buffer <> mess) 215 | {:error, msg} -> %Mongo.Error{msg: msg} 216 | end 217 | end 218 | 219 | # Convert TCP options to `:inet.setopts` compatible arguments. 220 | defp tcp_options(m) do 221 | args = options(m) 222 | 223 | # default to binary 224 | args = [:binary | args] 225 | 226 | args 227 | end 228 | # default server options 229 | defp options(mongo) do 230 | [ active: false, 231 | nodelay: true, 232 | send_timeout: mongo.timeout, 233 | send_timeout_close: true ] 234 | end 235 | 236 | defp mongo_prefix do 237 | case :inet.gethostname do 238 | {:ok, hostname} -> 239 | <> = :crypto.hash(:md5, (hostname ++ :os.getpid) |> to_string) 240 | prefix 241 | _ -> :rand.uniform(65535) 242 | end 243 | end 244 | @doc false 245 | def prefix(%Mongo.Server{id_prefix: prefix}) do 246 | for << <> <- <> >>, into: <<>> do 247 | <> 248 | end |> String.downcase 249 | end 250 | 251 | @doc """ 252 | Adds options to an existing mongo server connection 253 | 254 | new_opts must be a map with zero or more of the following keys: 255 | 256 | * read: `:awaitdata`, `:nocursortimeout`, `:slaveok`, `:tailablecursor` 257 | * write concern: `:wc` 258 | * socket: `:mode`, `:timeout` 259 | """ 260 | def opts(mongo, new_opts) do 261 | %Mongo.Server{mongo| opts: Map.merge(mongo.opts, new_opts)} 262 | end 263 | 264 | @doc """ 265 | Gets mongo connection default options 266 | """ 267 | def db_opts(mongo) do 268 | Map.take(mongo.opts, [:awaitdata, :nocursortimeout, :slaveok, :tailablecursor, :wc]) #, :mode, :timeout]) 269 | |> Map.put(:mode, mongo.mode) |> Map.put(:timeout, mongo.timeout) 270 | end 271 | 272 | use Bitwise, only_operators: true 273 | @doc """ 274 | Assigns radom ids to a list of documents when `:_id` is missing 275 | 276 | iex> [%{a: 1}] |> Mongo.Server.assign_id |> Enum.at(0) |> Map.keys 277 | [:_id, :a] 278 | 279 | #a prefix to ids can be set manually like this 280 | iex> prefix = case [%{a: 1}] |> Mongo.Server.assign_id(256*256-1) |> Enum.at(0) |> Map.get(:_id) do 281 | ...> %Bson.ObjectId{oid: <>} -> prefix 282 | ...> error -> error 283 | ...> end 284 | ...> prefix 285 | 256*256-1 286 | 287 | #by default prefix are set at connection time and remains identical for the entire connection 288 | iex> mongo = Mongo.connect! 289 | ...> prefix = case [%{a: 1}] |> Mongo.Server.assign_id(mongo) |> Enum.at(0) |> Map.get(:_id) do 290 | ...> %Bson.ObjectId{oid: <>} -> prefix 291 | ...> error -> error 292 | ...> end 293 | ...> prefix == mongo.id_prefix 294 | true 295 | 296 | """ 297 | def assign_id(docs, client_prefix \\ gen_client_prefix()) 298 | def assign_id(docs, client_prefix) do 299 | client_prefix = check_client_prefix(client_prefix) 300 | Enum.map_reduce( 301 | docs, 302 | {client_prefix, gen_trans_prefix(), :rand.uniform(4294967295)}, 303 | fn(doc, id) -> { Map.put(doc, :_id, %Bson.ObjectId{oid: to_oid(id)}), next_id(id) } end) 304 | |> elem(0) 305 | end 306 | 307 | # returns a 2 bites prefix integer 308 | defp check_client_prefix(%Mongo.Server{id_prefix: prefix}) when is_integer(prefix), do: prefix 309 | defp check_client_prefix(prefix) when is_integer(prefix), do: prefix 310 | defp check_client_prefix(_), do: gen_client_prefix() 311 | # generates a 2 bites prefix integer 312 | defp gen_client_prefix, do: :rand.uniform(65535) 313 | # returns a 6 bites prefix integer 314 | defp gen_trans_prefix do 315 | {gs, s, ms} = :erlang.timestamp() 316 | (gs * 1000000000000 + s * 1000000 + ms) &&& 281474976710655 317 | end 318 | 319 | # from a 3 integer tuple to ObjectID 320 | defp to_oid({client_prefix, trans_prefix, suffix}), do: <> 321 | # Selects next ID 322 | defp next_id({client_prefix, trans_prefix, suffix}), do: {client_prefix, trans_prefix, suffix+1} 323 | 324 | # add request ID to a payload message 325 | defp message(payload, reqid) 326 | defp message(payload, reqid) do 327 | [ 328 | <<(:erlang.iolist_size(payload) + 12)::size(32)-little>>, 329 | reqid, <<0::32>>, payload 330 | ] 331 | end 332 | # generates a request Id when not provided (makes sure it is a positive integer) 333 | defp gen_reqid() do 334 | <> = :crypto.strong_rand_bytes(4) 335 | <> 336 | end 337 | end 338 | -------------------------------------------------------------------------------- /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.5", 8 | elixir: "~> 1.7", 9 | source_url: "https://github.com/ejoy/elixir-mongo", 10 | description: "MongoDB driver for Elixir", 11 | deps: deps(), 12 | package: package(), 13 | docs: &docs/0 ] 14 | end 15 | 16 | # Configuration for the OTP application 17 | def application do 18 | [ 19 | applications: [:logger], 20 | env: [host: {"127.0.0.1", 27017}] 21 | ] 22 | end 23 | 24 | # Returns the list of dependencies for prod 25 | defp deps() do 26 | [ 27 | {:ex_doc, ">= 0.0.0", only: :doc }, 28 | {:earmark, ">= 0.0.0", only: :doc}, 29 | {:cbson, "~> 0.1.1"} 30 | ] 31 | end 32 | 33 | defp docs do 34 | [ #readme: false, 35 | #main: "README", 36 | source_ref: System.cmd("git", ["rev-parse", "--verify", "--quiet", "HEAD"])|>elem(0) ] 37 | end 38 | 39 | defp package do 40 | [ contributors: ["jerp"], 41 | licenses: ["MIT"], 42 | links: %{ 43 | "GitHub" => "https://github.com/ejoy/elixir-mongo", 44 | "Documentation" => "https://checkiz.github.io/elixir-mongo" 45 | } ] 46 | end 47 | 48 | end 49 | -------------------------------------------------------------------------------- /mix.lock: -------------------------------------------------------------------------------- 1 | %{ 2 | "bson": {:hex, :bson, "0.4.4"}, 3 | "cbson": {:hex, :cbson, "0.1.1", "17781c4342bb54f40a26e1641732158946a48da7bfde6671a2081037e7728b9a", [:make, :mix], [], "hexpm"}, 4 | "earmark": {:hex, :earmark, "1.3.0", "17f0c38eaafb4800f746b457313af4b2442a8c2405b49c645768680f900be603", [:mix], [], "hexpm"}, 5 | "ex_doc": {:hex, :ex_doc, "0.19.1", "519bb9c19526ca51d326c060cb1778d4a9056b190086a8c6c115828eaccea6cf", [:mix], [{:earmark, "~> 1.1", [hex: :earmark, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.7", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm"}, 6 | "makeup": {:hex, :makeup, "0.5.5", "9e08dfc45280c5684d771ad58159f718a7b5788596099bdfb0284597d368a882", [:mix], [{:nimble_parsec, "~> 0.4", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm"}, 7 | "makeup_elixir": {:hex, :makeup_elixir, "0.10.0", "0f09c2ddf352887a956d84f8f7e702111122ca32fbbc84c2f0569b8b65cbf7fa", [:mix], [{:makeup, "~> 0.5.5", [hex: :makeup, repo: "hexpm", optional: false]}], "hexpm"}, 8 | "nimble_parsec": {:hex, :nimble_parsec, "0.4.0", "ee261bb53214943679422be70f1658fff573c5d0b0a1ecd0f18738944f818efe", [:mix], [], "hexpm"}, 9 | } 10 | -------------------------------------------------------------------------------- /test/mongo_aggr_test.exs: -------------------------------------------------------------------------------- 1 | Code.require_file "test_helper.exs", __DIR__ 2 | 3 | defmodule Mongo.Aggr.Test do 4 | require Logger 5 | use ExUnit.Case, async: false 6 | 7 | # In order to run the tests a mongodb server must be listening locally on the default port 8 | setup do 9 | mongo = Mongo.connect! 10 | db = Mongo.db(mongo, "test") 11 | anycoll = Mongo.Db.collection(db, "coll_aggr") 12 | Mongo.Collection.drop anycoll 13 | [ 14 | %{a: 0, value: 0}, 15 | %{a: 1, value: 1}, 16 | %{a: 2, value: 1}, 17 | %{a: 3, value: 1}, 18 | %{a: 4, value: 1}, 19 | %{a: 5, value: 3} ] |> Mongo.Collection.insert(anycoll) 20 | { :ok, mongo: mongo, db: db, anycoll: anycoll } 21 | end 22 | 23 | test "count", ctx do 24 | if true do 25 | assert ctx[:anycoll] |> Mongo.Collection.count!(%{value: %{'$gt': 0}}) == 5 26 | end 27 | end 28 | 29 | test "distinct", ctx do 30 | if true do 31 | assert ctx[:anycoll] |> Mongo.Collection.distinct!("value", %{value: %{"$lt": 3}}) 32 | |> is_list 33 | end 34 | end 35 | 36 | test "mapreduce", ctx do 37 | if true do 38 | anycoll = ctx[:anycoll] 39 | assert Mongo.Collection.mr!(anycoll, "function(d){emit(this._id, this.value*2)}", "function(k, vs){return Array.sum(vs)}") |> is_list 40 | assert :ok == Mongo.Collection.mr!(anycoll, "function(d){emit('z', 3*this.value)}", "function(k, vs){return Array.sum(vs)}", "somecol") 41 | end 42 | end 43 | 44 | test "group", ctx do 45 | if true do 46 | assert ctx[:anycoll] |> Mongo.Collection.group!(%{a: true}) |> is_list 47 | end 48 | end 49 | 50 | test "aggregate", ctx do 51 | if true do 52 | assert [%{value: 1}|_] = ctx[:anycoll] |> Mongo.Collection.aggregate([ 53 | %{'$skip': 1}, 54 | %{'$limit': 5}, 55 | %{'$project': %{_id: false, value: true}} 56 | ], %{cursor: %{}}) 57 | end 58 | end 59 | 60 | test "error count", ctx do 61 | if true do 62 | assert %Mongo.Error{} = ctx[:anycoll] |> Mongo.Collection.count(%{value: %{'$in': 0}}) 63 | end 64 | end 65 | 66 | end 67 | -------------------------------------------------------------------------------- /test/mongo_collection_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Mongo.Db.Collection.Test do 2 | use ExUnit.Case, async: false 3 | 4 | test "drop database" do 5 | mongo = Mongo.connect! 6 | db = Mongo.db(mongo, "test") 7 | Mongo.Db.dropDatabase(db) 8 | 9 | anycoll = Mongo.Db.collection(db, "index_test") 10 | %{code: code} = Mongo.Collection.getIndexes(anycoll) 11 | assert code != 0 12 | 13 | [%{a: 1, b: 2}] 14 | |> Mongo.Collection.insert(anycoll) 15 | 16 | assert [_idx] = Mongo.Collection.getIndexes(anycoll) 17 | end 18 | end 19 | 20 | -------------------------------------------------------------------------------- /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 | require Logger 25 | anycoll = ctx[:anycoll] 26 | # count without retreiving 27 | assert anycoll |> Mongo.Collection.find |> Enum.count == 6 28 | # retreive all docs then count 29 | assert anycoll |> Mongo.Collection.find |> Enum.to_list |> Enum.count == 6 30 | # retreive all but one doc then count 31 | assert anycoll |> Mongo.Collection.find |> Mongo.Find.skip(1) |> Enum.to_list |> Enum.count == 5 32 | 33 | # retreive one doc 34 | assert anycoll |> Mongo.Collection.find |> Mongo.Find.limit(1) |> Enum.to_list |> Enum.count == 1 35 | end 36 | end 37 | 38 | test "find where", ctx do 39 | if true do 40 | assert ctx[:anycoll] |> Mongo.Collection.find("obj.value == 0") |> Enum.count == 1 41 | assert ctx[:anycoll] |> Mongo.Collection.find("obj.value == 0") |> Enum.to_list |> Enum.count == 1 42 | end 43 | end 44 | 45 | test "insert", ctx do 46 | anycoll = ctx[:anycoll] 47 | if true do 48 | assert %{a: 23} |> Mongo.Collection.insert_one!(anycoll) == %{a: 23} 49 | assert [%{a: 23}, %{a: 24, b: 1}] |> Mongo.Collection.insert!(anycoll) |> is_list 50 | end 51 | if true do 52 | assert %{_id: 2, a: 456} |> Mongo.Collection.insert_one!(anycoll) |> is_map 53 | assert {:ok, _} = ctx[:db] |> Mongo.Db.getLastError 54 | end 55 | end 56 | 57 | test "update", ctx do 58 | if true do 59 | ctx[:anycoll] |> Mongo.Collection.update(%{a: 456}, %{a: 123, b: 789}) 60 | assert {:ok, _} = ctx[:db] |> Mongo.Db.getLastError 61 | end 62 | end 63 | 64 | test "delete", ctx do 65 | if true do 66 | ctx[:anycoll] |> Mongo.Collection.delete(%{b: 789}) 67 | assert {:ok, _} = ctx[:db] |> Mongo.Db.getLastError 68 | end 69 | end 70 | 71 | test "objid", ctx do 72 | if true do 73 | anycoll = ctx[:anycoll] 74 | assert [%{a: -23}, %{a: -24, b: 1}] |> Mongo.Server.assign_id(ctx[:mongo]) |> Mongo.Collection.insert!(anycoll) |> is_list 75 | end 76 | end 77 | 78 | test "bang find", ctx do 79 | if true do 80 | assert %Mongo.Error{} = ctx[:anycoll] |> Mongo.Collection.find(%{value: %{'$in': 0}}) |> Mongo.Find.exec 81 | end 82 | end 83 | 84 | test "insert error", ctx do 85 | anycoll = ctx[:anycoll] 86 | if true do 87 | %{_id: 1, a: 31} |> Mongo.Collection.insert_one!(anycoll) 88 | %{_id: 1, a: 32} |> Mongo.Collection.insert_one!(anycoll) 89 | assert {:error, _} = ctx[:db] |> Mongo.Db.getLastError 90 | end 91 | end 92 | 93 | test "atom collection name", ctx do 94 | assert ctx.db |> Mongo.Db.collection(:coll_crud) |> Mongo.Collection.find("obj.value == 0") |> Enum.count == 1 95 | end 96 | end 97 | -------------------------------------------------------------------------------- /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, 3) |> 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 32 | end 33 | 34 | test "find hint", ctx do 35 | ctx[:anycoll] |> Mongo.Collection.createIndex("tst_value", %{value: true}) 36 | explain = ctx[:anycoll] |> Mongo.Collection.find |> Mongo.Find.hint(%{value: true}) |> Mongo.Find.explain 37 | assert "tst_value" == explain["queryPlanner"]["winningPlan"]["inputStage"][:indexName] 38 | end 39 | 40 | end 41 | -------------------------------------------------------------------------------- /test/mongo_db_test.exs: -------------------------------------------------------------------------------- 1 | defmodule Mongo.Db.Test do 2 | use ExUnit.Case, async: false 3 | 4 | test "drop database" do 5 | mongo = Mongo.connect! 6 | db = Mongo.db(mongo, "test_drop") 7 | anycoll = Mongo.Db.collection(db, "coll_db") 8 | Mongo.Collection.drop anycoll 9 | [ 10 | %{a: 0, value: 0}, 11 | %{a: 1, value: 1}, 12 | %{a: 2, value: 1}, 13 | %{a: 3, value: 1}, 14 | %{a: 4, value: 1}, 15 | %{a: 5, value: 3} ] |> Mongo.Collection.insert(anycoll) 16 | 17 | assert %{dropped: "test_drop", ok: 1.0} = Mongo.Db.dropDatabase!(db) 18 | 19 | assert anycoll |> Mongo.Collection.find |> Enum.count == 0 20 | end 21 | end 22 | 23 | -------------------------------------------------------------------------------- /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, _doc} = Mongo.Db.getLastError(db) 92 | db = Mongo.connect!(%{timeout: 5}) |> 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 | --------------------------------------------------------------------------------