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 }