Skip to content

Commit 5de8c1b

Browse files
test: self-detect nullable vector import support before running e2e cases
Signed-off-by: huanghaoyuanhhy <haoyuan.huang@zilliz.com>
1 parent 22a78ee commit 5de8c1b

1 file changed

Lines changed: 96 additions & 6 deletions

File tree

tests/testcases/test_restore_nullable_vector.py

Lines changed: 96 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,12 @@
1313
- dict-form inserts with `None` for vector fields are the official
1414
nullable-vector ingest path documented in the user guide;
1515
- describe_collection on MilvusClient exposes per-field `nullable` directly.
16-
"""
1716
17+
Each test gates on a capability probe that performs a small nullable-vector
18+
round-trip (create -> insert NULL -> backup -> restore). If the target
19+
Milvus does not support nullable-vector binlog import yet, all tests in
20+
the class are skipped with an actionable message.
21+
"""
1822

1923
import numpy as np
2024
import ml_dtypes
@@ -31,6 +35,91 @@
3135
BACKUP_PREFIX = "backup_nullable_vec"
3236
SUFFIX = "_bak"
3337

38+
_PROBE_BACKUP_NAME = "probe_nullable_vec"
39+
40+
# ---- capability probe -------------------------------------------------------
41+
42+
def _probe_nullable_vector_roundtrip(client, milvus_client):
43+
"""Return True if the Milvus server supports nullable vector binlog import.
44+
45+
The probe creates a tiny collection with a nullable FloatVector field,
46+
inserts one NULL and one non-NULL row, creates a backup, and tries to
47+
restore it. If the restore succeeds the server's BulkImport path
48+
understands the nullable-vector Arrow format; otherwise it does not.
49+
"""
50+
coll = cf.gen_unique_str("probe_nullable_vec")
51+
restored_coll = coll + SUFFIX
52+
dim = 16
53+
54+
try:
55+
schema = milvus_client.create_schema(auto_id=False, enable_dynamic_field=False)
56+
schema.add_field("id", DataType.INT64, is_primary=True)
57+
schema.add_field("embedding", DataType.FLOAT_VECTOR, dim=dim, nullable=True)
58+
milvus_client.create_collection(collection_name=coll, schema=schema)
59+
60+
rng = np.random.default_rng(seed=0)
61+
milvus_client.insert(collection_name=coll, data=[
62+
{"id": 1, "embedding": [np.float32(x) for x in rng.random(dim)]},
63+
{"id": 2, "embedding": None},
64+
])
65+
milvus_client.flush(collection_name=coll)
66+
67+
bp = client.create_backup({
68+
"async": False,
69+
"backup_name": _PROBE_BACKUP_NAME,
70+
"collection_names": [coll],
71+
})
72+
if bp.get("msg", "") != "success":
73+
return False
74+
75+
rp = client.restore_backup({
76+
"async": False,
77+
"backup_name": _PROBE_BACKUP_NAME,
78+
"collection_names": [coll],
79+
"collection_suffix": SUFFIX,
80+
})
81+
return rp.get("msg", "") == "success"
82+
except Exception:
83+
return False
84+
finally:
85+
# best-effort cleanup
86+
for name in (restored_coll, coll):
87+
try:
88+
milvus_client.drop_collection(name)
89+
except Exception:
90+
pass
91+
try:
92+
client.delete_backup(_PROBE_BACKUP_NAME)
93+
except Exception:
94+
pass
95+
96+
97+
def _check_nullable_vector_support(test_case):
98+
"""Run the capability probe once per class and skip if unsupported."""
99+
cls = type(test_case)
100+
if hasattr(cls, "_nullable_vec_checked"):
101+
if not cls._nullable_vec_supported:
102+
pytest.skip(
103+
"target Milvus does not support nullable vector binlog import "
104+
"(Arrow *array.Binary deserialization in BulkInsert); requires "
105+
"Milvus >= 2.6.18 or >= 3.0.0"
106+
)
107+
return
108+
109+
cls._nullable_vec_checked = True
110+
cls._nullable_vec_supported = _probe_nullable_vector_roundtrip(
111+
test_case.client,
112+
test_case.milvus_client,
113+
)
114+
if not cls._nullable_vec_supported:
115+
pytest.skip(
116+
"target Milvus does not support nullable vector binlog import "
117+
"(Arrow *array.Binary deserialization in BulkInsert); requires "
118+
"Milvus >= 2.6.18 or >= 3.0.0"
119+
)
120+
121+
122+
# ---- helpers ----------------------------------------------------------------
34123

