1 /**
2 * Copyright © Yurai Web Framework 2021
3 * License: MIT (https://github.com/YuraiWeb/yurai/blob/main/LICENSE)
4 * Author: Jacob Jensen (bausshf)
5 */
6 module yurai.data.mapping.mysql;
7 
8 import std.variant : Variant;
9 
10 public alias DbParam = Variant;
11 
12 import yurai.core.settings;
13 
14 public
15 {
16   struct DbIgnore
17   {
18   }
19 
20   struct DbName
21   {
22     string name;
23   }
24 
25   struct DbTimestamp
26   {
27   }
28 
29   struct DbCreateTime
30   {
31   }
32 
33   struct DbTable
34   {
35     string name;
36   }
37 }
38 
39 static if (Yurai_UseMysql)
40 {
41   import std.algorithm : map;
42   import std.string : format;
43   import std.traits : hasUDA, getUDAs;
44   import std.array : join;
45   import std.conv : to;
46   import std.datetime : Clock, SysTime, DateTime;
47 
48   private DateTime asDateTime(SysTime sysTime)
49   {
50     return DateTime(sysTime.year, sysTime.month, sysTime.day, sysTime.hour, sysTime.minute, sysTime.second);
51   }
52 
53   import mysql;
54 
55   private static __gshared MySQLPool[string] _pools;
56   private static shared globalPoolLock = new Object;
57 
58   MySQLPool getPool(string connectionString)
59   {
60     auto pool = _pools.get(connectionString, null);
61 
62     if (!pool)
63     {
64       synchronized (globalPoolLock)
65       {
66         pool = new MySQLPool(connectionString);
67 
68         _pools[connectionString] = pool;
69       }
70 
71       return getPool(connectionString);
72     }
73 
74     return pool;
75   }
76 
77   private string[] generateSelect(T)()
78   {
79     string[] generatedColumns = [];
80     auto generated = "";
81 
82     size_t index = 0;
83 
84     foreach (member; __traits(derivedMembers, T))
85     {{
86       static if (member != "__ctor")
87       {
88         mixin("static const isIgnore = hasUDA!(T.%s, DbIgnore);".format(member));
89         mixin("static const hasName = hasUDA!(T.%s, DbName);".format(member));
90 
91         static if (!isIgnore)
92         {
93           mixin("static const memberType = (typeof(T." ~ member ~ ")).stringof;");
94 
95           static if (hasName)
96           {
97             mixin("enum dbNameAttribute = getUDAs!(T.%s, DbName)[0];".format(member));
98 
99             static const dbName = dbNameAttribute.name;
100           }
101           else
102           {
103             static const dbName = member;
104           }
105 
106           generated ~= "model." ~ member ~ " = row[" ~ to!string(index) ~ "].get!(" ~ memberType ~ ");";
107           index++;
108 
109           generatedColumns ~= "`" ~ dbName ~ "`";
110         }
111       }
112     }}
113 
114     return [generatedColumns.join(", "), generated];
115   }
116 
117   private string generateInsert(T)()
118   {
119     enum columnFormat = "`%s`";
120     enum valueFormat = "?";
121     enum paramFormat = "params[%s] = model.%s;";
122     enum timestampFormat = "model.%s = currentTime;";
123 
124     string[] columns = [];
125     string[] values = [];
126     string[] params = [];
127     string[] preCode = [];
128     bool hasModelTimestamp = false;
129 
130     size_t index = 0;
131 
132     foreach (member; __traits(derivedMembers, T))
133     {
134       static if (member != "__ctor" && member != "id")
135       {
136         mixin("static const isIgnore = hasUDA!(T.%s, DbIgnore);".format(member));
137 
138         static if (!isIgnore)
139         {
140           mixin("static const hasName = hasUDA!(T.%s, DbName);".format(member));
141           mixin("static const hasTimestamp = hasUDA!(T.%s, DbTimestamp) || hasUDA!(T.%s, DbCreateTime);".format(member, member));
142 
143           mixin("static const memberType = (typeof(T." ~ member ~ ")).stringof;");
144 
145           static if (hasName)
146           {
147             mixin("enum dbNameAttribute = getUDAs!(T.%s, DbName)[0];".format(member));
148 
149             static const dbName = dbNameAttribute.name;
150           }
151           else
152           {
153             static const dbName = member;
154           }
155 
156           static if (hasTimestamp)
157           {
158             preCode ~= timestampFormat.format(member);
159             hasModelTimestamp = true;
160           }
161 
162           columns ~= columnFormat.format(dbName);
163           values ~= valueFormat;
164           params ~= paramFormat.format(index, member);
165           index++;
166         }
167       }
168     }
169 
170     if (hasModelTimestamp)
171     {
172       string[] timestampCode = [];
173 
174       timestampCode ~= "auto currentTime = Clock.currTime().asDateTime();";
175 
176       preCode = timestampCode ~ preCode;
177     }
178 
179     auto generated = preCode.join("\r\n");
180     generated ~= "auto values = \"%s\";\r\n".format(values.join(", "));
181     generated ~= "auto columns = \"%s\";\r\n".format(columns.join(", "));
182     generated ~= "auto params = new DbParam[" ~ to!string(params.length) ~ "];\r\n";
183     generated ~= params.join("\r\n");
184 
185     return generated;
186   }
187 
188   private string generateUpdate(T)()
189   {
190     enum columnFormat = "`%s` = ?";
191     enum paramFormat = "params[%s] = model.%s;";
192     enum timestampFormat = "model.%s = currentTime;";
193 
194     string[] columns = [];
195     string[] params = [];
196     string[] preCode = [];
197     bool hasModelTimestamp = false;
198 
199     size_t index = 0;
200 
201     foreach (member; __traits(derivedMembers, T))
202     {
203       static if (member != "__ctor" && member != "id")
204       {
205         mixin("static const isIgnore = hasUDA!(T.%s, DbIgnore);".format(member));
206 
207         static if (!isIgnore)
208         {
209           mixin("static const hasName = hasUDA!(T.%s, DbName);".format(member));
210           mixin("static const hasTimestamp = hasUDA!(T.%s, DbTimestamp);".format(member));
211 
212           mixin("static const memberType = (typeof(T." ~ member ~ ")).stringof;");
213 
214           static if (hasName)
215           {
216             mixin("enum dbNameAttribute = getUDAs!(T.%s, DbName)[0];".format(member));
217 
218             static const dbName = dbNameAttribute.name;
219           }
220           else
221           {
222             static const dbName = member;
223           }
224 
225           static if (hasTimestamp)
226           {
227             preCode ~= timestampFormat.format(member);
228             hasModelTimestamp = true;
229           }
230 
231           columns ~= columnFormat.format(dbName);
232           params ~= paramFormat.format(index, member);
233           index++;
234         }
235       }
236     }
237 
238     if (hasModelTimestamp)
239     {
240       string[] timestampCode = [];
241 
242       timestampCode ~= "auto currentTime = Clock.currTime().asDateTime();";
243 
244       preCode = timestampCode ~ preCode;
245     }
246     auto generated = preCode.join("\r\n");
247     generated ~= "auto columns = \"%s\";\r\n".format(columns.join(", "));
248     generated ~= "auto params = new DbParam[" ~ to!string(params.length + 1) ~ "];\r\n";
249     generated ~= params.join("\r\n");
250 
251     return generated;
252   }
253 
254   public final class MysqlDataService
255   {
256     private:
257     string _connectionString;
258     MySQLPool _pool;
259 
260     public:
261     final:
262     this(string connectionString)
263     {
264       _connectionString = connectionString;
265       _pool = getPool(_connectionString);
266     }
267 
268     void save(T)(T model, string table = null)
269     {
270       static if (hasUDA!(T, DbTable))
271       {
272         enum tableAttribute = getUDAs!(T, DbTable)[0];
273 
274         if (!table || !table.length)
275         {
276           table = tableAttribute.name;
277         }
278       }
279 
280       if (model.id)
281       {
282         update!T(model, table);
283       }
284       else
285       {
286         insert!T(model, table);
287       }
288     }
289 
290     private void insert(T)(T model, string table)
291     {
292       mixin(generateInsert!T);
293 
294       if (execute("INSERT INTO `%s` (%s) VALUES (%s);".format(table, columns, values), params) == 1)
295       {
296         auto id = executeScalar!(typeof(T.id))("SELECT LAST_INSERT_ID();", null);
297 
298         model.id = id;
299       }
300     }
301 
302     private void update(T)(T model, string table)
303     {
304       mixin(generateUpdate!T);
305 
306       params[params.length - 1] = model.id;
307 
308       execute("UPDATE `%s` SET %s WHERE `id` = ?".format(table, columns), params);
309     }
310 
311     void remove(T)(T model, string table = null)
312     {
313       static if (hasUDA!(T, DbTable))
314       {
315         enum tableAttribute = getUDAs!(T, DbTable)[0];
316 
317         if (!table || !table.length)
318         {
319           table = tableAttribute.name;
320         }
321       }
322 
323       auto params = new DbParam[1];
324       params[0] = model.id;
325 
326       execute("DELETE FROM `%s` WHERE `id` = ?".format(table), params);
327     }
328 
329     void save(T)(T[] models, string table = null)
330     {
331       foreach (model; models)
332       {
333         save!T(model, table);
334       }
335     }
336 
337     void remove(T)(T[] models, string table = null)
338     {
339       foreach (model; models)
340       {
341         remove(model, table);
342       }
343     }
344 
345     ulong execute(string sql, DbParam[] params)
346     {
347       auto connection = _pool.lockConnection();
348       auto prepared = connection.prepare(sql);
349 
350       prepared.setArgs(params ? params : new DbParam[0]);
351 
352       return connection.exec(prepared);
353     }
354 
355     T executeScalar(T)(string sql, DbParam[] params)
356     {
357       auto connection = _pool.lockConnection();
358       auto prepared = connection.prepare(sql);
359 
360       prepared.setArgs(params ? params : new DbParam[0]);
361 
362       auto result = connection.queryValue(prepared);
363 
364       if (result.isNull)
365       {
366         return T.init;
367       }
368 
369       return result.get.coerce!T;
370     }
371 
372     T selectSingle(T)(string query, DbParam[] params, string table = null)
373     {
374       enum generateResult = generateSelect!T;
375 
376       static if (hasUDA!(T, DbTable))
377       {
378         enum tableAttribute = getUDAs!(T, DbTable)[0];
379 
380         if (!table || !table.length)
381         {
382           table = tableAttribute.name;
383         }
384       }
385 
386       string sql = "SELECT " ~ generateResult[0] ~ " FROM `" ~ table ~ "`" ~ (query ? (" WHERE " ~ query) : "") ~ " LIMIT 1";
387 
388       auto connection = _pool.lockConnection();
389       auto prepared = connection.prepare(sql);
390 
391       prepared.setArgs(params ? params : new DbParam[0]);
392 
393       auto result = connection.queryRow(prepared);
394 
395       if (result.isNull)
396       {
397         return T.init;
398       }
399 
400       auto model = new T;
401 
402       mixin(generateResult[1]);
403 
404       return model;
405     }
406 
407     auto selectMany(T)(string query, DbParam[] params, string table = null)
408     {
409       enum generateResult = generateSelect!T;
410 
411       static if (hasUDA!(T, DbTable))
412       {
413         enum tableAttribute = getUDAs!(T, DbTable)[0];
414 
415         if (!table || !table.length)
416         {
417           table = tableAttribute.name;
418         }
419       }
420 
421       string sql = "SELECT " ~ generateResult[0] ~ " FROM `" ~ table ~ "`" ~ (query ? (" WHERE " ~ query) : "");
422 
423       auto connection = _pool.lockConnection();
424       auto prepared = connection.prepare(sql);
425 
426       prepared.setArgs(params ? params : new DbParam[0]);
427 
428       auto result = connection.query(prepared);
429 
430       return result.map!((row)
431       {
432         auto model = new T;
433 
434         mixin(generateResult[1]);
435 
436         return model;
437       });
438     }
439 
440     auto selectOffset(T)(string sql, DbParam[] params, int offset, int limit = 5000, string table = null, string sortColumn = "`id`", string sortType = null)
441     {
442       string orderSql = "";
443       if (sortColumn !is null)
444       {
445         orderSql = " ORDER BY " ~ sortColumn;
446 
447         if (sortType !is null)
448         {
449           orderSql ~= " " ~ sortType;
450         }
451         else
452         {
453           orderSql ~= " ASC";
454         }
455       }
456 
457       return selectMany!T("%s%s LIMIT %s,%s".format(sql, orderSql, offset, limit), params, table);
458     }
459   }
460 }
461 else
462 {
463   import std.range.interfaces : InputRange;
464 
465   public final class MysqlDataService
466   {
467     private:
468     string _connectionString;
469 
470     public:
471     final:
472     this(string connectionString)
473     {
474       _connectionString = connectionString;
475     }
476 
477     void insert(T)(T model, string table)
478     {
479       throw new Exception("...");
480     }
481 
482     void update(T)(T model, string table)
483     {
484       throw new Exception("...");
485     }
486 
487     void remove(T)(T model, string table)
488     {
489       throw new Exception("...");
490     }
491 
492     void insert(T)(T[] models, string table)
493     {
494       throw new Exception("...");
495     }
496 
497     void update(T)(T[] models, string table)
498     {
499       throw new Exception("...");
500     }
501 
502     void remove(T)(T[] models, string table)
503     {
504       throw new Exception("...");
505     }
506 
507     int execute(string sql, DbParam[] params)
508     {
509       throw new Exception("...");
510     }
511 
512     T executeScalar(T)(string sql, DbParam[] params)
513     {
514       throw new Exception("...");
515     }
516 
517     T selectSingle(T)(string sql, DbParam[] params)
518     {
519       throw new Exception("...");
520     }
521 
522     InputRange!T selectMany(T)(string sql, DbParam[] params)
523     {
524       throw new Exception("...");
525     }
526 
527     InputRange!T selectOffset(T)(string sql, DbParam[] params, int offset, int limit = 5000, string sortColumn = "`id`", string sortType = "ASC")
528     {
529       throw new Exception("...");
530     }
531   }
532 }