mirror of
https://github.com/milvus-io/milvus.git
synced 2026-07-21 10:15:43 +00:00
test: add REST e2e coverage for regex and array updates (#51189)
## Summary - Add REST v2 e2e coverage for partial update `ARRAY_APPEND` and `ARRAY_REMOVE` field operations on Array fields. - Add REST v2 e2e coverage for regex filters across query, search, delete, JSON paths, Array<Varchar> elements, nullable VarChar fields, negation, boolean expressions, and invalid expressions. ## Test Plan - [x] `PYTHONPYCACHEPREFIX=/private/tmp/milvus_pycache /Users/yiyang.li/gitup/milvus/tests/python_client/.venv/bin/python -m py_compile tests/restful_client_v2/testcases/test_partial_update_array_op.py tests/restful_client_v2/testcases/test_regex_filter.py` - [x] `git diff --cached --check` --------- Signed-off-by: lyyyuna <yiyang.li@zilliz.com>
This commit is contained in:
@@ -52,6 +52,15 @@ class Base:
|
||||
class TestBase(Base):
|
||||
req = None
|
||||
|
||||
@pytest.fixture(scope="class", autouse=True)
|
||||
def init_class_config(self, endpoint, token):
|
||||
self.endpoint = f"{endpoint}"
|
||||
self.api_key = f"{token}" if token is not None else None
|
||||
self.invalid_api_key = "invalid_token"
|
||||
|
||||
def _class_scope_clients(self):
|
||||
return CollectionClient(self.endpoint, self.api_key), VectorClient(self.endpoint, self.api_key)
|
||||
|
||||
def teardown_method(self):
|
||||
# Clean up collections
|
||||
if hasattr(self, "api_key") and self.api_key:
|
||||
|
||||
@@ -39,12 +39,12 @@ def pytest_addoption(parser):
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@pytest.fixture(scope="class")
|
||||
def endpoint(request):
|
||||
return request.config.getoption("--endpoint")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@pytest.fixture(scope="class")
|
||||
def token(request):
|
||||
return request.config.getoption("--token")
|
||||
|
||||
|
||||
@@ -0,0 +1,181 @@
|
||||
import pytest
|
||||
from base.testbase import TestBase
|
||||
from utils.constant import CaseLabel
|
||||
from utils.utils import gen_collection_name
|
||||
|
||||
|
||||
class TestPartialUpdateArrayOp(TestBase):
|
||||
def _create_array_collection(self):
|
||||
name = gen_collection_name()
|
||||
self.name = name
|
||||
payload = {
|
||||
"collectionName": name,
|
||||
"schema": {
|
||||
"fields": [
|
||||
{"fieldName": "id", "dataType": "Int64", "isPrimary": True, "elementTypeParams": {}},
|
||||
{"fieldName": "name", "dataType": "VarChar", "elementTypeParams": {"max_length": "64"}},
|
||||
{
|
||||
"fieldName": "tags",
|
||||
"dataType": "Array",
|
||||
"elementDataType": "Int64",
|
||||
"elementTypeParams": {"max_capacity": "16"},
|
||||
},
|
||||
{
|
||||
"fieldName": "labels",
|
||||
"dataType": "Array",
|
||||
"elementDataType": "VarChar",
|
||||
"elementTypeParams": {"max_capacity": "16", "max_length": "64"},
|
||||
},
|
||||
{"fieldName": "vector", "dataType": "FloatVector", "elementTypeParams": {"dim": "4"}},
|
||||
]
|
||||
},
|
||||
"indexParams": [{"fieldName": "vector", "indexName": "vector_index", "metricType": "L2"}],
|
||||
}
|
||||
rsp = self.collection_client.collection_create(payload)
|
||||
assert rsp["code"] == 0, rsp
|
||||
return name
|
||||
|
||||
def _insert_base_rows(self, name):
|
||||
rows = [
|
||||
{
|
||||
"id": 0,
|
||||
"name": "row_0",
|
||||
"tags": [1, 2],
|
||||
"labels": ["a", "b", "a"],
|
||||
"vector": [0.1, 0.2, 0.3, 0.4],
|
||||
},
|
||||
{
|
||||
"id": 1,
|
||||
"name": "row_1",
|
||||
"tags": [10, 20, 10, 30],
|
||||
"labels": ["x", "y"],
|
||||
"vector": [0.2, 0.3, 0.4, 0.5],
|
||||
},
|
||||
]
|
||||
rsp = self.vector_client.vector_insert({"collectionName": name, "data": rows})
|
||||
assert rsp["code"] == 0, rsp
|
||||
self.collection_client.flush(name)
|
||||
|
||||
def _query_rows(self, name):
|
||||
rsp = self.vector_client.vector_query(
|
||||
{
|
||||
"collectionName": name,
|
||||
"filter": "id >= 0",
|
||||
"outputFields": ["id", "name", "tags", "labels"],
|
||||
}
|
||||
)
|
||||
assert rsp["code"] == 0, rsp
|
||||
rows = {row["id"]: row for row in rsp["data"]}
|
||||
for row in rows.values():
|
||||
row["tags"] = self._array_data(row["tags"])
|
||||
row["labels"] = self._array_data(row["labels"])
|
||||
return rows
|
||||
|
||||
@staticmethod
|
||||
def _array_data(value):
|
||||
if not isinstance(value, dict):
|
||||
return value
|
||||
|
||||
data = value.get("Data", value.get("data"))
|
||||
if not isinstance(data, dict):
|
||||
return value
|
||||
|
||||
for field_name in ("LongData", "StringData", "BoolData", "FloatData", "DoubleData", "IntData"):
|
||||
typed_data = data.get(field_name)
|
||||
if isinstance(typed_data, dict) and "data" in typed_data:
|
||||
return typed_data["data"]
|
||||
return value
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L0)
|
||||
def test_partial_update_array_append(self):
|
||||
"""
|
||||
target: verify REST upsert fieldOps ARRAY_APPEND appends array payloads
|
||||
method: insert rows, upsert only tags with ARRAY_APPEND, then query rows
|
||||
expected: tags are appended and non-updated fields are preserved
|
||||
"""
|
||||
name = self._create_array_collection()
|
||||
self._insert_base_rows(name)
|
||||
|
||||
payload = {
|
||||
"collectionName": name,
|
||||
"data": [{"id": 0, "tags": [3, 4]}, {"id": 1, "tags": [40]}],
|
||||
"partialUpdate": True,
|
||||
"fieldOps": [{"fieldName": "tags", "op": "ARRAY_APPEND"}],
|
||||
}
|
||||
rsp = self.vector_client.vector_upsert(payload)
|
||||
assert rsp["code"] == 0, rsp
|
||||
|
||||
rows = self._query_rows(name)
|
||||
assert rows[0]["tags"] == [1, 2, 3, 4]
|
||||
assert rows[1]["tags"] == [10, 20, 10, 30, 40]
|
||||
assert rows[0]["name"] == "row_0"
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L0)
|
||||
def test_partial_update_array_remove(self):
|
||||
"""
|
||||
target: verify REST upsert fieldOps ARRAY_REMOVE removes matching array elements
|
||||
method: insert repeated array values, remove one value through fieldOps
|
||||
expected: all matching values are removed from the target array field
|
||||
"""
|
||||
name = self._create_array_collection()
|
||||
self._insert_base_rows(name)
|
||||
|
||||
payload = {
|
||||
"collectionName": name,
|
||||
"data": [{"id": 1, "tags": [10]}],
|
||||
"partialUpdate": True,
|
||||
"fieldOps": [{"fieldName": "tags", "op": "ARRAY_REMOVE"}],
|
||||
}
|
||||
rsp = self.vector_client.vector_upsert(payload)
|
||||
assert rsp["code"] == 0, rsp
|
||||
|
||||
rows = self._query_rows(name)
|
||||
assert rows[1]["tags"] == [20, 30]
|
||||
assert rows[1]["labels"] == ["x", "y"]
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L1)
|
||||
def test_partial_update_array_multiple_field_ops(self):
|
||||
"""
|
||||
target: verify REST upsert accepts multiple array fieldOps in one request
|
||||
method: append Int64 array field and remove VarChar array field together
|
||||
expected: each field uses its own op and unchanged fields are preserved
|
||||
"""
|
||||
name = self._create_array_collection()
|
||||
self._insert_base_rows(name)
|
||||
|
||||
payload = {
|
||||
"collectionName": name,
|
||||
"data": [{"id": 0, "tags": [3], "labels": ["a"]}],
|
||||
"partialUpdate": True,
|
||||
"fieldOps": [
|
||||
{"fieldName": "tags", "op": "ARRAY_APPEND"},
|
||||
{"fieldName": "labels", "op": "ARRAY_REMOVE"},
|
||||
],
|
||||
}
|
||||
rsp = self.vector_client.vector_upsert(payload)
|
||||
assert rsp["code"] == 0, rsp
|
||||
|
||||
rows = self._query_rows(name)
|
||||
assert rows[0]["tags"] == [1, 2, 3]
|
||||
assert rows[0]["labels"] == ["b"]
|
||||
assert rows[0]["name"] == "row_0"
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L1)
|
||||
def test_partial_update_array_op_rejects_non_array_field(self):
|
||||
"""
|
||||
target: verify REST rejects ARRAY_APPEND on a non-Array field
|
||||
method: send fieldOps ARRAY_APPEND for VarChar field name
|
||||
expected: request fails with a clear non-Array field validation error
|
||||
"""
|
||||
name = self._create_array_collection()
|
||||
self._insert_base_rows(name)
|
||||
|
||||
payload = {
|
||||
"collectionName": name,
|
||||
"data": [{"id": 0, "name": "bad_append"}],
|
||||
"partialUpdate": True,
|
||||
"fieldOps": [{"fieldName": "name", "op": "ARRAY_APPEND"}],
|
||||
}
|
||||
rsp = self.vector_client.vector_upsert(payload)
|
||||
assert rsp["code"] != 0, rsp
|
||||
assert 'op ARRAY_APPEND requires Array field, but field "name" is VarChar' in rsp["message"]
|
||||
@@ -0,0 +1,348 @@
|
||||
import pytest
|
||||
from base.testbase import TestBase
|
||||
from utils.constant import CaseLabel
|
||||
from utils.utils import gen_collection_name
|
||||
|
||||
DIM = 4
|
||||
|
||||
|
||||
def _vec(seed):
|
||||
return [float(seed), 0.0, 0.0, 0.0]
|
||||
|
||||
|
||||
def _regex_rows():
|
||||
return [
|
||||
{
|
||||
"id": 1,
|
||||
"vec": _vec(1),
|
||||
"text": "ERROR E1001: connection timeout",
|
||||
"email": "alice@gmail.com",
|
||||
"url": "/api/v1/users/123",
|
||||
"level": "ERROR",
|
||||
"metadata": {
|
||||
"level": "ERROR",
|
||||
"version": "v1.2",
|
||||
"trace": "abc-123",
|
||||
"version_num": 12,
|
||||
"enabled": True,
|
||||
"nested": {"x": "abc"},
|
||||
"arr": ["abc"],
|
||||
},
|
||||
"tags": ["release-v1", "prod"],
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"vec": _vec(2),
|
||||
"text": "WARN W2002: retry later",
|
||||
"email": "bob@example.com",
|
||||
"url": "/api/v2/orders/456",
|
||||
"level": "WARN",
|
||||
"metadata": {
|
||||
"level": "WARN",
|
||||
"version": "v2.0",
|
||||
"trace": "def-456",
|
||||
"version_num": 20,
|
||||
"enabled": False,
|
||||
"nested": {"x": "def"},
|
||||
"arr": ["def"],
|
||||
},
|
||||
"tags": ["release-v2", "staging"],
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"vec": _vec(3),
|
||||
"text": "DEBUG cache hit",
|
||||
"email": "carol@GMAIL.com",
|
||||
"url": "/api/v10/users/789",
|
||||
"level": "DEBUG",
|
||||
"metadata": {
|
||||
"level": "DEBUG",
|
||||
"version": "v10.1",
|
||||
"trace": "ghi-789",
|
||||
"version_num": 101,
|
||||
"enabled": True,
|
||||
"nested": {"x": "ghi"},
|
||||
"arr": ["ghi"],
|
||||
},
|
||||
"tags": ["debug", "dev"],
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"vec": _vec(4),
|
||||
"text": "cn log error code 555-1234",
|
||||
"email": None,
|
||||
"url": "/static/index.html",
|
||||
"level": "INFO",
|
||||
"metadata": {
|
||||
"level": "INFO",
|
||||
"version": "alpha",
|
||||
"trace": None,
|
||||
"version_num": 0,
|
||||
"enabled": False,
|
||||
"nested": {"x": "cn"},
|
||||
"arr": ["cn"],
|
||||
},
|
||||
"tags": ["cn", "release-alpha"],
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"vec": _vec(5),
|
||||
"text": "multi\nline c\nd pattern",
|
||||
"email": "dave@gmail.com",
|
||||
"url": "/api/v1/users/search",
|
||||
"level": "ERROR",
|
||||
"metadata": {
|
||||
"level": "ERROR",
|
||||
"version": "v1.3",
|
||||
"trace": "jkl-000",
|
||||
"version_num": 13,
|
||||
"enabled": True,
|
||||
"nested": {"x": "jkl"},
|
||||
"arr": ["jkl"],
|
||||
},
|
||||
"tags": ["release-v1-hotfix", "prod"],
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
"vec": _vec(6),
|
||||
"text": "",
|
||||
"email": "empty@gmail.com",
|
||||
"url": "",
|
||||
"level": "",
|
||||
"metadata": {
|
||||
"level": "",
|
||||
"version": "",
|
||||
"trace": "",
|
||||
"version_num": None,
|
||||
"enabled": False,
|
||||
"nested": {},
|
||||
"arr": [],
|
||||
},
|
||||
"tags": ["", "empty"],
|
||||
},
|
||||
{
|
||||
"id": 7,
|
||||
"vec": _vec(7),
|
||||
"text": "status OK deploy success",
|
||||
"email": "emo@dev.io",
|
||||
"url": "/api/health",
|
||||
"level": "INFO",
|
||||
"metadata": {
|
||||
"level": "INFO",
|
||||
"version": "v1.0",
|
||||
"trace": "emoji-001",
|
||||
"version_num": 10,
|
||||
"enabled": True,
|
||||
"nested": {"x": "ok"},
|
||||
"arr": ["ok"],
|
||||
},
|
||||
"tags": ["emoji", "dev"],
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
class TestRegexFilter(TestBase):
|
||||
def setup_class(self):
|
||||
self.collection_name = self.__class__.__name__ + gen_collection_name()
|
||||
|
||||
@pytest.fixture(scope="class", autouse=True)
|
||||
def prepare_shared_regex_collection(self, request, init_class_config):
|
||||
collection_client, vector_client = self._class_scope_clients()
|
||||
|
||||
def teardown():
|
||||
collection_client.collection_drop({"collectionName": self.collection_name})
|
||||
|
||||
request.addfinalizer(teardown)
|
||||
self._create_regex_collection(self.collection_name, collection_client, vector_client)
|
||||
|
||||
def _create_regex_collection(self, name, collection_client, vector_client):
|
||||
payload = {
|
||||
"collectionName": name,
|
||||
"schema": {
|
||||
"autoId": False,
|
||||
"enableDynamicField": False,
|
||||
"fields": [
|
||||
{"fieldName": "id", "dataType": "Int64", "isPrimary": True},
|
||||
{"fieldName": "vec", "dataType": "FloatVector", "elementTypeParams": {"dim": str(DIM)}},
|
||||
{"fieldName": "text", "dataType": "VarChar", "elementTypeParams": {"max_length": "512"}},
|
||||
{
|
||||
"fieldName": "email",
|
||||
"dataType": "VarChar",
|
||||
"nullable": True,
|
||||
"elementTypeParams": {"max_length": "256"},
|
||||
},
|
||||
{"fieldName": "url", "dataType": "VarChar", "elementTypeParams": {"max_length": "512"}},
|
||||
{"fieldName": "level", "dataType": "VarChar", "elementTypeParams": {"max_length": "32"}},
|
||||
{"fieldName": "metadata", "dataType": "JSON"},
|
||||
{
|
||||
"fieldName": "tags",
|
||||
"dataType": "Array",
|
||||
"elementDataType": "VarChar",
|
||||
"elementTypeParams": {"max_capacity": "8", "max_length": "128"},
|
||||
},
|
||||
],
|
||||
},
|
||||
"indexParams": [{"fieldName": "vec", "indexName": "vec_index", "metricType": "L2"}],
|
||||
}
|
||||
rsp = collection_client.collection_create(payload)
|
||||
assert rsp["code"] == 0, rsp
|
||||
collection_client.wait_load_completed(name, timeout=60)
|
||||
|
||||
rows = _regex_rows()
|
||||
rsp = vector_client.vector_insert({"collectionName": name, "data": rows})
|
||||
assert rsp["code"] == 0, rsp
|
||||
assert rsp["data"]["insertCount"] == len(rows)
|
||||
collection_client.flush(name)
|
||||
|
||||
def _shared_collection(self):
|
||||
return self.collection_name
|
||||
|
||||
def _query_ids(self, name, filter_expr, timeout=1):
|
||||
rsp = self.vector_client.vector_query(
|
||||
{"collectionName": name, "filter": filter_expr, "outputFields": ["id"], "limit": 100},
|
||||
timeout=timeout,
|
||||
)
|
||||
assert rsp["code"] == 0, rsp
|
||||
return sorted(row["id"] for row in rsp.get("data", []))
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L0)
|
||||
def test_regex_query_basic_semantics(self):
|
||||
"""
|
||||
target: verify REST query supports core regex =~ semantics
|
||||
method: query VarChar field with substring, anchors, classes, dot-newline, and empty pattern
|
||||
expected: returned ids match PyMilvus regex filter semantics
|
||||
"""
|
||||
name = self._shared_collection()
|
||||
|
||||
cases = [
|
||||
('text =~ "timeout"', [1]),
|
||||
('text =~ "ERROR"', [1]),
|
||||
('text =~ "^ERROR"', [1]),
|
||||
('text =~ "hit$"', [3]),
|
||||
('text =~ "^DEBUG cache hit$"', [3]),
|
||||
(r'text =~ "E[0-9]{4}:"', [1]),
|
||||
(r'text =~ "[0-9]{3}-[0-9]{4}"', [4]),
|
||||
('text =~ "c.d"', [4, 5]),
|
||||
('text =~ "(?-s)c.d"', [4]),
|
||||
('text =~ ""', [1, 2, 3, 4, 5, 6, 7]),
|
||||
('text =~ "^$"', [6]),
|
||||
]
|
||||
for filter_expr, expected in cases:
|
||||
assert self._query_ids(name, filter_expr) == expected
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L1)
|
||||
def test_regex_query_negation_and_boolean_expression(self):
|
||||
"""
|
||||
target: verify REST query supports !~, not(=~), and boolean combinations
|
||||
method: compare negated regex and combined scalar predicates
|
||||
expected: !~ is equivalent to not(=~) and boolean precedence is respected
|
||||
"""
|
||||
name = self._shared_collection()
|
||||
|
||||
assert self._query_ids(name, 'text !~ "^DEBUG"') == [1, 2, 4, 5, 6, 7]
|
||||
assert self._query_ids(name, 'not (text =~ "^DEBUG")') == [1, 2, 4, 5, 6, 7]
|
||||
assert self._query_ids(name, 'text =~ "ERROR" and id > 1') == []
|
||||
assert self._query_ids(name, 'text =~ "(?i)error" and id > 1') == [4]
|
||||
assert self._query_ids(name, 'text =~ "^WARN" or text =~ "^DEBUG"') == [2, 3]
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L1)
|
||||
def test_regex_query_json_array_and_nullable_paths(self):
|
||||
"""
|
||||
target: verify REST query supports regex on JSON paths, Array<Varchar> elements, and nullable fields
|
||||
method: query JSON string paths, array element paths, and nullable VarChar with =~ and !~
|
||||
expected: string paths match, non-string/missing paths return empty, null values are excluded unless explicit
|
||||
"""
|
||||
name = self._shared_collection()
|
||||
|
||||
assert self._query_ids(name, r'metadata["version"] =~ "^v[0-9]+\.[0-9]+$"') == [1, 2, 3, 5, 7]
|
||||
assert self._query_ids(name, 'metadata["nested"]["x"] =~ "^abc$"') == [1]
|
||||
assert self._query_ids(name, 'metadata["nested"]["x"] !~ "^abc$"') == [2, 3, 4, 5, 7]
|
||||
assert self._query_ids(name, r'metadata["trace"] =~ "[a-z]+-[0-9]+"') == [1, 2, 3, 5, 7]
|
||||
assert self._query_ids(name, 'metadata["version_num"] =~ "1"', timeout=0) == []
|
||||
assert self._query_ids(name, 'metadata["nested"]["missing"] =~ ".*"', timeout=0) == []
|
||||
|
||||
assert self._query_ids(name, 'tags[0] =~ "^release-v[0-9]+"') == [1, 2, 5]
|
||||
assert self._query_ids(name, 'tags[0] !~ "^release"') == [3, 4, 6, 7]
|
||||
assert self._query_ids(name, 'tags[10] =~ ".*"', timeout=0) == []
|
||||
assert self._query_ids(name, 'tags[0] =~ "^$"') == [6]
|
||||
|
||||
assert self._query_ids(name, 'email =~ "gmail"') == [1, 5, 6]
|
||||
assert self._query_ids(name, 'email !~ "gmail"') == [2, 3, 7]
|
||||
assert self._query_ids(name, 'email !~ "gmail" or email is null') == [2, 3, 4, 7]
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L0)
|
||||
def test_regex_search_filter(self):
|
||||
"""
|
||||
target: verify REST vector search supports regex filters
|
||||
method: run unfiltered search then filtered search with regex on url field
|
||||
expected: filtered search only returns ids with /api/v[0-9]+/users urls
|
||||
"""
|
||||
name = self._shared_collection()
|
||||
|
||||
unfiltered = self.vector_client.vector_search(
|
||||
{
|
||||
"collectionName": name,
|
||||
"data": [_vec(1)],
|
||||
"annsField": "vec",
|
||||
"limit": 7,
|
||||
"outputFields": ["id"],
|
||||
"searchParams": {"metricType": "L2", "params": {}},
|
||||
}
|
||||
)
|
||||
assert unfiltered["code"] == 0, unfiltered
|
||||
assert {row["id"] for row in unfiltered["data"]} == {1, 2, 3, 4, 5, 6, 7}
|
||||
|
||||
filtered = self.vector_client.vector_search(
|
||||
{
|
||||
"collectionName": name,
|
||||
"data": [_vec(1)],
|
||||
"annsField": "vec",
|
||||
"filter": 'url =~ "^/api/v[0-9]+/users"',
|
||||
"limit": 3,
|
||||
"outputFields": ["id", "url"],
|
||||
"searchParams": {"metricType": "L2", "params": {}},
|
||||
}
|
||||
)
|
||||
assert filtered["code"] == 0, filtered
|
||||
assert {row["id"] for row in filtered["data"]} == {1, 3, 5}
|
||||
assert all(row["url"].startswith("/api/v") for row in filtered["data"])
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L1)
|
||||
def test_regex_delete_filter(self):
|
||||
"""
|
||||
target: verify REST delete supports regex filters
|
||||
method: delete rows where text starts with DEBUG, then query remaining ids
|
||||
expected: id 3 is removed and other rows remain visible
|
||||
"""
|
||||
name = gen_collection_name()
|
||||
self.name = name
|
||||
self._create_regex_collection(name, self.collection_client, self.vector_client)
|
||||
|
||||
rsp = self.vector_client.vector_delete({"collectionName": name, "filter": 'text =~ "^DEBUG"'})
|
||||
assert rsp["code"] == 0, rsp
|
||||
|
||||
assert self._query_ids(name, "id > 0") == [1, 2, 4, 5, 6, 7]
|
||||
|
||||
@pytest.mark.tags(CaseLabel.L1)
|
||||
def test_regex_invalid_expressions(self):
|
||||
"""
|
||||
target: verify REST rejects invalid regex expressions
|
||||
method: send invalid pattern, unsupported field type, direct array field, and non-string RHS
|
||||
expected: each request fails with a clear validation error
|
||||
"""
|
||||
name = self._shared_collection()
|
||||
|
||||
cases = [
|
||||
('text =~ "(unclosed"', "missing closing"),
|
||||
('text =~ "(a)\\\\1"', "invalid regex pattern"),
|
||||
('id =~ "1"', "regex match on non-string or non-json field"),
|
||||
('tags =~ "prod"', "can not comparisons array fields directly"),
|
||||
("text =~ 123", "string literal or template variable"),
|
||||
('unknown_field =~ "test"', "field unknown_field not exist"),
|
||||
]
|
||||
for filter_expr, message in cases:
|
||||
rsp = self.vector_client.vector_query(
|
||||
{"collectionName": name, "filter": filter_expr, "outputFields": ["id"], "limit": 10}
|
||||
)
|
||||
assert rsp["code"] != 0, rsp
|
||||
assert message in rsp["message"], rsp
|
||||
Reference in New Issue
Block a user