1 module database.mysql.row;
2 
3 import std.algorithm;
4 import std.datetime;
5 import std.traits;
6 import std.typecons;
7 import std.format : format;
8 import std.ascii;
9 
10 import database.mysql.exception;
11 import database.mysql.type;
12 
13 enum Strict
14 {
15     yes = 0,
16     yesIgnoreNull,
17     no,
18 }
19 
20 private uint hashOf(const(char)[] x)
21 {
22     uint hash = 2166136261u;
23     foreach(i; 0..x.length)
24         hash = (hash ^ cast(uint)(std.ascii.toLower(x.ptr[i]))) * 16777619u;
25 
26     return hash;
27 }
28 
29 private bool equalsCI(const(char)[]x, const(char)[] y)
30 {
31     if (x.length != y.length)
32         return false;
33 
34     foreach(i; 0..x.length)
35     {
36         if (std.ascii.toLower(x.ptr[i]) != std.ascii.toLower(y.ptr[i]))
37             return false;
38     }
39 
40     return true;
41 }
42 
43 struct MySQLRow
44 {
45     @property size_t opDollar() const
46     {
47         return values_.length;
48     }
49 
50     @property const(const(char)[])[] columns() const
51     {
52         return names_;
53     }
54 
55     @property ref auto opDispatch(string key)() const
56     {
57         enum hash = hashOf(key);
58         return dispatchFast_(hash, key);
59     }
60 
61     auto opSlice() const
62     {
63         return values_;
64     }
65 
66     auto opSlice(size_t i, size_t j) const
67     {
68         return values_[i..j];
69     }
70 
71     ref auto opIndex(string key) const
72     {
73         if (auto index = find_(key.hashOf, key))
74             return values_[index - 1];
75         throw new MySQLErrorException("Column '" ~ key ~ "' was not found in this result set");
76     }
77 
78     ref auto opIndex(size_t index) const
79     {
80         return values_[index];
81     }
82 
83     const(MySQLValue)* opBinaryRight(string op)(string key) const if (op == "in")
84     {
85         if (auto index = find(key.hashOf, key))
86             return &values_[index - 1];
87         return null;
88     }
89 
90     int opApply(int delegate(const ref MySQLValue value) del) const
91     {
92         foreach (ref v; values_)
93             if (auto ret = del(v))
94                 return ret;
95         return 0;
96     }
97 
98     int opApply(int delegate(ref size_t, const ref MySQLValue) del) const
99     {
100         foreach (ref size_t i, ref v; values_)
101             if (auto ret = del(i, v))
102                 return ret;
103         return 0;
104     }
105 
106     int opApply(int delegate(const ref const(char)[], const ref MySQLValue) del) const
107     {
108         foreach (size_t i, ref v; values_)
109             if (auto ret = del(names_[i], v))
110                 return ret;
111         return 0;
112     }
113 
114     void toString(Appender)(ref Appender app) const
115     {
116         import std.format : formattedWrite;
117         formattedWrite(&app, "%s", values_);
118     }
119 
120     string toString() const
121     {
122         import std.conv : to;
123         return to!string(values_);
124     }
125 
126     string[] toStringArray(size_t start = 0, size_t end = ~cast(size_t)0) const
127     {
128         end = min(end, values_.length);
129         start = min(start, values_.length);
130         if (start > end)
131             swap(start, end);
132 
133         string[] result;
134         result.reserve(end - start);
135         foreach(i; start..end)
136             result ~= values_[i].toString;
137         return result;
138     }
139 
140     string[string] toAA()
141     {
142         string[string] result;
143         foreach(i, name; names_)
144         {
145             result[name] = values_[i].toString();
146         }
147 
148         return result;
149     }
150 
151     void toStruct(T, Strict strict = Strict.yesIgnoreNull)(ref T x) if(is(Unqual!T == struct) && !is(T == Strict))
152     {
153         static if (isTuple!(Unqual!T))
154         {
155             foreach(i, ref f; x.field)
156             {
157                 if (i < values_.length)
158                 {
159                     static if (strict != Strict.yes)
160                     {
161                         if (!this[i].isNull)
162                             f = this[i].get!(Unqual!(typeof(f)));
163                     }
164                     else
165                     {
166                         f = this[i].get!(Unqual!(typeof(f)));
167                     }
168                 }
169                 else static if ((strict == Strict.yes) || (strict == Strict.yesIgnoreNull))
170                 {
171                     throw new MySQLErrorException("Column " ~ i ~ " is out of range for this result set");
172                 }
173             }
174         }
175         else
176         {
177             structurize!(strict, null)(x);
178         }
179     }
180 
181     void toStruct(Strict strict, T)(ref T x) if (is(Unqual!T == struct)) {
182         toStruct!(T, strict)(x);
183     }
184 
185     T toStruct(T, Strict strict = Strict.yesIgnoreNull)() if (is(Unqual!T == struct))
186     {
187         T result;
188         toStruct!(T, strict)(result);
189 
190         return result;
191     }
192 
193 package:
194 
195     ref auto dispatchFast_(uint hash, string key) const
196     {
197         if (auto index = find_(hash, key))
198             return opIndex(index - 1);
199         throw new MySQLErrorException("Column '" ~ key ~ "' was not found in this result set");
200     }
201 
202     void header_(MySQLHeader header)
203     {
204         auto headerLen = header.length;
205         auto idealLen = (headerLen + (headerLen >> 2));
206         auto indexLen = index_.length;
207 
208         index_[] = 0;
209 
210         if (indexLen < idealLen)
211         {
212             indexLen = max(32, indexLen);
213 
214             while (indexLen < idealLen)
215                 indexLen <<= 1;
216 
217             index_.length = indexLen;
218         }
219 
220         auto mask = (indexLen - 1);
221         assert((indexLen & mask) == 0);
222 
223         names_.length = headerLen;
224         values_.length = headerLen;
225         foreach (index, ref column; header)
226         {
227             names_[index] = column.name;
228 
229             auto hash = hashOf(column.name) & mask;
230             auto probe = 1;
231 
232             while (true)
233             {
234                 if (index_[hash] == 0)
235                 {
236                     index_[hash] = cast(uint)index + 1;
237                     break;
238                 }
239 
240                 hash = (hash + probe++) & mask;
241             }
242         }
243     }
244 
245     uint find_(uint hash, const(char)[] key) const
246     {
247         if (auto mask = index_.length - 1) {
248             assert((index_.length & mask) == 0);
249 
250             hash = hash & mask;
251             auto probe = 1;
252 
253             while (true)
254             {
255                 auto index = index_[hash];
256                 if (index)
257                 {
258                     if (names_[index - 1].equalsCI(key))
259                         return index;
260                     hash = (hash + probe++) & mask;
261                 }
262                 else
263                 {
264                     break;
265                 }
266             }
267         }
268 
269         return 0;
270     }
271 
272     ref auto get_(size_t index)
273     {
274         return values_[index];
275     }
276 
277 private:
278 
279     void structurize(Strict strict = Strict.yesIgnoreNull, string path = null, T)(ref T result)
280     {
281         enum unCamel = hasUDA!(T, UnCamelCaseAttribute);
282 
283         foreach(member; __traits(allMembers, T))
284         {
285             static if (isWritableDataMember!(T, member))
286             {
287                 static if (!hasUDA!(__traits(getMember, result, member), NameAttribute))
288                 {
289                     enum pathMember = path ~ member;
290                     static if (unCamel)
291                     {
292                         enum pathMemberAlt = path ~ member.unCamelCase;
293                     }
294                 }
295                 else
296                 {
297                     enum pathMember = path ~ getUDAs!(__traits(getMember, result, member), NameAttribute)[0].name;
298                     static if (unCamel)
299                     {
300                         enum pathMemberAlt = pathMember;
301                     }
302                 }
303 
304                 alias MemberType = typeof(__traits(getMember, result, member));
305 
306                 static if (isPointer!MemberType && !isValueType!(PointerTarget!MemberType) || !isValueType!MemberType)
307                 {
308                     enum pathNew = pathMember ~ ".";
309                     enum st = Select!(hasUDA!(__traits(getMember, result, member), OptionalAttribute), Strict.no, strict);
310                     static if (isPointer!MemberType)
311                     {
312                         if (__traits(getMember, result, member))
313                             structurize!(st, pathNew)(*__traits(getMember, result, member));
314                     }
315                     else
316                     {
317                         structurize!(st, pathNew)(__traits(getMember, result, member));
318                     }
319                 }
320                 else
321                 {
322                     enum hash = pathMember.hashOf;
323                     static if (unCamel)
324                     {
325                         enum hashAlt = pathMemberAlt.hashOf;
326                     }
327 
328                     auto index = find_(hash, pathMember);
329                     static if (unCamel && (pathMember != pathMemberAlt))
330                     {
331                         if (!index)
332                             index = find_(hashAlt, pathMemberAlt);
333                     }
334 
335                     if (index)
336                     {
337                         auto pvalue = values_[index - 1];
338 
339                         static if ((strict == Strict.no) || (strict == Strict.yesIgnoreNull) || hasUDA!(__traits(getMember, result, member), OptionalAttribute))
340                         {
341                             if (pvalue.isNull)
342                                 continue;
343                         }
344 
345                         __traits(getMember, result, member) = pvalue.get!(Unqual!MemberType);
346                         continue;
347                     }
348 
349                     static if (((strict == Strict.yes) || (strict == Strict.yesIgnoreNull)) && !hasUDA!(__traits(getMember, result, member), OptionalAttribute))
350                     {
351                         static if (!unCamel || (pathMember == pathMemberAlt))
352                         {
353                             enum ColumnError = format("Column '%s' was not found in this result set", pathMember);
354                         }
355                         else
356                         {
357                             enum ColumnError = format("Column '%s' or '%s' was not found in this result set", pathMember, pathMemberAlt);
358                         }
359                         throw new MySQLErrorException(ColumnError);
360                     }
361                 }
362             }
363         }
364     }
365 
366     MySQLValue[] values_;
367     const(char)[][] names_;
368     uint[] index_;
369 }
370 
371 string unCamelCase(string x)
372 {
373     assert(x.length <= 64);
374 
375     enum CharClass
376     {
377         LowerCase,
378         UpperCase,
379         Underscore,
380         Digit,
381     }
382 
383     CharClass classify(char ch) @nogc @safe pure nothrow
384     {
385         switch (ch) with (CharClass)
386         {
387             case 'A':..case 'Z':
388                 return UpperCase;
389             case 'a':..case 'z':
390                 return LowerCase;
391             case '0':..case '9':
392                 return Digit;
393             case '_':
394                 return Underscore;
395             default:
396                 assert(false, "only supports identifier-type strings");
397         }
398     }
399 
400     if (x.length > 0)
401     {
402         char[128] buffer;
403         size_t length;
404 
405         auto pcls = classify(x.ptr[0]);
406         foreach (i; 0..x.length) with (CharClass)
407         {
408             auto ch = x.ptr[i];
409             auto cls = classify(ch);
410 
411             final switch (cls)
412             {
413                 case Underscore:
414                     buffer[length++] = '_';
415                     break;
416                 case LowerCase:
417                     buffer[length++] = ch;
418                     break;
419                 case UpperCase:
420                     if ((pcls != UpperCase) && (pcls != Underscore))
421                         buffer[length++] = '_';
422                     buffer[length++] = std.ascii.toLower(ch);
423                     break;
424                 case Digit:
425                     if (pcls != Digit)
426                         buffer[length++] = '_';
427                     buffer[length++] = ch;
428                     break;
429             }
430             pcls = cls;
431 
432             if (length == buffer.length)
433                 break;
434         }
435         return buffer[0..length].idup;
436     }
437     return x;
438 }