1 module database.postgresql.row;
2 
3 import std.algorithm;
4 import std.datetime;
5 import std.traits;
6 import std.typecons;
7 import std.ascii;
8 import std.format : format;
9 
10 import database.postgresql.exception;
11 import database.postgresql.type;
12 
13 enum Strict
14 {
15     yes = 0,
16     yesIgnoreNull,
17     no,
18 }
19 
20 package 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 PgSQLRow
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 PgSQLErrorException("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(PgSQLValue)* 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 PgSQLValue 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 PgSQLValue) 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 PgSQLValue) 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))
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 PgSQLErrorException("Column " ~ i ~ " is out of range for this result set");
172                 }
173             }
174         }
175         else
176         {
177             structurize!(T, strict, null)(x);
178         }
179     }
180 
181     T toStruct(T, Strict strict = Strict.yesIgnoreNull)() if (is(Unqual!T == struct))
182     {
183         T result;
184         toStruct!(T, strict)(result);
185 
186         return result;
187     }
188 
189 package:
190 
191     ref auto dispatchFast_(uint hash, string key) const
192     {
193         if (auto index = find_(hash, key))
194             return opIndex(index - 1);
195         throw new PgSQLErrorException("Column '" ~ key ~ "' was not found in this result set");
196     }
197 
198     void header_(PgSQLHeader header)
199     {
200         auto headerLen = header.length;
201         auto idealLen = (headerLen + (headerLen >> 2));
202         auto indexLen = index_.length;
203 
204         index_[] = 0;
205 
206         if (indexLen < idealLen)
207         {
208             indexLen = max(32, indexLen);
209 
210             while (indexLen < idealLen)
211                 indexLen <<= 1;
212 
213             index_.length = indexLen;
214         }
215 
216         auto mask = (indexLen - 1);
217         assert((indexLen & mask) == 0);
218 
219         names_.length = headerLen;
220         values_.length = headerLen;
221         foreach (index, ref column; header)
222         {
223             names_[index] = column.name;
224 
225             auto hash = hashOf(column.name) & mask;
226             auto probe = 1;
227 
228             while (true)
229             {
230                 if (index_[hash] == 0)
231                 {
232                     index_[hash] = cast(uint)index + 1;
233                     break;
234                 }
235 
236                 hash = (hash + probe++) & mask;
237             }
238         }
239     }
240 
241     uint find_(uint hash, const(char)[] key) const
242     {
243         if (auto mask = index_.length - 1)
244         {
245             assert((index_.length & mask) == 0);
246 
247             hash = hash & mask;
248             auto probe = 1;
249 
250             while (true)
251             {
252                 auto index = index_[hash];
253                 if (index)
254                 {
255                     if (names_[index - 1].equalsCI(key))
256                         return index;
257                     hash = (hash + probe++) & mask;
258                 }
259                 else
260                 {
261                     break;
262                 }
263             }
264         }
265         return 0;
266     }
267 
268     ref auto get_(size_t index)
269     {
270         return values_[index];
271     }
272 
273 private:
274 
275     void structurize(T, Strict strict = Strict.yesIgnoreNull, string path = null)(ref T result)
276     {
277         enum unCamel = hasUDA!(T, UnCamelCaseAttribute);
278 
279         foreach(member; __traits(allMembers, T))
280         {
281             static if (isWritableDataMember!(T, member))
282             {
283                 static if (!hasUDA!(__traits(getMember, result, member), NameAttribute))
284                 {
285                     enum pathMember = path ~ member;
286                     static if (unCamel)
287                     {
288                         enum pathMemberAlt = path ~ member.unCamelCase;
289                     }
290                 } else {
291                     enum pathMember = path ~ getUDAs!(__traits(getMember, result, member), NameAttribute)[0].name;
292                     static if (unCamel)
293                     {
294                         enum pathMemberAlt = pathMember;
295                     }
296                 }
297 
298                 alias MemberType = typeof(__traits(getMember, result, member));
299 
300                 static if (! isValueType!MemberType)
301                 {
302                     enum pathNew = pathMember ~ ".";
303                     static if (hasUDA!(__traits(getMember, result, member), OptionalAttribute))
304                     {
305                         structurize!(MemberType, Strict.no, pathNew)(__traits(getMember, result, member));
306                     }
307                     else
308                     {
309                         structurize!(MemberType, strict, pathNew)(__traits(getMember, result, member));
310                     }
311                 }
312                 else
313                 {
314                     enum hash = pathMember.hashOf;
315                     static if (unCamel)
316                     {
317                         enum hashAlt = pathMemberAlt.hashOf;
318                     }
319 
320                     auto index = find_(hash, pathMember);
321                     static if (unCamel && (pathMember != pathMemberAlt))
322                     {
323                         if (!index)
324                             index = find_(hashAlt, pathMemberAlt);
325                     }
326 
327                     if (index)
328                     {
329                         auto pvalue = values_[index - 1];
330 
331                         static if ((strict == Strict.no) || (strict == Strict.yesIgnoreNull) || hasUDA!(__traits(getMember, result, member), OptionalAttribute))
332                         {
333                             if (pvalue.isNull)
334                                 continue;
335                         }
336 
337                         __traits(getMember, result, member) = pvalue.get!(Unqual!MemberType);
338                         continue;
339                     }
340 
341                     static if (((strict == Strict.yes) || (strict == Strict.yesIgnoreNull)) && !hasUDA!(__traits(getMember, result, member), OptionalAttribute))
342                     {
343                         static if (!unCamel || (pathMember == pathMemberAlt))
344                         {
345                             enum ColumnError = format("Column '%s' was not found in this result set", pathMember);
346                         }
347                         else
348                         {
349                             enum ColumnError = format("Column '%s' or '%s' was not found in this result set", pathMember, pathMember);
350                         }
351                         throw new PgSQLErrorException(ColumnError);
352                     }
353                 }
354             }
355         }
356     }
357 
358     PgSQLValue[] values_;
359     const(char)[][] names_;
360     uint[] index_;
361 }
362 
363 string unCamelCase(string x)
364 {
365     assert(x.length <= 64);
366 
367     enum CharClass
368     {
369         LowerCase,
370         UpperCase,
371         Underscore,
372         Digit,
373     }
374 
375     CharClass classify(char ch) @nogc @safe pure nothrow
376     {
377         switch (ch) with (CharClass)
378         {
379             case 'A':..case 'Z':
380                 return UpperCase;
381             case 'a':..case 'z':
382                 return LowerCase;
383             case '0':..case '9':
384                 return Digit;
385             case '_':
386                 return Underscore;
387             default:
388                 assert(false, "only supports identifier-type strings");
389         }
390     }
391 
392     if (x.length > 0)
393     {
394         char[128] buffer;
395         size_t length;
396 
397         auto pcls = classify(x.ptr[0]);
398         foreach (i; 0..x.length) with (CharClass)
399         {
400             auto ch = x.ptr[i];
401             auto cls = classify(ch);
402 
403             final switch (cls)
404             {
405                 case Underscore:
406                     buffer[length++] = '_';
407                     break;
408                 case LowerCase:
409                     buffer[length++] = ch;
410                     break;
411                 case UpperCase:
412                     if ((pcls != UpperCase) && (pcls != Underscore))
413                         buffer[length++] = '_';
414                     buffer[length++] = std.ascii.toLower(ch);
415                     break;
416                 case Digit:
417                     if (pcls != Digit)
418                         buffer[length++] = '_';
419                     buffer[length++] = ch;
420                     break;
421             }
422             pcls = cls;
423 
424             if (length == buffer.length)
425                 break;
426         }
427         return buffer[0..length].idup;
428     }
429     return x;
430 }