Files
qdrant/tests/openapi/test_vector_dimension_validation.py
qdrant-cloud-bot ee2107c2db fix: validate vector dimensions before WAL write for async upserts (#9058)
* fix: validate vector dimensions before WAL write for async upserts

When upserting points with wait=false (the default), dimension
mismatches were silently discarded during background processing.
The API returned 200 "acknowledged" but the points were never stored,
causing silent data loss with no error feedback to the user.

This adds an early dimension validation check in do_upsert_points()
that runs before the operation is written to WAL. This ensures that
dimension errors are returned to the client regardless of the wait
parameter, matching the behavior of wait=true.

The validation handles all vector types:
- Dense single vectors
- Multi-dense vectors
- Named vectors (dense, multi-dense, sparse)
- Sparse vectors are skipped (no fixed dimension)

Closes #9039

Co-authored-by: Cursor <cursoragent@cursor.com>

* refactor: move vector dimension validation into dedicated module

Extract validate_vector_dimensions and helper functions from update.rs
into src/common/validate_vectors.rs for better code organization.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix: update shard update test for early dimension validation

The test expected a shard-level error message, but now dimension
mismatches are caught before reaching the shards. Update the assertion
to accept either the early validation error or the shard-level error.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix: assert actual dimension error message in shard update test

Check for the descriptive error ("Vector dimension error: expected dim: 4, got 3")
rather than the generic shard failure wrapper.

Co-authored-by: Cursor <cursoragent@cursor.com>

---------

Co-authored-by: Cursor Agent <agent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-22 10:43:19 +02:00

106 lines
3.3 KiB
Python

"""
Test that vector dimension validation works for both sync (wait=true) and async (wait=false) upserts.
Regression test for https://github.com/qdrant/qdrant/issues/9039
"""
import pytest
from .helpers.collection_setup import drop_collection
from .helpers.helpers import request_with_validation
@pytest.fixture(autouse=True)
def setup(collection_name):
drop_collection(collection_name=collection_name)
response = request_with_validation(
api='/collections/{collection_name}',
method="PUT",
path_params={'collection_name': collection_name},
body={
"vectors": {
"size": 4,
"distance": "Cosine",
},
}
)
assert response.ok
yield
drop_collection(collection_name=collection_name)
def test_sync_upsert_wrong_dimension_rejected(collection_name):
"""With wait=true, wrong-dimension vectors should be rejected with 4xx."""
response = request_with_validation(
api='/collections/{collection_name}/points',
method="PUT",
path_params={'collection_name': collection_name},
query_params={'wait': 'true'},
body={
"points": [
{"id": 1, "vector": [0.1, 0.2, 0.3]} # 3-dim into 4-dim
]
}
)
assert not response.ok
assert response.status_code == 400
assert "dimension" in response.json()["status"]["error"].lower()
def test_async_upsert_wrong_dimension_rejected(collection_name):
"""With wait=false (default), wrong-dimension vectors should also be rejected with 4xx."""
response = request_with_validation(
api='/collections/{collection_name}/points',
method="PUT",
path_params={'collection_name': collection_name},
query_params={'wait': 'false'},
body={
"points": [
{"id": 2, "vector": [0.1, 0.2, 0.3]} # 3-dim into 4-dim
]
}
)
assert not response.ok
assert response.status_code == 400
assert "dimension" in response.json()["status"]["error"].lower()
def test_async_upsert_correct_dimension_accepted(collection_name):
"""With wait=false, correct-dimension vectors should still succeed."""
response = request_with_validation(
api='/collections/{collection_name}/points',
method="PUT",
path_params={'collection_name': collection_name},
query_params={'wait': 'false'},
body={
"points": [
{"id": 3, "vector": [0.1, 0.2, 0.3, 0.4]} # 4-dim into 4-dim
]
}
)
assert response.ok
def test_async_batch_upsert_wrong_dimension_rejected(collection_name):
"""Batch upsert with wait=false should also reject wrong-dimension vectors."""
response = request_with_validation(
api='/collections/{collection_name}/points',
method="PUT",
path_params={'collection_name': collection_name},
query_params={'wait': 'false'},
body={
"batch": {
"ids": [10, 11],
"vectors": [
[0.1, 0.2, 0.3, 0.4], # correct
[0.1, 0.2, 0.3], # wrong dimension
]
}
}
)
assert not response.ok
assert response.status_code == 400
assert "dimension" in response.json()["status"]["error"].lower()