1 /** 2 Convenience wrappers types for accessing Redis keys. 3 4 Note that the API is still subject to change! 5 6 Copyright: © 2014 Sönke Ludwig 7 License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file. 8 Authors: Sönke Ludwig 9 */ 10 module vibe.db.redis.types; 11 12 import vibe.db.redis.redis; 13 14 import std.conv : to; 15 import std.datetime : SysTime; 16 import std.typecons : apply, Nullable; 17 import core.time : Duration, msecs, seconds; 18 19 20 /** Returns a handle to a string type value. 21 */ 22 RedisString!T getAsString(T = string)(RedisDatabase db, string key) 23 { 24 return RedisString!T(db, key); 25 } 26 27 /// 28 unittest { 29 void test() 30 { 31 auto db = connectRedis("127.0.0.1").getDatabase(0); 32 auto str = db.getAsString("some_string"); 33 str = "test"; 34 } 35 } 36 37 38 39 /** Returns a handle to a set type value. 40 */ 41 RedisSet!T getAsSet(T = string)(RedisDatabase db, string key) 42 { 43 return RedisSet!T(db, key); 44 } 45 46 /// 47 unittest { 48 void test() 49 { 50 auto db = connectRedis("127.0.0.1").getDatabase(0); 51 auto set = db.getAsSet("some_set"); 52 set.insert("test"); 53 } 54 } 55 56 57 /** Returns a handle to a set type value. 58 */ 59 RedisZSet!T getAsZSet(T = string)(RedisDatabase db, string key) 60 { 61 return RedisZSet!T(db, key); 62 } 63 64 /// 65 unittest { 66 void test() 67 { 68 auto db = connectRedis("127.0.0.1").getDatabase(0); 69 auto set = db.getAsZSet("some_sorted_set"); 70 set.insert(1, "test"); 71 } 72 } 73 74 75 /** Returns a handle to a hash type value. 76 */ 77 RedisHash!T getAsHash(T = string)(RedisDatabase db, string key) 78 { 79 return RedisHash!T(db, key); 80 } 81 82 /// 83 unittest { 84 void test() 85 { 86 auto db = connectRedis("127.0.0.1").getDatabase(0); 87 auto hash = db.getAsHash("some_hash"); 88 hash["test"] = "123"; 89 } 90 } 91 92 93 /** Returns a handle to a list type value. 94 */ 95 RedisList!T getAsList(T = string)(RedisDatabase db, string key) 96 { 97 return RedisList!T(db, key); 98 } 99 100 /// 101 unittest { 102 void test() 103 { 104 auto db = connectRedis("127.0.0.1").getDatabase(0); 105 auto list = db.getAsList!long("some_list"); 106 list.insertFront(123); 107 } 108 } 109 110 111 /** 112 Converts the given value to a binary/string representation suitable for 113 Redis storage. 114 115 These functions are used by the proxy types of this module to convert 116 between Redis and D. 117 118 See_also: $(D fromRedis) 119 */ 120 string toRedis(T)(T value) 121 { 122 import std.format; 123 import std.traits; 124 import vibe.data.serialization; 125 static if (is(T == bool)) return value ? "1": "0"; 126 else static if (is(T : long) || is(T : double)) return value.to!string; 127 else static if (isSomeString!T) return value.to!string; 128 else static if (is(T : const(ubyte)[])) return cast(string)value; 129 else static if (isISOExtStringSerializable!T) return value == T.init ? null : value.toISOExtString(); 130 else static if (isStringSerializable!T) return value.toString(); 131 else static assert(false, "Unsupported type: "~T.stringof); 132 } 133 /// ditto 134 void toRedis(R, T)(ref R dst, T value) 135 { 136 import std.format; 137 import std.traits; 138 import vibe.data.serialization; 139 static if (is(T == bool)) dst.put(value ? '1' : '0'); 140 else static if (is(T : long)) dst.formattedWrite("%s", value); 141 else static if (isSomeString!T) dst.formattedWrite("%s", value); 142 else static if(is(T : const(ubyte)[])) dst.put(value); 143 else static if (isISOExtStringSerializable!T) dst.put(value == T.init ? null : value.toISOExtString()); 144 else static if (isStringSerializable!T) dst.put(value.toString()); 145 else static assert(false, "Unsupported type: "~T.stringof); 146 } 147 148 149 /** 150 Converts a Redis value back to its original representation. 151 152 These functions are used by the proxy types of this module to convert 153 between Redis and D. 154 155 See_also: $(D toRedis) 156 */ 157 T fromRedis(T)(string value) 158 { 159 import std.conv; 160 import std.traits; 161 import vibe.data.serialization; 162 static if (is(T == bool)) return value != "0" && value != "false"; 163 else static if (is(T : long) || is(T : double)) return value.to!T; 164 else static if (isSomeString!T) return value.to!T; 165 else static if (is(T : const(ubyte)[])) return cast(T)value; 166 else static if (isISOExtStringSerializable!T) return value.length ? T.fromISOExtString(value) : T.init; 167 else static if (isStringSerializable!T) return T.fromString(value); 168 else static assert(false, "Unsupported type: "~T.stringof); 169 } 170 171 172 /** The type of a Redis key. 173 */ 174 enum RedisType { 175 none, /// Non-existent key 176 string, /// String/binary value 177 list, /// Linked list 178 set, /// Unsorted set 179 zset, /// Sorted set 180 hash /// Unsorted map 181 } 182 183 184 /** Represents a generic Redis value. 185 */ 186 struct RedisValue { 187 private { 188 RedisDatabase m_db; 189 string m_key; 190 } 191 192 this(RedisDatabase db, string key) { m_db = db; m_key = key; } 193 194 /** The database in which the key is stored. 195 */ 196 @property inout(RedisDatabase) database() inout { return m_db; } 197 198 /** Name of the corresponding key. 199 */ 200 @property string key() const { return m_key; } 201 202 /** Remaining time-to-live. 203 204 Returns: 205 The time until the key expires, if applicable. Returns 206 $(D Duration.max) otherwise. 207 208 See_also: $(LINK2 http://redis.io/commands/pttl, PTTL) 209 */ 210 @property Duration ttl() 211 { 212 auto ret = m_db.pttl(m_key); 213 return ret >= 0 ? ret.msecs : Duration.max; 214 } 215 216 /** The data type of the referenced value. 217 218 Queries the actual type of the value that is referenced by this 219 key. 220 221 See_also: $(LINK2 http://redis.io/commands/type, TYPE) 222 */ 223 @property RedisType type() { import std.conv; return m_db.type(m_key).to!RedisType; } 224 225 /** Checks if the referenced key exists. 226 227 See_also: $(LINK2 http://redis.io/commands/exists, EXISTS) 228 */ 229 @property bool exists() { return m_db.exists(m_key); } 230 231 /** Removes the referenced key. 232 233 Returns: $(D true) $(I iff) the key was successfully removed. 234 235 See_also: $(LINK2 http://redis.io/commands/del, DEL) 236 */ 237 bool remove() { return m_db.del(m_key) > 0; } 238 239 /** Sets the key for expiration after the given timeout. 240 241 Note that Redis handles timeouts in second resolution, so that the 242 timeout must be at least one second. 243 244 Returns: $(D true) $(I iff) the expiration time was successfully set. 245 246 See_also: $(LINK2 http://redis.io/commands/expire, EXPIRE) 247 */ 248 bool expire(Duration expire_time) { assert(expire_time >= 1.seconds); return m_db.expire(m_key, expire_time.total!"seconds"); } 249 250 /** Sets the key for expiration at the given point in time. 251 252 Note that Redis handles timeouts in second resolution, so that any 253 fractional seconds of the given $(D expire_time) will be truncated. 254 255 Returns: $(D true) $(I iff) the expiration time was successfully set. 256 257 See_also: $(LINK2 http://redis.io/commands/expireat, EXPIREAT) 258 */ 259 bool expireAt(SysTime expire_time) { return m_db.expireAt(m_key, expire_time.toUnixTime()); } 260 261 /** Removes any existing expiration time for the key. 262 263 Returns: 264 $(D true) $(I iff) the key exists and an existing timeout was removed. 265 266 See_also: $(LINK2 http://redis.io/commands/persist, PERSIST) 267 */ 268 bool persist() { return m_db.persist(m_key); } 269 270 /** Moves this key to a different database. 271 272 Existing keys will not be overwritten. 273 274 Returns: 275 $(D true) $(I iff) the key exists and was successfully moved to the 276 destination database. 277 278 See_also: $(LINK2 http://redis.io/commands/move, MOVE) 279 */ 280 bool moveTo(long dst_database) { return m_db.move(m_key, dst_database); } 281 282 /** Renames the referenced key. 283 284 This method will also update this instance to refer to the renamed 285 key. 286 287 See_also: $(LINK2 http://redis.io/commands/rename, RENAME), $(D renameIfNotExist) 288 */ 289 void rename(string new_name) { m_db.rename(m_key, new_name); m_key = new_name; } 290 291 /** Renames the referenced key if the destination key doesn't exist. 292 293 This method will also update this instance to refer to the renamed 294 key if the rename was successful. 295 296 Returns: 297 $(D true) $(I iff) the source key exists and the destination key doesn't 298 exist. 299 300 See_also: $(LINK2 http://redis.io/commands/renamenx, RENAMENX), $(D rename) 301 */ 302 bool renameIfNotExist(string new_name) 303 { 304 if (m_db.renameNX(m_key, new_name)) { 305 m_key = new_name; 306 return true; 307 } 308 return false; 309 } 310 311 //TODO sort 312 } 313 314 315 /** Represents a Redis string value. 316 317 In addition to the methods specific to string values, all operations of 318 $(D RedisValue) are available using an $(D alias this) declaration. 319 */ 320 struct RedisString(T = string) { 321 RedisValue value; 322 alias value this; 323 324 this(RedisDatabase db, string key) { value = RedisValue(db, key); } 325 326 /** The length in bytes of the string. 327 328 See_also: $(LINK2 http://redis.io/commands/strlen, STRLEN) 329 */ 330 @property long length() { return m_db.strlen(m_key); } 331 332 T get() { return m_db.get!string(m_key).fromRedis!T; } 333 334 T getSet(T value) { return m_db.getSet(m_key, value.toRedis).fromRedis!T; } 335 bool getBit(long offset) { return m_db.getBit(m_key, offset); } 336 bool setBit(long offset, bool value) { return m_db.setBit(m_key, offset, value); } 337 void setExpire(T value, Duration expire_time) { assert(expire_time >= 1.seconds); m_db.setEX(m_key, expire_time.total!"seconds", value.toRedis); } 338 bool setIfNotExist(T value) { return m_db.setNX(m_key, value.toRedis); } 339 340 string getSubString(long start, long end) { return m_db.getRange!string(m_key, start, end); } 341 long setSubString(long offset, string value) { return m_db.setRange(m_key, offset, value); } 342 343 void opAssign(T value) { m_db.set(m_key, value.toRedis); } 344 345 long opOpAssign(string OP)(string value) if (OP == "~") { return m_db.append(m_key, value); } 346 long opUnary(string OP)() if (OP == "++") { return m_db.incr(m_key); } 347 long opUnary(string OP)() if (OP == "--") { return m_db.decr(m_key); } 348 long opOpAssign(string OP)(long value) if (OP == "+") { 349 assert(value != 0); 350 if (value > 0) return m_db.incr(m_key, value); 351 else return m_db.decr(m_key, -value); 352 } 353 long opOpAssign(string OP)(long value) if (OP == "-") { 354 assert(value != 0); 355 if (value > 0) return m_db.incr(m_key, value); 356 else return m_db.decr(m_key, -value); 357 } 358 long opOpAssign(string OP)(double value) if (OP == "+") { return m_db.incr(m_key, value); } 359 long opOpAssign(string OP)(double value) if (OP == "-") { return m_db.incr(m_key, -value); } 360 } 361 362 363 /** Represents a Redis hash value. 364 365 In addition to the methods specific to hash values, all operations of 366 $(D RedisValue) are available using an $(D alias this) declaration. 367 */ 368 struct RedisHash(T = string) { 369 RedisValue value; 370 alias value this; 371 372 this(RedisDatabase db, string key) { value = RedisValue(db, key); } 373 374 bool remove() { return value.remove(); } 375 size_t remove(scope string[] fields...) { return cast(size_t)m_db.hdel(m_key, fields); } 376 bool exists(string field) { return m_db.hexists(m_key, field); } 377 bool exists() { return value.exists; } 378 379 void opIndexAssign(T value, string field) { m_db.hset(m_key, field, value.toRedis()); } 380 T opIndex(string field) { return m_db.hget!string(m_key, field).fromRedis!T(); } 381 382 T get(string field, T def_value) 383 { 384 import std.typecons; 385 auto ret = m_db.hget!(Nullable!string)(m_key, field); 386 return ret.isNull ? def_value : ret.get().fromRedis!T; 387 } 388 389 bool setIfNotExist(string field, T value) 390 { 391 return m_db.hsetNX(m_key, field, value.toRedis()); 392 } 393 394 void opIndexOpAssign(string op)(T value, string field) if (op == "+") { m_db.hincr(m_key, field, value); } 395 void opIndexOpAssign(string op)(T value, string field) if (op == "-") { m_db.hincr(m_key, field, -value); } 396 397 int opApply(scope int delegate(string key, T value) del) 398 { 399 auto reply = m_db.hgetAll(m_key); 400 while (reply.hasNext()) { 401 auto key = reply.next!string(); 402 auto value = reply.next!string(); 403 if (auto ret = del(key, value.fromRedis!T)) 404 return ret; 405 } 406 return 0; 407 } 408 409 410 int opApply(scope int delegate(string key) del) 411 { 412 auto reply = m_db.hkeys(m_key); 413 while (reply.hasNext()) { 414 if (auto ret = del(reply.next!string())) 415 return ret; 416 } 417 return 0; 418 } 419 420 long length() { return m_db.hlen(m_key); } 421 422 // FIXME: support other types! 423 void getMultiple(T[] dst, scope string[] fields...) 424 { 425 assert(dst.length == fields.length); 426 auto reply = m_db.hmget(m_key, fields); 427 size_t idx = 0; 428 while (reply.hasNext()) 429 dst[idx++] = reply.next!string().fromRedis!T(); 430 } 431 432 // FIXME: support other types! 433 /*void setMultiple(in string[] src, scope string[] fields...) 434 { 435 m_db.hmset(m_key, ...); 436 }*/ 437 438 //RedisReply hvals(string key) { return request!RedisReply("HVALS", key); } 439 } 440 441 442 /** Represents a Redis list value. 443 444 In addition to the methods specific to list values, all operations of 445 $(D RedisValue) are available using an $(D alias this) declaration. 446 */ 447 struct RedisList(T = string) { 448 RedisValue value; 449 alias value this; 450 451 this(RedisDatabase db, string key) { value = RedisValue(db, key); } 452 453 Dollar opDollar() { return Dollar(0); } 454 455 T opIndex(long index) 456 { 457 assert(index >= 0); 458 return m_db.lindex!string(m_key, index).fromRedis!T; 459 } 460 T opIndex(Dollar index) 461 { 462 assert(index.offset < 0); 463 return m_db.lindex!string(m_key, index.offset).fromRedis!T; 464 } 465 void opIndexAssign(T value, long index) 466 { 467 assert(index >= 0); 468 m_db.lset(m_key, index, value.toRedis); 469 } 470 void opIndexAssign(T value, Dollar index) 471 { 472 assert(index.offset < 0); 473 m_db.lset(m_key, index.offset, value.toRedis); 474 } 475 auto opSlice(S, E)(S start, E end) 476 if ((is(S : long) || is(S == Dollar)) && (is(E : long) || is(E == Dollar))) 477 { 478 import std.algorithm; 479 long s, e; 480 static if (is(S == Dollar)) { 481 assert(start.offset <= 0); 482 s = start.offset; 483 } else { 484 assert(start >= 0); 485 s = start; 486 } 487 static if (is(E == Dollar)) { 488 assert(end.offset <= 0); 489 e = end.offset - 1; 490 } else { 491 assert(end >= 0); 492 e = end - 1; 493 } 494 return map!(e => e.fromRedis!T)(m_db.lrange(m_key, s, e)); 495 } 496 auto opSlice()() { return this[0 .. $]; } 497 498 long length() { return m_db.llen(m_key); } 499 500 long insertBefore(T pivot, T value) { return m_db.linsertBefore(m_key, pivot.toRedis, value.toRedis); } 501 long insertAfter(T pivot, T value) { return m_db.linsertAfter(m_key, pivot.toRedis, value.toRedis); } 502 503 long insertFront(T value) { return m_db.lpush(m_key, value.toRedis); } 504 long insertFrontIfExists(T value) { return m_db.lpushX(m_key, value.toRedis); } 505 long insertBack(T value) { return m_db.rpush(m_key, value.toRedis); } 506 long insertBackIfExists(T value) { return m_db.rpushX(m_key, value.toRedis); } 507 508 long removeAll(T value) { return m_db.lrem(m_key, 0, value.toRedis); } 509 long removeFirst(T value, long count = 1) { assert(count > 0); return m_db.lrem(m_key, count, value.toRedis); } 510 long removeLast(T value, long count = 1) { assert(count > 0); return m_db.lrem(m_key, -count, value.toRedis); } 511 512 void trim(long start, long end) { m_db.ltrim(m_key, start, end); } 513 514 T removeFront() { return m_db.lpop!string(m_key).fromRedis!T; } 515 T removeBack() { return m_db.rpop!string(m_key).fromRedis!T; } 516 Nullable!T removeFrontBlock(Duration max_wait = 0.seconds) { 517 assert(max_wait == 0.seconds || max_wait >= 1.seconds); 518 auto r = m_db.blpop!string(m_key, max_wait.total!"seconds"); 519 return r.apply!(r => r[1].fromRedis!T); 520 } 521 522 struct Dollar { 523 long offset = 0; 524 Dollar opAdd(long off) { return Dollar(offset + off); } 525 Dollar opSub(long off) { return Dollar(offset - off); } 526 } 527 528 int opApply(scope int delegate(T) del) 529 { 530 foreach (v; this[0 .. $]) 531 if (auto ret = del(v)) 532 return ret; 533 return 0; 534 } 535 536 //RedisReply lrange(string key, long start, long stop) { return request!RedisReply("LRANGE", key, start, stop); } 537 //T rpoplpush(T : E[], E)(string key, string destination) { return request!T("RPOPLPUSH", key, destination); } 538 } 539 540 541 /** Represents a Redis set value. 542 543 In addition to the methods specific to set values, all operations of 544 $(D RedisValue) are available using an $(D alias this) declaration. 545 */ 546 struct RedisSet(T = string) { 547 RedisValue value; 548 alias value this; 549 550 this(RedisDatabase db, string key) { value = RedisValue(db, key); } 551 552 long insert(ARGS...)(ARGS args) { return m_db.sadd(m_key, args); } 553 long remove(T value) { return m_db.srem(m_key, value.toRedis()); } 554 bool remove() { return value.remove(); } 555 string pop() { return m_db.spop!string(m_key); } 556 long length() { return m_db.scard(m_key); } 557 558 string getRandom() { return m_db.srandMember!string(m_key); } 559 560 //RedisReply sdiff(string[] keys...) { return request!RedisReply("SDIFF", keys); } 561 //long sdiffStore(string destination, string[] keys...) { return request!long("SDIFFSTORE", destination, keys); } 562 //RedisReply sinter(string[] keys) { return request!RedisReply("SINTER", keys); } 563 //long sinterStore(string destination, string[] keys...) { return request!long("SINTERSTORE", destination, keys); } 564 bool contains(T value) { return m_db.sisMember(m_key, value.toRedis()); } 565 566 int opApply(scope int delegate(T value) del) 567 { 568 foreach (m; m_db.smembers!string(m_key)) 569 if (auto ret = del(m.fromRedis!T())) 570 return ret; 571 return 0; 572 } 573 574 bool intersects(scope RedisSet[] sets...) 575 { 576 import std.algorithm; 577 import std.array; 578 return !value.database.sinter(value.key ~ sets.map!(s => s.key).array).empty; 579 } 580 581 auto getAll() 582 { 583 import std.algorithm; 584 return map!(r => r.fromRedis!T)(value.database.smembers(value.key)); 585 } 586 587 //bool smove(T : E[], E)(string source, string destination, T member) { return request!bool("SMOVE", source, destination, member); } 588 //RedisReply sunion(string[] keys...) { return request!RedisReply("SUNION", keys); } 589 //long sunionStore(string[] keys...) { return request!long("SUNIONSTORE", keys); } 590 } 591 592 593 /** Represents a Redis sorted set value. 594 595 In addition to the methods specific to sorted set values, all operations of 596 $(D RedisValue) are available using an $(D alias this) declaration. 597 */ 598 struct RedisZSet(T = string) { 599 RedisValue value; 600 alias value this; 601 602 this(RedisDatabase db, string key) { value = RedisValue(db, key); } 603 604 long insert(ARGS...)(ARGS args) { return m_db.zadd(m_key, args); } 605 long remove(ARGS...)(ARGS members) { return m_db.zrem(m_key, members); } 606 bool remove() { return value.remove(); } 607 long length() { return m_db.zcard(m_key); } 608 609 long count(string INT = "[]")(double min, double max) 610 if (INT == "[]") 611 { 612 return m_db.zcount(m_key, min, max); 613 } 614 615 long removeRangeByRank(long start, long end) { return m_db.zremRangeByRank(m_key, start, end); } 616 long removeRangeByScore(string INT = "[]")(double min, double max) if (INT == "[]") { return m_db.zremRangeByScore(m_key, min, max); } 617 618 double opIndexOpAssign(string op)(double value, string member) if (op == "+") { return m_db.zincrby(m_key, value, member); } 619 620 long getRank(string member) { return m_db.zrank(m_key, member); } 621 long getReverseRank(string member) { return m_db.zrevRank(m_key, member); } 622 623 long countByLex(string min, string max) { return m_db.zlexCount(m_key, min, max); } 624 625 //TODO: zinterstore 626 627 //RedisReply zrange(string key, long start, long end, bool withScores=false); 628 629 // TODO: 630 // supports only inclusive intervals 631 // see http://redis.io/commands/zrangebyscore 632 //RedisReply zrangeByScore(string key, double start, double end, bool withScores=false); 633 634 // TODO: 635 // supports only inclusive intervals 636 // see http://redis.io/commands/zrangebyscore 637 //RedisReply zrangeByScore(string key, double start, double end, long offset, long count, bool withScores=false); 638 639 //RedisReply zrevRange(string key, long start, long end, bool withScores=false); 640 641 // TODO: 642 // supports only inclusive intervals 643 // see http://redis.io/commands/zrangebyscore 644 //RedisReply zrevRangeByScore(string key, double min, double max, bool withScores=false); 645 646 auto rangeByLex(T = string)(string min = "-", string max = "+", long offset = 0, long count = -1) 647 { 648 return m_db.zrangeByLex!T(m_key, min, max, offset, count); 649 } 650 651 // TODO: 652 // supports only inclusive intervals 653 // see http://redis.io/commands/zrangebyscore 654 //RedisReply zrevRangeByScore(string key, double min, double max, long offset, long count, bool withScores=false); 655 656 //RedisReply zscore(string key, string member) { return request!RedisReply("ZSCORE", key, member); } 657 //TODO: zunionstore 658 }