1 /** 2 Convenience wrappers types for accessing Redis keys. 3 4 Note that the API is still subject to change! 5 6 Copyright: © 2014 RejectedSoftware e.K. 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 : 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 size_t remove(scope string[] fields...) { return cast(size_t)m_db.hdel(m_key, fields); } 375 bool exists(string field) { return m_db.hexists(m_key, field); } 376 377 void opIndexAssign(T value, string field) { m_db.hset(m_key, field, value.toRedis()); } 378 T opIndex(string field) { return m_db.hget!string(m_key, field).fromRedis!T(); } 379 380 T get(string field, T def_value) 381 { 382 import std.typecons; 383 auto ret = m_db.hget!(Nullable!string)(m_key, field); 384 return ret.isNull ? def_value : ret.fromRedis!T; 385 } 386 387 bool setIfNotExist(string field, T value) 388 { 389 return m_db.hsetNX(m_key, field, value.toRedis()); 390 } 391 392 void opIndexOpAssign(string op)(T value, string field) if (op == "+") { m_db.hincr(m_key, field, value.toRedis()); } 393 394 int opApply(scope int delegate(string key, T value) del) 395 { 396 auto reply = m_db.hgetAll(m_key); 397 while (reply.hasNext()) { 398 auto key = reply.next!string(); 399 auto value = reply.next!string(); 400 if (auto ret = del(key, value.fromRedis!T)) 401 return ret; 402 } 403 return 0; 404 } 405 406 407 int opApply(scope int delegate(string key) del) 408 { 409 auto reply = m_db.hkeys(m_key); 410 while (reply.hasNext()) { 411 if (auto ret = del(reply.next!string())) 412 return ret; 413 } 414 return 0; 415 } 416 417 long length() { return m_db.hlen(m_key); } 418 419 // FIXME: support other types! 420 void getMultiple(T[] dst, scope string[] fields...) 421 { 422 assert(dst.length == fields.length); 423 auto reply = m_db.hmget(m_key, fields); 424 size_t idx = 0; 425 while (reply.hasNext()) 426 dst[idx++] = reply.next!string().fromRedis!T(); 427 } 428 429 // FIXME: support other types! 430 /*void setMultiple(in string[] src, scope string[] fields...) 431 { 432 m_db.hmset(m_key, ...); 433 }*/ 434 435 //RedisReply hvals(string key) { return request!RedisReply("HVALS", key); } 436 } 437 438 439 /** Represents a Redis list value. 440 441 In addition to the methods specific to list values, all operations of 442 $(D RedisValue) are available using an $(D alias this) declaration. 443 */ 444 struct RedisList(T = string) { 445 RedisValue value; 446 alias value this; 447 448 this(RedisDatabase db, string key) { value = RedisValue(db, key); } 449 450 Dollar opDollar() { return Dollar(0); } 451 452 T opIndex(long index) 453 { 454 assert(index >= 0); 455 return m_db.lindex!string(m_key, index).fromRedis!T; 456 } 457 T opIndex(Dollar index) 458 { 459 assert(index.offset < 0); 460 return m_db.lindex!string(m_key, index.offset).fromRedis!T; 461 } 462 void opIndexAssign(T value, long index) 463 { 464 assert(index >= 0); 465 m_db.lset(m_key, index, value.toRedis); 466 } 467 void opIndexAssign(T value, Dollar index) 468 { 469 assert(index.offset < 0); 470 m_db.lset(m_key, index.offset, value.toRedis); 471 } 472 auto opSlice(S, E)(S start, E end) 473 if ((is(S : long) || is(S == Dollar)) && (is(E : long) || is(E == Dollar))) 474 { 475 import std.algorithm; 476 long s, e; 477 static if (is(S == Dollar)) { 478 assert(start.offset <= 0); 479 s = start.offset; 480 } else { 481 assert(start >= 0); 482 s = start; 483 } 484 static if (is(E == Dollar)) { 485 assert(end.offset <= 0); 486 e = end.offset - 1; 487 } else { 488 assert(end >= 0); 489 e = end - 1; 490 } 491 return map!(e => e.fromRedis!T)(m_db.lrange(m_key, s, e)); 492 } 493 auto opSlice()() { return this[0 .. $]; } 494 495 long length() { return m_db.llen(m_key); } 496 497 long insertBefore(T pivot, T value) { return m_db.linsertBefore(m_key, pivot.toRedis, value.toRedis); } 498 long insertAfter(T pivot, T value) { return m_db.linsertAfter(m_key, pivot.toRedis, value.toRedis); } 499 500 long insertFront(T value) { return m_db.lpush(m_key, value.toRedis); } 501 long insertFrontIfExists(T value) { return m_db.lpushX(m_key, value.toRedis); } 502 long insertBack(T value) { return m_db.rpush(m_key, value.toRedis); } 503 long insertBackIfExists(T value) { return m_db.rpushX(m_key, value.toRedis); } 504 505 long removeAll(T value) { return m_db.lrem(m_key, 0, value.toRedis); } 506 long removeFirst(T value, long count = 1) { assert(count > 0); return m_db.lrem(m_key, count, value.toRedis); } 507 long removeLast(T value, long count = 1) { assert(count > 0); return m_db.lrem(m_key, -count, value.toRedis); } 508 509 void trim(long start, long end) { m_db.ltrim(m_key, start, end); } 510 511 T removeFront() { return m_db.lpop!string(m_key).fromRedis!T; } 512 T removeBack() { return m_db.rpop!string(m_key).fromRedis!T; } 513 Nullable!T removeFrontBlock(Duration max_wait = 0.seconds) { 514 assert(max_wait == 0.seconds || max_wait >= 1.seconds); 515 auto r = m_db.blpop!string(m_key, max_wait.total!"seconds"); 516 return r.isNull ? Nullable!T.init : Nullable!T(r[1].fromRedis!T); 517 } 518 519 struct Dollar { 520 long offset = 0; 521 Dollar opAdd(long off) { return Dollar(offset + off); } 522 Dollar opSub(long off) { return Dollar(offset - off); } 523 } 524 525 int opApply(scope int delegate(T) del) 526 { 527 foreach (v; this[0 .. $]) 528 if (auto ret = del(v)) 529 return ret; 530 return 0; 531 } 532 533 //RedisReply lrange(string key, long start, long stop) { return request!RedisReply("LRANGE", key, start, stop); } 534 //T rpoplpush(T : E[], E)(string key, string destination) { return request!T("RPOPLPUSH", key, destination); } 535 } 536 537 538 /** Represents a Redis set value. 539 540 In addition to the methods specific to set values, all operations of 541 $(D RedisValue) are available using an $(D alias this) declaration. 542 */ 543 struct RedisSet(T = string) { 544 RedisValue value; 545 alias value this; 546 547 this(RedisDatabase db, string key) { value = RedisValue(db, key); } 548 549 long insert(ARGS...)(ARGS args) { return m_db.sadd(m_key, args); } 550 long remove(T value) { return m_db.srem(m_key, value.toRedis()); } 551 void remove()() { value.remove(); } 552 string pop() { return m_db.spop!string(m_key); } 553 long length() { return m_db.scard(m_key); } 554 555 string getRandom() { return m_db.srandMember!string(m_key); } 556 557 //RedisReply sdiff(string[] keys...) { return request!RedisReply("SDIFF", keys); } 558 //long sdiffStore(string destination, string[] keys...) { return request!long("SDIFFSTORE", destination, keys); } 559 //RedisReply sinter(string[] keys) { return request!RedisReply("SINTER", keys); } 560 //long sinterStore(string destination, string[] keys...) { return request!long("SINTERSTORE", destination, keys); } 561 bool contains(T value) { return m_db.sisMember(m_key, value.toRedis()); } 562 563 int opApply(scope int delegate(T value) del) 564 { 565 foreach (m; m_db.smembers!string(m_key)) 566 if (auto ret = del(m.fromRedis!T())) 567 return ret; 568 return 0; 569 } 570 571 bool intersects(scope RedisSet[] sets...) 572 { 573 import std.algorithm; 574 import std.array; 575 return !value.database.sinter(value.key ~ sets.map!(s => s.key).array).empty; 576 } 577 578 auto getAll() 579 { 580 import std.algorithm; 581 return map!(r => r.fromRedis!T)(value.database.smembers(value.key)); 582 } 583 584 //bool smove(T : E[], E)(string source, string destination, T member) { return request!bool("SMOVE", source, destination, member); } 585 //RedisReply sunion(string[] keys...) { return request!RedisReply("SUNION", keys); } 586 //long sunionStore(string[] keys...) { return request!long("SUNIONSTORE", keys); } 587 } 588 589 590 /** Represents a Redis sorted set value. 591 592 In addition to the methods specific to sorted set values, all operations of 593 $(D RedisValue) are available using an $(D alias this) declaration. 594 */ 595 struct RedisZSet(T = string) { 596 RedisValue value; 597 alias value this; 598 599 this(RedisDatabase db, string key) { value = RedisValue(db, key); } 600 601 long insert(ARGS...)(ARGS args) { return m_db.zadd(m_key, args); } 602 long remove(ARGS...)(ARGS members) { return m_db.zrem(m_key, members); } 603 long length() { return m_db.zcard(m_key); } 604 605 long count(string INT = "[]")(double min, double max) 606 if (INT == "[]") 607 { 608 return m_db.zcount(m_key, min, max); 609 } 610 611 long removeRangeByRank(long start, long end) { return m_db.zremRangeByRank(m_key, start, end); } 612 long removeRangeByScore(string INT = "[]")(double min, double max) if (INT == "[]") { return m_db.zremRangeByScore(m_key, min, max); } 613 614 double opIndexOpAssign(string op)(double value, string member) if (op == "+") { return m_db.zincrby(m_key, value, member); } 615 616 long getRank(string member) { return m_db.zrank(m_key, member); } 617 long getReverseRank(string member) { return m_db.zrevRank(m_key, member); } 618 619 //TODO: zinterstore 620 621 //RedisReply zrange(string key, long start, long end, bool withScores=false); 622 623 // TODO: 624 // supports only inclusive intervals 625 // see http://redis.io/commands/zrangebyscore 626 //RedisReply zrangeByScore(string key, double start, double end, bool withScores=false); 627 628 // TODO: 629 // supports only inclusive intervals 630 // see http://redis.io/commands/zrangebyscore 631 //RedisReply zrangeByScore(string key, double start, double end, long offset, long count, bool withScores=false); 632 633 //RedisReply zrevRange(string key, long start, long end, bool withScores=false); 634 635 // TODO: 636 // supports only inclusive intervals 637 // see http://redis.io/commands/zrangebyscore 638 //RedisReply zrevRangeByScore(string key, double min, double max, bool withScores=false); 639 640 // TODO: 641 // supports only inclusive intervals 642 // see http://redis.io/commands/zrangebyscore 643 //RedisReply zrevRangeByScore(string key, double min, double max, long offset, long count, bool withScores=false); 644 645 //RedisReply zscore(string key, string member) { return request!RedisReply("ZSCORE", key, member); } 646 //TODO: zunionstore 647 }