Summary
Executor's FT.SEARCH reply parsing assumes every per-document field-array in the reply is a list and slices it directly. When one document's field-array comes back as None, dict(zip(row_data[::2], row_data[1::2])) raises TypeError: 'NoneType' object is not subscriptable and the entire query fails.
Where
sql_redis/executor.py, the standard (non-WITHSCORES) FT.SEARCH branch:
# Standard format: [count, key1, [fields1], key2, [fields2], ...]
for i in range(2, len(raw_result), 2):
row_data = raw_result[i]
row = dict(zip(row_data[::2], row_data[1::2])) # row_data is None -> TypeError
The same unguarded pattern is in every reply branch — sync and async, across WITHSCORES / standard / FT.AGGREGATE (≈ lines 256, 271, 276 and the async mirror ≈ 382, 397, 402 on main, v0.6.2). Any of them crashes if its row_data is nil.
Reproduction
The crash is deterministic given the reply shape — a FT.SEARCH reply where one document's field-array is None instead of a [field, value, …] list:
# Mirrors sql_redis/executor.py, standard FT.SEARCH branch.
# reply layout: [count, key1, <fields1>, key2, <fields2>, ...]
# here <fields1> came back nil:
raw_result = [1, "doc:1", None]
for i in range(2, len(raw_result), 2):
row_data = raw_result[i] # None
row = dict(zip(row_data[::2], row_data[1::2])) # TypeError: 'NoneType' object is not subscriptable
How we hit it
We use sql-redis in redis-vl-python (a Redis vector/search library), whose integration tests run SQL SELECTs through Executor.execute against several Redis server versions. The crash appears:
- Only against the
redis:latest Docker image (currently Redis 8.8.0); never on pinned redis:8.2 / 8.4.
- Across redis-py 5.x / 6.x / 7.x.
- On different
SELECT queries from run to run — i.e. data-independent, which points at the reply shape rather than any specific query.
Confirmed vs. unconfirmed
- Confirmed: the parser isn't defensive against a nil field-array — the proximate cause of the exception.
- Unconfirmed: why a slot comes back nil. We could not reproduce it directly, even with matched versions (Redis 8.8.0, redis-py 7.4.0, sql-redis 0.6.x) and high concurrency. Leading guesses: a RESP2/RESP3 reply-shape difference negotiated with the newer server/client, or a document expiring between
FT.SEARCH returning ids and field materialization. We are not claiming a Redis 8.8.0 regression — it doesn't reproduce on 8.8.0.
Suggested fix
Tolerate a nil/empty field-array at each parse site, turning an intermittent hard crash into a well-defined result regardless of the underlying trigger:
row_data = raw_result[i]
if not row_data:
continue # or rows.append({})
row = dict(zip(row_data[::2], row_data[1::2]))
If the nil turns out to originate from a RESP3 reply layout, that path may warrant dedicated handling — happy to help probe HELLO 3 against 8.8.0.
Environment
- sql-redis 0.6.0–0.6.2 (pattern present on
main)
- redis-py 5.x / 6.x / 7.x (observed on 7.4.0)
- Redis server: 8.8.0 (
redis:latest); not seen on 8.2 / 8.4
- Python 3.11–3.14
Summary
Executor'sFT.SEARCHreply parsing assumes every per-document field-array in the reply is a list and slices it directly. When one document's field-array comes back asNone,dict(zip(row_data[::2], row_data[1::2]))raisesTypeError: 'NoneType' object is not subscriptableand the entire query fails.Where
sql_redis/executor.py, the standard (non-WITHSCORES)FT.SEARCHbranch:The same unguarded pattern is in every reply branch — sync and async, across WITHSCORES / standard / FT.AGGREGATE (≈ lines 256, 271, 276 and the async mirror ≈ 382, 397, 402 on
main, v0.6.2). Any of them crashes if itsrow_datais nil.Reproduction
The crash is deterministic given the reply shape — a
FT.SEARCHreply where one document's field-array isNoneinstead of a[field, value, …]list:How we hit it
We use sql-redis in redis-vl-python (a Redis vector/search library), whose integration tests run SQL
SELECTs throughExecutor.executeagainst several Redis server versions. The crash appears:redis:latestDocker image (currently Redis 8.8.0); never on pinnedredis:8.2/8.4.SELECTqueries from run to run — i.e. data-independent, which points at the reply shape rather than any specific query.Confirmed vs. unconfirmed
FT.SEARCHreturning ids and field materialization. We are not claiming a Redis 8.8.0 regression — it doesn't reproduce on 8.8.0.Suggested fix
Tolerate a nil/empty field-array at each parse site, turning an intermittent hard crash into a well-defined result regardless of the underlying trigger:
If the nil turns out to originate from a RESP3 reply layout, that path may warrant dedicated handling — happy to help probe
HELLO 3against 8.8.0.Environment
main)redis:latest); not seen on 8.2 / 8.4