{"record":{"id":"cc7b0aa25a99ec39","repo":"redis/redis-py","slug":"aggregation-requires-exactly-one-aggregation-spec","errorCode":null,"errorMessage":"AGGREGATION requires exactly one aggregation spec per key (expected {numkeys}, got {len(specs)}); a spec may list multiple comma-separated aggregators.","messagePattern":"AGGREGATION requires exactly one aggregation spec per key \\(expected (.+?), got (.+?)\\); a spec may list multiple comma-separated aggregators\\.","errorType":"validation","errorClass":"DataError","httpStatus":null,"severity":"error","filePath":"redis/commands/timeseries/commands.py","lineNumber":2062,"sourceCode":"        bucket_size_msec: int | None,\n        numkeys: int,\n    ):\n        \"\"\"Append the AGGREGATION clause for TS.NRANGE / TS.NREVRANGE.\n\n        These commands take exactly one aggregation spec token per queried key;\n        a single aggregator is never broadcast across keys. A spec token may hold\n        several comma-separated aggregators, so the wire form is e.g.\n        ``AGGREGATION avg,max sum 1000`` for two keys -- key 0 aggregated by both\n        ``avg`` and ``max``, key 1 by ``sum``. Pass one spec string per key, each\n        optionally comma-joined (``[\"avg,max\", \"sum\"]``); a bare string is the\n        spec for a single-key query. (Matches RedisTimeSeries PR #2079, which\n        replaced the earlier single comma-joined / broadcast token.)\n        \"\"\"\n        if aggregators is None:\n            return\n        specs = [aggregators] if isinstance(aggregators, str) else list(aggregators)\n        if len(specs) != numkeys:\n            raise DataError(\n                \"AGGREGATION requires exactly one aggregation spec per key \"\n                f\"(expected {numkeys}, got {len(specs)}); a spec may list \"\n                \"multiple comma-separated aggregators.\"\n            )\n        params.extend([\"AGGREGATION\", *specs, bucket_size_msec])\n\n    @staticmethod\n    def _append_chunk_size(params: list[EncodableT], chunk_size: int | None):\n        \"\"\"Append CHUNK_SIZE property to params.\"\"\"\n        if chunk_size is not None:\n            params.extend([\"CHUNK_SIZE\", chunk_size])\n\n    @staticmethod\n    def _append_duplicate_policy(\n        params: list[EncodableT], duplicate_policy: str | None\n    ):\n        \"\"\"Append DUPLICATE_POLICY property to params.\"\"\"\n        if duplicate_policy is not None:","sourceCodeStart":2044,"sourceCodeEnd":2080,"githubUrl":"https://github.com/redis/redis-py/blob/6a6b581b48225afa0b76912d1028c6035baee932/redis/commands/timeseries/commands.py#L2044-L2080","documentation":"Raised by _append_n_aggregation (TS.NRANGE / TS.NREVRANGE) when the number of aggregation specs does not equal the number of queried keys. NRANGE takes exactly one aggregation spec per key (each spec may itself be a comma-joined list of aggregators); a count mismatch is rejected client-side. The message reports expected (numkeys) vs received (len(specs)).","triggerScenarios":"Calling nrange(['k1','k2'], ...) with aggregators='avg' (one spec for two keys), or aggregators=['avg','max','sum'] (three specs for two keys). Passing a single string when multiple keys are present.","commonSituations":"Assuming a single aggregator broadcasts across all keys (the old behavior pre-RedisTimeSeries #2079). Mixing up NRANGE semantics with MRANGE. Off-by-one when building the spec list.","solutions":["Provide exactly one spec per key: aggregators=['avg','max'] for two keys.","For multiple aggregators on one key, comma-join them: aggregators='avg,max' for a single key.","For a single-key query, a bare string is fine: aggregators='avg'."],"exampleFix":"// before\nclient.ts().nrange(['k1','k2'], f, t, aggregators='avg')\n// after\nclient.ts().nrange(['k1','k2'], f, t, aggregators=['avg','max'])\n# multiple aggregators on a single key:\nclient.ts().nrange(['k1'], f, t, aggregators='avg,max')","handlingStrategy":"validation","validationCode":"if aggregators is not None:\n    specs = [aggregators] if isinstance(aggregators, str) else list(aggregators)\n    assert len(specs) == len(keys), f'{len(specs)} specs for {len(keys)} keys'\nclient.ts().nrange(keys, f, t, aggregators=aggregators)","typeGuard":null,"tryCatchPattern":"from redis.exceptions import DataError\ntry:\n    client.ts().nrange(keys, f, t, aggregators=aggregators)\nexcept DataError:\n    aggregators = [aggregators] * len(keys) if isinstance(aggregators, str) else aggregators\n    client.ts().nrange(keys, f, t, aggregators=aggregators)","preventionTips":["Build one spec per key; comma-join within a spec for multiple aggregators.","Do not assume a single aggregator broadcasts across keys."],"tags":["redistimeseries","nrange","aggregation","argument-validation"],"backgroundTag":null,"analyzedSha":"6a6b581b48225afa0b76912d1028c6035baee932","analyzedAt":"2026-08-10T12:52:44.840Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-21T04:17:39.646Z"}