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