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 }