{"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/da03cdc7e8731092b13e395605c3c1fb2de25de1/redis/commands/timeseries/commands.py#L2044-L2080","documentation":"Raised by _append_n_aggregation for TS.NRANGE/TS.NREVRANGE when the number of aggregation specs does not equal the number of queried keys. Each key needs exactly one spec token (which may itself list multiple comma-separated aggregators, e.g. 'avg,max'). A bare string is the spec for a single-key query.","triggerScenarios":"client.ts().nrange(['a','b'], '-', '+', aggregators=['avg']) (1 spec, 2 keys); or nrange(['a'], '-', '+', aggregators=['avg','sum']) (2 specs, 1 key). Correct multi-key form: aggregators=['avg,max','sum'] for keys=[a,b].","commonSituations":"Assuming one aggregator broadcasts across all keys; passing a flat list of aggregators instead of one spec per key; migrating from the old comma-joined broadcast syntax.","solutions":["Provide exactly one spec per key: for keys=[a,b] use aggregators=['avg','sum'] or aggregators=['avg,max','sum'].","For a single key, pass a bare string: aggregators='avg'.","When unsure, build the list dynamically: aggregators=['avg'] * len(keys)."],"exampleFix":"// before\nclient.ts().nrange(['a','b'], '-', '+', aggregators=['avg'])\n// after\nclient.ts().nrange(['a','b'], '-', '+', aggregators=['avg','sum'])","handlingStrategy":"validation","validationCode":"def normalize_n_agg(keys, aggregators):\n    if aggregators is None:\n        return None\n    if isinstance(aggregators, str):\n        return aggregators if len(keys) == 1 else [aggregators] * len(keys)\n    specs = list(aggregators)\n    if len(specs) != len(keys):\n        raise ValueError(f\"Expected {len(keys)} specs, got {len(specs)}\")\n    return specs","typeGuard":"def n_agg_matches(keys, aggregators) -> bool:\n    if aggregators is None:\n        return True\n    if isinstance(aggregators, str):\n        return len(keys) == 1\n    return len(list(aggregators)) == len(keys)","tryCatchPattern":"try:\n    client.ts().nrange(keys, '-', '+', aggregators=agg)\nexcept Exception as e:\n    if 'exactly one aggregation spec' in str(e):\n        client.ts().nrange(keys, '-', '+', aggregators=[agg] * len(keys) if isinstance(agg, str) else agg)\n    else:\n        raise","preventionTips":["Provide one spec per key (a string is shorthand for a single key).","Use comma-joined specs for multi-aggregator per key: ['avg,max','sum'].","Build the spec list from len(keys) when broadcasting one aggregator."],"tags":["redistimeseries","timeseries","nrange","aggregation","argument-error"],"analyzedSha":"da03cdc7e8731092b13e395605c3c1fb2de25de1","analyzedAt":"2026-08-04T20:26:47.563Z","schemaVersion":2}