# EVAL + Lua `EVAL script numkeys key1 ... keyN arg1 ... argM` runs `script` server-side as one atomic operation. the script sees: - `KEYS[1..N]` — the named keys (1-indexed) - `ARGV[1..M]` — the named args - `redis.call(cmd, ...)` — invokes another Redis command from inside the script; raises on error - `redis.pcall(cmd, ...)` — same but returns `{err = "..."}` instead of raising a script either runs to completion or errors; either way, no other client sees any intermediate state. ## why it's lua 5.1 forever redis embeds [Lua 5.1](https://www.lua.org/manual/5.1/) and uses the included sandbox restrictions. 5.1 has been frozen for ~17 years. that's a feature, not a bug — every redis 5.1 script you've ever written keeps working unchanged forever. it also means: - `unpack(table)` is global (in 5.2+ it moved to `table.unpack`) - `#tbl` returns array length but has nil-hole edge cases - no `goto`, no integer type (everything's a double until 5.3) - `pcall`/`xpcall` exist; `error()` works the obvious way if you're writing a redis-compatible engine, embed Lua 5.1 specifically. `mlua` (rust) and `ziglua` (zig) both let you pick the version explicitly — `lang = .lua51` in build.zig.zon. ## script return values redis maps Lua return values back to RESP this way: | lua type | RESP reply | |--------------------|--------------------------| | `nil` | nil | | `true` | integer `1` | | `false` | nil (!) | | number | integer (truncated) | | string | bulk string | | `{ok = "OK"}` | simple string `"+OK"` | | `{err = "msg"}` | error `"-msg"` | | table (sequential) | array (recursive) | `{ok = ...}` and `{err = ...}` are the *only* way for a script to return a status or error reply. otherwise you can only return strings, integers, arrays, or nil. ## composability via redis.call `redis.call` lets the script call any other command. typical pattern: a one-shot "do these N things atomically": ```lua -- schedule.lua if is_immediate == '1' then redis.call('XADD', stream_key, '*', unpack(fields)) else redis.call('HSET', parked_key, unpack(fields)) redis.call('ZADD', queue_key, when, task_key) end redis.call('HSET', runs_key, 'state', 'parked', 'task_key', task_key) return 'SCHEDULED' ``` inside `EVAL`, redis is *still single-threaded*. no other client can observe state between any two of those `redis.call`s. that's the entire reason EVAL exists. ## error semantics - `redis.call('NOPE')` — raises a Lua error → script terminates → redis returns error reply. - `redis.pcall('NOPE')` — returns `{err = "..."}` → script continues, can decide what to do. real redis does *not* roll back side effects on error. if your script did `XADD` and then errored, the XADD stays. matters because anyone parked on that stream (XREADGROUP BLOCK) needs to be woken up regardless of whether the script succeeded — pydocket/burner-redis track this as the `had_xadd` / `had_list_mutation` flags on `EvalOutcome`. ## SCRIPT LOAD + EVALSHA `SCRIPT LOAD
` returns a SHA1; `EVALSHA