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 @safe: 193 this(RedisDatabase db, string key) { m_db = db; m_key = key; } 194 195 /** The database in which the key is stored. 196 */ 197 @property inout(RedisDatabase) database() inout { return m_db; } 198 199 /** Name of the corresponding key. 200 */ 201 @property string key() const { return m_key; } 202 203 /** Remaining time-to-live. 204 205 Returns: 206 The time until the key expires, if applicable. Returns 207 $(D Duration.max) otherwise. 208 209 See_also: $(LINK2 http://redis.io/commands/pttl, PTTL) 210 */ 211 @property Duration ttl() 212 { 213 auto ret = m_db.pttl(m_key); 214 return ret >= 0 ? ret.msecs : Duration.max; 215 } 216 217 /** The data type of the referenced value. 218 219 Queries the actual type of the value that is referenced by this 220 key. 221 222 See_also: $(LINK2 http://redis.io/commands/type, TYPE) 223 */ 224 @property RedisType type() { import std.conv; return m_db.type(m_key).to!RedisType; } 225 226 /** Checks if the referenced key exists. 227 228 See_also: $(LINK2 http://redis.io/commands/exists, EXISTS) 229 */ 230 @property bool exists() { return m_db.exists(m_key); } 231 232 /** Removes the referenced key. 233 234 Returns: $(D true) $(I iff) the key was successfully removed. 235 236 See_also: $(LINK2 http://redis.io/commands/del, DEL) 237 */ 238 bool remove() { return m_db.del(m_key) > 0; } 239 240 /** Sets the key for expiration after the given timeout. 241 242 Note that Redis handles timeouts in second resolution, so that the 243 timeout must be at least one second. 244 245 Returns: $(D true) $(I iff) the expiration time was successfully set. 246 247 See_also: $(LINK2 http://redis.io/commands/expire, EXPIRE) 248 */ 249 bool expire(Duration expire_time) { assert(expire_time >= 1.seconds); return m_db.expire(m_key, expire_time.total!"seconds"); } 250 251 /** Sets the key for expiration at the given point in time. 252 253 Note that Redis handles timeouts in second resolution, so that any 254 fractional seconds of the given $(D expire_time) will be truncated. 255 256 Returns: $(D true) $(I iff) the expiration time was successfully set. 257 258 See_also: $(LINK2 http://redis.io/commands/expireat, EXPIREAT) 259 */ 260 bool expireAt(SysTime expire_time) { return m_db.expireAt(m_key, expire_time.toUnixTime()); } 261 262 /** Removes any existing expiration time for the key. 263 264 Returns: 265 $(D true) $(I iff) the key exists and an existing timeout was removed. 266 267 See_also: $(LINK2 http://redis.io/commands/persist, PERSIST) 268 */ 269 bool persist() { return m_db.persist(m_key); } 270 271 /** Moves this key to a different database. 272 273 Existing keys will not be overwritten. 274 275 Returns: 276 $(D true) $(I iff) the key exists and was successfully moved to the 277 destination database. 278 279 See_also: $(LINK2 http://redis.io/commands/move, MOVE) 280 */ 281 bool moveTo(long dst_database) { return m_db.move(m_key, dst_database); } 282 283 /** Renames the referenced key. 284 285 This method will also update this instance to refer to the renamed 286 key. 287 288 See_also: $(LINK2 http://redis.io/commands/rename, RENAME), $(D renameIfNotExist) 289 */ 290 void rename(string new_name) { m_db.rename(m_key, new_name); m_key = new_name; } 291 292 /** Renames the referenced key if the destination key doesn't exist. 293 294 This method will also update this instance to refer to the renamed 295 key if the rename was successful. 296 297 Returns: 298 $(D true) $(I iff) the source key exists and the destination key doesn't 299 exist. 300 301 See_also: $(LINK2 http://redis.io/commands/renamenx, RENAMENX), $(D rename) 302 */ 303 bool renameIfNotExist(string new_name) 304 { 305 if (m_db.renameNX(m_key, new_name)) { 306 m_key = new_name; 307 return true; 308 } 309 return false; 310 } 311 312 //TODO sort 313 } 314 315 316 /** Represents a Redis string value. 317 318 In addition to the methods specific to string values, all operations of 319 $(D RedisValue) are available using an $(D alias this) declaration. 320 */ 321 struct RedisString(T = string) { 322 RedisValue value; 323 alias value this; 324 325 this(RedisDatabase db, string key) { value = RedisValue(db, key); } 326 327 /** The length in bytes of the string. 328 329 See_also: $(LINK2 http://redis.io/commands/strlen, STRLEN) 330 */ 331 @property long length() { return m_db.strlen(m_key); } 332 333 T get() { return m_db.get!string(m_key).fromRedis!T; } 334 335 T getSet(T value) { return m_db.getSet(m_key, value.toRedis).fromRedis!T; } 336 bool getBit(long offset) { return m_db.getBit(m_key, offset); } 337 bool setBit(long offset, bool value) { return m_db.setBit(m_key, offset, value); } 338 void setExpire(T value, Duration expire_time) { assert(expire_time >= 1.seconds); m_db.setEX(m_key, expire_time.total!"seconds", value.toRedis); } 339 bool setIfNotExist(T value) { return m_db.setNX(m_key, value.toRedis); } 340 341 string getSubString(long start, long end) { return m_db.getRange!string(m_key, start, end); } 342 long setSubString(long offset, string value) { return m_db.setRange(m_key, offset, value); } 343 344 void opAssign(T value) { m_db.set(m_key, value.toRedis); } 345 346 long opOpAssign(string OP)(string value) if (OP == "~") { return m_db.append(m_key, value); } 347 long opUnary(string OP)() if (OP == "++") { return m_db.incr(m_key); } 348 long opUnary(string OP)() if (OP == "--") { return m_db.decr(m_key); } 349 long opOpAssign(string OP)(long value) if (OP == "+") { 350 assert(value != 0); 351 if (value > 0) return m_db.incr(m_key, value); 352 else return m_db.decr(m_key, -value); 353 } 354 long opOpAssign(string OP)(long value) if (OP == "-") { 355 assert(value != 0); 356 if (value > 0) return m_db.incr(m_key, value); 357 else return m_db.decr(m_key, -value); 358 } 359 long opOpAssign(string OP)(double value) if (OP == "+") { return m_db.incr(m_key, value); } 360 long opOpAssign(string OP)(double value) if (OP == "-") { return m_db.incr(m_key, -value); } 361 } 362 363 364 /** Represents a Redis hash value. 365 366 In addition to the methods specific to hash values, all operations of 367 $(D RedisValue) are available using an $(D alias this) declaration. 368 */ 369 struct RedisHash(T = string) { 370 RedisValue value; 371 alias value this; 372 373 this(RedisDatabase db, string key) { value = RedisValue(db, key); } 374 375 bool remove() { return value.remove(); } 376 size_t remove(scope string[] fields...) { return cast(size_t)m_db.hdel(m_key, fields); } 377 bool exists(string field) { return m_db.hexists(m_key, field); } 378 bool exists() { return value.exists; } 379 380 void opIndexAssign(T value, string field) { m_db.hset(m_key, field, value.toRedis()); } 381 T opIndex(string field) { return m_db.hget!string(m_key, field).fromRedis!T(); } 382 383 T get(string field, T def_value) 384 { 385 import std.typecons; 386 auto ret = m_db.hget!(Nullable!string)(m_key, field); 387 return ret.isNull ? def_value : ret.get().fromRedis!T; 388 } 389 390 bool setIfNotExist(string field, T value) 391 { 392 return m_db.hsetNX(m_key, field, value.toRedis()); 393 } 394 395 void opIndexOpAssign(string op)(T value, string field) if (op == "+") { m_db.hincr(m_key, field, value); } 396 void opIndexOpAssign(string op)(T value, string field) if (op == "-") { m_db.hincr(m_key, field, -value); } 397 398 int opApply(scope int delegate(string key, T value) @safe del) 399 { 400 auto reply = m_db.hgetAll(m_key); 401 while (reply.hasNext()) { 402 auto key = reply.next!string(); 403 auto value = reply.next!string(); 404 if (auto ret = del(key, value.fromRedis!T)) 405 return ret; 406 } 407 return 0; 408 } 409 410 411 int opApply(scope int delegate(string key) @safe del) 412 { 413 auto reply = m_db.hkeys(m_key); 414 while (reply.hasNext()) { 415 if (auto ret = del(reply.next!string())) 416 return ret; 417 } 418 return 0; 419 } 420 421 long length() { return m_db.hlen(m_key); } 422 423 // FIXME: support other types! 424 void getMultiple(T[] dst, scope string[] fields...) 425 { 426 assert(dst.length == fields.length); 427 auto reply = m_db.hmget(m_key, fields); 428 size_t idx = 0; 429 while (reply.hasNext()) 430 dst[idx++] = reply.next!string().fromRedis!T(); 431 } 432 433 // FIXME: support other types! 434 /*void setMultiple(in string[] src, scope string[] fields...) 435 { 436 m_db.hmset(m_key, ...); 437 }*/ 438 439 //RedisReply hvals(string key) { return request!RedisReply("HVALS", key); } 440 } 441 442 443 /** Represents a Redis list value. 444 445 In addition to the methods specific to list values, all operations of 446 $(D RedisValue) are available using an $(D alias this) declaration. 447 */ 448 struct RedisList(T = string) { 449 RedisValue value; 450 alias value this; 451 452 this(RedisDatabase db, string key) { value = RedisValue(db, key); } 453 454 Dollar opDollar() { return Dollar(0); } 455 456 T opIndex(long index) 457 { 458 assert(index >= 0); 459 return m_db.lindex!string(m_key, index).fromRedis!T; 460 } 461 T opIndex(Dollar index) 462 { 463 assert(index.offset < 0); 464 return m_db.lindex!string(m_key, index.offset).fromRedis!T; 465 } 466 void opIndexAssign(T value, long index) 467 { 468 assert(index >= 0); 469 m_db.lset(m_key, index, value.toRedis); 470 } 471 void opIndexAssign(T value, Dollar index) 472 { 473 assert(index.offset < 0); 474 m_db.lset(m_key, index.offset, value.toRedis); 475 } 476 auto opSlice(S, E)(S start, E end) 477 if ((is(S : long) || is(S == Dollar)) && (is(E : long) || is(E == Dollar))) 478 { 479 import std.algorithm; 480 long s, e; 481 static if (is(S == Dollar)) { 482 assert(start.offset <= 0); 483 s = start.offset; 484 } else { 485 assert(start >= 0); 486 s = start; 487 } 488 static if (is(E == Dollar)) { 489 assert(end.offset <= 0); 490 e = end.offset - 1; 491 } else { 492 assert(end >= 0); 493 e = end - 1; 494 } 495 return map!(e => e.fromRedis!T)(m_db.lrange(m_key, s, e)); 496 } 497 auto opSlice()() { return this[0 .. $]; } 498 499 long length() { return m_db.llen(m_key); } 500 501 long insertBefore(T pivot, T value) { return m_db.linsertBefore(m_key, pivot.toRedis, value.toRedis); } 502 long insertAfter(T pivot, T value) { return m_db.linsertAfter(m_key, pivot.toRedis, value.toRedis); } 503 504 long insertFront(T value) { return m_db.lpush(m_key, value.toRedis); } 505 long insertFrontIfExists(T value) { return m_db.lpushX(m_key, value.toRedis); } 506 long insertBack(T value) { return m_db.rpush(m_key, value.toRedis); } 507 long insertBackIfExists(T value) { return m_db.rpushX(m_key, value.toRedis); } 508 509 long removeAll(T value) { return m_db.lrem(m_key, 0, value.toRedis); } 510 long removeFirst(T value, long count = 1) { assert(count > 0); return m_db.lrem(m_key, count, value.toRedis); } 511 long removeLast(T value, long count = 1) { assert(count > 0); return m_db.lrem(m_key, -count, value.toRedis); } 512 513 void trim(long start, long end) { m_db.ltrim(m_key, start, end); } 514 515 T removeFront() { return m_db.lpop!string(m_key).fromRedis!T; } 516 T removeBack() { return m_db.rpop!string(m_key).fromRedis!T; } 517 Nullable!T removeFrontBlock(Duration max_wait = 0.seconds) { 518 assert(max_wait == 0.seconds || max_wait >= 1.seconds); 519 auto r = m_db.blpop!string(m_key, max_wait.total!"seconds"); 520 return r.apply!(r => r[1].fromRedis!T); 521 } 522 523 struct Dollar { 524 long offset = 0; 525 Dollar opAdd(long off) { return Dollar(offset + off); } 526 Dollar opSub(long off) { return Dollar(offset - off); } 527 } 528 529 int opApply(scope int delegate(T) @safe del) 530 { 531 foreach (v; this[0 .. $]) 532 if (auto ret = del(v)) 533 return ret; 534 return 0; 535 } 536 537 //RedisReply lrange(string key, long start, long stop) { return request!RedisReply("LRANGE", key, start, stop); } 538 //T rpoplpush(T : E[], E)(string key, string destination) { return request!T("RPOPLPUSH", key, destination); } 539 } 540 541 542 /** Represents a Redis set value. 543 544 In addition to the methods specific to set values, all operations of 545 $(D RedisValue) are available using an $(D alias this) declaration. 546 */ 547 struct RedisSet(T = string) { 548 RedisValue value; 549 alias value this; 550 551 this(RedisDatabase db, string key) { value = RedisValue(db, key); } 552 553 long insert(ARGS...)(ARGS args) { return m_db.sadd(m_key, args); } 554 long remove(T value) { return m_db.srem(m_key, value.toRedis()); } 555 bool remove() { return value.remove(); } 556 string pop() { return m_db.spop!string(m_key); } 557 long length() { return m_db.scard(m_key); } 558 559 string getRandom() { return m_db.srandMember!string(m_key); } 560 561 //RedisReply sdiff(string[] keys...) { return request!RedisReply("SDIFF", keys); } 562 //long sdiffStore(string destination, string[] keys...) { return request!long("SDIFFSTORE", destination, keys); } 563 //RedisReply sinter(string[] keys) { return request!RedisReply("SINTER", keys); } 564 //long sinterStore(string destination, string[] keys...) { return request!long("SINTERSTORE", destination, keys); } 565 bool contains(T value) { return m_db.sisMember(m_key, value.toRedis()); } 566 567 int opApply(scope int delegate(T value) @safe del) 568 { 569 foreach (m; m_db.smembers!string(m_key)) 570 if (auto ret = del(m.fromRedis!T())) 571 return ret; 572 return 0; 573 } 574 575 bool intersects(scope RedisSet[] sets...) 576 { 577 import std.algorithm; 578 import std.array; 579 return !value.database.sinter(value.key ~ sets.map!(s => s.key).array).empty; 580 } 581 582 auto getAll() 583 { 584 import std.algorithm; 585 return map!(r => r.fromRedis!T)(value.database.smembers(value.key)); 586 } 587 588 //bool smove(T : E[], E)(string source, string destination, T member) { return request!bool("SMOVE", source, destination, member); } 589 //RedisReply sunion(string[] keys...) { return request!RedisReply("SUNION", keys); } 590 //long sunionStore(string[] keys...) { return request!long("SUNIONSTORE", keys); } 591 } 592 593 594 /** Represents a Redis sorted set value. 595 596 In addition to the methods specific to sorted set values, all operations of 597 $(D RedisValue) are available using an $(D alias this) declaration. 598 */ 599 struct RedisZSet(T = string) { 600 RedisValue value; 601 alias value this; 602 603 this(RedisDatabase db, string key) { value = RedisValue(db, key); } 604 605 long insert(ARGS...)(ARGS args) { return m_db.zadd(m_key, args); } 606 long remove(ARGS...)(ARGS members) { return m_db.zrem(m_key, members); } 607 bool remove() { return value.remove(); } 608 long length() { return m_db.zcard(m_key); } 609 610 long count(string INT = "[]")(double min, double max) 611 if (INT == "[]") 612 { 613 return m_db.zcount(m_key, min, max); 614 } 615 616 long removeRangeByRank(long start, long end) { return m_db.zremRangeByRank(m_key, start, end); } 617 long removeRangeByScore(string INT = "[]")(double min, double max) if (INT == "[]") { return m_db.zremRangeByScore(m_key, min, max); } 618 619 double opIndexOpAssign(string op)(double value, string member) if (op == "+") { return m_db.zincrby(m_key, value, member); } 620 621 long getRank(string member) { return m_db.zrank(m_key, member); } 622 long getReverseRank(string member) { return m_db.zrevRank(m_key, member); } 623 624 long countByLex(string min, string max) { return m_db.zlexCount(m_key, min, max); } 625 626 //TODO: zinterstore 627 628 //RedisReply zrange(string key, long start, long end, bool withScores=false); 629 630 // TODO: 631 // supports only inclusive intervals 632 // see http://redis.io/commands/zrangebyscore 633 //RedisReply zrangeByScore(string key, double start, double end, bool withScores=false); 634 635 // TODO: 636 // supports only inclusive intervals 637 // see http://redis.io/commands/zrangebyscore 638 //RedisReply zrangeByScore(string key, double start, double end, long offset, long count, bool withScores=false); 639 640 //RedisReply zrevRange(string key, long start, long end, bool withScores=false); 641 642 // TODO: 643 // supports only inclusive intervals 644 // see http://redis.io/commands/zrangebyscore 645 //RedisReply zrevRangeByScore(string key, double min, double max, bool withScores=false); 646 647 auto rangeByLex(T = string)(string min = "-", string max = "+", long offset = 0, long count = -1) 648 { 649 return m_db.zrangeByLex!T(m_key, min, max, offset, count); 650 } 651 652 // TODO: 653 // supports only inclusive intervals 654 // see http://redis.io/commands/zrangebyscore 655 //RedisReply zrevRangeByScore(string key, double min, double max, long offset, long count, bool withScores=false); 656 657 //RedisReply zscore(string key, string member) { return request!RedisReply("ZSCORE", key, member); } 658 //TODO: zunionstore 659 }