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