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