fix(redis): pair withscores members and scores in the console
This commit is contained in:
1 parent
8475c84d48
commit
8c735206e0
3 files changed
+103
-2
No files matched your search
@@ -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"],
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -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")) {
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user