{"record":{"id":"ce24b46e1409c4f0","repo":"chroma-core/chroma","slug":"conditional-transaction-cannot-span-collections","errorCode":null,"errorMessage":"conditional transaction cannot span collections","messagePattern":"conditional transaction cannot span collections","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"chromadb/api/conditional_http.py","lineNumber":228,"sourceCode":"        scope = self._require_scope()\n        return (scope, prepared_commit)\n\n    def close(self, first_inserted_record_offset: Optional[int] = None) -> None:\n        self._ensure_open()\n        self._closed = True\n\n    def _ensure_open(self) -> None:\n        if self._closed:\n            raise ValueError(\"conditional transaction is closed\")\n\n    def _record_scope(\n        self, collection_id: UUID, tenant: str, database: str\n    ) -> ConditionalHttpScope:\n        scope = ConditionalHttpScope(str(collection_id), tenant, database)\n        if self._scope is None:\n            self._scope = scope\n        elif self._scope != scope:\n            raise ValueError(\"conditional transaction cannot span collections\")\n        return scope\n\n    def _require_scope(self) -> ConditionalHttpScope:\n        if self._scope is None:\n            raise ValueError(\"conditional transaction has no collection scope\")\n        return self._scope\n\n    def _validate_get_request(self, request_payload: ConditionalHttpGetPayload) -> None:\n        ids = request_payload.get(\"ids\")\n        if ids is not None:\n            for id in ids:\n                if id in self._buffered_write_ids:\n                    raise _invalid_read_after_write(id)\n            return\n\n        limit = request_payload.get(\"limit\")\n        if not isinstance(limit, int) or limit <= 0:\n            raise InvalidArgumentError(","sourceCodeStart":210,"sourceCodeEnd":246,"githubUrl":"https://github.com/chroma-core/chroma/blob/aecdd12c8a891610db8653630b066b32ceb678b5/chromadb/api/conditional_http.py#L210-L246","documentation":"This ValueError comes from ConditionalHttpTransaction._record_scope: the first buffered operation in a conditional transaction fixes its scope (collection id + tenant + database), and any subsequent operation against a different collection, tenant, or database raises 'conditional transaction cannot span collections'. A conditional HTTP transaction validates reads/writes against one collection's state, so it cannot batch work for multiple collections.","triggerScenarios":"Opening a conditional transaction, buffering an add/update/delete or issuing a get against collection A, then buffering another operation against collection B with the same transaction object. Also triggered by switching tenant or database between operations inside one transaction (the scope is a (collection_id, tenant, database) triple).","commonSituations":"Batch pipelines that fan writes out to multiple collections inside one 'transaction' for atomicity; multi-tenant code that changes tenant/database mid-transaction; generic helper functions that accept a transaction and a collection and are called in a loop over collections.","solutions":["Use one transaction per collection: commit/close the current one and open a new transaction before touching another collection (or tenant/database)","If you need cross-collection atomicity, note the HTTP conditional transaction does not provide it — restructure to per-collection transactions with your own compensation, or use the local PersistentClient with SQLite transactions where applicable","Record the scope (collection id/tenant/database) alongside the transaction in your wrapper and assert it matches before each buffered call"],"exampleFix":"# before\ntx = client.create_conditional_transaction()\nbuffer_add(tx, coll_a, ids=['1'], embeddings=[[0.1]])\nbuffer_add(tx, coll_b, ids=['2'], embeddings=[[0.2]])  # ValueError: cannot span collections\n\n# after\ntx1 = client.create_conditional_transaction()\nbuffer_add(tx1, coll_a, ids=['1'], embeddings=[[0.1]])\ncommit(tx1)\n\ntx2 = client.create_conditional_transaction()  # separate transaction per collection\nbuffer_add(tx2, coll_b, ids=['2'], embeddings=[[0.2]])\ncommit(tx2)","handlingStrategy":"validation","validationCode":"def buffer_add_scoped(tx, scope_key, collection, **kwargs):\n    \"\"\"scope_key: (collection_id, tenant, database) the tx was opened for\"\"\"\n    current = (str(collection.id), collection.tenant, collection.database)\n    if tx._scope is not None and tuple(tx._scope) != current:\n        raise ValueError(\n            f'transaction is scoped to {tx._scope}; commit it and open a new one for {current}'\n        )\n    return buffer_add(tx, collection, **kwargs)","typeGuard":"def same_scope(tx, collection_id, tenant, database) -> bool:\n    s = getattr(tx, '_scope', None)\n    return s is None or (s.collection_id == str(collection_id) and s.tenant == tenant and s.database == database)","tryCatchPattern":"try:\n    buffered_op(tx, coll_b, ...)\nexcept ValueError as e:\n    if 'cannot span collections' in str(e):\n        commit(tx)\n        tx = client.create_conditional_transaction()\n        buffered_op(tx, coll_b, ...)\n    else:\n        raise","preventionTips":["One transaction per (collection, tenant, database); commit before switching","Group work by collection before opening transactions so each tx has a single target","Do not change tenant/database mid-transaction; open a new transaction after switching","If you need multi-collection atomicity, design compensation logic — HTTP conditional transactions will not provide it"],"tags":["chroma","transaction","conditional-http","multi-collection","scope-violation"],"backgroundTag":"transaction-scope-violation","analyzedSha":"aecdd12c8a891610db8653630b066b32ceb678b5","analyzedAt":"2026-08-16T21:53:27.228Z","schemaVersion":2},"datasetVersion":"2026-08-16T23:17:17.608Z"}