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.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   @nogc static auto clientVersion() { return fromStringz(mg_client_version()); }
16 
17   /// Obtains the error message stored in the current session (if any).
18   @nogc @property auto error() {
19     assert(session_ != null);
20     return fromStringz(mg_session_error(session_));
21   }
22 
23   /// Returns the status of the current session.
24   /// Return: One of the session codes in `mg_session_code`.
25   @nogc @property auto status() inout {
26     assert(session_ != null);
27     return mg_session_status(session_);
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     string[string] emptyParams;
46     return execute(statement, emptyParams);
47   }
48 
49   /// Executes the given Cypher `statement`, supplied with additional `params`.
50   /// Return: `Result` that can be used as a range e.g. using foreach() to process all results.
51   /// After executing the statement, the method is blocked until all incoming
52   /// data (execution results) are handled, i.e. until the returned `Result` has been completely processed.
53   Result execute(const string statement, const string[string] params) {
54     assert(status == mg_session_code.MG_SESSION_READY);
55     mg_error status;
56     if (params.length == 0)
57       status = mg_session_run(session_, toStringz(statement), null, null, null, null);
58     else {
59       import std.conv : to;
60       extraParams_ = mg_map_make_empty(to!uint(params.length));
61       foreach (ref key, ref value; params) {
62         mg_error rc;
63         if (key == "n") // TODO: this needs a better solution
64           rc = mg_map_insert(extraParams_, toStringz(key), mg_value_make_integer(to!int(value)));
65         else
66           rc = mg_map_insert(extraParams_, toStringz(key), mg_value_make_string(toStringz(value)));
67         assert(rc == mg_error.MG_SUCCESS);
68       }
69       status = mg_session_run(session_, toStringz(statement), extraParams_, null, null, null);
70     }
71     if (status < 0)
72       return Result();
73     return Result(session_, &result_, extraParams_);
74   }
75 
76   /// Start a transaction.
77   /// Return: true when the transaction was successfully started, false otherwise.
78   @nogc bool begin() {
79     assert(session_ != null);
80     return mg_session_begin_transaction(session_, null) == 0;
81   }
82 
83   /// Commit current transaction.
84   /// Return: true when the transaction was successfully committed, false otherwise.
85   @nogc bool commit() {
86     assert(session_ != null);
87     return mg_session_commit_transaction(session_, &result_) == 0;
88   }
89 
90   /// Rollback current transaction.
91   /// Return: true when the transaction was successfully rolled back, false otherwise.
92   @nogc bool rollback() {
93     assert(session_ != null);
94     return mg_session_rollback_transaction(session_, &result_) == 0;
95   }
96 
97   /// Static method that creates a Memgraph client instance using default parameters 127.0.0.1:7687
98   /// Return: client connection instance.
99   /// Returns an unconnected instance if the connection couldn't be established.
100   static Client connect() {
101     Params params;
102     return connect(params);
103   }
104 
105   /// Static method that creates a Memgraph client instance.
106   /// Return: client connection instance.
107   /// If the connection couldn't be established given the `params`, it will
108   /// return an unconnected instance.
109   static Client connect(ref Params params) {
110     mg_session *session = null;
111     immutable status = mg_connect(params.ptr, &session);
112     if (status < 0) {
113       if (session)
114         mg_session_destroy(session);
115       return Client();
116     }
117     return Client(session);
118   }
119 
120   /// Destroy the internal `mg_session`.
121   @nogc ~this() {
122     if (session_)
123       mg_session_destroy(session_);
124     if (extraParams_)
125       mg_map_destroy(extraParams_);
126   }
127 
128   /// Status of this client connection as boolean value.
129   /// Returns: true = the client connection was established
130   ///          false = this client is not connected
131   @nogc auto opCast(T : bool)() const { return session_ != null; }
132 
133 package:
134   /// Create a new instance using the given `mg_session` pointer.
135   @nogc this(mg_session *session) {
136     assert(session != null);
137     session_ = session;
138   }
139 
140   auto ptr() inout { return session_; }
141 
142 private:
143   mg_session *session_;
144   mg_result *result_;
145   mg_map *extraParams_;
146 }
147 
148 unittest {
149   import std.exception, core.exception;
150   import testutils;
151 
152   const client = connectContainer();
153   assert(client);
154 
155   assert(client.status == mg_session_code.MG_SESSION_READY);
156   assert(client.clientVersion.length > 0);
157   assert(client.ptr != null);
158 }
159 
160 unittest {
161   import testutils;
162 
163   auto client = connectContainer();
164   assert(client);
165 
166   assert(client.status == mg_session_code.MG_SESSION_READY);
167   assert(client.error() == "", client.error);
168   assert(client.clientVersion.length > 0);
169 }
170 
171 unittest {
172   import testutils;
173   import std.algorithm : count;
174 
175   auto client = connectContainer();
176   assert(client);
177 
178   createTestIndex(client);
179 
180   deleteTestData(client);
181 
182   // Create some test data inside a transaction, then roll it back.
183   client.begin();
184 
185   createTestData(client);
186 
187   // Inside the transaction the row count should be 1.
188   auto result = client.execute("MATCH (n) RETURN n;");
189   assert(result, client.error);
190   assert(result.count == 5);
191 
192   client.rollback();
193 
194   // Outside the transaction the row count should be 0.
195   result = client.execute("MATCH (n) RETURN n;");
196   assert(result, client.error);
197   assert(result.count == 0);
198 
199   // Create some test data inside a transaction, then commit it.
200   client.begin();
201 
202   createTestData(client);
203 
204   // Inside the transaction the row count should be 1.
205   result = client.execute("MATCH (n) RETURN n;");
206   assert(result, client.error);
207   assert(result.count == 5);
208 
209   client.commit();
210 
211   // Outside the transaction the row count should still be 1.
212   result = client.execute("MATCH (n) RETURN n;");
213   assert(result, client.error);
214   assert(result.count == 5);
215 
216   // Just some test for execute() using extra parameters.
217   string[string] params;
218   params["mode"] = "r";
219   result = client.execute("MATCH (n) RETURN n;", params);
220   assert(result, client.error);
221   assert(result.count == 5);
222 
223   // Just for coverage at the moment
224   assert(client.error.length >= 0);
225   assert(result.summary.length >= 0);
226   assert(result.columns == ["n"]);
227 }
228 
229 unittest {
230   Params params;
231   params.host = "0.0.0.0";
232   params.port = 12_345;
233   const client = Client.connect(params);
234   assert(!client);
235 }
236 
237 unittest {
238   import testutils : connectContainer;
239   auto client = connectContainer();
240   assert(client);
241   assert(!client.run("WHAT IS THE ANSWER TO LIFE, THE UNIVERSE AND EVERYTHING?"));
242   string[string] params;
243   params["mode"] = "r";
244   assert(!client.execute("WHAT IS THE ANSWER TO LIFE, THE UNIVERSE AND EVERYTHING?", params));
245 }
246 
247 /// Connect example
248 unittest {
249   import std.stdio : writefln;
250   // Connect to memgraph DB at 127.0.0.1:7688
251   Params p = { host: "127.0.0.1", port: 7688 };
252   auto client = Client.connect(p);
253   if (!client) writefln("cannot connect to %s:%s: %s", p.host, p.port, client.status);
254 }
255 
256 unittest {
257   // Just for coverage. It probably will fail - unless there happens
258   // to be a memgraph server running at 127.0.0.1:7687
259   cast(void)Client.connect();
260 }