dbx/apps/desktop/src/lib/redisCommandTable.ts

464 lines
16 KiB
TypeScript

/**
* Static Redis command metadata, distilled from the official Redis command table
* (redis-doc `commands.json` / the `COMMAND` output). Kept offline and lightweight:
* we only retain what the editor syntax diagnostics need — `arity` and `group` —
* plus a `safety` hint for danger/write-command highlighting.
*
* Arity semantics (matches the Redis `COMMAND` reply, command name INCLUDED in the count):
* arity > 0 → the command takes exactly `arity` tokens (e.g. GET key → arity 2).
* arity < 0 → the command takes AT LEAST `-arity` tokens (e.g. MSET k v [k v ...] → arity -3).
*
* `safety` is aligned with the token-level sets in `redisCommandSafety.ts` (used to gate
* execution). Here it is recorded per-command (including subcommands) so diagnostics can
* be more precise than the first-token classification.
*
* Subcommands are keyed as `"MAIN SUB"` in UPPER CASE (e.g. `"CONFIG GET"`, `"XGROUP CREATE"`).
*/
import type { RedisCommandSafety } from "@/lib/redisCommandSafety";
export interface RedisCommandSpec {
/** Token count including the command name. Positive = exact, negative = minimum (-N). */
arity: number;
/** Redis command group: string/list/hash/.../server. */
group: string;
/** Danger level for diagnostic highlighting. */
safety: RedisCommandSafety;
}
type Spec = [arity: number, group: string, safety?: RedisCommandSafety];
// Compact tuple form → expanded into the record below.
const RAW_COMMANDS: Record<string, Spec> = {
// ---- String ----
APPEND: [3, "string", "confirm"],
DECR: [2, "string", "confirm"],
DECRBY: [3, "string", "confirm"],
GET: [2, "string"],
GETDEL: [2, "string", "confirm"],
GETEX: [-2, "string"],
GETRANGE: [4, "string"],
GETSET: [3, "string", "confirm"],
INCR: [2, "string", "confirm"],
INCRBY: [3, "string", "confirm"],
INCRBYFLOAT: [3, "string", "confirm"],
LCS: [-3, "string"],
MGET: [-2, "string"],
MSET: [-3, "string", "confirm"],
MSETNX: [-3, "string", "confirm"],
PSETEX: [4, "string", "confirm"],
SET: [-3, "string", "confirm"],
SETEX: [4, "string", "confirm"],
SETNX: [3, "string", "confirm"],
SETRANGE: [4, "string", "confirm"],
STRLEN: [2, "string"],
// ---- Generic key ----
COPY: [-3, "generic", "confirm"],
DEL: [-2, "generic", "confirm"],
DUMP: [2, "generic"],
EXISTS: [-2, "generic"],
EXPIRE: [-3, "generic", "confirm"],
EXPIREAT: [-3, "generic", "confirm"],
KEYS: [2, "generic", "blocked"],
MIGRATE: [-6, "generic", "blocked"],
MOVE: [3, "generic", "confirm"],
OBJECT_ENCODING: [3, "generic"],
OBJECT_FREQ: [3, "generic"],
OBJECT_IDLETIME: [3, "generic"],
OBJECT_REFCOUNT: [3, "generic"],
OBJECT_HELP: [2, "generic"],
PERSIST: [2, "generic", "confirm"],
PEXPIRE: [-3, "generic", "confirm"],
PEXPIREAT: [-3, "generic", "confirm"],
PTTL: [2, "generic"],
RANDOMKEY: [1, "generic"],
RENAME: [3, "generic", "confirm"],
RENAMENX: [3, "generic", "confirm"],
RESTORE: [-4, "generic", "confirm"],
SCAN: [-2, "generic"],
SORT: [-2, "generic", "confirm"],
SORT_RO: [-2, "generic"],
TOUCH: [-2, "generic"],
TTL: [2, "generic"],
TYPE: [2, "generic"],
UNLINK: [-2, "generic", "confirm"],
WAIT: [3, "generic"],
WAITAOF: [4, "generic"],
// ---- List ----
BLMOVE: [6, "list"],
BLMPOP: [-4, "list"],
BLPOP: [-3, "list"],
BRPOP: [-3, "list"],
BRPOPLPUSH: [4, "list"],
LINDEX: [3, "list"],
LINSERT: [5, "list", "confirm"],
LLEN: [2, "list"],
LMOVE: [5, "list", "confirm"],
LMPOP: [-4, "list", "confirm"],
LPOP: [-2, "list", "confirm"],
LPOS: [-2, "list"],
LPUSH: [-3, "list", "confirm"],
LPUSHX: [-3, "list", "confirm"],
LRANGE: [4, "list"],
LREM: [4, "list", "confirm"],
LSET: [4, "list", "confirm"],
LTRIM: [4, "list", "confirm"],
RPOP: [-2, "list", "confirm"],
RPOPLPUSH: [4, "list", "confirm"],
RPUSH: [-3, "list", "confirm"],
RPUSHX: [-3, "list", "confirm"],
// ---- Hash ----
HDEL: [-3, "hash", "confirm"],
HEXISTS: [3, "hash"],
HGET: [3, "hash"],
HGETALL: [2, "hash"],
HINCRBY: [4, "hash", "confirm"],
HINCRBYFLOAT: [4, "hash", "confirm"],
HKEYS: [2, "hash"],
HLEN: [2, "hash"],
HMGET: [-3, "hash"],
HMSET: [-4, "hash", "confirm"],
HRANDFIELD: [-2, "hash"],
HSCAN: [-3, "hash"],
HSET: [-4, "hash", "confirm"],
HSETNX: [4, "hash", "confirm"],
HSTRLEN: [3, "hash"],
HVALS: [2, "hash"],
// ---- Set ----
SADD: [-3, "set", "confirm"],
SCARD: [2, "set"],
SDIFF: [-2, "set"],
SDIFFSTORE: [-3, "set", "confirm"],
SINTER: [-2, "set"],
SINTERCARD: [-3, "set"],
SINTERSTORE: [-3, "set", "confirm"],
SISMEMBER: [3, "set"],
SMEMBERS: [2, "set"],
SMISMEMBER: [-3, "set"],
SMOVE: [4, "set", "confirm"],
SPOP: [-2, "set", "confirm"],
SRANDMEMBER: [-2, "set"],
SREM: [-3, "set", "confirm"],
SSCAN: [-3, "set"],
SUNION: [-2, "set"],
SUNIONSTORE: [-3, "set", "confirm"],
// ---- Sorted set ----
BZMPOP: [-3, "zset"],
BZPOPMAX: [-3, "zset"],
BZPOPMIN: [-3, "zset"],
ZADD: [-4, "zset", "confirm"],
ZCARD: [2, "zset"],
ZCOUNT: [4, "zset"],
ZDIFF: [-3, "zset"],
ZDIFFSTORE: [-4, "zset", "confirm"],
ZINCRBY: [4, "zset", "confirm"],
ZINTER: [-3, "zset"],
ZINTERCARD: [-3, "zset"],
ZINTERSTORE: [-4, "zset", "confirm"],
ZLEXCOUNT: [4, "zset"],
ZMPOP: [-3, "zset", "confirm"],
ZMSCORE: [-3, "zset"],
ZPOPMAX: [-2, "zset", "confirm"],
ZPOPMIN: [-2, "zset", "confirm"],
ZRANGE: [-4, "zset"],
ZRANGEBYLEX: [-4, "zset"],
ZRANGEBYSCORE: [-4, "zset"],
ZRANGESTORE: [-5, "zset", "confirm"],
ZRANK: [-3, "zset"],
ZREM: [-3, "zset", "confirm"],
ZREMRANGEBYLEX: [4, "zset", "confirm"],
ZREMRANGEBYRANK: [4, "zset", "confirm"],
ZREMRANGEBYSCORE: [4, "zset", "confirm"],
ZREVRANGE: [-4, "zset"],
ZREVRANGEBYLEX: [-4, "zset"],
ZREVRANGEBYSCORE: [-4, "zset"],
ZREVRANK: [-3, "zset"],
ZSCAN: [-3, "zset"],
ZSCORE: [3, "zset"],
ZUNION: [-3, "zset"],
ZUNIONSTORE: [-4, "zset", "confirm"],
// ---- Bitmap ----
BITCOUNT: [-2, "bitmap"],
BITFIELD: [-2, "bitmap", "confirm"],
BITFIELD_RO: [-2, "bitmap"],
BITOP: [-4, "bitmap", "confirm"],
BITPOS: [-3, "bitmap"],
GETBIT: [3, "bitmap"],
SETBIT: [4, "bitmap", "confirm"],
// ---- Hyperloglog ----
PFADD: [-2, "hyperloglog", "confirm"],
PFCOUNT: [-2, "hyperloglog"],
PFMERGE: [-2, "hyperloglog", "confirm"],
// ---- Geo ----
GEOADD: [-5, "geo", "confirm"],
GEODIST: [-4, "geo"],
GEOHASH: [-3, "geo"],
GEOPOS: [-3, "geo"],
GEORADIUS: [-6, "geo", "confirm"],
GEORADIUS_RO: [-6, "geo"],
GEORADIUSBYMEMBER: [-5, "geo", "confirm"],
GEORADIUSBYMEMBER_RO: [-5, "geo"],
GEOSEARCH: [-7, "geo"],
GEOSEARCHSTORE: [-8, "geo", "confirm"],
// ---- Stream ----
XACK: [-4, "stream", "confirm"],
XADD: [-5, "stream", "confirm"],
XAUTOCLAIM: [-7, "stream", "confirm"],
XCLAIM: [-6, "stream", "confirm"],
XDEL: [-3, "stream", "confirm"],
"XGROUP CREATE": [-6, "stream", "confirm"],
"XGROUP CREATECONSUMER": [5, "stream", "confirm"],
"XGROUP DELCONSUMER": [4, "stream", "confirm"],
"XGROUP DESTROY": [4, "stream", "confirm"],
"XGROUP HELP": [2, "stream"],
"XGROUP SETID": [-5, "stream", "confirm"],
"XINFO CONSUMERS": [4, "stream"],
"XINFO GROUPS": [3, "stream"],
"XINFO HELP": [2, "stream"],
"XINFO STREAM": [-3, "stream"],
XLEN: [2, "stream"],
XPENDING: [-3, "stream"],
XRANGE: [-4, "stream"],
XREAD: [-4, "stream"],
XREADGROUP: [-7, "stream"],
XREVRANGE: [-4, "stream"],
XSETID: [-3, "stream", "confirm"],
XTRIM: [-4, "stream", "confirm"],
// ---- Pub/Sub ----
PSUBSCRIBE: [-2, "pubsub"],
PUBLISH: [3, "pubsub"],
"PUBSUB CHANNELS": [-2, "pubsub"],
"PUBSUB NUMPAT": [1, "pubsub"],
"PUBSUB NUMSUB": [-2, "pubsub"],
"PUBSUB SHARDCHANNELS": [-2, "pubsub"],
"PUBSUB SHARDNUMSUB": [-2, "pubsub"],
PUNSUBSCRIBE: [-1, "pubsub"],
SPUBLISH: [3, "pubsub"],
SSUBSCRIBE: [2, "pubsub"],
SUBSCRIBE: [-2, "pubsub"],
SUNSUBSCRIBE: [-1, "pubsub"],
UNSUBSCRIBE: [-1, "pubsub"],
// ---- Transactions ----
DISCARD: [1, "transactions"],
EXEC: [1, "transactions"],
MULTI: [1, "transactions"],
UNWATCH: [1, "transactions"],
WATCH: [-2, "transactions"],
// ---- Scripting ----
EVAL: [-3, "scripting", "blocked"],
EVALSHA: [-3, "scripting", "blocked"],
EVAL_RO: [-3, "scripting", "blocked"],
EVALSHA_RO: [-3, "scripting", "blocked"],
FCALL: [-3, "scripting"],
FCALL_RO: [-3, "scripting"],
"FUNCTION DELETE": [3, "scripting", "confirm"],
"FUNCTION DUMP": [2, "scripting"],
"FUNCTION FLUSH": [-2, "scripting", "confirm"],
"FUNCTION LIST": [-2, "scripting"],
"FUNCTION LOAD": [-3, "scripting", "confirm"],
"FUNCTION RESTORE": [-4, "scripting", "confirm"],
"FUNCTION STATS": [1, "scripting"],
"SCRIPT EXISTS": [-2, "scripting", "blocked"],
"SCRIPT FLUSH": [-2, "scripting", "blocked"],
"SCRIPT KILL": [1, "scripting", "blocked"],
"SCRIPT LOAD": [2, "scripting", "blocked"],
// ---- Connection ----
AUTH: [-2, "connection"],
ECHO: [2, "connection"],
HELLO: [-1, "connection"],
PING: [-1, "connection"],
QUIT: [1, "connection"],
RESET: [1, "connection"],
SELECT: [2, "connection"],
"CLIENT CACHING": [3, "connection"],
"CLIENT GETNAME": [1, "connection"],
"CLIENT GETREDIR": [1, "connection"],
"CLIENT ID": [1, "connection"],
"CLIENT INFO": [1, "connection"],
"CLIENT KILL": [-3, "connection", "confirm"],
"CLIENT LIST": [-2, "connection"],
"CLIENT NO-EVICT": [2, "connection"],
"CLIENT NO-TOUCH": [2, "connection"],
"CLIENT PAUSE": [-2, "connection"],
"CLIENT REPLY": [2, "connection"],
"CLIENT SETINFO": [-3, "connection"],
"CLIENT SETNAME": [3, "connection"],
"CLIENT TRACKING": [-3, "connection"],
"CLIENT TRACKINGINFO": [1, "connection"],
"CLIENT UNPAUSE": [1, "connection"],
// ---- Server ----
BGREWRITEAOF: [1, "server"],
BGSAVE: [-1, "server", "blocked"],
"COMMAND COUNT": [1, "server"],
"COMMAND DOCS": [-2, "server"],
"COMMAND GETKEYS": [-3, "server"],
"COMMAND INFO": [-2, "server"],
"COMMAND LIST": [-2, "server"],
"CONFIG GET": [-2, "server", "blocked"],
"CONFIG RESETSTAT": [1, "server", "blocked"],
"CONFIG REWRITE": [1, "server", "blocked"],
"CONFIG SET": [-3, "server", "blocked"],
DBSIZE: [1, "server"],
FAILOVER: [-1, "server", "confirm"],
FLUSHALL: [-1, "server", "blocked"],
FLUSHDB: [-1, "server", "confirm"],
INFO: [-1, "server"],
LASTSAVE: [1, "server"],
"MEMORY USAGE": [-3, "server"],
"MEMORY STATS": [1, "server"],
"MEMORY PURGE": [1, "server"],
"MEMORY DOCS": [1, "server"],
"MODULE LIST": [1, "server", "blocked"],
"MODULE LOAD": [-3, "server", "blocked"],
"MODULE LOADEX": [-3, "server", "blocked"],
"MODULE UNLOAD": [3, "server", "blocked"],
MONITOR: [1, "server"],
REPLICAOF: [3, "server", "blocked"],
ROLE: [1, "server"],
SAVE: [1, "server", "blocked"],
SHUTDOWN: [-1, "server", "blocked"],
SLAVEOF: [3, "server", "blocked"],
"SLOWLOG GET": [-2, "server"],
"SLOWLOG LEN": [1, "server"],
"SLOWLOG RESET": [1, "server"],
"SLOWLOG HELP": [1, "server"],
SWAPDB: [3, "server", "confirm"],
SYNC: [1, "server"],
TIME: [1, "server"],
// ---- Cluster ----
"CLUSTER ADDSLOTS": [-3, "cluster", "confirm"],
"CLUSTER COUNT-FAILURE-REPORTS": [2, "cluster"],
"CLUSTER COUNTKEYSINSLOT": [3, "cluster"],
"CLUSTER DELSLOTS": [-3, "cluster", "confirm"],
"CLUSTER FAILOVER": [-2, "cluster", "confirm"],
"CLUSTER FLUSHSLOTS": [1, "cluster", "confirm"],
"CLUSTER FORGET": [2, "cluster", "confirm"],
"CLUSTER GETKEYSINSLOT": [4, "cluster"],
"CLUSTER INFO": [1, "cluster"],
"CLUSTER KEYSLOT": [2, "cluster"],
"CLUSTER LINKS": [1, "cluster"],
"CLUSTER MEET": [-4, "cluster", "confirm"],
"CLUSTER MYID": [1, "cluster"],
"CLUSTER NODES": [1, "cluster"],
"CLUSTER REPLICAS": [2, "cluster"],
"CLUSTER REPLICATE": [2, "cluster", "confirm"],
"CLUSTER RESET": [-2, "cluster", "confirm"],
"CLUSTER SAVECONFIG": [1, "cluster", "confirm"],
"CLUSTER SET-CONFIG-EPOCH": [2, "cluster", "confirm"],
"CLUSTER SETSLOT": [-4, "cluster", "confirm"],
"CLUSTER SHARDS": [1, "cluster"],
"CLUSTER SLAVES": [2, "cluster"],
"CLUSTER SLOTS": [1, "cluster"],
READONLY: [1, "cluster"],
READWRITE: [1, "cluster"],
};
export const REDIS_COMMAND_TABLE: Record<string, RedisCommandSpec> = Object.fromEntries(Object.entries(RAW_COMMANDS).map(([name, [arity, group, safety]]) => [name.toUpperCase(), { arity, group, safety: safety ?? "allowed" }]));
/**
* Resolve a command spec. Handles two-token subcommands (e.g. `XGROUP CREATE`,
* `CONFIG GET`) by trying `MAIN SUB` first, then falling back to the main token.
*/
export function resolveRedisCommandSpec(argvUpper: readonly string[]): RedisCommandSpec | undefined {
if (argvUpper.length === 0) return undefined;
const main = argvUpper[0];
if (argvUpper.length >= 2) {
const sub = `${main} ${argvUpper[1]}`;
const subSpec = REDIS_COMMAND_TABLE[sub];
if (subSpec) return subSpec;
}
return REDIS_COMMAND_TABLE[main];
}
/**
* Minimal argv tokenizer for the first two tokens of a Redis command line —
* enough to resolve a spec (which only needs the command head, optionally a
* subcommand). Single-quoted, double-quoted and unquoted tokens are supported
* with backslash escapes; quoting errors are tolerated (the token still resolves).
* Kept local so this module stays self-contained (no dependency on the syntax
* diagnostics tokenizer).
*/
function firstRedisArgvUpper(line: string): string[] {
const tokens: string[] = [];
let i = 0;
const n = line.length;
while (i < n && tokens.length < 2) {
while (i < n && /\s/.test(line[i])) i++;
if (i >= n) break;
const quote = line[i] === '"' || line[i] === "'" ? line[i] : "";
let token = "";
let escaping = false;
if (quote) i++;
while (i < n) {
const ch = line[i];
if (escaping) {
token += ch;
escaping = false;
i++;
continue;
}
if (ch === "\\") {
escaping = true;
i++;
continue;
}
if (quote && ch === quote) {
i++;
break;
}
if (!quote && /\s/.test(ch)) break;
token += ch;
i++;
}
tokens.push(token.toUpperCase());
}
return tokens;
}
// Commands marked `safety: "blocked"` because they are dangerous/administrative,
// but which do NOT actually change the key set of the current db — so they should
// NOT trigger key-name completion cache invalidation. (e.g. KEYS is read-only;
// SAVE/BGSAVE/SHUTDOWN/REPLICAOF/SLAVEOF are server admin.) Everything else in the
// `blocked` set (FLUSHALL, MIGRATE, EVAL[ESHA]) may mutate keys and is left in.
const NON_MUTATING_BLOCKED = new Set(["KEYS", "BGSAVE", "SAVE", "SHUTDOWN", "REPLICAOF", "SLAVEOF"]);
/**
* True when a command may change the key set of the db it ran on (adds/edits/
* removes/renames keys, or wipes the db), and therefore the cached key-name
* completion for that db is potentially stale and should be dropped.
*
* Mapping from the diagnostic `safety` field:
* - `confirm` → every write command mutates keys (SET/DEL/INCR/HSET/...).
* - `blocked` → mostly destructive/admin; we keep the ones that may touch keys
* (FLUSHALL, MIGRATE, EVAL[ESHA]) and exclude the read-only/admin
* ones in `NON_MUTATING_BLOCKED`.
* - `allowed` → read-only (GET/LRANGE/SCAN/...), never invalidates.
*
* Commands absent from the table (unknown or read-only extensions) are treated as
* non-mutating so we never thrash the cache on lookups.
*/
export function isRedisMutatingCommand(command: string): boolean {
const argv = firstRedisArgvUpper(command);
const spec = resolveRedisCommandSpec(argv);
if (!spec) return false;
if (spec.safety === "confirm") return true;
if (spec.safety === "blocked") {
return !NON_MUTATING_BLOCKED.has(argv[0]);
}
return false;
}