Skip to content

Delete keys and documents without reading the records first, add WithDocumentDeletionKey, WithEmptyDocumentDeletions and WithReadsBeyondKeyQuota, and let a collection client choose once how it deletes documents - #126

Merged
Laurianti merged 21 commits into
mainfrom
concurrent-deletions
Oct 12, 2026

Conversation

@Laurianti

@Laurianti Laurianti commented Oct 11, 2026 •

Copy link
Copy Markdown
Member

Updates and upserts that delete a metadata key or a document no longer read the records first, so two writes of the same record at the same time leave the record of one of them. Two new methods of the collection client, WithDocumentDeletionKey and WithEmptyDocumentDeletions, join WithDocumentCopyKey as the ways to delete a document, and a collection client chooses one of them once. On Chroma Cloud a write beyond the quota of 32 metadata keys throws, unless the new option WithReadsBeyondKeyQuota().

  • Before: to delete a key, UpdateAsync and UpsertAsync read the records and sent the null only to the ones that had the key. To delete a document with NullDocumentsDelete, they read the records and wrote an empty document only to the ones that had one. A write of another client between the read and the write could stay in the record: A writes {s: "A", x: null} while B writes {s: "B", x: 2}, and the record ends up {s: "A", x: 2}.
  • Now:
    • The null of a metadata key goes to the record whether it has the key or not, without a read. Every tested Chroma takes it, also in an upsert that creates the record.
    • A collection client chooses how it deletes a document with NullDocumentsDelete, in one of three ways. In each, the empty document goes in the same write, without a read:
      • WithDocumentCopyKey(key), which already copied each document into a metadata key: the null of the copy goes with the empty document, which reads back as null.
      • WithDocumentDeletionKey(key), new: true under the key goes with the empty document, which reads back as null. Any other document that an update or an upsert writes deletes the mark with a null.
      • WithEmptyDocumentDeletions(), new: the empty document goes alone, and reads back as an empty string.
    • In all three, a record without a document gets the empty document too. A where_document filter that matches an empty text now sees it as one with an empty document: on Chroma 1.5.9 $contains "" and $regex "^$" find it and $not_regex "^$" does not; on Chroma Cloud $contains "" finds it.
    • Without one of the three, a null document with NullDocumentsDelete throws an ArgumentException before any request, which lists them. Without NullDocumentsDelete, a null document keeps the stored one, as before.
    • A collection client chooses once: the same call again changes nothing, while another of the three methods, or the same one with another key, throws an InvalidOperationException that says the way the client has. Up to 2.12.0, WithDocumentCopyKey with another key replaced the key; the same key again works as before.
    • An empty text, in a metadata key with a BM25 index or as the document, gets its empty BM25 vector, as with the Python client: the vector of the old text no longer stays, to be found by a search of the old text. This was so on main too.
    • On Chroma Cloud, which counts a null against its quota of 32 keys, a write with nulls that would take a record beyond the quota throws an ArgumentException before it goes, without a read. The count has the keys that the client adds too, like the BM25 sparse vectors, which Chroma Cloud counts. A record with more than 32 values and no null goes, and Chroma Cloud rejects it with 422. The new option WithReadsBeyondKeyQuota() of ChromaConfigurationOptions reads that record first instead, with two risks that the docs say: a write of another client between the read and the write can mix with this one, and with the same id more than once in one write, a key to delete can stay.
  • Tests:
    • ConcurrentWritesTests: a write that lands between the start of a deletion and its write, for a key and for a document in each of the three ways. Also 8 writes of one record at the same time, 20 rounds; its comment says that it catches a client that reads first only in some runs.
    • DeletionsInWritesTests: no get before the write, on a fake server, for the keys and for each way; beyond the 32 keys of Chroma Cloud the error without any request by default, and the read with ReadsBeyondKeyQuota, which stays through the other options; neither within the quota or on a single server; the null of a sparse vector and of its text counted once; the sparse vectors that the client computes counted too, while an empty list given for one still deletes it; a record with 33 values and no null sent as it is; the error without a way, also from a client made by name; the same call again, which changes nothing in the writes and in the reads, and each change of way, which throws.
    • DocumentCopyReadTests: the mark in a get, a query and a search, and the reads of empty document deletions.
    • MetadataTests: DocumentCopy, DocumentDeletionKey and EmptyDocumentDeletions on a server; on Chroma Cloud 32 keys with the nulls, and 33: the error without a request by default, the read with the option; a computed sparse vector that takes a write to 33 keys; 33 values without nulls, which Chroma Cloud rejects.
    • AnEmptyTextGetsAnEmptyVector on a fake server, and AnEmptyTextReplacesItsVectorOnChromaCloud on Chroma Cloud.
    • ApiV2CompleteTests.QuotasOfChromaCloud expects the empty Metadata of a record without keys, as the client returns it: it failed on main on Chroma Cloud, which the CI does not run.
    • The tests of each commit fail without its change, on Chroma 1.5.9 and on Chroma Cloud; for the new methods, with a stub that does nothing. With the changes the whole suite passes on Chroma 1.5.9 and 0.6.3, and the touched classes on Chroma Cloud, 1.0.0, 0.4.10 and the netstandard2.0 build.
  • Checked outside the tests, against what the docs describe: every combination of a value, a null, an empty list or nothing for a key, a text with a BM25 index and its vector, a document, NullDocumentsDelete, the three ways, update and upsert, on records that do not exist, are empty, are full or have a deleted document. 4,160 on Chroma 1.5.9, 16,640 and 6,656 on Chroma Cloud with the index on a metadata key and on the documents, and 880 around the quota of keys on each. No difference.
  • Docs: docs/metadata.md, docs/chroma-cloud.md, docs/search.md, the XML comments of UpdateAsync, UpsertAsync, WithDocumentCopyKey, WithDocumentDeletionKey, WithEmptyDocumentDeletions, ChromaRecords.NullDocumentsDelete and ChromaConfigurationOptions.ReadsBeyondKeyQuota, and PublicAPI.Unshipped.txt.

