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 }