fix(redis): pair withscores members and scores in the console

This commit is contained in:
Melody Ruch authored and GitHub committed 2026-08-26 15:31:42 +08:00
1 parent 8475c84d48
commit 8c735206e0
3 files changed
+103 -2

No files matched your search

+74
View File
@@ -0,0 +1,74 @@
import { describe, expect, it } from "vitest";
import { redisCommandResultToQueryResult } from "@/lib/redis/redisQueryResult";
describe("redisCommandResultToQueryResult", () => {
it("pairs member/score rows for ZREVRANGE ... WITHSCORES instead of one row per array element", () => {
const flat = ["carol", "300", "bob", "200", "alice", "100"];
const result = redisCommandResultToQueryResult(flat, 5, "ZREVRANGE issue7229_repro:zset 0 -1 WITHSCORES");
expect(result.columns).toEqual(["member", "score"]);
expect(result.rows).toEqual([
["carol", "300"],
["bob", "200"],
["alice", "100"],
]);
expect(result.affected_rows).toBe(3);
});
it("pairs member/score rows for ZRANGE ... WITHSCORES (lowercase modifier)", () => {
const flat = ["alice", "1.5"];
const result = redisCommandResultToQueryResult(flat, 5, "zrange myset 0 -1 withscores");
expect(result.columns).toEqual(["member", "score"]);
expect(result.rows).toEqual([["alice", "1.5"]]);
});
it("does not pair a plain ZRANGE result (no WITHSCORES modifier)", () => {
const flat = ["alice", "bob"];
const result = redisCommandResultToQueryResult(flat, 5, "ZRANGE myset 0 -1");
expect(result.columns).toEqual(["(index)", "value"]);
expect(result.rows).toEqual([
[1, "alice"],
[2, "bob"],
]);
});
it("still pairs field/value rows for HGETALL when given the full raw command text (not just the bare command head)", () => {
const flat = ["name", "alice", "age", "30"];
const result = redisCommandResultToQueryResult(flat, 5, "HGETALL myhash");
expect(result.columns).toEqual(["field", "value"]);
expect(result.rows).toEqual([
["name", "alice"],
["age", "30"],
]);
});
it("does not pair when a non-sorted-set command merely contains WITHSCORES in a key", () => {
const flat = ["a", "b", "c"];
const result = redisCommandResultToQueryResult(flat, 5, "LRANGE my:WITHSCORES:list 0 -1");
expect(result.columns).toEqual(["(index)", "value"]);
expect(result.rows).toEqual([
[1, "a"],
[2, "b"],
[3, "c"],
]);
});
it("does not pair SMEMBERS when WITHSCORES appears as an argument", () => {
const flat = ["x", "y"];
const result = redisCommandResultToQueryResult(flat, 5, "SMEMBERS WITHSCORES");
expect(result.columns).toEqual(["(index)", "value"]);
expect(result.rows).toEqual([
[1, "x"],
[2, "y"],
]);
});
it("pairs member/score rows for ZRANGEBYSCORE ... WITHSCORES", () => {
const flat = ["alice", "1", "bob", "2"];
const result = redisCommandResultToQueryResult(flat, 5, "ZRANGEBYSCORE myzset -inf +inf WITHSCORES");
expect(result.columns).toEqual(["member", "score"]);
expect(result.rows).toEqual([
["alice", "1"],
["bob", "2"],
]);
});
});
+28 -1
View File
@@ -2,9 +2,24 @@ import type { QueryResult } from "@/types/database";
import { formatRedisCommandResult } from "@/lib/redis/redisValuePresentation";
const KEY_VALUE_COMMANDS = new Set(["HGETALL"]);
// Sorted-set commands whose WITHSCORES modifier returns a flat member/score array.
const WITHSCORES_COMMANDS = new Set(["ZRANGE", "ZREVRANGE", "ZRANGEBYSCORE", "ZREVRANGEBYSCORE", "ZRANDMEMBER", "ZDIFF", "ZINTER", "ZUNION"]);
const WITHSCORES_MODIFIER = /\bWITHSCORES\b/i;
function commandHead(command: string): string {
return command.trim().split(/\s+/, 1)[0]?.toUpperCase() ?? "";
}
function isKeyValueCommand(command: string): boolean {
return KEY_VALUE_COMMANDS.has(command.toUpperCase().trim());
return KEY_VALUE_COMMANDS.has(commandHead(command));
}
// ZRANGE/ZREVRANGE/ZRANGEBYSCORE/ZDIFF/ZINTER/ZUNION/... with a WITHSCORES modifier
// return a flat [member1, score1, member2, score2, ...] array — pair it up instead of
// dumping each element as its own row. Gate on the command head so a key or argument
// that merely contains the token WITHSCORES cannot trigger pairing.
function hasWithScoresModifier(command: string): boolean {
return WITHSCORES_COMMANDS.has(commandHead(command)) && WITHSCORES_MODIFIER.test(command);
}
export function redisCommandResultToQueryResult(value: unknown, elapsedMs: number, command?: string): QueryResult {
@@ -20,6 +35,18 @@ export function redisCommandResultToQueryResult(value: unknown, elapsedMs: numbe
execution_time_ms: Math.max(0, Math.round(elapsedMs)),
};
}
if (Array.isArray(value) && command && hasWithScoresModifier(command)) {
const rows: (string | number | boolean | null)[][] = [];
for (let i = 0; i + 1 < value.length; i += 2) {
rows.push([formatRedisCommandResult(value[i]), formatRedisCommandResult(value[i + 1])]);
}
return {
columns: ["member", "score"],
rows,
affected_rows: rows.length,
execution_time_ms: Math.max(0, Math.round(elapsedMs)),
};
}
// INFO commands in cluster mode → [[node_addr, info_text], ...] pairs.
// Render as a two-column table with the addr as the index and info text as the value.
if (Array.isArray(value) && value.length > 0 && value.every((item) => Array.isArray(item) && item.length === 2 && typeof item[0] === "string" && typeof item[1] === "string")) {
+1 -1
View File
@@ -4652,7 +4652,7 @@ export const useQueryStore = defineStore("query", () => {
const sourceRange = commandRange && options?.sourceOffset !== undefined ? { from: options.sourceOffset + commandRange.from, to: options.sourceOffset + commandRange.to } : undefined;
try {
const result = await api.redisExecuteCommand(executionConnectionId, currentDb, command, skipSafety);
allResults.push(markQueryResultRowsRaw(annotateQueryResultSource(redisCommandResultToQueryResult(result.value, performance.now() - startedAt, result.command), command, undefined, undefined, sourceRange)));
allResults.push(markQueryResultRowsRaw(annotateQueryResultSource(redisCommandResultToQueryResult(result.value, performance.now() - startedAt, command), command, undefined, undefined, sourceRange)));
// Track db switches from SELECT N so later commands in the same batch run on the right db.
currentDb = nextRedisCommandDb(currentDb, command, result.value);
// Write commands (SET/DEL/...) mutate the key set — drop the cached key-name completion