1 module pgsql.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 pgsql.exception;
12 import pgsql.type;
13 
14 enum Strict {
15 	yes = 0,
16 	yesIgnoreNull,
17 	no,
18 }
19 
20 
21 package 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 PgSQLRow {
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 PgSQLErrorException("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(PgSQLValue)* 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 PgSQLValue 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 PgSQLValue) 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 PgSQLValue) 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)) {
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 PgSQLErrorException("Column " ~ i ~ " is out of range for this result set");
137 				}
138 			}
139 		} else {
140 			structurize!(T, strict, null)(x);
141 		}
142 	}
143 
144 	T toStruct(T, Strict strict = Strict.yesIgnoreNull)() if (is(Unqual!T == struct)) {
145 		T result;
146 		toStruct!(T, strict)(result);
147 		return result;
148 	}
149 
150 package:
151 	ref auto dispatchFast_(uint hash, string key) const {
152 		if (auto index = find_(hash, key))
153 			return opIndex(index - 1);
154 		throw new PgSQLErrorException("Column '" ~ key ~ "' was not found in this result set");
155 	}
156 
157 	void header_(PgSQLHeader header) {
158 		auto headerLen = header.length;
159 		auto idealLen = (headerLen + (headerLen >> 2));
160 		auto indexLen = index_.length;
161 
162 		index_[] = 0;
163 
164 		if (indexLen < idealLen) {
165 			indexLen = max(32, indexLen);
166 
167 			while (indexLen < idealLen)
168 				indexLen <<= 1;
169 
170 			index_.length = indexLen;
171 		}
172 
173 		auto mask = (indexLen - 1);
174 		assert((indexLen & mask) == 0);
175 
176 		names_.length = headerLen;
177 		values_.length = headerLen;
178 		foreach (index, ref column; header) {
179 			names_[index] = column.name;
180 
181 			auto hash = hashOf(column.name) & mask;
182 			auto probe = 1;
183 
184 			while (true) {
185 				if (index_[hash] == 0) {
186 					index_[hash] = cast(uint)index + 1;
187 					break;
188 				}
189 
190 				hash = (hash + probe++) & mask;
191 			}
192 		}
193 	}
194 
195 	uint find_(uint hash, const(char)[] key) const {
196 		if (auto mask = index_.length - 1) {
197 			assert((index_.length & mask) == 0);
198 
199 			hash = hash & mask;
200 			auto probe = 1;
201 
202 			while (true) {
203 				auto index = index_[hash];
204 				if (index) {
205 					if (names_[index - 1].equalsCI(key))
206 						return index;
207 					hash = (hash + probe++) & mask;
208 				} else {
209 					break;
210 				}
211 			}
212 		}
213 		return 0;
214 	}
215 
216 	ref auto get_(size_t index) {
217 		return values_[index];
218 	}
219 
220 private:
221 	void structurize(T, Strict strict = Strict.yesIgnoreNull, string path = null)(ref T result) {
222 		enum unCamel = hasUDA!(T, UnCamelCaseAttribute);
223 
224 		foreach(member; __traits(allMembers, T)) {
225 			static if (isWritableDataMember!(T, member)) {
226 				static if (!hasUDA!(__traits(getMember, result, member), NameAttribute)) {
227 					enum pathMember = path ~ member;
228 					static if (unCamel) {
229 						enum pathMemberAlt = path ~ member.unCamelCase;
230 					}
231 				} else {
232 					enum pathMember = path ~ getUDAs!(__traits(getMember, result, member), NameAttribute)[0].name;
233 					static if (unCamel) {
234 						enum pathMemberAlt = pathMember;
235 					}
236 				}
237 
238 				alias MemberType = typeof(__traits(getMember, result, member));
239 
240 				static if (! isValueType!MemberType) {
241 					enum pathNew = pathMember ~ ".";
242 					static if (hasUDA!(__traits(getMember, result, member), OptionalAttribute)) {
243 						structurize!(MemberType, Strict.no, pathNew)(__traits(getMember, result, member));
244 					} else {
245 						structurize!(MemberType, strict, pathNew)(__traits(getMember, result, member));
246 					}
247 				} else {
248 					enum hash = pathMember.hashOf;
249 					static if (unCamel) {
250 						enum hashAlt = pathMemberAlt.hashOf;
251 					}
252 
253 					auto index = find_(hash, pathMember);
254 					static if (unCamel && (pathMember != pathMemberAlt)) {
255 						if (!index)
256 							index = find_(hashAlt, pathMemberAlt);
257 					}
258 
259 					if (index) {
260 						auto pvalue = values_[index - 1];
261 
262 						static if ((strict == Strict.no) || (strict == Strict.yesIgnoreNull) || hasUDA!(__traits(getMember, result, member), OptionalAttribute)) {
263 							if (pvalue.isNull)
264 								continue;
265 						}
266 
267 						__traits(getMember, result, member) = pvalue.get!(Unqual!MemberType);
268 						continue;
269 					}
270 
271 					static if (((strict == Strict.yes) || (strict == Strict.yesIgnoreNull)) && !hasUDA!(__traits(getMember, result, member), OptionalAttribute)) {
272 						static if (!unCamel || (pathMember == pathMemberAlt)) {
273 							enum ColumnError = format("Column '%s' was not found in this result set", pathMember);
274 						} else {
275 							enum ColumnError = format("Column '%s' or '%s' was not found in this result set", pathMember, pathMember);
276 						}
277 						throw new PgSQLErrorException(ColumnError);
278 					}
279 				}
280 			}
281 		}
282 	}
283 
284 	PgSQLValue[] values_;
285 	const(char)[][] names_;
286 	uint[] index_;
287 }
288 
289 string unCamelCase(string x) {
290 	assert(x.length <= 64);
291 
292 	enum CharClass {
293 		LowerCase,
294 		UpperCase,
295 		Underscore,
296 		Digit,
297 	}
298 
299 	CharClass classify(char ch) @nogc @safe pure nothrow {
300 		switch (ch) with (CharClass) {
301 		case 'A':..case 'Z':
302 			return UpperCase;
303 		case 'a':..case 'z':
304 			return LowerCase;
305 		case '0':..case '9':
306 			return Digit;
307 		case '_':
308 			return Underscore;
309 		default:
310 			assert(false, "only supports identifier-type strings");
311 		}
312 	}
313 
314 	if (x.length > 0) {
315 		char[128] buffer;
316 		size_t length;
317 
318 		auto pcls = classify(x.ptr[0]);
319 		foreach (i; 0..x.length) with (CharClass) {
320 			auto ch = x.ptr[i];
321 			auto cls = classify(ch);
322 
323 			final switch (cls) {
324 			case Underscore:
325 				buffer[length++] = '_';
326 				break;
327 			case LowerCase:
328 				buffer[length++] = ch;
329 				break;
330 			case UpperCase:
331 				if ((pcls != UpperCase) && (pcls != Underscore))
332 					buffer[length++] = '_';
333 				buffer[length++] = std.ascii.toLower(ch);
334 				break;
335 			case Digit:
336 				if (pcls != Digit)
337 					buffer[length++] = '_';
338 				buffer[length++] = ch;
339 				break;
340 			}
341 			pcls = cls;
342 
343 			if (length == buffer.length)
344 				break;
345 		}
346 		return buffer[0..length].idup;
347 	}
348 	return x;
349 }
350 
351 
352 unittest {
353 	assert("AA".unCamelCase == "aa");
354 	assert("AaA".unCamelCase == "aa_a");
355 	assert("AaA1".unCamelCase == "aa_a_1");
356 	assert("AaA11".unCamelCase == "aa_a_11");
357 	assert("_AaA1".unCamelCase == "_aa_a_1");
358 	assert("_AaA11_".unCamelCase == "_aa_a_11_");
359 	assert("aaA".unCamelCase == "aa_a");
360 	assert("aaAA".unCamelCase == "aa_aa");
361 	assert("aaAA1".unCamelCase == "aa_aa_1");
362 	assert("aaAA11".unCamelCase == "aa_aa_11");
363 	assert("authorName".unCamelCase == "author_name");
364 	assert("authorBio".unCamelCase == "author_bio");
365 	assert("authorPortraitId".unCamelCase == "author_portrait_id");
366 	assert("authorPortraitID".unCamelCase == "author_portrait_id");
367 	assert("coverURL".unCamelCase == "cover_url");
368 	assert("coverImageURL".unCamelCase == "cover_image_url");
369 }