1 /**
2 * Copyright © DiamondMVC 2019
3 * License: MIT (https://github.com/DiamondMVC/Diamond/blob/master/LICENSE)
4 * Author: Jacob Jensen (bausshf)
5 */
6 module diamond.data.mapping.engines.mysql.mysqlentityformatter;
7 
8 import std..string : format;
9 import std.traits : hasUDA, FieldNameTuple;
10 import std.algorithm : map;
11 import std.array : join, array;
12 
13 import diamond.core.traits;
14 import diamond.data.mapping.attributes;
15 import diamond.data.mapping.engines.mysql.mysqlmodel : IMySqlModel;
16 import diamond.data.mapping.engines.sqlshared.sqlentityformatter;
17 
18 package(diamond.data.mapping.engines.mysql)
19 {
20   import diamond.database;
21 
22   /**
23   * Converts a system time to a datetime.
24   * Params:
25   *   sysTime = The system time to convert.
26   * Returns:
27   *   The system time as a datetime.
28   */
29   DateTime asDateTime(SysTime sysTime)
30   {
31     return DateTime(sysTime.year, sysTime.month, sysTime.day, sysTime.hour, sysTime.minute, sysTime.second);
32   }
33 
34   /// The format for nullable enums.
35   enum readNullEnumFomat = "model.%s = retrieve!(%s, true, true);";
36 
37   /// The format for nullable proxies.
38   enum readNullProxyFormat = q{
39     import std.variant : Variant;
40     mixin("model." ~ proxy.readHandler ~ "!(\"%s\")(_row.isNull(_index) ? Variant.init : _row[_index]);");
41     _index++;
42   };
43 
44   /// The format for nullables.
45   enum readNullFormat = "model.%s = retrieve!(%s, true, false);";
46 
47   /// The format for enums.
48   enum readEnumFormat = "model.%s = retrieve!(%s, false, true);";
49 
50   /// The format for proxies.
51   enum readProxyFormat = q{
52     mixin("model." ~ proxy.readHandler ~ "!(\"%s\")(_row[_index]);");
53     _index++;
54   };
55 
56   /// The format for reading relationships.
57   enum readRelationshipFormat = q{
58     if (relationship.sql)
59     {
60       model.%1$s = (getMySqlAdapter!%3$s).readMany(relationship.sql, null);
61     }
62     else if (relationship.members)
63     {
64       auto params = getParams();
65 
66       string[] whereClause = [];
67 
68       static foreach (memberLocal,memberRemote; relationship.members)
69       {
70         mixin("whereClause ~= \"`" ~ memberRemote ~ "` = @" ~ memberLocal ~ "\";");
71         mixin("params[\"" ~ memberLocal ~ "\"] = model." ~ memberLocal ~ ";");
72       }
73 
74       import std.array : join;
75 
76       model.%1$s = (getMySqlAdapter!%3$s).readMany("SELECT * FROM `@table` WHERE " ~ whereClause.join(" AND "), params);
77     }
78   };
79 
80   /// The format for booleans.
81   enum readBoolFormat = "model.%s = retrieve!(%s);";
82 
83   /// The format for default reading.
84   enum readFormat = "model.%s = retrieve!(%s);";
85 }
86 
87 /// Wrapper around a mysql entity formatter.
88 final class MySqlEntityFormatter(TModel) : SqlEntityFormatter!TModel
89 {
90   public:
91   final:
92   /// Creates a new mysql entity formatter.
93   this()
94   {
95 
96   }
97 
98   /// Generates the read mixin.
99   override string generateRead() const
100   {
101     string s = q{
102       {
103         %s
104       }
105     };
106 
107     mixin HandleFields!(TModel, q{{
108       enum hasNoMap = hasUDA!({{fullName}}, DbNoMap);
109       enum hasRelationship = hasUDA!({{fullName}}, DbRelationship);
110 
111       static if (!hasNoMap && !hasRelationship)
112       {
113         import std.traits : getUDAs;
114 
115         enum hasNull = hasUDA!({{fullName}}, DbNull);
116         enum hasEnum = hasUDA!({{fullName}}, DbEnum);
117         enum hasProxy = hasUDA!({{fullName}}, DbProxy);
118 
119         enum typeName = typeof({{fullName}}).stringof;
120 
121         static if (hasNull && hasEnum)
122         {
123           mixin(readNullEnumFormat.format("{{fieldName}}", typeName));
124         }
125         else static if (hasNull && hasProxy)
126         {
127           mixin("enum proxy = getUDAs!(%s, DbProxy)[0];".format("{{fullName}}"));
128 
129           mixin(readNullProxyFormat.format("{{fieldName}}"));
130         }
131         else static if (hasNull)
132         {
133           mixin(readNullFormat.format("{{fieldName}}", typeName));
134         }
135         else static if (hasEnum)
136         {
137           mixin(readEnumFormat.format("{{fieldName}}", typeName));
138         }
139         else static if (hasProxy)
140         {
141           mixin("enum proxy = getUDAs!(%s, DbProxy)[0];".format("{{fullName}}"));
142 
143           mixin(readProxyFormat.format("{{fieldName}}"));
144         }
145         else static if (is(typeof({{fullName}}) == bool))
146         {
147           mixin(readBoolFormat.format("{{fieldName}}", typeName));
148         }
149         else
150         {
151           mixin(readFormat.format("{{fieldName}}", typeName));
152         }
153       }
154     }});
155 
156     return s.format(handleThem());
157   }
158 
159   /// Generates the insert mixin.
160   override string generateInsert() const
161   {
162     import models;
163 
164     string s = q{
165       {
166         static const sql = "INSERT INTO `%s` (%s) VALUES (%s)";
167         auto params = getParams(%s);
168 
169         size_t index;
170 
171         %s
172 
173         %s
174       }
175     };
176 
177     string[] columns;
178     string[] paramsInserts;
179     string idName;
180     string idType;
181     string execution;
182 
183     {
184       mixin HandleFields!(TModel, q{{
185         enum hasId = hasUDA!({{fullName}}, DbId);
186 
187         static if (hasId)
188         {
189           idName = "{{fieldName}}";
190           idType = typeof({{fullName}}).stringof;
191         }
192       }});
193       mixin(handleThem());
194 
195       if (idName)
196       {
197         execution = "model.%s = adapter.scalarInsertRaw!%s(sql, params);".format(idName, idType);
198       }
199       else
200       {
201         execution = "adapter.executeRaw(sql, params);";
202       }
203     }
204 
205     {
206       mixin HandleFields!(TModel, q{{
207         enum hasNoMap = hasUDA!({{fullName}}, DbNoMap);
208         enum hasId = hasUDA!({{fullName}}, DbId);
209 
210         static if (!hasNoMap && !hasId)
211         {
212           columns ~= "`{{fieldName}}`";
213         }
214       }});
215       mixin(handleThem());
216     }
217 
218     if (!columns || !columns.length)
219     {
220       return "null";
221     }
222 
223     {
224       mixin HandleFields!(TModel, q{{
225         enum hasNoMap = hasUDA!({{fullName}}, DbNoMap);
226         enum hasId = hasUDA!({{fullName}}, DbId);
227 
228         static if (!hasNoMap && !hasId)
229         {
230           import std.traits : getUDAs;
231 
232           enum hasEnum = hasUDA!({{fullName}}, DbEnum);
233           enum hasTimestamp = hasUDA!({{fullName}}, DbTimestamp);
234           enum hasProxy = hasUDA!({{fullName}}, DbProxy);
235 
236           static if (hasEnum)
237           {
238             paramsInserts ~= "params[index++] = cast(string)model.{{fieldName}};";
239           }
240           else static if (hasProxy)
241           {
242             mixin("enum proxy = getUDAs!(%s, DbProxy)[0];".format("{{fullName}}"));
243 
244             paramsInserts ~= "params[index++] = model." ~ proxy.writeHandler  ~ "!(\"{{fieldName}}\")();";
245           }
246           else static if (hasTimestamp)
247           {
248             paramsInserts ~= `
249              model.timestamp = Clock.currTime().asDateTime();
250              params[index++] = model.timestamp;
251             `;
252           }
253           else static if (is(typeof({{fullName}}) == bool))
254           {
255             paramsInserts ~= "params[index++] = cast(ubyte)model.{{fieldName}};";
256           }
257           else
258           {
259             paramsInserts ~= "params[index++] = model.{{fieldName}};";
260           }
261         }
262       }});
263       mixin(handleThem());
264     }
265 
266     if (!paramsInserts || !paramsInserts.length)
267     {
268       return "null";
269     }
270 
271     return s.format(TModel.table, columns.join(","), columns.map!(c => "?").array.join(","), columns.length, paramsInserts.join("\r\n"), execution);
272   }
273 
274   /// Generates the update mixin.
275   override string generateUpdate() const
276   {
277     import models;
278 
279     string s = q{
280       {
281         static const sql = "UPDATE `%s` SET %s WHERE `%s` = ?";
282         auto params = getParams(%s);
283 
284         size_t index;
285 
286         %s
287 
288         %s
289 
290         adapter.executeRaw(sql, params);
291       }
292     };
293 
294     string[] columns;
295     string[] paramsUpdates;
296     string idName;
297     string idParams;
298 
299     {
300       mixin HandleFields!(TModel, q{{
301         enum hasNoMap = hasUDA!({{fullName}}, DbNoMap);
302         enum hasId = hasUDA!({{fullName}}, DbId);
303 
304         static if (!hasNoMap && !hasId)
305         {
306           columns ~= "`{{fieldName}}` = ?";
307         }
308       }});
309       mixin(handleThem());
310     }
311 
312     if (!columns || !columns.length)
313     {
314       return "null";
315     }
316 
317     {
318       mixin HandleFields!(TModel, q{{
319         enum hasId = hasUDA!({{fullName}}, DbId);
320 
321         static if (hasId)
322         {
323           idName = "{{fieldName}}";
324           idParams = "params[%s] = model.{{fieldName}};".format(columns.length);
325         }
326       }});
327       mixin(handleThem());
328 
329       if (!idName)
330       {
331         return "null";
332       }
333     }
334 
335     {
336       mixin HandleFields!(TModel, q{{
337         enum hasNoMap = hasUDA!({{fullName}}, DbNoMap);
338         enum hasId = hasUDA!({{fullName}}, DbId);
339 
340         static if (!hasNoMap && !hasId)
341         {
342           import std.traits : getUDAs;
343           
344           enum hasEnum = hasUDA!({{fullName}}, DbEnum);
345           enum hasTimestamp = hasUDA!({{fullName}}, DbTimestamp);
346           enum hasProxy = hasUDA!({{fullName}}, DbProxy);
347 
348           static if (hasEnum)
349           {
350             paramsUpdates ~= "params[index++] = cast(string)model.{{fieldName}};";
351           }
352           else static if (hasProxy)
353           {
354             mixin("enum proxy = getUDAs!(%s, DbProxy)[0];".format("{{fullName}}"));
355 
356             paramsUpdates ~= "params[index++] = model." ~ proxy.writeHandler  ~ "!(\"{{fieldName}}\")();";
357           }
358           else static if (hasTimestamp)
359           {
360             paramsUpdates ~= `
361              model.timestamp = Clock.currTime().asDateTime();
362              params[index++] = model.timestamp;
363             `;
364           }
365           else static if (is(typeof({{fullName}}) == bool))
366           {
367             paramsUpdates ~= "params[index++] = cast(ubyte)model.{{fieldName}};";
368           }
369           else
370           {
371             paramsUpdates ~= "params[index++] = model.{{fieldName}};";
372           }
373         }
374       }});
375       mixin(handleThem());
376     }
377 
378     if (!paramsUpdates || !paramsUpdates.length)
379     {
380       return "null";
381     }
382 
383     return s.format(TModel.table, columns.join(","), idName, (columns.length + 1), paramsUpdates.join("\r\n"), idParams);
384   }
385 
386   /// Generates the delete mixin.
387   override string generateDelete() const
388   {
389     import models;
390 
391     string s = q{
392       {
393         static const sql = "DELETE FROM `%s` WHERE `%s` = ?";
394         auto params = getParams(1);
395 
396         %s
397 
398         adapter.executeRaw(sql, params);
399       }
400     };
401 
402     string idName;
403     string idParams;
404 
405     {
406       mixin HandleFields!(TModel, q{{
407         enum hasId = hasUDA!({{fullName}}, DbId);
408 
409         static if (hasId)
410         {
411           idName = "{{fieldName}}";
412           idParams = "params[0] = model.{{fieldName}};";
413         }
414       }});
415       mixin(handleThem());
416 
417       if (!idName)
418       {
419         return "null";
420       }
421     }
422 
423     return s.format(TModel.table, idName, idParams);
424   }
425 
426   /// Generates the read relationship mixin.
427   override string generateReadRelationship() const
428   {
429     string s = q{
430       {
431         %s
432       }
433     };
434 
435     mixin HandleFields!(TModel, q{{
436       enum hasNoMap = hasUDA!({{fullName}}, DbNoMap);
437 
438       static if (!hasNoMap)
439       {
440         import std..string : indexOf;
441         import std.traits : getUDAs;
442 
443         enum hasRelationship = hasUDA!({{fullName}}, DbRelationship);
444 
445         enum typeName = typeof({{fullName}}).stringof;
446 
447         static if (hasRelationship)
448         {
449           enum shortTypeName = typeof({{fullName}}).stringof[0 .. typeof({{fullName}}).stringof.indexOf('[')];
450 
451           mixin("enum relationship = getUDAs!(%s, DbRelationship)[0];".format("{{fullName}}"));
452 
453           mixin(readRelationshipFormat.format("{{fieldName}}", typeName, shortTypeName));
454         }
455       }
456     }});
457 
458     return s.format(handleThem());
459   }
460 }