Repository navigation
Refuse a repeated record UUID or a non-finite property value at upsert, and state the datetime property contract - #1788
Open
edwinyyyu wants to merge 5 commits into
Conversation
This was referenced Oct 7, 2026
Draft
[session storage 2/2] Remove open-or-create from both stores, and close from the segment store
#1625
Draft
[qdrant options] Let a deployment tune a Qdrant collection's HNSW, optimizers and quantization
#1618
Draft
edwinyyyu
force-pushed
the
feat/vector-store-write-validation-speedkick
branch
from
October 8, 2026 23:54
61c2045 to
3fe0b44
Compare
Two versions of one record in one batch are ambiguous, and the stores disagreed on them: SQLiteVectorStore and Qdrant kept the last, SQLiteVecVectorStore failed partway with a raw sqlite3 UNIQUE constraint error, and Milvus refused the batch. Every store now raises ValueError naming the UUID before anything is sent to the backend, from one helper beside the other upsert input checks; the check costs one pass over the batch. The VectorStoreCollection.upsert docstring says so, and the registry-backed _upsert hook may rely on distinct UUIDs. Each store's test upserts a batch repeating a UUID after another record, expects the error, and finds nothing stored. It fails with the check removed: SQLiteVectorStore and Qdrant raise nothing, SQLiteVecVectorStore raises sqlite3's OperationalError, and Milvus 2.6.24 raises its MilvusException. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
The stores disagreed on nan, inf, and -inf as property values: Qdrant over REST stores them as null, so the property reads back absent, and over gRPC refuses them with a raw error; Milvus 2.6.24 refuses one in a declared float field, takes one in an undeclared property, and cannot parse a filter on it; and the SQLite stores store them. Every store now raises ValueError naming the property key before anything is sent to the backend, for declared and undeclared properties alike, as the query path already refuses a non-finite query vector coordinate or score threshold. The VectorStoreCollection.upsert docstring says so, and the registry-backed _upsert hook may rely on finite values. Each store's test upserts a finite record and one with a non-finite value of each kind, on a declared and an undeclared property, expects the error, and finds nothing stored. It fails with the check removed: the SQLite stores, Qdrant over REST, and Milvus on an undeclared property raise nothing; Qdrant over gRPC raises grpc's AioRpcError; and Milvus on a declared property raises its MilvusException. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
A filter compares datetime values by their instant, for equality and ordering alike, and a store keeps a datetime property's UTC offset as written, taking a naive datetime as UTC. Every store behaves so: the SQLite stores keep the offset beside the UTC instant in their type-tagged JSON, Qdrant keeps the RFC 3339 text with its offset in the payload, and Milvus keeps a declared datetime's offset in a field beside its TIMESTAMPTZ and an undeclared one's in its properties JSON. The VectorStoreCollection docstring says so. Each store's tests write a datetime at +05:30, declared and undeclared, read its offset back from the backend, and filter on it at other offsets: the same instant at -08:00 matches =, <=, and >=, a later instant at an earlier wall-clock time matches <, and an earlier instant at a later wall-clock time matches >. The read-back tests fail with the offset dropped at write (the type-tagged JSON's offset written as 0, Qdrant's payload converted to UTC, or Milvus's offset field written as 0). The filter tests fail with the filter node reading a value's wall-clock time as UTC, and on the SQLite stores also with the wall-clock text stored in place of the UTC instant. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
A vector store's properties exist for filters, which compare a datetime by its instant, and a search answers with record UUIDs and scores, so no caller reads a stored offset back. The VectorStoreCollection docstring now says a store keeps a datetime property's instant, taking a naive datetime as UTC, and may drop its UTC offset; a filter compares datetime values by their instant, as before. Qdrant's read-back test checks the stored instant, not its offset. The tests of the type-tagged JSON, which keeps the offset (the SQLite stores and Milvus's undeclared properties), are unchanged. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
A datetime written to Qdrant keeps its UTC offset only to the minute, over REST and gRPC alike: on 1.19.1, one written at -00:44:30 reads back at -00:44. The store wrote a datetime property at its own offset, so one at an offset with a seconds component, as local mean time had until 1972, was stored up to 59 seconds off its instant, and a filter on that instant missed it: the store did not keep the instant the collection's contract promises. The store now converts the datetime to UTC before writing it, which applies the whole offset to the instant, so nothing is left for Qdrant to cut. The read-back test checks that the payload holds the written instant in UTC. The new filter test writes a datetime at -00:44:30, under a declared key and an undeclared one, over REST and gRPC, and filters on its instant with =, <, and >. Without the change, all 8 of their cases fail. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
edwinyyyu
force-pushed
the
feat/vector-store-write-validation-speedkick
branch
from
October 9, 2026 18:02
3fe0b44 to
18e89e0
Compare
This was referenced Oct 9, 2026
Draft
marvinyu-memverge
approved these changes
Oct 9, 2026
marvinyu-memverge
left a comment
Collaborator
There was a problem hiding this comment.
Approving at 18e89e0. No asks.
What I checked:
- Both new checks run before anything is sent, on all four stores. The registry-backed handle (Qdrant, Milvus) and both SQLite stores check every record's properties, then the batch's UUIDs, ahead of the first write. So "no record is written" in the upsert docstring holds.
- No caller today sends a repeated UUID in one batch. Event memory's derivatives each get a fresh uuid4 in the text deriver, and semantic memory upserts one record at a time with its own vector_uuid. The UUID check refuses nothing that is produced now; it pins the contract the stores disagreed on.
- The filter side already agrees with the new Qdrant write. The filter parser's Comparison and In nodes convert every datetime value to UTC when they are built, so Qdrant's eq and range filters never send a seconds offset. That is why the -00:44:30 =, < and > cases hold over both transports.
- The SQLite stores (encode_properties) and Milvus keep the UTC instant plus the original offset, so Qdrant is the only store that drops the offset. That matches "may drop its UTC offset" in the VectorStoreCollection docstring and the new line in design/qdrant_vector_store.md.
- CI is green at head on every job, including the common/ integration matrix.
edwinyyyu
added a commit
to edwinyyyu/MemMachine
that referenced
this pull request
Oct 10, 2026
A declared datetime on the SQLite stores had a `tz_<key>` column beside its microseconds column, holding the UTC offset in seconds that no filter reads. The vector stores keep a datetime property's instant only, as MemMachine#1736 now does for Milvus and MemMachine#1788 for Qdrant, and the segment store keeps the offset, so the column, `offset_column_name`, and the value written to it go: each declared property is one column, and a datetime stays microseconds since the epoch. The tests read a stored datetime back as its instant in UTC, and the roundtrip test checks that the stored instant equals the written one. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> Claude-Session: https://claude.ai/code/session_01ESpWYTmCR7X3bJEpoA8SAn
edwinyyyu
added a commit
to edwinyyyu/MemMachine
that referenced
this pull request
Oct 10, 2026
Every store refuses a batch naming a record UUID twice. A non-finite float is already refused by the declared schema's type check, so its separate check and tests go, and the datetime tests run on the declared key alone, since an undeclared key is refused. The store keeps the strict mode of MemMachine#1628 over the one MemMachine#1813 merged into the base, and MemMachine#1813's test of it off stays out. Co-Authored-By: Claude Opus 5.5 <[email protected]>
edwinyyyu
added a commit
to edwinyyyu/MemMachine
that referenced
this pull request
Oct 10, 2026
…chine#1672 to MemMachine#1676) into the final state Their upsert keeps the delete and fresh insert, writing the declared columns, and refuses a repeated UUID as MemMachine#1788 does; open-or-create stays out, as MemMachine#1625 removes it. Their tests create their partition as a session does and build filters with the closed union of MemMachine#1616. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Purpose of the change
Summary
SQLiteVectorStore,SQLiteVecVectorStore, and the registry-backed handle behindQdrantVectorStoreandMilvusVectorStore) refuses an upsert batch that names a record UUID twice: it raisesValueErrornaming the UUID before anything is sent to the backend. Two versions of one record in a batch are ambiguous, and the stores disagreed on them:SQLiteVectorStoreand Qdrant kept the last,SQLiteVecVectorStorefailed partway with a raw sqlite3 UNIQUE constraint error, and Milvus refused the batch. The check costs one pass over the batch.nan,inf, or-inf), declared or undeclared: it raisesValueErrornaming the property key before anything is sent, as the query path already refuses a non-finite query vector coordinate or score threshold. Qdrant over REST stored such a value as null and over gRPC refused it with a raw error, Milvus refused it in a declared float field, took it in an undeclared one, and cannot parse a filter on it, and the SQLite stores stored it.VectorStoreCollectiondocstring states the datetime property contract: a filter compares datetime values by their instant, for equality and ordering alike, and a store keeps a datetime property's instant, taking a naive datetime as UTC, and may drop its UTC offset. A search answers with record UUIDs and scores, so no caller reads a stored offset back; the segment store the vector records are derived from keeps it. The tests read the instant back from each backend and filter on it at other offsets.-00:44:30reads back at-00:44), so one at an offset with a seconds component was stored up to 59 seconds off its instant. Converting to UTC first applies the whole offset to the instant.Both checks are helpers in
common/vector_store/utils.py, beside the other upsert input checks, and theVectorStoreCollection.upsertdocstring lists them under Raises.Directly on
feat/horizontal-scaling, where #1736 is merged.Fixes #1783. Fixes #1784.
Commits
Stack
21 open PRs: three independent PRs, and the vector store tree of short parallel branches. Every PR in the tree has
feat/horizontal-scalingas its GitHub base, and the independent PRs havemain. The branches are in a fork, and a pull request can target only this repository's branches, so the on column gives the order the PRs build on each other. A stacked PR's diff on GitHub includes the PRs under it until they merge.Independent of the vector store tree, directly on
main:mainmainmainThe vector store tree. Each PR builds on the one in its on column; PRs on the same parent are parallel branches and do not depend on each other. #1631 is closed, superseded by #1733–#1736, which hold its changes split in four, with review changes since. #1702 and #1670 sit beneath #1627, whose code depends on them. Until the PRs under it merge, their changes show in a stacked PR's diff.
mainfeat/horizontal-scaling)feat/horizontal-scalingfeat/horizontal-scaling)feat/horizontal-scalingfeat/horizontal-scaling)feat/horizontal-scalingfeat/horizontal-scaling)feat/horizontal-scalingfeat/horizontal-scaling)feat/horizontal-scalingfeat/horizontal-scaling)feat/horizontal-scalingfeat/horizontal-scaling)feat/horizontal-scalingfeat/horizontal-scalingfeat/horizontal-scalingThis PR is its 5 commits,
299d9d41a,779cc8d41,299e5902e,e136eb082,18e89e062, directly onfeat/horizontal-scaling.Verification
Rebased onto
feat/horizontal-scalingcd96676bc, #1736's squash merge: the 5 commits replay unchanged, and the head's tree differs from3fe0b442bonly by #1779's change toqdrant_vector_store.py, which the merge brought. At the head18e89e062, in order:uv run --frozen --all-extras ruff check: all checks passed.uv run --frozen --all-extras ruff format --check: 553 files already formatted.uv run --frozen --all-extras ty check --project packages/server: all checks passed.uv lock --check: passed.uv run --frozen --all-extras pytest packages/server -m "not integration" -q: 2,189 passed, 8 skipped.uv run --frozen --all-extras pytest packages/server -m integration -q(Qdrant 1.19.1, Milvus 2.6.24, and PostgreSQL in testcontainers): 1,712 passed, 266 skipped.Rebased onto #1736's head
49b9ec2dd, with commits 4 and 5: at the head3fe0b442b, in order:uv run --frozen --all-extras ruff check: all checks passed.uv run --frozen --all-extras ruff format --check: 553 files already formatted.uv run --frozen --all-extras ty check --project packages/server: all checks passed.uv lock --check: passed.uv run --frozen --all-extras pytest packages/server -m "not integration" -q: 2,189 passed, 8 skipped.uv run --frozen --all-extras pytest packages/server -m integration -q(Qdrant 1.19.1, Milvus 2.6.24, and PostgreSQL in testcontainers): 1,712 passed, 266 skipped.Commits 1-3 replay unchanged. Commit 5's tests write a datetime at
-00:44:30, under a declared key and an undeclared one, over REST and gRPC, read the payload back in UTC, and filter on its instant with =, <, and >; without commit 5 all 8 cases fail.Before the rebase:
At 61c2045, in order:
uv run --frozen --all-extras ruff check: all checks passed.uv run --frozen --all-extras ruff format --check: 553 files already formatted.uv run --frozen --all-extras ty check --project packages/server: all checks passed.uv lock --check: passed.uv run --frozen --all-extras pytest packages/server -m "not integration" -q: 2,180 passed, 8 skipped.uv run --frozen --all-extras pytest packages/server -m integration -q(Qdrant 1.19.1, Milvus 2.6.24, and PostgreSQL in testcontainers): 1,704 passed, 266 skipped.Each new or changed test failed under its named mutation, on every store it runs on:
SQLiteVectorStoreand Qdrant (REST and gRPC) raised nothing,SQLiteVecVectorStoreraised sqlite3'sOperationalError, and Milvus raisedMilvusException(code 1100).AioRpcError(INVALID_ARGUMENT); and Milvus on a declared property raisedMilvusException(code 1100).🤖 Generated with Claude Code