1 /**
2 * Copyright © DiamondMVC 2018
3 * License: MIT (https://github.com/DiamondMVC/Diamond/blob/master/LICENSE)
4 * Author: Jacob Jensen (bausshf)
5 */
6 module diamond.data.mapping.engines.mysql;
7 
8 import std.variant : Variant;
9 import std.traits : hasMember;
10 import std.algorithm : map;
11 import std.conv : to;
12 import std..string : format;
13 import std.typecons : Nullable;
14 import std.array : array;
15 
16 import vibe.data.serialization : optional;
17 
18 import mysql;
19 
20 public
21 {
22   import diamond.data.mapping.engines.mysql.generators;
23   import diamond.data.mapping.engines.mysql.model;
24 }
25 
26 /// A variant db parameter type.
27 alias DbParam = Variant;
28 
29 /// The connection string format.
30 private enum connectionStringFormat = "host=%s;port=%s;user=%s;pwd=%s;db=%s";
31 
32 /// The db connection string.
33 private static __gshared immutable string _dbConnectionString;
34 
35 /// Static shared constructor for the module.
36 shared static this()
37 {
38   import diamond.core.webconfig;
39 
40   if (!webConfig)
41   {
42     loadWebConfig();
43   }
44 
45   if (!webConfig.dbConnections)
46   {
47     return;
48   }
49 
50   auto dbConfig = webConfig.dbConnections.mysql.get("default", null);
51 
52   if (!dbConfig)
53   {
54     return;
55   }
56 
57   _dbConnectionString = connectionStringFormat.format(
58     dbConfig.host, dbConfig.port ? dbConfig.port : 3306,
59     dbConfig.user, dbConfig.password,
60     dbConfig.database
61   );
62 
63   _pools[_dbConnectionString] = new MySQLPool(_dbConnectionString);
64 }
65 
66 @property
67 {
68   /// Gets the connection string.
69   auto dbConnectionString()
70   {
71     return _dbConnectionString;
72   }
73 }
74 
75 /// Collection of connection pools.
76 private static __gshared MySQLPool[string] _pools;
77 
78 /// Global pool lock to ensure we don't attempt to create a connection pool twice on same connection string.
79 private static shared globalPoolLock = new Object;
80 
81 /**
82 * Gets or creates a mysql pool from a connection string.
83 * Params:
84 *   connectionString = The connection string for the pool.
85 * Returns:
86 *   The mysql pool.
87 */
88 private MySQLPool getPool(string connectionString)
89 {
90   auto pool = _pools.get(connectionString, null);
91 
92   if (!pool)
93   {
94     synchronized (globalPoolLock)
95     {
96       pool = new MySQLPool(connectionString);
97 
98       _pools[connectionString] = pool;
99     }
100 
101     return getPool(connectionString);
102   }
103 
104   return pool;
105 }
106 
107 /**
108 * Prepares a specialized parameter sql.
109 * Params:
110 *   sql =            The sql.
111 *   params =         The params.
112 *   transformedSql = The newly transformed sql.
113 * Returns:
114 *   The raw db parameters.
115 */
116 private DbParam[] prepareSql(string sql, DbParam[string] params, out string transformedSql)
117 {
118 	transformedSql = "";
119 	string paramName = "";
120 	bool selectParam = false;
121 	DbParam[] sqlParams;
122 
123 	foreach (i; 0 .. sql.length)
124   {
125 		auto c = sql[i];
126 
127 		if (c == 13) continue;
128 
129 		bool isEnd = i == (sql.length - 1);
130 
131 		if (c == '@')
132     {
133 			paramName = "";
134 			selectParam = true;
135 		}
136 		else if (selectParam && (
137 			c == ';' || c == '=' ||
138 			c == '+' || c == '-' ||
139 			c == 9 || c == 13 ||
140 			c == 10 || c == ' ' ||
141 			c == 0 || c == '|' ||
142 			c == '.' || c == '/' ||
143 			c == '*' || c == '(' ||
144 			c == ')' || c == '[' ||
145 			c == ']' || c == ',' ||
146       c == '`' || c == 39
147 		))
148     {
149       if (paramName == "table")
150       {
151         transformedSql ~= params[paramName].get!string ~ to!string(c);
152         selectParam = false;
153         paramName = "";
154       }
155       else
156       {
157     		sqlParams ~= params[paramName];
158     		transformedSql ~= "?" ~ c;
159 
160     		selectParam = false;
161     		paramName = "";
162       }
163 		}
164 		else if (selectParam)
165     {
166 			paramName ~= c;
167 
168 			if (isEnd)
169       {
170 				if (paramName == "table")
171         {
172   				transformedSql ~= params[paramName].get!string ~ to!string(c);
173         }
174         else
175         {
176           sqlParams ~= params[paramName];
177   				transformedSql ~= "?";
178         }
179 			}
180 		}
181 		else
182     {
183 			transformedSql ~= c;
184 		}
185 	}
186 
187 	return sqlParams;
188 }
189 
190 /// CTFE string for mixin MySql connection setup with specialized parameters.
191 private enum MySqlConnectionNamedParametersSetup = q{
192   auto useDbConnectionString = connectionString ? connectionString : _dbConnectionString;
193 
194   // Prepare statement
195   string newSql;
196   DbParam[] newParams = null;
197   if (params)
198   {
199     newParams = prepareSql(sql, params, newSql);
200   }
201   else
202   {
203     newSql = sql;
204   }
205 
206   auto pool = getPool(useDbConnectionString);
207   auto connection = pool.lockConnection();
208   auto prepared = connection.prepare(newSql);
209 
210   prepared.setArgs(newParams);
211 };
212 
213 /// CTFE string for mixin MySql connection setup.
214 private enum MySqlConnectionSetup = q{
215   auto useDbConnectionString = connectionString ? connectionString : _dbConnectionString;
216 
217   auto pool = getPool(useDbConnectionString);
218   auto connection = pool.lockConnection();
219   auto prepared = connection.prepare(sql);
220 
221   prepared.setArgs(params);
222 };
223 
224 /**
225 *	Executes an sql statement.
226 *	Params:
227 *		sql =				        The sql query.
228 *		params = 			      The parameters.
229 *		connectionString =	The connection string. (If null, it will select the default)
230 *	Returns:
231 *		The amount of rows affected.
232 */
233 ulong execute(string sql, DbParam[string] params, string connectionString = null)
234 {
235   mixin(MySqlConnectionNamedParametersSetup);
236 
237   return connection.exec(prepared);
238 }
239 
240 /**
241 *	Executes a raw sql statement.
242 *	Params:
243 *		sql =				        The sql query.
244 *		params = 			      The parameters.
245 *		connectionString =	The connection string. (If null, it will select the default)
246 *	Returns:
247 *		The amount of rows affected.
248 */
249 ulong executeRaw(string sql, DbParam[] params, string connectionString = null)
250 {
251   mixin(MySqlConnectionSetup);
252 
253   return connection.exec(prepared);
254 }
255 
256 /**
257 *	Executes a scalar sql statement.
258 *	Params:
259 *		sql =				        The sql query.
260 *		params = 			      The parameters.
261 *		connectionString =	The connection string. (If null, it will select the default)
262 *	Returns:
263 *		The value of the statement.
264 */
265 T scalar(T)(string sql, DbParam[string] params, string connectionString = null)
266 {
267   mixin(MySqlConnectionNamedParametersSetup);
268 
269   auto value = connection.queryValue(prepared);
270 
271   if (value.isNull)
272   {
273     return T.init;
274   }
275 
276   return value.get.get!T;
277 }
278 
279 /**
280 *	Executes a raw scalar sql statement.
281 *	Params:
282 *		sql =				        The sql query.
283 *		params = 			      The parameters.
284 *		connectionString =	The connection string. (If null, it will select the default)
285 *	Returns:
286 *		The value of the statement.
287 */
288 T scalarRaw(T)(string sql, DbParam[] params, string connectionString = null)
289 {
290   mixin(MySqlConnectionSetup);
291 
292   auto value = connection.queryValue(prepared);
293 
294   if (value.isNull)
295   {
296     return T.init;
297   }
298 
299   return value.get.get!T;
300 }
301 
302 /**
303 *	Executes a scalar insert sql statement.
304 *	Params:
305 *		sql =				        The sql query.
306 *		params = 			      The parameters.
307 *		connectionString =	The connection string. (If null, it will select the default)
308 *	Returns:
309 *		The id of inserted row.
310 */
311 T scalarInsert(T)(string sql, DbParam[string] params, string connectionString = null)
312 {
313   auto rows = execute(sql, params, connectionString);
314 
315   if (!rows)
316   {
317     return T.init;
318   }
319 
320   static const idSql = "SELECT last_insert_id()";
321 
322   return scalar!T(sql, null, connectionString);
323 }
324 
325 /**
326 *	Executes a raw scalar insert sql statement.
327 *	Params:
328 *		sql =				        The sql query.
329 *		params = 			      The parameters.
330 *		connectionString =	The connection string. (If null, it will select the default)
331 *	Returns:
332 *		The id of the inserted row.
333 */
334 T scalarInsertRaw(T)(string sql, DbParam[] params, string connectionString = null)
335 {
336   auto rows = executeRaw(sql, params, connectionString);
337 
338   if (!rows)
339   {
340     return T.init;
341   }
342 
343   static const idSql = "SELECT last_insert_id()";
344 
345   return scalarRaw!T(sql, null, connectionString);
346 }
347 
348 /**
349 *	Validates whether a row is selected from the query or not.
350 *	Params:
351 *		sql =				        The sql query.
352 *		params = 			      The parameters.
353 *		connectionString =	The connection string. (If null, it will select the default)
354 *	Returns:
355 *		True if the row exists, false otherwise.
356 */
357 bool exists(string sql, DbParam[string] params, string connectionString = null)
358 {
359   auto rows = execute(sql, params, connectionString);
360 
361   return cast(bool)rows;
362 }
363 
364 /**
365 *	Validates whether a row is selected from the raw query or not.
366 *	Params:
367 *		sql =				        The sql query.
368 *		params = 			      The parameters.
369 *		connectionString =	The connection string. (If null, it will select the default)
370 *	Returns:
371 *		True if the row exists, false otherwise.
372 */
373 bool existsRaw(string sql, DbParam[] params, string connectionString = null)
374 {
375   auto rows = executeRaw(sql, params, connectionString);
376 
377   return cast(bool)rows;
378 }
379 
380 /**
381 *	Executes a single sql read.
382 *	Params:
383 *		sql =				        The sql query.
384 *		params = 			      The parameters.
385 *		connectionString =	The connection string. (If null, it will select the default)
386 *	Returns:
387 *		The model of the first row read.
388 */
389 TModel readSingle(TModel : IMySqlModel)(string sql, DbParam[string] params, string connectionString = null)
390 {
391   params["table"] = TModel.table;
392 
393   mixin(MySqlConnectionNamedParametersSetup);
394 
395   auto row = connection.queryRow(prepared);
396 
397   if (row.isNull)
398   {
399     return TModel.init;
400   }
401 
402   auto model = new TModel;
403   model.row = row.get;
404   model.readModel();
405   return model;
406 }
407 
408 /**
409 *	Executes a single raw sql read.
410 *	Params:
411 *		sql =				        The sql query.
412 *		params = 			      The parameters.
413 *		connectionString =	The connection string. (If null, it will select the default)
414 *	Returns:
415 *		The model of the first row read.
416 */
417 TModel readSingleRaw(TModel : IMySqlModel)(string sql, DbParam[] params, string connectionString = null)
418 {
419   mixin(MySqlConnectionSetup);
420 
421   auto row = connection.queryRow(prepared);
422 
423   if (row.isNull)
424   {
425     return TModel.init;
426   }
427 
428   auto model = new TModel;
429   model.row = row.get;
430   model.readModel();
431   return model;
432 }
433 
434 /**
435 *	Executes a multi sql read.
436 *	Params:
437 *		sql =				        The sql query.
438 *		params = 			      The parameters.
439 *		connectionString =	The connection string. (If null, it will select the default)
440 *	Returns:
441 *		A range filled models with the rows returned by the sql read.
442 */
443 auto readMany(TModel : IMySqlModel)(string sql, DbParam[string] params, string connectionString = null)
444 {
445   params["table"] = TModel.table;
446 
447   mixin(MySqlConnectionNamedParametersSetup);
448 
449   return connection.query(prepared).map!((row)
450   {
451     auto model = new TModel;
452     model.row = row;
453     model.readModel();
454     return model;
455   }).array;
456 }
457 
458 /**
459 *	Executes a raw multi sql read.
460 *	Params:
461 *		sql =				        The sql query.
462 *		params = 			      The parameters.
463 *		connectionString =	The connection string. (If null, it will select the default)
464 *	Returns:
465 *		A range filled models with the rows returned by the sql read.
466 */
467 auto readManyRaw(TModel : IMySqlModel)(string sql, DbParam[] params, string connectionString = null)
468 {
469   mixin(MySqlConnectionSetup);
470 
471   return connection.query(prepared).map!((row)
472   {
473     auto model = new TModel;
474     model.row = row;
475     model.readModel();
476     return model;
477   }).array;
478 }