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