35124
def _vector_value(data_type: DataType, dim: int, seed: int):
36125
"""Generate a deterministic non-NULL vector value for the given dtype."""
@@ -44,10 +133,8 @@ def _vector_value(data_type: DataType, dim: int, seed: int):
44133
if data_type == DataType.INT8_VECTOR:
45134
return np.asarray(rng.integers(-128, 127, size=dim), dtype=np.int8)
46135
if data_type == DataType.BINARY_VECTOR:
47-
# BINARY_VECTOR expects bytes; dim is in bits, so dim // 8 bytes.
48136
return bytes(rng.integers(0, 256, size=dim // 8, dtype=np.uint8).tolist())
49137
if data_type == DataType.SPARSE_FLOAT_VECTOR:
50-
# Sparse vectors are dict[int -> float] in pymilvus.
51138
return {int(i): float(rng.random()) for i in rng.choice(10_000, size=8, replace=False)}
52139
raise AssertionError(f"unsupported vector data type: {data_type}")
53140

@@ -60,9 +147,6 @@ def _add_vector_field(schema, data_type: DataType, name: str, dim: int, nullable
60147

61148

62149
def _flush(client, collection_name):
63-
"""pymilvus flush() blocks until the underlying FlushAll returns, so a
64-
helper here is mostly for readability and future hardening if we need
65-
to add row-count polling."""
66150
client.flush(collection_name=collection_name)
67151

68152

@@ -86,6 +170,8 @@ def _describe_field(client, collection_name, field_name):
86170
]
87171

88172

173+
# ---- test cases -------------------------------------------------------------
174+
89175
class TestRestoreNullableVector(TestcaseBase):
90176
"""Backup/restore behavior for vector fields declared with nullable=True."""
91177

@@ -96,6 +182,7 @@ def test_restore_nullable_vector_round_trip(self, data_type, dim):
96182
97183
Verifies (a) schema nullable flag preserved, (b) per-row NULL preserved.
98184
"""
185+
_check_nullable_vector_support(self)
99186
self._connect()
100187
collection_name = cf.gen_unique_str(PREFIX)
101188
backup_name = cf.gen_unique_str(BACKUP_PREFIX)
@@ -197,6 +284,7 @@ def test_restore_nullable_vector_round_trip(self, data_type, dim):
197284
@pytest.mark.tags(CaseLabel.L1)
198285
def test_restore_nullable_vector_search_skips_null(self):
199286
"""Search on the restored nullable vector should skip NULL rows."""
287+
_check_nullable_vector_support(self)
200288
self._connect()
201289
collection_name = cf.gen_unique_str(PREFIX)
202290
backup_name = cf.gen_unique_str(BACKUP_PREFIX)
@@ -270,6 +358,7 @@ def test_restore_nullable_vector_search_skips_null(self):
270358
@pytest.mark.tags(CaseLabel.L1)
271359
def test_restore_add_nullable_vector_field(self):
272360
"""add_collection_field with a nullable vector field round-trips through backup."""
361+
_check_nullable_vector_support(self)
273362
self._connect()
274363
collection_name = cf.gen_unique_str(PREFIX)
275364
backup_name = cf.gen_unique_str(BACKUP_PREFIX)
@@ -369,6 +458,7 @@ def test_restore_nullable_vector_with_skip_create_collection(self):
369458
370459
Marked L2 because it exercises the negative path.
371460
"""
461+
_check_nullable_vector_support(self)
372462
self._connect()
373463
collection_name = cf.gen_unique_str(PREFIX)
374464
backup_name = cf.gen_unique_str(BACKUP_PREFIX)

0 commit comments

Comments
 (0)