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 }