From b94cec2f8405b7878e2b0fdcaf42b6b719841f35 Mon Sep 17 00:00:00 2001 From: Oiseje Ojeikere Date: Wed, 6 Sep 2023 19:55:25 +0200 Subject: [PATCH 1/4] feat: add zunion command implementation --- deno.lock | 33 ++++++++++++++++++++++ pkg/commands/mod.ts | 1 + pkg/commands/zunion.ts | 64 ++++++++++++++++++++++++++++++++++++++++++ pkg/redis.ts | 7 +++++ 4 files changed, 105 insertions(+) create mode 100644 pkg/commands/zunion.ts diff --git a/deno.lock b/deno.lock index 3710e3b7..f6abf3b9 100644 --- a/deno.lock +++ b/deno.lock @@ -97,6 +97,39 @@ "https://deno.land/std@0.177.0/testing/_test_suite.ts": "30f018feeb3835f12ab198d8a518f9089b1bcb2e8c838a8b615ab10d5005465c", "https://deno.land/std@0.177.0/testing/asserts.ts": "984ab0bfb3faeed92ffaa3a6b06536c66811185328c5dd146257c702c41b01ab", "https://deno.land/std@0.177.0/testing/bdd.ts": "c5ca6d85940dbcc19b4d2bc3608d49ab65d81470aa91306d5efa4b0d5c945731", + "https://deno.land/std@0.201.0/assert/_constants.ts": "8a9da298c26750b28b326b297316cdde860bc237533b07e1337c021379e6b2a9", + "https://deno.land/std@0.201.0/assert/_diff.ts": "1a3c044aedf77647d6cac86b798c6417603361b66b54c53331b312caeb447aea", + "https://deno.land/std@0.201.0/assert/_format.ts": "a69126e8a469009adf4cf2a50af889aca364c349797e63174884a52ff75cf4c7", + "https://deno.land/std@0.201.0/assert/assert.ts": "9a97dad6d98c238938e7540736b826440ad8c1c1e54430ca4c4e623e585607ee", + "https://deno.land/std@0.201.0/assert/assert_almost_equals.ts": "e15ca1f34d0d5e0afae63b3f5d975cbd18335a132e42b0c747d282f62ad2cd6c", + "https://deno.land/std@0.201.0/assert/assert_array_includes.ts": "6856d7f2c3544bc6e62fb4646dfefa3d1df5ff14744d1bca19f0cbaf3b0d66c9", + "https://deno.land/std@0.201.0/assert/assert_equals.ts": "d8ec8a22447fbaf2fc9d7c3ed2e66790fdb74beae3e482855d75782218d68227", + "https://deno.land/std@0.201.0/assert/assert_exists.ts": "407cb6b9fb23a835cd8d5ad804e2e2edbbbf3870e322d53f79e1c7a512e2efd7", + "https://deno.land/std@0.201.0/assert/assert_false.ts": "a9962749f4bf5844e3fa494257f1de73d69e4fe0e82c34d0099287552163a2dc", + "https://deno.land/std@0.201.0/assert/assert_greater.ts": "ae2158a2d19313bf675bf7251d31c6dc52973edb12ac64ac8fc7064152af3e63", + "https://deno.land/std@0.201.0/assert/assert_greater_or_equal.ts": "1439da5ebbe20855446cac50097ac78b9742abe8e9a43e7de1ce1426d556e89c", + "https://deno.land/std@0.201.0/assert/assert_instance_of.ts": "3aedb3d8186e120812d2b3a5dea66a6e42bf8c57a8bd927645770bd21eea554c", + "https://deno.land/std@0.201.0/assert/assert_is_error.ts": "c21113094a51a296ffaf036767d616a78a2ae5f9f7bbd464cd0197476498b94b", + "https://deno.land/std@0.201.0/assert/assert_less.ts": "aec695db57db42ec3e2b62e97e1e93db0063f5a6ec133326cc290ff4b71b47e4", + "https://deno.land/std@0.201.0/assert/assert_less_or_equal.ts": "5fa8b6a3ffa20fd0a05032fe7257bf985d207b85685fdbcd23651b70f928c848", + "https://deno.land/std@0.201.0/assert/assert_match.ts": "c4083f80600bc190309903c95e397a7c9257ff8b5ae5c7ef91e834704e672e9b", + "https://deno.land/std@0.201.0/assert/assert_not_equals.ts": "9f1acab95bd1f5fc9a1b17b8027d894509a745d91bac1718fdab51dc76831754", + "https://deno.land/std@0.201.0/assert/assert_not_instance_of.ts": "0c14d3dfd9ab7a5276ed8ed0b18c703d79a3d106102077ec437bfe7ed912bd22", + "https://deno.land/std@0.201.0/assert/assert_not_match.ts": "3796a5b0c57a1ce6c1c57883dd4286be13a26f715ea662318ab43a8491a13ab0", + "https://deno.land/std@0.201.0/assert/assert_not_strict_equals.ts": "ca6c6d645e95fbc873d25320efeb8c4c6089a9a5e09f92d7c1c4b6e935c2a6ad", + "https://deno.land/std@0.201.0/assert/assert_object_match.ts": "d8fc2867cfd92eeacf9cea621e10336b666de1874a6767b5ec48988838370b54", + "https://deno.land/std@0.201.0/assert/assert_rejects.ts": "45c59724de2701e3b1f67c391d6c71c392363635aad3f68a1b3408f9efca0057", + "https://deno.land/std@0.201.0/assert/assert_strict_equals.ts": "b1f538a7ea5f8348aeca261d4f9ca603127c665e0f2bbfeb91fa272787c87265", + "https://deno.land/std@0.201.0/assert/assert_string_includes.ts": "b821d39ebf5cb0200a348863c86d8c4c4b398e02012ce74ad15666fc4b631b0c", + "https://deno.land/std@0.201.0/assert/assert_throws.ts": "63784e951475cb7bdfd59878cd25a0931e18f6dc32a6077c454b2cd94f4f4bcd", + "https://deno.land/std@0.201.0/assert/assertion_error.ts": "4d0bde9b374dfbcbe8ac23f54f567b77024fb67dbb1906a852d67fe050d42f56", + "https://deno.land/std@0.201.0/assert/equal.ts": "9f1a46d5993966d2596c44e5858eec821859b45f783a5ee2f7a695dfc12d8ece", + "https://deno.land/std@0.201.0/assert/fail.ts": "c36353d7ae6e1f7933d45f8ea51e358c8c4b67d7e7502028598fe1fea062e278", + "https://deno.land/std@0.201.0/assert/mod.ts": "37c49a26aae2b254bbe25723434dc28cd7532e444cf0b481a97c045d110ec085", + "https://deno.land/std@0.201.0/assert/unimplemented.ts": "d56fbeecb1f108331a380f72e3e010a1f161baa6956fd0f7cf3e095ae1a4c75a", + "https://deno.land/std@0.201.0/assert/unreachable.ts": "4600dc0baf7d9c15a7f7d234f00c23bca8f3eba8b140286aaca7aa998cf9a536", + "https://deno.land/std@0.201.0/fmt/colors.ts": "87544aa2bc91087bb37f9c077970c85bfb041b48e4c37356129d7b450a415b6f", + "https://deno.land/std@0.201.0/testing/asserts.ts": "b4e4b1359393aeff09e853e27901a982c685cb630df30426ed75496961931946", "https://deno.land/x/base64@v0.2.1/base.ts": "47dc8d68f07dc91524bdd6db36eccbe59cf4d935b5fc09f27357a3944bb3ff7b", "https://deno.land/x/base64@v0.2.1/base64url.ts": "18bbf879b31f1f32cca8adaa2b6885ae325c2cec6a66c5817b684ca12c46ad5e", "https://deno.land/x/code_block_writer@11.0.0/comment_char.ts": "22b66890bbdf7a2d59777ffd8231710c1fda1c11fadada67632a596937a1a314", diff --git a/pkg/commands/mod.ts b/pkg/commands/mod.ts index 30d8b62c..10bcabd9 100644 --- a/pkg/commands/mod.ts +++ b/pkg/commands/mod.ts @@ -137,4 +137,5 @@ export * from "./zremrangebyscore.ts"; export * from "./zrevrank.ts"; export * from "./zscan.ts"; export * from "./zscore.ts"; +export * from "./zunion.ts"; export * from "./zunionstore.ts"; diff --git a/pkg/commands/zunion.ts b/pkg/commands/zunion.ts new file mode 100644 index 00000000..1c95311f --- /dev/null +++ b/pkg/commands/zunion.ts @@ -0,0 +1,64 @@ +import { Command, CommandOptions } from "./command.ts"; + +export type ZUnionCommandOptions = + & { + withScores?: boolean; + aggregate?: "sum" | "min" | "max"; + } + & ( + | { weight: number; weights?: never } + | { weight?: never; weights: number[] } + | { weight?: never; weights?: never } + ); + +/** + * @see https://redis.io/commands/zunion + */ +export class ZUnionCommand + extends Command { + constructor( + cmd: [ + numKeys: 1, + key: string, + opts?: ZUnionCommandOptions, + ], + cmdOpts?: CommandOptions, + ); + constructor( + cmd: [ + numKeys: number, + keys: string[], + opts?: ZUnionCommandOptions, + ], + cmdOpts?: CommandOptions, + ); + constructor( + [numKeys, keyOrKeys, opts]: [ + numKeys: number, + keyOrKeys: string | string[], + opts?: ZUnionCommandOptions, + ], + cmdOpts?: CommandOptions, + ) { + const command: unknown[] = ["zunion", numKeys]; + if (Array.isArray(keyOrKeys)) { + command.push(...keyOrKeys); + } else { + command.push(keyOrKeys); + } + if (opts) { + if ("weights" in opts && opts.weights) { + command.push("weights", ...opts.weights); + } else if ("weight" in opts && typeof opts.weight === "number") { + command.push("weights", opts.weight); + } + if ("aggregate" in opts) { + command.push("aggregate", opts.aggregate); + } + if (opts?.withScores) { + command.push("withscores"); + } + } + super(command, cmdOpts); + } +} diff --git a/pkg/redis.ts b/pkg/redis.ts index d717b498..0fcea859 100644 --- a/pkg/redis.ts +++ b/pkg/redis.ts @@ -143,6 +143,7 @@ import { ZRevRankCommand, ZScanCommand, ZScoreCommand, + ZUnionCommand, ZUnionStoreCommand, } from "./commands/mod.ts"; import { Requester, UpstashRequest, UpstashResponse } from "./http.ts"; @@ -1166,6 +1167,12 @@ export class Redis { zscore = (key: string, member: TData) => new ZScoreCommand([key, member], this.opts).exec(this.client); + /** + * @see https://redis.io/commands/zunion + */ + zunion = (...args: CommandArgs) => + new ZUnionCommand(args, this.opts).exec(this.client); + /** * @see https://redis.io/commands/zunionstore */ From 6951e871f0b95c10e62055e5c6ae4b919de431c0 Mon Sep 17 00:00:00 2001 From: Oiseje Ojeikere Date: Wed, 6 Sep 2023 19:56:55 +0200 Subject: [PATCH 2/4] test: add zunion tests --- pkg/commands/zunion.test.ts | 261 ++++++++++++++++++++++++++++++++++++ 1 file changed, 261 insertions(+) create mode 100644 pkg/commands/zunion.test.ts diff --git a/pkg/commands/zunion.test.ts b/pkg/commands/zunion.test.ts new file mode 100644 index 00000000..79ed19c7 --- /dev/null +++ b/pkg/commands/zunion.test.ts @@ -0,0 +1,261 @@ +import { keygen, newHttpClient, randomID } from "../test-utils.ts"; + +import { afterAll } from "https://deno.land/std@0.177.0/testing/bdd.ts"; +import { assertEquals } from "https://deno.land/std@0.177.0/testing/asserts.ts"; +import { ZUnionCommand } from "./zunion.ts"; +import { ZAddCommand } from "./zadd.ts"; + +const client = newHttpClient(); + +const { newKey, cleanup } = keygen(); +afterAll(cleanup); + +Deno.test("command format", async (t) => { + await t.step("without options", async (t) => { + await t.step("builds the correct command", () => { + assertEquals(new ZUnionCommand([1, "key"]).command, [ + "zunion", + 1, + "key", + ]); + }); + }); + await t.step("with multiple keys", async (t) => { + await t.step("builds the correct command", () => { + assertEquals( + new ZUnionCommand([2, ["key1", "key2"]]).command, + ["zunion", 2, "key1", "key2"], + ); + }); + }); + await t.step("with single weight", async (t) => { + await t.step("builds the correct command", () => { + assertEquals( + new ZUnionCommand([1, "key", { weight: 4 }]) + .command, + ["zunion", 1, "key", "weights", 4], + ); + }); + }); + await t.step("with multiple weights", async (t) => { + await t.step("builds the correct command", () => { + assertEquals( + new ZUnionCommand([2, ["key1", "key2"], { + weights: [2, 3], + }]).command, + [ + "zunion", + 2, + "key1", + "key2", + "weights", + 2, + 3, + ], + ); + }); + await t.step("with aggregate", async (t) => { + await t.step("sum", async (t) => { + await t.step("builds the correct command", () => { + assertEquals( + new ZUnionCommand([1, "key", { + aggregate: "sum", + }]).command, + ["zunion", 1, "key", "aggregate", "sum"], + ); + }); + }); + await t.step("min", async (t) => { + await t.step("builds the correct command", () => { + assertEquals( + new ZUnionCommand([1, "key", { + aggregate: "min", + }]).command, + ["zunion", 1, "key", "aggregate", "min"], + ); + }); + }); + await t.step("max", async (t) => { + await t.step("builds the correct command", () => { + assertEquals( + new ZUnionCommand([1, "key", { + aggregate: "max", + }]).command, + ["zunion", 1, "key", "aggregate", "max"], + ); + }); + }); + }); + await t.step("complex", async (t) => { + await t.step("builds the correct command", () => { + assertEquals( + new ZUnionCommand([2, ["key1", "key2"], { + weights: [4, 2], + aggregate: "max", + }]).command, + [ + "zunion", + 2, + "key1", + "key2", + "weights", + 4, + 2, + "aggregate", + "max", + ], + ); + }); + }); + }); +}); + +Deno.test("without options", async (t) => { + await t.step("returns the union", async () => { + const key1 = newKey(); + const key2 = newKey(); + const score1 = 1; + const member1 = randomID(); + const score2 = 2; + const member2 = randomID(); + + await new ZAddCommand([key1, { score: score1, member: member1 }]).exec( + client, + ); + await new ZAddCommand([key2, { score: score2, member: member2 }]).exec( + client, + ); + + const res = await new ZUnionCommand([2, [key1, key2]]) + .exec( + client, + ); + + assertEquals(res.length, 2); + assertEquals(res?.sort(), [member1, member2].sort()); + }); +}); + +Deno.test("with weights", async (t) => { + await t.step("returns the set", async () => { + const key1 = newKey(); + const key2 = newKey(); + const score1 = 1; + const member1 = randomID(); + const score2 = 2; + const member2 = randomID(); + + await new ZAddCommand([key1, { score: score1, member: member1 }]).exec( + client, + ); + + await new ZAddCommand([key2, { score: score2, member: member2 }]).exec( + client, + ); + + const res = await new ZUnionCommand([2, [key1, key2], { + weights: [2, 3], + }]).exec(client); + + assertEquals(res.length, 2); + }); +}); + +Deno.test("aggregate", async (t) => { + await t.step("sum", async (t) => { + await t.step("returns the set", async () => { + const key1 = newKey(); + const key2 = newKey(); + const score1 = 1; + const member1 = randomID(); + const score2 = 2; + const member2 = randomID(); + + await new ZAddCommand([key1, { score: score1, member: member1 }]).exec( + client, + ); + await new ZAddCommand([key2, { score: score2, member: member2 }]).exec( + client, + ); + + const res = await new ZUnionCommand([2, [key1, key2], { + aggregate: "sum", + }]).exec(client); + + assertEquals(Array.isArray(res), true); + assertEquals(res.length, 2); + }); + }); + await t.step("min", async (t) => { + await t.step("returns the set ", async () => { + const key1 = newKey(); + const key2 = newKey(); + const score1 = 1; + const member1 = randomID(); + const score2 = 2; + const member2 = randomID(); + + await new ZAddCommand([key1, { score: score1, member: member1 }]).exec( + client, + ); + await new ZAddCommand([key2, { score: score2, member: member2 }]).exec( + client, + ); + + const res = await new ZUnionCommand([2, [key1, key2], { + aggregate: "min", + }]).exec(client); + assertEquals(res.length, 2); + }); + }); + await t.step("max", async (t) => { + await t.step("returns the set ", async () => { + const key1 = newKey(); + const key2 = newKey(); + const score1 = 1; + const member1 = randomID(); + const score2 = 2; + const member2 = randomID(); + + await new ZAddCommand([key1, { score: score1, member: member1 }]).exec( + client, + ); + await new ZAddCommand([key2, { score: score2, member: member2 }]).exec( + client, + ); + + const res = await new ZUnionCommand([2, [key1, key2], { + aggregate: "max", + }]).exec(client); + assertEquals(res.length, 2); + }); + }); +}); + +Deno.test("withscores", async (t) => { + await t.step("returns the set", async () => { + const key1 = newKey(); + const score1 = 1; + const member1 = randomID(); + + const key2 = newKey(); + const member2 = randomID(); + const score2 = 5; + + await new ZAddCommand([key1, { score: score1, member: member1 }]).exec( + client, + ); + + await new ZAddCommand([key2, { score: score2, member: member2 }]).exec( + client, + ); + + const res = await new ZUnionCommand([2, [key1, key2], { + withScores: true, + }]).exec(client); + + assertEquals(res.length, 4); + assertEquals(res[0], member1); + assertEquals(res[1], score1); + }); +}); From 3331a8721f25193d4076a5858ea01cd04cd644c9 Mon Sep 17 00:00:00 2001 From: Oiseje Ojeikere Date: Wed, 6 Sep 2023 19:58:50 +0200 Subject: [PATCH 3/4] feat: add zunion to pipeline command --- pkg/pipeline.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/pkg/pipeline.ts b/pkg/pipeline.ts index 7f675d26..46585423 100644 --- a/pkg/pipeline.ts +++ b/pkg/pipeline.ts @@ -141,6 +141,7 @@ import { ZRevRankCommand, ZScanCommand, ZScoreCommand, + ZUnionCommand, ZUnionStoreCommand, } from "./commands/mod.ts"; import { Command, CommandOptions } from "./commands/command.ts"; @@ -1079,6 +1080,12 @@ export class Pipeline[] = []> { zunionstore = (...args: CommandArgs) => this.chain(new ZUnionStoreCommand(args, this.commandOptions)); + /** + * @see https://redis.io/commands/zunion + */ + zunion = (...args: CommandArgs) => + this.chain(new ZUnionCommand(args, this.commandOptions)); + /** * @see https://redis.io/commands/?group=json */ From 2244c700417a64fc47e1b95ba6e2cb0df81f4eac Mon Sep 17 00:00:00 2001 From: Oiseje Ojeikere Date: Wed, 6 Sep 2023 19:59:19 +0200 Subject: [PATCH 4/4] test: add zunion test for pipeline command --- pkg/pipeline.test.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pkg/pipeline.test.ts b/pkg/pipeline.test.ts index 8f6ad3b2..e83d2b76 100644 --- a/pkg/pipeline.test.ts +++ b/pkg/pipeline.test.ts @@ -217,9 +217,10 @@ Deno.test("use all the things", async (t) => { .zscan(newKey(), 0) .zscore(newKey(), "member") .zunionstore(newKey(), 1, [newKey()]) + .zunion(1, [newKey()]) .json.set(newKey(), "$", { hello: "world" }); const res = await p.exec(); - assertEquals(res.length, 120); + assertEquals(res.length, 121); }); });