1 /// Provides a connection for memgraph. 2 module memgraph.client; 3 4 import std.string : fromStringz, toStringz; 5 6 import memgraph.mgclient, memgraph.value, memgraph.map, memgraph.params, memgraph.result; 7 8 /// Provides a connection for memgraph. 9 struct Client { 10 /// Disable copying. 11 @disable this(this); 12 13 /// Client software version. 14 /// Return: Client version in the major.minor.patch format. 15 static auto clientVersion() { return fromStringz(mg_client_version()); } 16 17 /// Obtains the error message stored in the current session (if any). 18 @property auto error() { 19 assert(ptr_ != null); 20 return fromStringz(mg_session_error(ptr_)); 21 } 22 23 /// Returns the status of the current session. 24 /// Return: One of the session codes in `mg_session_code`. 25 @property auto status() inout { 26 assert(ptr_ != null); 27 return mg_session_status(ptr_); 28 } 29 30 /// Runs the given Cypher `statement` and discards any possible results. 31 /// Return: true when the statement ran successfully, false otherwise. 32 bool run(const string statement) { 33 auto result = execute(statement); 34 if (!result) 35 return false; 36 foreach (r; result) { } 37 return true; 38 } 39 40 /// Executes the given Cypher `statement`. 41 /// Return: `Result` that can be used as a range e.g. using foreach() to process all results. 42 /// After executing the statement, the method is blocked until all incoming 43 /// data (execution results) are handled, i.e. until the returned `Result` has been completely processed. 44 Result execute(const string statement) { 45 return execute(statement, Map(0)); 46 } 47 48 /// Executes the given Cypher `statement`, supplied with additional `params`. 49 /// Return: `Result` that can be used as a range e.g. using foreach() to process all results. 50 /// After executing the statement, the method is blocked until all incoming 51 /// data (execution results) are handled, i.e. until the returned `Result` has been completely processed. 52 Result execute(const string statement, const Map params) { 53 assert(ptr_ != null); 54 int status = mg_session_run(ptr_, toStringz(statement), params.ptr, null, null, null); 55 if (status < 0) 56 return Result(); 57 status = mg_session_pull(ptr_, null); 58 if (status < 0) 59 return Result(); 60 return Result(ptr_); 61 } 62 63 /* 64 /// Fetches the next result from the input stream. 65 /// Return next result from the input stream. 66 /// If there is nothing to fetch, an empty array is returned. 67 Value[] fetchOne() { 68 // TODO: encapsulate mg_result as `Result` 69 mg_result *result; 70 Value[] values; 71 immutable status = mg_session_fetch(session, &result); 72 if (status != 1) 73 return values; 74 75 const (mg_list) *list = mg_result_row(result); 76 const size_t list_length = mg_list_size(list); 77 values.length = list_length; 78 for (uint i = 0; i < list_length; ++i) 79 values[i] = Value(mg_list_at(list, i)); 80 return values; 81 } 82 83 /// Fetches all results and discards them. 84 void discardAll() { 85 while (fetchOne()) { } 86 } 87 88 /// Fetches all results. 89 Value[][] fetchAll() { 90 Value[] maybeResult; 91 Value[][] data; 92 while ((maybeResult = fetchOne()).length > 0) 93 data ~= maybeResult; 94 return data; 95 } 96 */ 97 98 /// Start a transaction. 99 /// Return: true when the transaction was successfully started, false otherwise. 100 bool begin() { 101 assert(ptr_ != null); 102 return mg_session_begin_transaction(ptr_, null) == 0; 103 } 104 105 /// Commit current transaction. 106 /// Return: true when the transaction was successfully committed, false otherwise. 107 bool commit() { 108 assert(ptr_ != null); 109 mg_result *result; 110 return mg_session_commit_transaction(ptr_, &result) == 0; 111 } 112 113 /// Rollback current transaction. 114 /// Return: true when the transaction was successfully rolled back, false otherwise. 115 bool rollback() { 116 assert(ptr_ != null); 117 mg_result *result; 118 return mg_session_rollback_transaction(ptr_, &result) == 0; 119 } 120 121 /// Static method that creates a Memgraph client instance using default parameters 127.0.0.1:7687 122 /// Return: client connection instance. 123 /// Returns an unconnected instance if the connection couldn't be established. 124 static Client connect() { 125 Params params; 126 return connect(params); 127 } 128 129 /// Static method that creates a Memgraph client instance. 130 /// Return: client connection instance. 131 /// If the connection couldn't be established given the `params`, it will 132 /// return an unconnected instance. 133 static Client connect(ref Params params) { 134 mg_session *session = null; 135 immutable status = mg_connect(params.ptr, &session); 136 if (status < 0) { 137 if (session) 138 mg_session_destroy(session); 139 return Client(); 140 } 141 return Client(session); 142 } 143 144 /// Assigns a client to another. The target of the assignment gets detached from 145 /// whatever client it was attached to, and attaches itself to the new client. 146 ref Client opAssign(ref Client rhs) @safe return { 147 import std.algorithm.mutation : swap; 148 swap(this, rhs); 149 return this; 150 } 151 152 /// Create a copy of `other` client. 153 this(ref Client other) { 154 import std.algorithm.mutation : swap; 155 swap(this, other); 156 } 157 158 /// Destroy the internal `mg_session`. 159 @safe @nogc ~this() { 160 if (ptr_) 161 mg_session_destroy(ptr_); 162 } 163 164 /// Status of this client connection as boolean value. 165 /// Returns: true = the client connection was established 166 /// false = this client is not connected 167 auto opCast(T : bool)() const { 168 return ptr_ != null; 169 } 170 171 package: 172 /// Create a new instance using the given `mg_session` pointer. 173 this(mg_session *session) { 174 assert(session != null); 175 ptr_ = session; 176 } 177 178 private: 179 mg_session *ptr_; 180 } 181 182 unittest { 183 import std.exception, core.exception; 184 import testutils; 185 import memgraph; 186 187 auto client = connectContainer(); 188 assert(client); 189 190 assert(client.status == mg_session_code.MG_SESSION_READY); 191 assert(client.clientVersion.length > 0); 192 193 auto client2 = Client(); 194 client2 = client; 195 assert(client2.status == mg_session_code.MG_SESSION_READY); 196 assert(client2.clientVersion.length > 0); 197 198 assertThrown!AssertError(client.status); 199 assertThrown!AssertError(client.error); 200 201 auto client3 = Client(client2); 202 assert(client3.status == mg_session_code.MG_SESSION_READY); 203 assert(client3.clientVersion.length > 0); 204 205 assertThrown!AssertError(client2.status); 206 assertThrown!AssertError(client2.error); 207 } 208 209 unittest { 210 import testutils; 211 import memgraph; 212 213 auto client = connectContainer(); 214 assert(client); 215 216 assert(client.status == mg_session_code.MG_SESSION_READY); 217 218 // TODO: something weird is going on with error: 219 // with ldc2, the first character seems to be random garbage if there actually is no error 220 // and with dmd, the whole error message seems to retain it's last state, even after successful connect 221 // assert(client.error() == "", client.error); 222 223 assert(client.clientVersion.length > 0); 224 } 225 226 unittest { 227 import testutils; 228 import memgraph; 229 import std.algorithm : count; 230 231 auto client = connectContainer(); 232 assert(client); 233 234 createTestIndex(client); 235 236 deleteTestData(client); 237 238 // Create some test data inside a transaction, then roll it back. 239 client.begin(); 240 241 createTestData(client); 242 243 // Inside the transaction the row count should be 1. 244 auto result = client.execute("MATCH (n) RETURN n;"); 245 assert(result, client.error); 246 assert(result.count == 5); 247 248 client.rollback(); 249 250 // Outside the transaction the row count should be 0. 251 result = client.execute("MATCH (n) RETURN n;"); 252 assert(result, client.error); 253 assert(result.count == 0); 254 255 // Create some test data inside a transaction, then commit it. 256 client.begin(); 257 258 createTestData(client); 259 260 // Inside the transaction the row count should be 1. 261 result = client.execute("MATCH (n) RETURN n;"); 262 assert(result, client.error); 263 assert(result.count == 5); 264 265 client.commit(); 266 267 // Outside the transaction the row count should still be 1. 268 result = client.execute("MATCH (n) RETURN n;"); 269 assert(result, client.error); 270 assert(result.count == 5); 271 272 // Just some test for execute() using Map parameters. 273 auto m = Map(10); 274 m["test"] = 42; 275 result = client.execute("MATCH (n) RETURN n;", m); 276 assert(result, client.error); 277 assert(result.count == 5); 278 279 // Just for coverage at the moment 280 assert(client.error.length >= 0); 281 assert(result.summary.length >= 0); 282 assert(result.columns == ["n"]); 283 } 284 285 unittest { 286 Params params; 287 params.host = "0.0.0.0"; 288 params.port = 12_345; 289 auto client = Client.connect(params); 290 assert(!client); 291 } 292 293 unittest { 294 import testutils; 295 import memgraph; 296 auto client = connectContainer(); 297 assert(client); 298 assert(!client.run("WHAT IS THE ANSWER TO LIFE, THE UNIVERSE AND EVERYTHING?")); 299 auto m = Map(10); 300 m["answer"] = 42; 301 assert(!client.execute("WHAT IS THE ANSWER TO LIFE, THE UNIVERSE AND EVERYTHING?", m)); 302 } 303 304 /// Connect example 305 unittest { 306 import std.stdio; 307 import memgraph; 308 // Connect to memgraph DB at 127.0.0.1:7688 309 Params p = { host: "127.0.0.1", port: 7688 }; 310 auto client = Client.connect(p); 311 if (!client) writefln("cannot connect to %s:%s: %s", p.host, p.port, client.status); 312 } 313 314 unittest { 315 // Just for coverage. It probably will fail - unless there happens 316 // to be a memgraph server running at 127.0.0.1:7687 317 Client.connect(); 318 }