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