@Laurianti Laurianti changed the title Delete in updates and upserts without reading the records first, so that concurrent writes of a record do not mix Delete keys and documents without reading the records first, add WithDocumentDeletionKey and WithEmptyDocumentDeletions, and let a collection client choose once how it deletes documents Oct 12, 2026
@Laurianti
Laurianti marked this pull request as ready for review October 12, 2026 10:23
@Laurianti
Laurianti requested a review from a team as a code owner October 12, 2026 10:23
Copilot AI balanced review requested due to automatic review settings October 12, 2026 10:23

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Cloud quota filtering miscounts duplicate deletion keys and breaks ordered behavior for repeated IDs.

2 open findings
What changed in this PR

This PR makes metadata/document deletions atomic with their writes and adds configurable document-deletion strategies.

Changes:

  • Sends deletion values without preliminary reads, except for Chroma Cloud quota handling.
  • Adds deletion-key and empty-document strategies alongside document-copy keys.
  • Expands concurrency, serialization, and integration coverage.
File Description
docs/​metadata.md Documents deletion behavior and strategies.
ChromaDB.Client/​PublicAPI.Unshipped.txt Registers new public methods.
ChromaDB.Client/​Common/​DocumentReader.cs Generalizes document reconstruction.
ChromaDB.Client/​Common/​DocumentCopyReader.cs Removes superseded reader.
ChromaDB.Client/​Common/​CollectionQueryEntryMapper.cs Uses generalized reader.
ChromaDB.Client/​Common/​CollectionEntryMapper.cs Uses generalized reader.
ChromaDB.Client/​ChromaRecords.cs Updates deletion documentation.
ChromaDB.Client/​ChromaCollectionClient.cs Implements deletion configuration and write behavior.
ChromaDB.Client.Tests/​RequestValuesTests.cs Verifies direct deletion writes.
ChromaDB.Client.Tests/​MetadataTests.cs Covers strategies and Cloud quotas.
ChromaDB.Client.Tests/​DocumentCopyReadTests.cs Tests deletion-marker reads.
ChromaDB.Client.Tests/​DeletionsInWritesTests.cs Tests request construction and strategy selection.
ChromaDB.Client.Tests/​ConcurrentWritesTests.cs Tests concurrent deletion semantics.

🧠 Review effort: Balanced


💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread ChromaDB.Client/ChromaCollectionClient.cs
Comment thread ChromaDB.Client/ChromaCollectionClient.cs Outdated
…tor that the caller gives with the null of its text
…oud, and read the record first only with WithReadsBeyondKeyQuota
@Laurianti Laurianti changed the title Delete keys and documents without reading the records first, add WithDocumentDeletionKey and WithEmptyDocumentDeletions, and let a collection client choose once how it deletes documents Delete keys and documents without reading the records first, add WithDocumentDeletionKey, WithEmptyDocumentDeletions and WithReadsBeyondKeyQuota, and let a collection client choose once how it deletes documents Oct 12, 2026
…at the client computes, which go with the write
…e with nulls, and that Chroma Cloud rejects a record with more than 32 values
…he client adds, and name ChromaConfigurationOptions.WithReadsBeyondKeyQuota() on the ChromaClient
@Laurianti
Laurianti merged commit 4e4037f into main Oct 12, 2026
15 checks passed
@Laurianti
Laurianti deleted the concurrent-deletions branch October 12, 2026 15:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants