diff --git a/.claude/skills/tce-examples/assets/hiredis/HIREDIS_TEST_PATTERNS.md b/.claude/skills/tce-examples/assets/hiredis/HIREDIS_TEST_PATTERNS.md index d0ed1fac95..edbfa2933b 100644 --- a/.claude/skills/tce-examples/assets/hiredis/HIREDIS_TEST_PATTERNS.md +++ b/.claude/skills/tce-examples/assets/hiredis/HIREDIS_TEST_PATTERNS.md @@ -84,6 +84,44 @@ int main(int argc, char **argv) { ## Key Patterns +### 0. CHECK_REPLY — mandatory, and the reason C examples are testable at all + +**A C example has no assertion framework, so the harness can only see the process exit +code.** hiredis returns an *error reply* (a valid `redisReply` with +`type == REDIS_REPLY_ERROR`) for a malformed command rather than failing the call, and it +does not abort. Without a guard, an example can send a broken command, print a wrong value +and still exit 0 — a green run that proves nothing. That happened for real in DOC-6968: +`redisCommand(c, "SET mykey \"Hello World\"")` split on whitespace (hiredis does **not** +honour CLI-style quoting — bind a value containing spaces with `%s`), the server answered +`ERR syntax error`, and the sweep still reported PASS. + +So every C example defines this macro in a `REMOVE` block after the includes, and calls it +after **every** `redisCommand`, also inside a `REMOVE` block, so the published snippet stays +plain `redisCommand`: + +```c +// REMOVE_START +#define CHECK_REPLY(r) do { \ + if ((r) == NULL || (r)->type == REDIS_REPLY_ERROR) { \ + printf("REDIS ERROR: %s\n", (r) ? (r)->str : "no reply from server"); \ + return 1; \ + } \ +} while (0) +// REMOVE_END + +reply = redisCommand(c, "SET mykey %s", "Hello World"); +// REMOVE_START +CHECK_REPLY(reply); +// REMOVE_END +``` + +Note why a cheaper check is not enough: scanning the program's output for `ERR` misses the +common case, because `printf("%lld", reply->integer)` on an error reply prints **`0`**, not +the error text. The guard has to sit at the call site. + +Value assertions in these files must also `return 1`, not merely `printf("ASSERTION +FAILED")` — otherwise they cannot fail the harness either. + ### 1. Includes ```c // STEP_START includes diff --git a/local_examples/client-specific/c/landing.c b/local_examples/client-specific/c/landing.c index b4b2ef8859..d79d5e1321 100644 --- a/local_examples/client-specific/c/landing.c +++ b/local_examples/client-specific/c/landing.c @@ -9,6 +9,19 @@ #include +// REMOVE_START +// Fail loudly on a NULL or error reply. hiredis returns an error REPLY (not a +// connection error) for things like a bad command, and the examples would +// otherwise print a wrong value and still exit 0 — a green harness run that +// proves nothing. Kept in a REMOVE block so the published example stays plain. +#define CHECK_REPLY(r) do { \ + if ((r) == NULL || (r)->type == REDIS_REPLY_ERROR) { \ + printf("REDIS ERROR: %s\n", (r) ? (r)->str : "no reply from server"); \ + return 1; \ + } \ +} while (0) +// REMOVE_END + int main() { // The `redisContext` type represents the connection // to the Redis server. Here, we connect to the @@ -30,11 +43,17 @@ int main() { // Set a string key. redisReply *reply = redisCommand(c, "SET foo bar"); + // REMOVE_START + CHECK_REPLY(reply); + // REMOVE_END printf("Reply: %s\n", reply->str); freeReplyObject(reply); // Get the key we have just stored. reply = redisCommand(c, "GET foo"); + // REMOVE_START + CHECK_REPLY(reply); + // REMOVE_END printf("Reply: %s\n", reply->str); freeReplyObject(reply); diff --git a/local_examples/cmds_generic/go-redis/cmds_generic_test.go b/local_examples/cmds_generic/go-redis/cmds_generic_test.go index 0c4c77e494..7e3c0ab6cb 100644 --- a/local_examples/cmds_generic/go-redis/cmds_generic_test.go +++ b/local_examples/cmds_generic/go-redis/cmds_generic_test.go @@ -320,3 +320,211 @@ func ExampleClient_ttl_cmd() { // OK // 10 } + +func ExampleClient_scan1_cmd() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + }) + + // REMOVE_START + rdb.FlushDB(ctx) + // REMOVE_END + + // STEP_START scan1 + scan1Result1, err := rdb.SAdd(ctx, "myset", "1", "2", "3", "foo", "foobar", "feelsgood").Result() + + if err != nil { + panic(err) + } + + fmt.Println(scan1Result1) // >>> 6 + + scan1Result2, _, err := rdb.SScan(ctx, "myset", 0, "f*", 0).Result() + + if err != nil { + panic(err) + } + + sort.Strings(scan1Result2) + fmt.Println(scan1Result2) // >>> [feelsgood foo foobar] + // STEP_END + + // Output: + // 6 + // [feelsgood foo foobar] +} + +func ExampleClient_scan2_cmd() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + }) + + // REMOVE_START + rdb.FlushDB(ctx) + + for i := 1; i <= 1000; i++ { + rdb.Set(ctx, fmt.Sprintf("key:%d", i), i, 0) + } + // REMOVE_END + + // STEP_START scan2 + // MATCH is applied after elements are fetched, so with the default COUNT most + // iterations return few keys or none at all. + var scan2Cursor uint64 + var scan2Keys []string + var err error + + for i := 0; i < 4; i++ { + scan2Keys, scan2Cursor, err = rdb.Scan(ctx, scan2Cursor, "*11*", 0).Result() + + if err != nil { + panic(err) + } + } + + // A larger COUNT forces more scanning in a single iteration, so the remaining + // matches arrive together. This continues from the cursor reached above. + scan2Keys, _, err = rdb.Scan(ctx, scan2Cursor, "*11*", 1000).Result() + + if err != nil { + panic(err) + } + + fmt.Println(len(scan2Keys)) // >>> 18 + // STEP_END + + // REMOVE_START + rdb.FlushDB(ctx) + // REMOVE_END + + // Output: + // 18 +} + +func ExampleClient_scan3_cmd() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + }) + + // REMOVE_START + rdb.FlushDB(ctx) + // REMOVE_END + + // STEP_START scan3 + scan3Result1, err := rdb.GeoAdd(ctx, "geokey", &redis.GeoLocation{ + Longitude: 0, Latitude: 0, Name: "value", + }).Result() + + if err != nil { + panic(err) + } + + fmt.Println(scan3Result1) // >>> 1 + + scan3Result2, err := rdb.ZAdd(ctx, "zkey", redis.Z{Score: 1000, Member: "value"}).Result() + + if err != nil { + panic(err) + } + + fmt.Println(scan3Result2) // >>> 1 + + scan3Result3, err := rdb.Type(ctx, "geokey").Result() + + if err != nil { + panic(err) + } + + fmt.Println(scan3Result3) // >>> zset + + scan3Result4, err := rdb.Type(ctx, "zkey").Result() + + if err != nil { + panic(err) + } + + fmt.Println(scan3Result4) // >>> zset + + scan3Result5, _, err := rdb.ScanType(ctx, 0, "", 0, "zset").Result() + + if err != nil { + panic(err) + } + + sort.Strings(scan3Result5) + fmt.Println(scan3Result5) // >>> [geokey zkey] + // STEP_END + + // Output: + // 1 + // 1 + // zset + // zset + // [geokey zkey] +} + +func ExampleClient_scan4_cmd() { + ctx := context.Background() + + rdb := redis.NewClient(&redis.Options{ + Addr: "localhost:6379", + Password: "", // no password docs + DB: 0, // use default DB + }) + + // REMOVE_START + rdb.FlushDB(ctx) + // REMOVE_END + + // STEP_START scan4 + scan4Result1, err := rdb.HSet(ctx, "myhash", "a", 1, "b", 2).Result() + + if err != nil { + panic(err) + } + + fmt.Println(scan4Result1) // >>> 2 + + scan4Result2, _, err := rdb.HScan(ctx, "myhash", 0, "", 0).Result() + + if err != nil { + panic(err) + } + + // HSCAN returns field and value interleaved. Redis does not promise an order, so + // collect the pairs into a map: fmt prints map keys sorted, whatever order they arrived in. + scan4Fields := map[string]string{} + + for i := 0; i < len(scan4Result2); i += 2 { + scan4Fields[scan4Result2[i]] = scan4Result2[i+1] + } + + fmt.Println(scan4Fields) // >>> map[a:1 b:2] + + scan4Result3, _, err := rdb.HScanNoValues(ctx, "myhash", 0, "", 0).Result() + + if err != nil { + panic(err) + } + + sort.Strings(scan4Result3) + fmt.Println(scan4Result3) // >>> [a b] + // STEP_END + + // Output: + // 2 + // map[a:1 b:2] + // [a b] +} diff --git a/local_examples/cmds_generic/hiredis/cmds_generic.c b/local_examples/cmds_generic/hiredis/cmds_generic.c index 377113525a..2674bcfa72 100644 --- a/local_examples/cmds_generic/hiredis/cmds_generic.c +++ b/local_examples/cmds_generic/hiredis/cmds_generic.c @@ -5,6 +5,19 @@ #include #include #include + +// REMOVE_START +// Fail loudly on a NULL or error reply. hiredis returns an error REPLY (not a +// connection error) for things like a bad command, and the examples would +// otherwise print a wrong value and still exit 0 — a green harness run that +// proves nothing. Kept in a REMOVE block so the published example stays plain. +#define CHECK_REPLY(r) do { \ + if ((r) == NULL || (r)->type == REDIS_REPLY_ERROR) { \ + printf("REDIS ERROR: %s\n", (r) ? (r)->str : "no reply from server"); \ + return 1; \ + } \ +} while (0) +// REMOVE_END // STEP_END int main(int argc, char **argv) { @@ -24,6 +37,7 @@ int main(int argc, char **argv) { // REMOVE_START redisReply *del_reply = redisCommand(c, "DEL firstname lastname age"); + CHECK_REPLY(del_reply); freeReplyObject(del_reply); // REMOVE_END @@ -33,12 +47,18 @@ int main(int argc, char **argv) { // Set up keys reply = redisCommand(c, "MSET %s %s %s %s %s %s", "firstname", "Jack", "lastname", "Stuntman", "age", "35"); + // REMOVE_START + CHECK_REPLY(reply); + // REMOVE_END printf("MSET firstname Jack lastname Stuntman age 35: %s\n", reply->str); // >>> OK freeReplyObject(reply); // Keys matching *name* reply = redisCommand(c, "KEYS %s", "*name*"); + // REMOVE_START + CHECK_REPLY(reply); + // REMOVE_END printf("KEYS *name*:\n"); for (size_t i = 0; i < reply->elements; i++) { printf(" %s\n", reply->element[i]->str); @@ -48,12 +68,16 @@ int main(int argc, char **argv) { // REMOVE_START if (reply->elements != 2) { printf("ASSERTION FAILED: Expected 2 elements, got %zu\n", reply->elements); + return 1; } // REMOVE_END freeReplyObject(reply); // Keys matching a?? reply = redisCommand(c, "KEYS %s", "a??"); + // REMOVE_START + CHECK_REPLY(reply); + // REMOVE_END printf("KEYS a??:\n"); for (size_t i = 0; i < reply->elements; i++) { printf(" %s\n", reply->element[i]->str); @@ -62,15 +86,20 @@ int main(int argc, char **argv) { // REMOVE_START if (reply->elements != 1) { printf("ASSERTION FAILED: Expected 1 element, got %zu\n", reply->elements); + return 1; } if (strcmp(reply->element[0]->str, "age") != 0) { printf("ASSERTION FAILED: Expected 'age', got '%s'\n", reply->element[0]->str); + return 1; } // REMOVE_END freeReplyObject(reply); // All keys reply = redisCommand(c, "KEYS %s", "*"); + // REMOVE_START + CHECK_REPLY(reply); + // REMOVE_END printf("KEYS *:\n"); for (size_t i = 0; i < reply->elements; i++) { printf(" %s\n", reply->element[i]->str); @@ -81,6 +110,7 @@ int main(int argc, char **argv) { // REMOVE_START if (reply->elements != 3) { printf("ASSERTION FAILED: Expected 3 elements, got %zu\n", reply->elements); + return 1; } // REMOVE_END freeReplyObject(reply); @@ -88,6 +118,401 @@ int main(int argc, char **argv) { // REMOVE_START reply = redisCommand(c, "DEL firstname lastname age"); + CHECK_REPLY(reply); + freeReplyObject(reply); + // REMOVE_END + + // STEP_START scan1 + reply = redisCommand(c, "SADD myset 1 2 3 foo foobar feelsgood"); + // REMOVE_START + CHECK_REPLY(reply); + // REMOVE_END + printf("%lld\n", reply->integer); + // >>> 6 + freeReplyObject(reply); + + // SCAN-family replies are a two-element array: the next cursor, then the results. + reply = redisCommand(c, "SSCAN myset 0 MATCH f*"); + // REMOVE_START + CHECK_REPLY(reply); + // REMOVE_END + printf("%zu\n", reply->element[1]->elements); + // >>> 3 + // REMOVE_START + if (reply->element[1]->elements != 3) { + printf("ASSERTION FAILED: Expected 3 members, got %zu\n", reply->element[1]->elements); + return 1; + } + // REMOVE_END + freeReplyObject(reply); + // STEP_END + + // REMOVE_START + reply = redisCommand(c, "DEL myset"); + CHECK_REPLY(reply); + freeReplyObject(reply); + + for (int i = 1; i <= 1000; i++) { + reply = redisCommand(c, "SET key:%d %d", i, i); + CHECK_REPLY(reply); + freeReplyObject(reply); + } + // REMOVE_END + + // STEP_START scan2 + // MATCH is applied after elements are fetched, so with the default COUNT most + // iterations return few keys or none at all. + char cursor[64] = "0"; + + for (int i = 0; i < 4; i++) { + reply = redisCommand(c, "SCAN %s MATCH *11*", cursor); + // REMOVE_START + CHECK_REPLY(reply); + // REMOVE_END + snprintf(cursor, sizeof(cursor), "%s", reply->element[0]->str); + printf("%zu\n", reply->element[1]->elements); + freeReplyObject(reply); + } + + // A larger COUNT forces more scanning in a single iteration, so the remaining + // matches arrive together. This continues from the cursor reached above. + reply = redisCommand(c, "SCAN %s MATCH *11* COUNT 1000", cursor); + // REMOVE_START + CHECK_REPLY(reply); + // REMOVE_END + printf("%zu\n", reply->element[1]->elements); + // >>> 18 + // REMOVE_START + if (reply->element[1]->elements != 18) { + printf("ASSERTION FAILED: Expected 18 keys, got %zu\n", reply->element[1]->elements); + return 1; + } + // REMOVE_END + freeReplyObject(reply); + // STEP_END + + // REMOVE_START + reply = redisCommand(c, "FLUSHDB"); + CHECK_REPLY(reply); + freeReplyObject(reply); + // REMOVE_END + + // STEP_START scan3 + reply = redisCommand(c, "GEOADD geokey 0 0 value"); + // REMOVE_START + CHECK_REPLY(reply); + // REMOVE_END + printf("%lld\n", reply->integer); + // >>> 1 + freeReplyObject(reply); + + reply = redisCommand(c, "ZADD zkey 1000 value"); + // REMOVE_START + CHECK_REPLY(reply); + // REMOVE_END + printf("%lld\n", reply->integer); + // >>> 1 + freeReplyObject(reply); + + reply = redisCommand(c, "TYPE geokey"); + // REMOVE_START + CHECK_REPLY(reply); + // REMOVE_END + printf("%s\n", reply->str); + // >>> zset + freeReplyObject(reply); + + reply = redisCommand(c, "TYPE zkey"); + // REMOVE_START + CHECK_REPLY(reply); + // REMOVE_END + printf("%s\n", reply->str); + // >>> zset + freeReplyObject(reply); + + reply = redisCommand(c, "SCAN 0 TYPE zset"); + // REMOVE_START + CHECK_REPLY(reply); + // REMOVE_END + printf("%zu\n", reply->element[1]->elements); + // >>> 2 + // REMOVE_START + if (reply->element[1]->elements != 2) { + printf("ASSERTION FAILED: Expected 2 keys, got %zu\n", reply->element[1]->elements); + return 1; + } + // REMOVE_END + freeReplyObject(reply); + // STEP_END + + // REMOVE_START + reply = redisCommand(c, "DEL geokey zkey"); + CHECK_REPLY(reply); + freeReplyObject(reply); + // REMOVE_END + + // STEP_START scan4 + reply = redisCommand(c, "HSET myhash a 1 b 2"); + // REMOVE_START + CHECK_REPLY(reply); + // REMOVE_END + printf("%lld\n", reply->integer); + // >>> 2 + freeReplyObject(reply); + + // Without NOVALUES the results alternate field, value, field, value. + reply = redisCommand(c, "HSCAN myhash 0"); + // REMOVE_START + CHECK_REPLY(reply); + // REMOVE_END + for (size_t i = 0; i < reply->element[1]->elements; i += 2) { + printf("%s=%s\n", reply->element[1]->element[i]->str, + reply->element[1]->element[i + 1]->str); + } + // >>> a=1 + // >>> b=2 + // REMOVE_START + if (reply->element[1]->elements != 4) { + printf("ASSERTION FAILED: Expected 4 entries, got %zu\n", reply->element[1]->elements); + return 1; + } + // REMOVE_END + freeReplyObject(reply); + + reply = redisCommand(c, "HSCAN myhash 0 NOVALUES"); + // REMOVE_START + CHECK_REPLY(reply); + // REMOVE_END + for (size_t i = 0; i < reply->element[1]->elements; i++) { + printf("%s\n", reply->element[1]->element[i]->str); + } + // >>> a + // >>> b + // REMOVE_START + if (reply->element[1]->elements != 2) { + printf("ASSERTION FAILED: Expected 2 fields, got %zu\n", reply->element[1]->elements); + return 1; + } + // REMOVE_END + freeReplyObject(reply); + // STEP_END + + // REMOVE_START + reply = redisCommand(c, "DEL myhash"); + CHECK_REPLY(reply); + freeReplyObject(reply); + // REMOVE_END + + // STEP_START del + reply = redisCommand(c, "SET key1 Hello"); + // REMOVE_START + CHECK_REPLY(reply); + // REMOVE_END + printf("%s\n", reply->str); + // >>> OK + freeReplyObject(reply); + + reply = redisCommand(c, "SET key2 World"); + // REMOVE_START + CHECK_REPLY(reply); + // REMOVE_END + printf("%s\n", reply->str); + // >>> OK + freeReplyObject(reply); + + reply = redisCommand(c, "DEL key1 key2 key3"); + // REMOVE_START + CHECK_REPLY(reply); + // REMOVE_END + printf("%lld\n", reply->integer); + // >>> 2 + // REMOVE_START + if (reply->integer != 2) { + printf("ASSERTION FAILED: Expected 2, got %lld\n", reply->integer); + return 1; + } + // REMOVE_END + freeReplyObject(reply); + // STEP_END + + // STEP_START exists + reply = redisCommand(c, "SET key1 Hello"); + // REMOVE_START + CHECK_REPLY(reply); + // REMOVE_END + printf("%s\n", reply->str); + // >>> OK + freeReplyObject(reply); + + reply = redisCommand(c, "EXISTS key1"); + // REMOVE_START + CHECK_REPLY(reply); + // REMOVE_END + printf("%lld\n", reply->integer); + // >>> 1 + freeReplyObject(reply); + + reply = redisCommand(c, "EXISTS nosuchkey"); + // REMOVE_START + CHECK_REPLY(reply); + // REMOVE_END + printf("%lld\n", reply->integer); + // >>> 0 + freeReplyObject(reply); + + reply = redisCommand(c, "SET key2 World"); + // REMOVE_START + CHECK_REPLY(reply); + // REMOVE_END + printf("%s\n", reply->str); + // >>> OK + freeReplyObject(reply); + + reply = redisCommand(c, "EXISTS key1 key2 nosuchkey"); + // REMOVE_START + CHECK_REPLY(reply); + // REMOVE_END + printf("%lld\n", reply->integer); + // >>> 2 + // REMOVE_START + if (reply->integer != 2) { + printf("ASSERTION FAILED: Expected 2, got %lld\n", reply->integer); + return 1; + } + // REMOVE_END + freeReplyObject(reply); + // STEP_END + + // REMOVE_START + reply = redisCommand(c, "DEL key1 key2"); + CHECK_REPLY(reply); + freeReplyObject(reply); + // REMOVE_END + + // STEP_START expire + reply = redisCommand(c, "SET mykey Hello"); + // REMOVE_START + CHECK_REPLY(reply); + // REMOVE_END + printf("%s\n", reply->str); + // >>> OK + freeReplyObject(reply); + + reply = redisCommand(c, "EXPIRE mykey 10"); + // REMOVE_START + CHECK_REPLY(reply); + // REMOVE_END + printf("%lld\n", reply->integer); + // >>> 1 + freeReplyObject(reply); + + reply = redisCommand(c, "TTL mykey"); + // REMOVE_START + CHECK_REPLY(reply); + // REMOVE_END + printf("%lld\n", reply->integer); + // >>> 10 + freeReplyObject(reply); + + // Overwriting a key with SET clears its expiry. + reply = redisCommand(c, "SET mykey %s", "Hello World"); + // REMOVE_START + CHECK_REPLY(reply); + // REMOVE_END + printf("%s\n", reply->str); + // >>> OK + freeReplyObject(reply); + + reply = redisCommand(c, "TTL mykey"); + // REMOVE_START + CHECK_REPLY(reply); + // REMOVE_END + printf("%lld\n", reply->integer); + // >>> -1 + freeReplyObject(reply); + + // XX only sets the expiry when one already exists, so this is a no-op. + reply = redisCommand(c, "EXPIRE mykey 10 XX"); + // REMOVE_START + CHECK_REPLY(reply); + // REMOVE_END + printf("%lld\n", reply->integer); + // >>> 0 + freeReplyObject(reply); + + reply = redisCommand(c, "TTL mykey"); + // REMOVE_START + CHECK_REPLY(reply); + // REMOVE_END + printf("%lld\n", reply->integer); + // >>> -1 + freeReplyObject(reply); + + // NX only sets the expiry when there is none, so this one applies. + reply = redisCommand(c, "EXPIRE mykey 10 NX"); + // REMOVE_START + CHECK_REPLY(reply); + // REMOVE_END + printf("%lld\n", reply->integer); + // >>> 1 + freeReplyObject(reply); + + reply = redisCommand(c, "TTL mykey"); + // REMOVE_START + CHECK_REPLY(reply); + // REMOVE_END + printf("%lld\n", reply->integer); + // >>> 10 + // REMOVE_START + if (reply->integer != 10) { + printf("ASSERTION FAILED: Expected TTL 10, got %lld\n", reply->integer); + return 1; + } + // REMOVE_END + freeReplyObject(reply); + // STEP_END + + // REMOVE_START + reply = redisCommand(c, "DEL mykey"); + CHECK_REPLY(reply); + freeReplyObject(reply); + // REMOVE_END + + // STEP_START ttl + reply = redisCommand(c, "SET mykey Hello"); + // REMOVE_START + CHECK_REPLY(reply); + // REMOVE_END + printf("%s\n", reply->str); + // >>> OK + freeReplyObject(reply); + + reply = redisCommand(c, "EXPIRE mykey 10"); + // REMOVE_START + CHECK_REPLY(reply); + // REMOVE_END + printf("%lld\n", reply->integer); + // >>> 1 + freeReplyObject(reply); + + reply = redisCommand(c, "TTL mykey"); + // REMOVE_START + CHECK_REPLY(reply); + // REMOVE_END + printf("%lld\n", reply->integer); + // >>> 10 + // REMOVE_START + if (reply->integer != 10) { + printf("ASSERTION FAILED: Expected TTL 10, got %lld\n", reply->integer); + return 1; + } + // REMOVE_END + freeReplyObject(reply); + // STEP_END + + // REMOVE_START + reply = redisCommand(c, "DEL mykey"); + CHECK_REPLY(reply); freeReplyObject(reply); // REMOVE_END diff --git a/local_examples/cmds_generic/ioredis/cmds-generic.js b/local_examples/cmds_generic/ioredis/cmds-generic.js index bc345bad17..ca4325fa83 100644 --- a/local_examples/cmds_generic/ioredis/cmds-generic.js +++ b/local_examples/cmds_generic/ioredis/cmds-generic.js @@ -33,6 +33,158 @@ assert.deepEqual(keysRes4.sort(), ['age', 'firstname', 'lastname']); await redis.del('firstname', 'lastname', 'age'); // REMOVE_END +// STEP_START scan1 +const scan1Res1 = await redis.sadd('myset', '1', '2', '3', 'foo', 'foobar', 'feelsgood'); +console.log(scan1Res1); // >>> 6 + +const [, scan1Members] = await redis.sscan('myset', 0, 'MATCH', 'f*'); +console.log(scan1Members.sort()); // >>> ['feelsgood', 'foo', 'foobar'] +// STEP_END + +// REMOVE_START +assert.equal(scan1Res1, 6); +assert.deepEqual(scan1Members.sort(), ['feelsgood', 'foo', 'foobar']); +await redis.del('myset'); +// REMOVE_END + +// STEP_START scan2 +// REMOVE_START +const scan2Pipeline = redis.pipeline(); +for (let i = 1; i <= 1000; i++) { + scan2Pipeline.set(`key:${i}`, i); +} +await scan2Pipeline.exec(); +// REMOVE_END + +// MATCH filters after the elements are fetched, so most iterations return nothing. +let [scan2Cursor, scan2Keys] = await redis.scan(0, 'MATCH', '*11*'); +console.log(scan2Keys.length); + +for (let i = 0; i < 3; i++) { + [scan2Cursor, scan2Keys] = await redis.scan(scan2Cursor, 'MATCH', '*11*'); + console.log(scan2Keys.length); +} + +// A larger COUNT forces more scanning in a single iteration, so the rest of the +// matches arrive together. The scan continues from the cursor reached above. +[scan2Cursor, scan2Keys] = await redis.scan(scan2Cursor, 'MATCH', '*11*', 'COUNT', 1000); +console.log(scan2Keys.length); // >>> 18 +// STEP_END + +// REMOVE_START +assert.equal(scan2Keys.length, 18); +await redis.flushdb(); +// REMOVE_END + +// STEP_START scan3 +const scan3Res1 = await redis.geoadd('geokey', '0', '0', 'value'); +console.log(scan3Res1); // >>> 1 + +const scan3Res2 = await redis.zadd('zkey', '1000', 'value'); +console.log(scan3Res2); // >>> 1 + +console.log(await redis.type('geokey')); // >>> zset +console.log(await redis.type('zkey')); // >>> zset + +const [, scan3Keys] = await redis.scan(0, 'TYPE', 'zset'); +console.log(scan3Keys.sort()); // >>> ['geokey', 'zkey'] +// STEP_END + +// REMOVE_START +assert.deepEqual(scan3Keys.sort(), ['geokey', 'zkey']); +await redis.del('geokey', 'zkey'); +// REMOVE_END + +// STEP_START scan4 +const scan4Res1 = await redis.hset('myhash', { a: 1, b: 2 }); +console.log(scan4Res1); // >>> 2 + +// HSCAN returns field and value interleaved. Redis does not promise an order, so pair +// them up into an object rather than relying on the position of each element. +const [, scan4Flat] = await redis.hscan('myhash', 0); +const scan4Pairs = Object.fromEntries( + scan4Flat.reduce((acc, v, i) => (i % 2 ? acc : [...acc, [v, scan4Flat[i + 1]]]), []) +); +console.log(scan4Pairs); // >>> { a: '1', b: '2' } + +const [, scan4Fields] = await redis.hscan('myhash', 0, 'NOVALUES'); +console.log(scan4Fields.sort()); // >>> [ 'a', 'b' ] +// STEP_END + +// REMOVE_START +assert.equal(scan4Res1, 2); +assert.deepEqual(scan4Pairs, { a: '1', b: '2' }); +assert.deepEqual(scan4Fields.sort(), ['a', 'b']); +await redis.del('myhash'); +// REMOVE_END + +// STEP_START del +console.log(await redis.set('key1', 'Hello')); // >>> OK +console.log(await redis.set('key2', 'World')); // >>> OK + +const delResult = await redis.del('key1', 'key2', 'key3'); +console.log(delResult); // >>> 2 +// STEP_END + +// REMOVE_START +assert.equal(delResult, 2); +// REMOVE_END + +// STEP_START exists +console.log(await redis.set('key1', 'Hello')); // >>> OK + +console.log(await redis.exists('key1')); // >>> 1 +console.log(await redis.exists('nosuchkey')); // >>> 0 + +console.log(await redis.set('key2', 'World')); // >>> OK + +const existsResult = await redis.exists('key1', 'key2', 'nosuchkey'); +console.log(existsResult); // >>> 2 +// STEP_END + +// REMOVE_START +assert.equal(existsResult, 2); +await redis.del('key1', 'key2'); +// REMOVE_END + +// STEP_START expire +console.log(await redis.set('mykey', 'Hello')); // >>> OK + +console.log(await redis.expire('mykey', 10)); // >>> 1 +console.log(await redis.ttl('mykey')); // >>> 10 + +// Overwriting a key with SET clears its expiry. +console.log(await redis.set('mykey', 'Hello World')); // >>> OK +console.log(await redis.ttl('mykey')); // >>> -1 + +// XX only sets the expiry when one already exists, so this is a no-op. +console.log(await redis.expire('mykey', 10, 'XX')); // >>> 0 +console.log(await redis.ttl('mykey')); // >>> -1 + +// NX only sets the expiry when there is none, so this one applies. +console.log(await redis.expire('mykey', 10, 'NX')); // >>> 1 +const expireTtl = await redis.ttl('mykey'); +console.log(expireTtl); // >>> 10 +// STEP_END + +// REMOVE_START +assert.equal(expireTtl, 10); +await redis.del('mykey'); +// REMOVE_END + +// STEP_START ttl +console.log(await redis.set('mykey', 'Hello')); // >>> OK +console.log(await redis.expire('mykey', 10)); // >>> 1 + +const ttlResult = await redis.ttl('mykey'); +console.log(ttlResult); // >>> 10 +// STEP_END + +// REMOVE_START +assert.equal(ttlResult, 10); +await redis.del('mykey'); +// REMOVE_END + // HIDE_START redis.disconnect(); // HIDE_END diff --git a/local_examples/cmds_generic/jedis/CmdsGenericExample.java b/local_examples/cmds_generic/jedis/CmdsGenericExample.java index 7ad950252e..673b231a52 100644 --- a/local_examples/cmds_generic/jedis/CmdsGenericExample.java +++ b/local_examples/cmds_generic/jedis/CmdsGenericExample.java @@ -8,9 +8,13 @@ // HIDE_START import redis.clients.jedis.RedisClient; import redis.clients.jedis.args.ExpiryOption; +import redis.clients.jedis.params.ScanParams; +import redis.clients.jedis.resps.ScanResult; import java.util.ArrayList; import java.util.Collections; +import java.util.List; +import java.util.Map; import java.util.Set; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -166,6 +170,105 @@ public void run() { jedis.del("firstname", "lastname", "age"); // REMOVE_END + // STEP_START scan1 + long scan1Result1 = jedis.sadd("myset", "1", "2", "3", "foo", "foobar", "feelsgood"); + System.out.println(scan1Result1); // >>> 6 + + ScanResult scan1Result2 = jedis.sscan( + "myset", "0", new ScanParams().match("f*") + ); + ArrayList scan1Members = new ArrayList<>(scan1Result2.getResult()); + Collections.sort(scan1Members); + System.out.println(scan1Members); // >>> [feelsgood, foo, foobar] + // STEP_END + + // REMOVE_START + assertEquals(6, scan1Result1); + assertEquals(3, scan1Members.size()); + jedis.del("myset"); + // REMOVE_END + + // STEP_START scan2 + // REMOVE_START + for (int i = 1; i <= 1000; i++) { + jedis.set("key:" + i, String.valueOf(i)); + } + // REMOVE_END + + // MATCH is applied after elements are fetched, so with the default COUNT most + // iterations return few keys or none at all. + String scan2Cursor = "0"; + ScanResult scan2Result; + + for (int i = 0; i < 4; i++) { + scan2Result = jedis.scan(scan2Cursor, new ScanParams().match("*11*")); + scan2Cursor = scan2Result.getCursor(); + System.out.println(scan2Result.getResult().size()); + } + + // A larger COUNT forces more scanning in a single iteration, so the remaining + // matches arrive together. This continues from the cursor reached above. + scan2Result = jedis.scan(scan2Cursor, new ScanParams().match("*11*").count(1000)); + System.out.println(scan2Result.getResult().size()); // >>> 18 + // STEP_END + + // REMOVE_START + assertEquals(18, scan2Result.getResult().size()); + jedis.flushDB(); + // REMOVE_END + + // STEP_START scan3 + long scan3Result1 = jedis.geoadd("geokey", 0, 0, "value"); + System.out.println(scan3Result1); // >>> 1 + + long scan3Result2 = jedis.zadd("zkey", 1000, "value"); + System.out.println(scan3Result2); // >>> 1 + + System.out.println(jedis.type("geokey")); // >>> zset + System.out.println(jedis.type("zkey")); // >>> zset + + ScanResult scan3Result3 = jedis.scan( + "0", new ScanParams(), "zset" + ); + ArrayList scan3Keys = new ArrayList<>(scan3Result3.getResult()); + Collections.sort(scan3Keys); + System.out.println(scan3Keys); // >>> [geokey, zkey] + // STEP_END + + // REMOVE_START + assertEquals(2, scan3Keys.size()); + jedis.del("geokey", "zkey"); + // REMOVE_END + + // STEP_START scan4 + long scan4Result1 = jedis.hset("myhash", Map.of("a", "1", "b", "2")); + System.out.println(scan4Result1); // >>> 2 + + ScanResult> scan4Result2 = jedis.hscan( + "myhash", "0", new ScanParams() + ); + ArrayList scan4Pairs = new ArrayList<>(); + for (Map.Entry entry : scan4Result2.getResult()) { + scan4Pairs.add(entry.getKey() + "=" + entry.getValue()); + } + Collections.sort(scan4Pairs); + System.out.println(scan4Pairs); // >>> [a=1, b=2] + + ScanResult scan4Result3 = jedis.hscanNoValues( + "myhash", "0", new ScanParams() + ); + ArrayList scan4Fields = new ArrayList<>(scan4Result3.getResult()); + Collections.sort(scan4Fields); + System.out.println(scan4Fields); // >>> [a, b] + // STEP_END + + // REMOVE_START + assertEquals(2, scan4Result1); + assertEquals(2, scan4Pairs.size()); + assertEquals(2, scan4Fields.size()); + jedis.del("myhash"); + // REMOVE_END + // HIDE_START jedis.close(); } diff --git a/local_examples/cmds_generic/lettuce-async/CmdsGenericExample.java b/local_examples/cmds_generic/lettuce-async/CmdsGenericExample.java index 99aad44348..dc407f23a3 100644 --- a/local_examples/cmds_generic/lettuce-async/CmdsGenericExample.java +++ b/local_examples/cmds_generic/lettuce-async/CmdsGenericExample.java @@ -112,6 +112,225 @@ public void run() { // REMOVE_START asyncCommands.del("firstname", "lastname", "age").toCompletableFuture().join(); // REMOVE_END + + // STEP_START scan1 + CompletableFuture scan1Example = asyncCommands + .sadd("myset", "1", "2", "3", "foo", "foobar", "feelsgood") + .thenCompose(scan1Res1 -> { + System.out.println(scan1Res1); // >>> 6 + // REMOVE_START + assertThat(scan1Res1).isEqualTo(6L); + // REMOVE_END + return asyncCommands.sscan("myset", ScanArgs.Builder.matches("f*")); + }) + .thenAccept(scan1Res2 -> { + List members = new java.util.ArrayList<>(scan1Res2.getValues()); + Collections.sort(members); + System.out.println(members); // >>> [feelsgood, foo, foobar] + // REMOVE_START + assertThat(members).hasSize(3); + // REMOVE_END + }) + .toCompletableFuture(); + // STEP_END + + scan1Example.join(); + // REMOVE_START + asyncCommands.del("myset").toCompletableFuture().join(); + // REMOVE_END + + // STEP_START scan2 + // REMOVE_START + for (int i = 1; i <= 1000; i++) { + asyncCommands.set("key:" + i, String.valueOf(i)).toCompletableFuture().join(); + } + // REMOVE_END + + // MATCH is applied after elements are fetched, so with the default COUNT most + // iterations return few keys or none at all. Each iteration is awaited because + // the next one needs the cursor this one returns. + KeyScanCursor scan2Cursor = asyncCommands + .scan(ScanArgs.Builder.matches("*11*")).toCompletableFuture().join(); + System.out.println(scan2Cursor.getKeys().size()); + + for (int i = 0; i < 3; i++) { + scan2Cursor = asyncCommands + .scan(scan2Cursor, ScanArgs.Builder.matches("*11*")) + .toCompletableFuture().join(); + System.out.println(scan2Cursor.getKeys().size()); + } + + // A larger COUNT forces more scanning in a single iteration, so the remaining + // matches arrive together. This continues from the cursor reached above. + scan2Cursor = asyncCommands + .scan(scan2Cursor, ScanArgs.Builder.matches("*11*").limit(1000)) + .toCompletableFuture().join(); + System.out.println(scan2Cursor.getKeys().size()); // >>> 18 + // STEP_END + + // REMOVE_START + assertThat(scan2Cursor.getKeys()).hasSize(18); + asyncCommands.flushdb().toCompletableFuture().join(); + // REMOVE_END + + // STEP_START scan3 + CompletableFuture scan3Example = asyncCommands + .geoadd("geokey", 0, 0, "value") + .thenCompose(scan3Res1 -> { + System.out.println(scan3Res1); // >>> 1 + return asyncCommands.zadd("zkey", 1000, "value"); + }) + .thenCompose(scan3Res2 -> { + System.out.println(scan3Res2); // >>> 1 + return asyncCommands.type("geokey"); + }) + .thenCompose(scan3Res3 -> { + System.out.println(scan3Res3); // >>> zset + return asyncCommands.type("zkey"); + }) + .thenCompose(scan3Res4 -> { + System.out.println(scan3Res4); // >>> zset + return asyncCommands.scan(KeyScanArgs.Builder.type("zset")); + }) + .thenAccept(scan3Res5 -> { + List keys = new java.util.ArrayList<>(scan3Res5.getKeys()); + Collections.sort(keys); + System.out.println(keys); // >>> [geokey, zkey] + // REMOVE_START + assertThat(keys).hasSize(2); + // REMOVE_END + }) + .toCompletableFuture(); + // STEP_END + + scan3Example.join(); + // REMOVE_START + asyncCommands.del("geokey", "zkey").toCompletableFuture().join(); + // REMOVE_END + + // STEP_START scan4 + CompletableFuture scan4Example = asyncCommands + .hset("myhash", Map.of("a", "1", "b", "2")) + .thenCompose(scan4Res1 -> { + System.out.println(scan4Res1); // >>> 2 + return asyncCommands.hscan("myhash"); + }) + .thenCompose(scan4Res2 -> { + System.out.println(new java.util.TreeMap<>(scan4Res2.getMap())); + // >>> {a=1, b=2} + return asyncCommands.hscanNovalues("myhash"); + }) + .thenAccept(scan4Res3 -> { + List fields = new java.util.ArrayList<>(scan4Res3.getKeys()); + Collections.sort(fields); + System.out.println(fields); // >>> [a, b] + // REMOVE_START + assertThat(fields).containsExactly("a", "b"); + // REMOVE_END + }) + .toCompletableFuture(); + // STEP_END + + scan4Example.join(); + // REMOVE_START + asyncCommands.del("myhash").toCompletableFuture().join(); + // REMOVE_END + // STEP_START del + CompletableFuture delExample = asyncCommands.set("key1", "Hello") + .thenCompose(r1 -> { + System.out.println(r1); // >>> OK + return asyncCommands.set("key2", "World"); + }) + .thenCompose(r2 -> { + System.out.println(r2); // >>> OK + return asyncCommands.del("key1", "key2", "key3"); + }) + .thenAccept(r3 -> { + System.out.println(r3); // >>> 2 + // REMOVE_START + assertThat(r3).isEqualTo(2L); + // REMOVE_END + }) + .toCompletableFuture(); + // STEP_END + + delExample.join(); + + // STEP_START expire + CompletableFuture expireExample = asyncCommands.set("mykey", "Hello") + .thenCompose(r1 -> { + System.out.println(r1); // >>> OK + return asyncCommands.expire("mykey", 10); + }) + .thenCompose(r2 -> { + System.out.println(r2); // >>> true + return asyncCommands.ttl("mykey"); + }) + .thenCompose(r3 -> { + System.out.println(r3); // >>> 10 + // Overwriting a key with SET clears its expiry. + return asyncCommands.set("mykey", "Hello World"); + }) + .thenCompose(r4 -> { + System.out.println(r4); // >>> OK + return asyncCommands.ttl("mykey"); + }) + .thenCompose(r5 -> { + System.out.println(r5); // >>> -1 + // XX only sets the expiry when one already exists, so this is a no-op. + return asyncCommands.expire("mykey", 10, ExpireArgs.Builder.xx()); + }) + .thenCompose(r6 -> { + System.out.println(r6); // >>> false + return asyncCommands.ttl("mykey"); + }) + .thenCompose(r7 -> { + System.out.println(r7); // >>> -1 + // NX only sets the expiry when there is none, so this one applies. + return asyncCommands.expire("mykey", 10, ExpireArgs.Builder.nx()); + }) + .thenCompose(r8 -> { + System.out.println(r8); // >>> true + return asyncCommands.ttl("mykey"); + }) + .thenAccept(r9 -> { + System.out.println(r9); // >>> 10 + // REMOVE_START + assertThat(r9).isEqualTo(10L); + // REMOVE_END + }) + .toCompletableFuture(); + // STEP_END + + expireExample.join(); + // REMOVE_START + asyncCommands.del("mykey").toCompletableFuture().join(); + // REMOVE_END + + // STEP_START ttl + CompletableFuture ttlExample = asyncCommands.set("mykey", "Hello") + .thenCompose(r1 -> { + System.out.println(r1); // >>> OK + return asyncCommands.expire("mykey", 10); + }) + .thenCompose(r2 -> { + System.out.println(r2); // >>> true + return asyncCommands.ttl("mykey"); + }) + .thenAccept(r3 -> { + System.out.println(r3); // >>> 10 + // REMOVE_START + assertThat(r3).isEqualTo(10L); + // REMOVE_END + }) + .toCompletableFuture(); + // STEP_END + + ttlExample.join(); + // REMOVE_START + asyncCommands.del("mykey").toCompletableFuture().join(); + // REMOVE_END + } finally { redisClient.shutdown(); } diff --git a/local_examples/cmds_generic/lettuce-reactive/CmdsGenericExample.java b/local_examples/cmds_generic/lettuce-reactive/CmdsGenericExample.java index 61c54ade87..207dc1ae3a 100644 --- a/local_examples/cmds_generic/lettuce-reactive/CmdsGenericExample.java +++ b/local_examples/cmds_generic/lettuce-reactive/CmdsGenericExample.java @@ -10,6 +10,7 @@ import reactor.core.publisher.Mono; import java.util.Collections; +import java.util.List; import java.util.Map; // REMOVE_START @@ -100,6 +101,223 @@ public void run() { reactiveCommands.del("firstname", "lastname", "age").block(); // REMOVE_END + // STEP_START scan1 + Mono scan1Example = reactiveCommands + .sadd("myset", "1", "2", "3", "foo", "foobar", "feelsgood") + .flatMap(scan1Res1 -> { + System.out.println(scan1Res1); // >>> 6 + // REMOVE_START + assertThat(scan1Res1).isEqualTo(6L); + // REMOVE_END + return reactiveCommands.sscan("myset", ScanArgs.Builder.matches("f*")); + }) + .doOnNext(scan1Res2 -> { + List members = new java.util.ArrayList<>(scan1Res2.getValues()); + Collections.sort(members); + System.out.println(members); // >>> [feelsgood, foo, foobar] + // REMOVE_START + assertThat(members).hasSize(3); + // REMOVE_END + }) + .then(); + // STEP_END + + Mono.when(scan1Example).block(); + // REMOVE_START + reactiveCommands.del("myset").block(); + // REMOVE_END + + // STEP_START scan2 + // REMOVE_START + for (int i = 1; i <= 1000; i++) { + reactiveCommands.set("key:" + i, String.valueOf(i)).block(); + } + // REMOVE_END + + // MATCH is applied after elements are fetched, so with the default COUNT most + // iterations return few keys or none at all. Each iteration is subscribed to in + // turn because the next one needs the cursor this one returns. + KeyScanCursor scan2Cursor = reactiveCommands + .scan(ScanArgs.Builder.matches("*11*")).block(); + System.out.println(scan2Cursor.getKeys().size()); + + for (int i = 0; i < 3; i++) { + scan2Cursor = reactiveCommands + .scan(scan2Cursor, ScanArgs.Builder.matches("*11*")).block(); + System.out.println(scan2Cursor.getKeys().size()); + } + + // A larger COUNT forces more scanning in a single iteration, so the remaining + // matches arrive together. This continues from the cursor reached above. + scan2Cursor = reactiveCommands + .scan(scan2Cursor, ScanArgs.Builder.matches("*11*").limit(1000)).block(); + System.out.println(scan2Cursor.getKeys().size()); // >>> 18 + // STEP_END + + // REMOVE_START + assertThat(scan2Cursor.getKeys()).hasSize(18); + reactiveCommands.flushdb().block(); + // REMOVE_END + + // STEP_START scan3 + Mono scan3Example = reactiveCommands + .geoadd("geokey", 0, 0, "value") + .flatMap(scan3Res1 -> { + System.out.println(scan3Res1); // >>> 1 + return reactiveCommands.zadd("zkey", 1000, "value"); + }) + .flatMap(scan3Res2 -> { + System.out.println(scan3Res2); // >>> 1 + return reactiveCommands.type("geokey"); + }) + .flatMap(scan3Res3 -> { + System.out.println(scan3Res3); // >>> zset + return reactiveCommands.type("zkey"); + }) + .flatMap(scan3Res4 -> { + System.out.println(scan3Res4); // >>> zset + return reactiveCommands.scan(KeyScanArgs.Builder.type("zset")); + }) + .doOnNext(scan3Res5 -> { + List keys = new java.util.ArrayList<>(scan3Res5.getKeys()); + Collections.sort(keys); + System.out.println(keys); // >>> [geokey, zkey] + // REMOVE_START + assertThat(keys).hasSize(2); + // REMOVE_END + }) + .then(); + // STEP_END + + Mono.when(scan3Example).block(); + // REMOVE_START + reactiveCommands.del("geokey", "zkey").block(); + // REMOVE_END + + // STEP_START scan4 + Mono scan4Example = reactiveCommands + .hset("myhash", Map.of("a", "1", "b", "2")) + .flatMap(scan4Res1 -> { + System.out.println(scan4Res1); // >>> 2 + return reactiveCommands.hscan("myhash"); + }) + .flatMap(scan4Res2 -> { + System.out.println(new java.util.TreeMap<>(scan4Res2.getMap())); + // >>> {a=1, b=2} + return reactiveCommands.hscanNovalues("myhash"); + }) + .doOnNext(scan4Res3 -> { + List fields = new java.util.ArrayList<>(scan4Res3.getKeys()); + Collections.sort(fields); + System.out.println(fields); // >>> [a, b] + // REMOVE_START + assertThat(fields).containsExactly("a", "b"); + // REMOVE_END + }) + .then(); + // STEP_END + + Mono.when(scan4Example).block(); + // REMOVE_START + reactiveCommands.del("myhash").block(); + // REMOVE_END + // STEP_START del + Mono delExample = reactiveCommands.set("key1", "Hello") + .flatMap(r1 -> { + System.out.println(r1); // >>> OK + return reactiveCommands.set("key2", "World"); + }) + .flatMap(r2 -> { + System.out.println(r2); // >>> OK + return reactiveCommands.del("key1", "key2", "key3"); + }) + .doOnNext(r3 -> { + System.out.println(r3); // >>> 2 + // REMOVE_START + assertThat(r3).isEqualTo(2L); + // REMOVE_END + }) + .then(); + // STEP_END + + Mono.when(delExample).block(); + + // STEP_START expire + Mono expireExample = reactiveCommands.set("mykey", "Hello") + .flatMap(r1 -> { + System.out.println(r1); // >>> OK + return reactiveCommands.expire("mykey", 10); + }) + .flatMap(r2 -> { + System.out.println(r2); // >>> true + return reactiveCommands.ttl("mykey"); + }) + .flatMap(r3 -> { + System.out.println(r3); // >>> 10 + // Overwriting a key with SET clears its expiry. + return reactiveCommands.set("mykey", "Hello World"); + }) + .flatMap(r4 -> { + System.out.println(r4); // >>> OK + return reactiveCommands.ttl("mykey"); + }) + .flatMap(r5 -> { + System.out.println(r5); // >>> -1 + // XX only sets the expiry when one already exists, so this is a no-op. + return reactiveCommands.expire("mykey", 10, ExpireArgs.Builder.xx()); + }) + .flatMap(r6 -> { + System.out.println(r6); // >>> false + return reactiveCommands.ttl("mykey"); + }) + .flatMap(r7 -> { + System.out.println(r7); // >>> -1 + // NX only sets the expiry when there is none, so this one applies. + return reactiveCommands.expire("mykey", 10, ExpireArgs.Builder.nx()); + }) + .flatMap(r8 -> { + System.out.println(r8); // >>> true + return reactiveCommands.ttl("mykey"); + }) + .doOnNext(r9 -> { + System.out.println(r9); // >>> 10 + // REMOVE_START + assertThat(r9).isEqualTo(10L); + // REMOVE_END + }) + .then(); + // STEP_END + + Mono.when(expireExample).block(); + // REMOVE_START + reactiveCommands.del("mykey").block(); + // REMOVE_END + + // STEP_START ttl + Mono ttlExample = reactiveCommands.set("mykey", "Hello") + .flatMap(r1 -> { + System.out.println(r1); // >>> OK + return reactiveCommands.expire("mykey", 10); + }) + .flatMap(r2 -> { + System.out.println(r2); // >>> true + return reactiveCommands.ttl("mykey"); + }) + .doOnNext(r3 -> { + System.out.println(r3); // >>> 10 + // REMOVE_START + assertThat(r3).isEqualTo(10L); + // REMOVE_END + }) + .then(); + // STEP_END + + Mono.when(ttlExample).block(); + // REMOVE_START + reactiveCommands.del("mykey").block(); + // REMOVE_END + + } finally { redisClient.shutdown(); } diff --git a/local_examples/cmds_generic/predis/CmdsGenericTest.php b/local_examples/cmds_generic/predis/CmdsGenericTest.php index 6a0bef71f8..2bb4f65459 100644 --- a/local_examples/cmds_generic/predis/CmdsGenericTest.php +++ b/local_examples/cmds_generic/predis/CmdsGenericTest.php @@ -68,5 +68,117 @@ public function testCmdsGeneric() { $r->del('firstname', 'lastname', 'age'); // REMOVE_END + // STEP_START scan1 + $scan1Result1 = $r->sadd('myset', ['1', '2', '3', 'foo', 'foobar', 'feelsgood']); + echo $scan1Result1 . PHP_EOL; // >>> 6 + + [$scan1Cursor, $scan1Members] = $r->sscan('myset', 0, ['MATCH' => 'f*']); + sort($scan1Members); + echo implode(', ', $scan1Members) . PHP_EOL; // >>> feelsgood, foo, foobar + // STEP_END + + // REMOVE_START + $this->assertEquals(6, $scan1Result1); + $this->assertEquals(['feelsgood', 'foo', 'foobar'], $scan1Members); + $r->del('myset'); + // REMOVE_END + + // STEP_START scan2 + // REMOVE_START + for ($i = 1; $i <= 1000; $i++) { + $r->set("key:$i", $i); + } + // REMOVE_END + + // MATCH is applied after elements are fetched, so with the default COUNT most + // iterations return few keys or none at all. + $scan2Cursor = 0; + + for ($i = 0; $i < 4; $i++) { + [$scan2Cursor, $scan2Keys] = $r->scan($scan2Cursor, ['MATCH' => '*11*']); + echo count($scan2Keys) . PHP_EOL; + } + + // A larger COUNT forces more scanning in a single iteration, so the remaining + // matches arrive together. This continues from the cursor reached above. + [$scan2Cursor, $scan2Keys] = $r->scan($scan2Cursor, ['MATCH' => '*11*', 'COUNT' => 1000]); + echo count($scan2Keys) . PHP_EOL; // >>> 18 + // STEP_END + + // REMOVE_START + $this->assertEquals(18, count($scan2Keys)); + $r->flushdb(); + // REMOVE_END + + // STEP_START scan4 + $scan4Result1 = $r->hset('myhash', 'a', 1, 'b', 2); + echo $scan4Result1 . PHP_EOL; // >>> 2 + + [$scan4Cursor, $scan4Pairs] = $r->hscan('myhash', 0); + echo json_encode($scan4Pairs) . PHP_EOL; // >>> {"a":"1","b":"2"} + + // Redis does not promise a field order, so sort before comparing. + [$scan4Cursor, $scan4Fields] = $r->hscan('myhash', 0, ['NOVALUES' => true]); + sort($scan4Fields); + echo implode(', ', $scan4Fields) . PHP_EOL; // >>> a, b + // STEP_END + + // REMOVE_START + $this->assertEquals(2, $scan4Result1); + $this->assertEquals(['a' => '1', 'b' => '2'], $scan4Pairs); + $this->assertEquals(['a', 'b'], $scan4Fields); + $r->del('myhash'); + // REMOVE_END + + // STEP_START del + echo $r->set('key1', 'Hello') . PHP_EOL; // >>> OK + echo $r->set('key2', 'World') . PHP_EOL; // >>> OK + + $delResult = $r->del('key1', 'key2', 'key3'); + echo $delResult . PHP_EOL; // >>> 2 + // STEP_END + + // REMOVE_START + $this->assertEquals(2, $delResult); + // REMOVE_END + + // STEP_START expire + echo $r->set('mykey', 'Hello') . PHP_EOL; // >>> OK + + echo $r->expire('mykey', 10) . PHP_EOL; // >>> 1 + echo $r->ttl('mykey') . PHP_EOL; // >>> 10 + + // Overwriting a key with SET clears its expiry. + echo $r->set('mykey', 'Hello World') . PHP_EOL; // >>> OK + echo $r->ttl('mykey') . PHP_EOL; // >>> -1 + + // XX only sets the expiry when one already exists, so this is a no-op. + echo $r->expire('mykey', 10, 'XX') . PHP_EOL; // >>> 0 + echo $r->ttl('mykey') . PHP_EOL; // >>> -1 + + // NX only sets the expiry when there is none, so this one applies. + echo $r->expire('mykey', 10, 'NX') . PHP_EOL; // >>> 1 + $expireTtl = $r->ttl('mykey'); + echo $expireTtl . PHP_EOL; // >>> 10 + // STEP_END + + // REMOVE_START + $this->assertEquals(10, $expireTtl); + $r->del('mykey'); + // REMOVE_END + + // STEP_START ttl + echo $r->set('mykey', 'Hello') . PHP_EOL; // >>> OK + echo $r->expire('mykey', 10) . PHP_EOL; // >>> 1 + + $ttlResult = $r->ttl('mykey'); + echo $ttlResult . PHP_EOL; // >>> 10 + // STEP_END + + // REMOVE_START + $this->assertEquals(10, $ttlResult); + $r->del('mykey'); + // REMOVE_END + } } diff --git a/local_examples/cmds_hash/hiredis/cmds_hash.c b/local_examples/cmds_hash/hiredis/cmds_hash.c index 372534a6da..27961612be 100644 --- a/local_examples/cmds_hash/hiredis/cmds_hash.c +++ b/local_examples/cmds_hash/hiredis/cmds_hash.c @@ -5,6 +5,19 @@ #include #include #include + +// REMOVE_START +// Fail loudly on a NULL or error reply. hiredis returns an error REPLY (not a +// connection error) for things like a bad command, and the examples would +// otherwise print a wrong value and still exit 0 — a green harness run that +// proves nothing. Kept in a REMOVE block so the published example stays plain. +#define CHECK_REPLY(r) do { \ + if ((r) == NULL || (r)->type == REDIS_REPLY_ERROR) { \ + printf("REDIS ERROR: %s\n", (r) ? (r)->str : "no reply from server"); \ + return 1; \ + } \ +} while (0) +// REMOVE_END // STEP_END int main(int argc, char **argv) { @@ -23,7 +36,9 @@ int main(int argc, char **argv) { // STEP_END // REMOVE_START - redisCommand(c, "DEL myhash"); + redisReply *cleanup1 = redisCommand(c, "DEL myhash"); + CHECK_REPLY(cleanup1); + freeReplyObject(cleanup1); // REMOVE_END // STEP_START hmget @@ -32,11 +47,17 @@ int main(int argc, char **argv) { // Set up hash with fields reply = redisCommand(c, "HSET %s %s %s %s %s", "myhash", "field1", "Hello", "field2", "World"); + // REMOVE_START + CHECK_REPLY(reply); + // REMOVE_END freeReplyObject(reply); // Get multiple fields at once reply = redisCommand(c, "HMGET %s %s %s %s", "myhash", "field1", "field2", "nofield"); + // REMOVE_START + CHECK_REPLY(reply); + // REMOVE_END printf("HMGET myhash field1 field2 nofield:\n"); for (size_t i = 0; i < reply->elements; i++) { @@ -69,12 +90,17 @@ int main(int argc, char **argv) { freeReplyObject(reply); // REMOVE_START - redisCommand(c, "DEL myhash"); + redisReply *cleanup2 = redisCommand(c, "DEL myhash"); + CHECK_REPLY(cleanup2); + freeReplyObject(cleanup2); // REMOVE_END // STEP_START hlen // Add two new fields to the hash reply = redisCommand(c, "HSET %s %s %s", "myhash", "field1", "Hello"); + // REMOVE_START + CHECK_REPLY(reply); + // REMOVE_END printf("HSET myhash field1 Hello: %lld\n", reply->integer); // >>> 1 // REMOVE_START if (reply->integer != 1) { @@ -84,6 +110,9 @@ int main(int argc, char **argv) { freeReplyObject(reply); reply = redisCommand(c, "HSET %s %s %s", "myhash", "field2", "World"); + // REMOVE_START + CHECK_REPLY(reply); + // REMOVE_END printf("HSET myhash field2 World: %lld\n", reply->integer); // >>> 1 // REMOVE_START if (reply->integer != 1) { @@ -94,6 +123,9 @@ int main(int argc, char **argv) { // Count the fields in the hash reply = redisCommand(c, "HLEN %s", "myhash"); + // REMOVE_START + CHECK_REPLY(reply); + // REMOVE_END printf("HLEN myhash: %lld\n", reply->integer); // >>> 2 // REMOVE_START if (reply->type != REDIS_REPLY_INTEGER) { @@ -108,9 +140,278 @@ int main(int argc, char **argv) { // REMOVE_START redisReply *del_reply = redisCommand(c, "DEL myhash"); + CHECK_REPLY(del_reply); freeReplyObject(del_reply); // REMOVE_END + + // STEP_START hset + reply = redisCommand(c, "HSET myhash field1 Hello"); + // REMOVE_START + CHECK_REPLY(reply); + // REMOVE_END + printf("%lld\n", reply->integer); + // >>> 1 + freeReplyObject(reply); + + reply = redisCommand(c, "HGET myhash field1"); + // REMOVE_START + CHECK_REPLY(reply); + // REMOVE_END + printf("%s\n", reply->str); + // >>> Hello + freeReplyObject(reply); + + reply = redisCommand(c, "HSET myhash field2 Hi field3 World"); + // REMOVE_START + CHECK_REPLY(reply); + // REMOVE_END + printf("%lld\n", reply->integer); + // >>> 2 + freeReplyObject(reply); + + reply = redisCommand(c, "HGET myhash field2"); + // REMOVE_START + CHECK_REPLY(reply); + // REMOVE_END + printf("%s\n", reply->str); + // >>> Hi + freeReplyObject(reply); + + reply = redisCommand(c, "HGET myhash field3"); + // REMOVE_START + CHECK_REPLY(reply); + // REMOVE_END + printf("%s\n", reply->str); + // >>> World + freeReplyObject(reply); + + // HGETALL returns field and value alternating in a flat array. + reply = redisCommand(c, "HGETALL myhash"); + // REMOVE_START + CHECK_REPLY(reply); + // REMOVE_END + for (size_t i = 0; i < reply->elements; i += 2) { + printf("%s=%s\n", reply->element[i]->str, reply->element[i + 1]->str); + } + // >>> field1=Hello + // >>> field2=Hi + // >>> field3=World + // REMOVE_START + if (reply->elements != 6) { + printf("ASSERTION FAILED: Expected 6 entries, got %zu\n", reply->elements); + return 1; + } + // REMOVE_END + freeReplyObject(reply); + // STEP_END + + // REMOVE_START + reply = redisCommand(c, "DEL myhash"); + CHECK_REPLY(reply); + freeReplyObject(reply); + // REMOVE_END + + // STEP_START hget + reply = redisCommand(c, "HSET myhash field1 foo"); + // REMOVE_START + CHECK_REPLY(reply); + // REMOVE_END + printf("%lld\n", reply->integer); + // >>> 1 + freeReplyObject(reply); + + reply = redisCommand(c, "HGET myhash field1"); + // REMOVE_START + CHECK_REPLY(reply); + // REMOVE_END + printf("%s\n", reply->str); + // >>> foo + freeReplyObject(reply); + + // A field that does not exist gives a nil reply, not an empty string. + reply = redisCommand(c, "HGET myhash field2"); + // REMOVE_START + CHECK_REPLY(reply); + // REMOVE_END + printf("%s\n", reply->type == REDIS_REPLY_NIL ? "(nil)" : reply->str); + // >>> (nil) + // REMOVE_START + if (reply->type != REDIS_REPLY_NIL) { + printf("ASSERTION FAILED: Expected nil for a missing field\n"); + return 1; + } + // REMOVE_END + freeReplyObject(reply); + // STEP_END + + // REMOVE_START + reply = redisCommand(c, "DEL myhash"); + CHECK_REPLY(reply); + freeReplyObject(reply); + // REMOVE_END + + // STEP_START hgetall + reply = redisCommand(c, "HSET myhash field1 Hello field2 World"); + // REMOVE_START + CHECK_REPLY(reply); + // REMOVE_END + freeReplyObject(reply); + + reply = redisCommand(c, "HGETALL myhash"); + // REMOVE_START + CHECK_REPLY(reply); + // REMOVE_END + for (size_t i = 0; i < reply->elements; i += 2) { + printf("%s=%s\n", reply->element[i]->str, reply->element[i + 1]->str); + } + // >>> field1=Hello + // >>> field2=World + // REMOVE_START + if (reply->elements != 4) { + printf("ASSERTION FAILED: Expected 4 entries, got %zu\n", reply->elements); + return 1; + } + // REMOVE_END + freeReplyObject(reply); + // STEP_END + + // REMOVE_START + reply = redisCommand(c, "DEL myhash"); + CHECK_REPLY(reply); + freeReplyObject(reply); + // REMOVE_END + + // STEP_START hdel + reply = redisCommand(c, "HSET myhash field1 foo"); + // REMOVE_START + CHECK_REPLY(reply); + // REMOVE_END + printf("%lld\n", reply->integer); + // >>> 1 + freeReplyObject(reply); + + reply = redisCommand(c, "HDEL myhash field1"); + // REMOVE_START + CHECK_REPLY(reply); + // REMOVE_END + printf("%lld\n", reply->integer); + // >>> 1 + freeReplyObject(reply); + + // Deleting a field that is not there removes nothing. + reply = redisCommand(c, "HDEL myhash field2"); + // REMOVE_START + CHECK_REPLY(reply); + // REMOVE_END + printf("%lld\n", reply->integer); + // >>> 0 + // REMOVE_START + if (reply->integer != 0) { + printf("ASSERTION FAILED: Expected 0, got %lld\n", reply->integer); + return 1; + } + // REMOVE_END + freeReplyObject(reply); + // STEP_END + + // REMOVE_START + reply = redisCommand(c, "DEL myhash"); + CHECK_REPLY(reply); + freeReplyObject(reply); + // REMOVE_END + + // STEP_START hvals + reply = redisCommand(c, "HSET myhash field1 Hello field2 World"); + // REMOVE_START + CHECK_REPLY(reply); + // REMOVE_END + freeReplyObject(reply); + + reply = redisCommand(c, "HVALS myhash"); + // REMOVE_START + CHECK_REPLY(reply); + // REMOVE_END + for (size_t i = 0; i < reply->elements; i++) { + printf("%s\n", reply->element[i]->str); + } + // >>> Hello + // >>> World + // REMOVE_START + if (reply->elements != 2) { + printf("ASSERTION FAILED: Expected 2 values, got %zu\n", reply->elements); + return 1; + } + // REMOVE_END + freeReplyObject(reply); + // STEP_END + + // REMOVE_START + reply = redisCommand(c, "DEL myhash"); + CHECK_REPLY(reply); + freeReplyObject(reply); + // REMOVE_END + + // STEP_START hexpire + reply = redisCommand(c, "HSET myhash field1 Hello field2 World"); + // REMOVE_START + CHECK_REPLY(reply); + // REMOVE_END + freeReplyObject(reply); + + // HEXPIRE needs the field count before the field names. + reply = redisCommand(c, "HEXPIRE myhash 10 FIELDS 2 field1 field2"); + // REMOVE_START + CHECK_REPLY(reply); + // REMOVE_END + for (size_t i = 0; i < reply->elements; i++) { + printf("%lld\n", reply->element[i]->integer); + } + // >>> 1 + // >>> 1 + freeReplyObject(reply); + + reply = redisCommand(c, "HTTL myhash FIELDS 2 field1 field2"); + // REMOVE_START + CHECK_REPLY(reply); + // REMOVE_END + for (size_t i = 0; i < reply->elements; i++) { + printf("%lld\n", reply->element[i]->integer); + } + // >>> 10 + // >>> 10 + // REMOVE_START + for (size_t i = 0; i < reply->elements; i++) { + if (reply->element[i]->integer <= 0 || reply->element[i]->integer > 10) { + printf("ASSERTION FAILED: Unexpected TTL %lld\n", reply->element[i]->integer); + return 1; + } + } + // REMOVE_END + freeReplyObject(reply); + + // -2 means the field does not exist. + reply = redisCommand(c, "HEXPIRE myhash 10 FIELDS 1 nonexistent"); + // REMOVE_START + CHECK_REPLY(reply); + // REMOVE_END + printf("%lld\n", reply->element[0]->integer); + // >>> -2 + // REMOVE_START + if (reply->element[0]->integer != -2) { + printf("ASSERTION FAILED: Expected -2, got %lld\n", reply->element[0]->integer); + return 1; + } + // REMOVE_END + freeReplyObject(reply); + // STEP_END + + // REMOVE_START + reply = redisCommand(c, "DEL myhash"); + CHECK_REPLY(reply); + freeReplyObject(reply); + // REMOVE_END + // STEP_START disconnect redisFree(c); // STEP_END diff --git a/local_examples/cmds_hash/ioredis/cmds-hash.js b/local_examples/cmds_hash/ioredis/cmds-hash.js index a220afebff..4b1efb69aa 100644 --- a/local_examples/cmds_hash/ioredis/cmds-hash.js +++ b/local_examples/cmds_hash/ioredis/cmds-hash.js @@ -41,6 +41,114 @@ assert.equal(hlenResult, 2); await redis.del('myhash'); // REMOVE_END +// STEP_START hset +const hsetRes1 = await redis.hset('myhash', 'field1', 'Hello'); +console.log(hsetRes1); // >>> 1 + +console.log(await redis.hget('myhash', 'field1')); // >>> Hello + +const hsetRes2 = await redis.hset('myhash', { field2: 'Hi', field3: 'World' }); +console.log(hsetRes2); // >>> 2 + +console.log(await redis.hget('myhash', 'field2')); // >>> Hi +console.log(await redis.hget('myhash', 'field3')); // >>> World + +const hsetRes3 = await redis.hgetall('myhash'); +console.log(hsetRes3); +// >>> { field1: 'Hello', field2: 'Hi', field3: 'World' } +// STEP_END + +// REMOVE_START +assert.equal(hsetRes1, 1); +assert.equal(hsetRes2, 2); +assert.deepEqual(hsetRes3, { field1: 'Hello', field2: 'Hi', field3: 'World' }); +await redis.del('myhash'); +// REMOVE_END + +// STEP_START hget +const hgetRes1 = await redis.hset('myhash', 'field1', 'foo'); +console.log(hgetRes1); // >>> 1 + +const hgetRes2 = await redis.hget('myhash', 'field1'); +console.log(hgetRes2); // >>> foo + +// A field that does not exist reads back as null. +const hgetRes3 = await redis.hget('myhash', 'field2'); +console.log(hgetRes3); // >>> null +// STEP_END + +// REMOVE_START +assert.equal(hgetRes1, 1); +assert.equal(hgetRes2, 'foo'); +assert.equal(hgetRes3, null); +await redis.del('myhash'); +// REMOVE_END + +// STEP_START hgetall +await redis.hset('myhash', { field1: 'Hello', field2: 'World' }); + +const hgetallRes = await redis.hgetall('myhash'); +console.log(hgetallRes); // >>> { field1: 'Hello', field2: 'World' } +// STEP_END + +// REMOVE_START +assert.deepEqual(hgetallRes, { field1: 'Hello', field2: 'World' }); +await redis.del('myhash'); +// REMOVE_END + +// STEP_START hdel +const hdelRes1 = await redis.hset('myhash', 'field1', 'foo'); +console.log(hdelRes1); // >>> 1 + +const hdelRes2 = await redis.hdel('myhash', 'field1'); +console.log(hdelRes2); // >>> 1 + +// Deleting a field that is not there removes nothing. +const hdelRes3 = await redis.hdel('myhash', 'field2'); +console.log(hdelRes3); // >>> 0 +// STEP_END + +// REMOVE_START +assert.equal(hdelRes1, 1); +assert.equal(hdelRes2, 1); +assert.equal(hdelRes3, 0); +await redis.del('myhash'); +// REMOVE_END + +// STEP_START hvals +await redis.hset('myhash', { field1: 'Hello', field2: 'World' }); + +// HVALS follows the hash's field order, which Redis does not promise, so sort. +const hvalsRes = await redis.hvals('myhash'); +console.log(hvalsRes.sort()); // >>> [ 'Hello', 'World' ] +// STEP_END + +// REMOVE_START +assert.deepEqual(hvalsRes.sort(), ['Hello', 'World']); +await redis.del('myhash'); +// REMOVE_END + +// STEP_START hexpire +await redis.hset('myhash', { field1: 'Hello', field2: 'World' }); + +const hexpireRes1 = await redis.hexpire('myhash', 10, 'FIELDS', 2, 'field1', 'field2'); +console.log(hexpireRes1); // >>> [ 1, 1 ] + +const hexpireRes2 = await redis.httl('myhash', 'FIELDS', 2, 'field1', 'field2'); +console.log(hexpireRes2); // >>> [ 10, 10 ] + +// -2 means the field does not exist. +const hexpireRes3 = await redis.hexpire('myhash', 10, 'FIELDS', 1, 'nonexistent'); +console.log(hexpireRes3); // >>> [ -2 ] +// STEP_END + +// REMOVE_START +assert.deepEqual(hexpireRes1, [1, 1]); +assert.deepEqual(hexpireRes3, [-2]); +assert.ok(hexpireRes2.every((ttl) => ttl > 0 && ttl <= 10)); +await redis.del('myhash'); +// REMOVE_END + // HIDE_START redis.disconnect(); // HIDE_END diff --git a/local_examples/cmds_string/hiredis/cmds_string.c b/local_examples/cmds_string/hiredis/cmds_string.c index 0e6d065c35..29cb73d361 100644 --- a/local_examples/cmds_string/hiredis/cmds_string.c +++ b/local_examples/cmds_string/hiredis/cmds_string.c @@ -5,6 +5,19 @@ #include #include #include + +// REMOVE_START +// Fail loudly on a NULL or error reply. hiredis returns an error REPLY (not a +// connection error) for things like a bad command, and the examples would +// otherwise print a wrong value and still exit 0 — a green harness run that +// proves nothing. Kept in a REMOVE block so the published example stays plain. +#define CHECK_REPLY(r) do { \ + if ((r) == NULL || (r)->type == REDIS_REPLY_ERROR) { \ + printf("REDIS ERROR: %s\n", (r) ? (r)->str : "no reply from server"); \ + return 1; \ + } \ +} while (0) +// REMOVE_END // STEP_END int main(int argc, char **argv) { @@ -24,17 +37,27 @@ int main(int argc, char **argv) { // REMOVE_START redisReply *cleanup = redisCommand(c, "DEL key1 key2 mykey nonexisting"); + CHECK_REPLY(cleanup); freeReplyObject(cleanup); // REMOVE_END // STEP_START mget redisReply *reply = redisCommand(c, "SET key1 Hello"); + // REMOVE_START + CHECK_REPLY(reply); + // REMOVE_END freeReplyObject(reply); reply = redisCommand(c, "SET key2 World"); + // REMOVE_START + CHECK_REPLY(reply); + // REMOVE_END freeReplyObject(reply); reply = redisCommand(c, "MGET key1 key2 nonexisting"); + // REMOVE_START + CHECK_REPLY(reply); + // REMOVE_END for (size_t i = 0; i < reply->elements; i++) { if (i > 0) { @@ -70,11 +93,17 @@ int main(int argc, char **argv) { // STEP_START incr reply = redisCommand(c, "SET mykey 10"); + // REMOVE_START + CHECK_REPLY(reply); + // REMOVE_END printf("%s\n", reply->str); // >>> OK freeReplyObject(reply); reply = redisCommand(c, "INCR mykey"); + // REMOVE_START + CHECK_REPLY(reply); + // REMOVE_END printf("%lld\n", reply->integer); // >>> 11 // REMOVE_START @@ -85,6 +114,9 @@ int main(int argc, char **argv) { freeReplyObject(reply); reply = redisCommand(c, "GET mykey"); + // REMOVE_START + CHECK_REPLY(reply); + // REMOVE_END printf("%s\n", reply->str); // >>> 11 // REMOVE_START @@ -96,6 +128,9 @@ int main(int argc, char **argv) { // STEP_END redisReply *cleanup2 = redisCommand(c, "DEL key1 key2 mykey nonexisting"); + // REMOVE_START + CHECK_REPLY(cleanup2); + // REMOVE_END freeReplyObject(cleanup2); // STEP_START disconnect