test: cover add-field default values on growing segments (#51303)

## What this PR does

- Remove the `xfail` marker from the existing drop-and-readd
analyzer-field regression for #50484.
- Add L1 E2E coverage for the live QueryNode growing-segment reopen path
when an `enable_match` VARCHAR field with `default_value` is added to
loaded growing data.
- Verify the default value is immediately searchable on pre-existing
unflushed rows, eventually searchable on a subsequent defaulted insert
after the growing text index catches up, and remains searchable after
flush/reload.

Related fix: #51201

issue: #50484

## Scope note

This PR validates the live reopen path:

```text
loaded collection
-> insert rows that stay in a QueryNode growing segment
-> add an `enable_match` VARCHAR field with `default_value`
-> query triggers LazyCheckSchema/Reopen
-> default values for pre-existing growing rows are indexed and searchable immediately
```

It does not cover the QueryNode recovery / LoadGrowing path. That path
requires forcing a QueryNode restart, replacement, or channel rewatch
while the data is still growing: the new QueryNode can create the
segment with the latest schema and then load old binlogs written before
AddField. Since the segment schema is already current in that path,
LazyCheckSchema/Reopen is not the mechanism being tested here. Recovery
coverage should be handled by a focused stability test.

## Test results

-
`test_milvus_client_add_match_field_with_default_value_on_growing_data`:
3/3 passed
- `test_drop_then_add_same_name_analyzer_field`: passed
- Ruff lint and format checks: passed
- `git diff --check`: passed

Signed-off-by: lyyyuna <yiyang.li@zilliz.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Li Yiyang
2026-07-13 20:12:37 +08:00
committed by GitHub
co-authored by Claude Opus 4.6
parent 10adefb7d4
commit c322c84c22
2 changed files with 114 additions and 4 deletions
@@ -930,6 +930,120 @@ class TestMilvusClientAddFieldFeature(TestMilvusClientV2Base):
self.release_collection(client, collection_name)
self.drop_collection(client, collection_name)
@pytest.mark.tags(CaseLabel.L1)
def test_milvus_client_add_match_field_with_default_value_on_growing_data(self):
"""
target: verify schema reopen builds a text index for an added enable_match VARCHAR field
and indexes its default value for pre-existing growing rows
method: load a basic collection, insert rows without flush, add an analyzer field with default_value,
query text_match immediately, insert another row that uses the default, then flush/reload and repeat
expected: old rows immediately match the default token, the new row matches after the growing text index
catches up, and all results remain unchanged after reload
"""
client = self._client()
collection_name = cf.gen_collection_name_by_testcase_name()
dim = 8
field_name = "text_content"
default_text = "defaultmarker"
old_row_count = default_nb
old_ids = set(range(old_row_count))
# Create and load before insert so the historical rows live in a QueryNode growing segment.
schema = client.create_schema(enable_dynamic_field=False, auto_id=False)
schema.add_field("id", DataType.INT64, is_primary=True)
schema.add_field("vec", DataType.FLOAT_VECTOR, dim=dim)
index_params = client.prepare_index_params()
index_params.add_index(field_name="vec", index_type="AUTOINDEX", metric_type="L2")
client.create_collection(
collection_name=collection_name,
schema=schema,
index_params=index_params,
consistency_level="Strong",
)
client.load_collection(collection_name)
vectors = cf.gen_vectors(old_row_count + 1, dim, vector_data_type=DataType.FLOAT_VECTOR)
old_rows = [{"id": i, "vec": vectors[i]} for i in range(old_row_count)]
client.insert(collection_name=collection_name, data=old_rows)
# Confirm the unflushed tail is query-visible before changing the schema.
visible = client.query(
collection_name,
filter=f"id == {old_row_count - 1}",
output_fields=["id"],
)
assert [row["id"] for row in visible] == [old_row_count - 1]
client.add_collection_field(
collection_name,
field_name=field_name,
data_type=DataType.VARCHAR,
nullable=True,
default_value=default_text,
max_length=64,
enable_analyzer=True,
enable_match=True,
analyzer_params={"tokenizer": "standard"},
)
# Critical regression assertion: do not flush, create an index, or reload before this query.
default_match = client.query(
collection_name,
filter=f"text_match({field_name}, '{default_text}')",
output_fields=["id", field_name],
limit=old_row_count + 1,
)
assert {row["id"] for row in default_match} == old_ids
assert len(default_match) == old_row_count
assert all(row[field_name] == default_text for row in default_match)
new_id = old_row_count
all_ids = set(range(old_row_count + 1))
client.insert(
collection_name=collection_name,
data=[{"id": new_id, "vec": vectors[new_id]}],
)
inserted = client.query(
collection_name,
filter=f"id == {new_id}",
output_fields=["id", field_name],
)
assert [row["id"] for row in inserted] == [new_id]
assert inserted[0][field_name] == default_text
# Growing text index updates asynchronously for newly inserted rows. Poll with a bounded timeout.
default_after_insert = []
for _ in range(30):
default_after_insert = client.query(
collection_name,
filter=f"text_match({field_name}, '{default_text}')",
output_fields=["id", field_name],
limit=old_row_count + 1,
)
if {row["id"] for row in default_after_insert} == all_ids:
break
time.sleep(1)
assert {row["id"] for row in default_after_insert} == all_ids
assert len(default_after_insert) == old_row_count + 1
assert all(row[field_name] == default_text for row in default_after_insert)
client.flush(collection_name)
client.release_collection(collection_name)
client.load_collection(collection_name)
default_after_reload = client.query(
collection_name,
filter=f"text_match({field_name}, '{default_text}')",
output_fields=["id", field_name],
limit=old_row_count + 1,
)
assert {row["id"] for row in default_after_reload} == all_ids
assert len(default_after_reload) == old_row_count + 1
assert all(row[field_name] == default_text for row in default_after_reload)
client.drop_collection(collection_name)
@pytest.mark.tags(CaseLabel.L1)
def test_milvus_client_add_field_and_update_existing_data(self):
"""
@@ -2008,10 +2008,6 @@ class TestMilvusClientDropFieldFeature(TestMilvusClientV2Base):
self.drop_collection(client, collection_name)
@pytest.mark.tags(CaseLabel.L1)
@pytest.mark.xfail(
reason="Known issue: https://github.com/milvus-io/milvus/issues/50484",
strict=False,
)
def test_drop_then_add_same_name_analyzer_field(self):
"""
TC-L1-14: Drop analyzer field and re-add same-name field with different analyzer params.