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 }