mirror of
https://github.com/milvus-io/milvus.git
synced 2026-10-28 17:05:53 +08:00
issue: #52114 Makes `ruff check` and `ruff format --check` pass over all of `tests/`, so the `Ruff (changed files only)` job stops failing PRs for violations they did not introduce (most recently #51903, which got 69 errors from an 8-line change). Almost all of this is mechanical. The behavioural changes are called out individually below: one `NameError` fix, three shadowed duplicate definitions, 13 assertion gaps, and two undefined names that only became visible once star imports were expanded. ### Layout Seven reviewable commits: | commit | what | files | |---|---|---| | 1 | `ruff format` | 170 | | 2 | `ruff check --fix` (safe fixes only, no `--unsafe-fixes`) | 156 | | 3 | F821 fix + `tests/ruff.toml` | 4 | | 4 | three shadowed duplicate definitions | 4 | | 5 | close 13 assertion gaps behind F841 | 7 | | 6 | expand star imports, re-enable F403/F405/F821 | 33 | | 7 | enable E402, E712, E722 | 16 | 1745 violations → 0. Only `tests/` is touched. ### The undefined-name bug `chaos/test_chaos_memory_stress.py`: `TestMemoryStressReplica.prepare_collection` referenced bare `nb` and `dim`. Those are class attributes, never module-level names, so the fixture raised `NameError` the moment it ran — and the line directly above it already used `self.dim`. Changed to `self.nb` / `self.dim`. This was the only F821 in `tests/`; **the rule stays enabled**. ### Three shadowed duplicate definitions Three places define the same name twice in the same scope. Python keeps the later one, so the earlier definition is dead while still reading as live code — a test that looks like it exists and never runs. **`restful_client/testcases/test_vector_operations.py` — `TestDeleteVector`** ```python @pytest.mark.L0 class TestDeleteVector(TestBase): # line 778 — shadowed, never ran def test_delete_vector_default(...) # 2x2 parametrized @pytest.mark.L1 class TestDeleteVector(TestBase): # line 838 — wins def test_delete_vector_with_invalid_api_key(...) def test_delete_vector_with_invalid_collection_name(...) def test_delete_vector_with_non_primary_key(...) ``` The second holds only negative cases, and the file already uses `TestInsertVectorNegative` / `TestSearchVectorNegative`, so this is a copy whose class name was never updated. Renamed it to `TestDeleteVectorNegative`. **This is the one to look at:** it brings back an `L0` case, so 4 parametrizations of `test_delete_vector_default` start running in PR CI. They have never executed, so if the restful delete path has drifted since they were written, this is where it surfaces. **`python_client/testcases/test_compaction.py` — `test_compact_after_delete`** Defined twice in `TestCompactionParams`, covering different things: the shadowed one (L3, `@parametrize("delete_pos", [1, tmp_nb // 2])`) covers *partial* deletes plus a compaction-plan check; the surviving one (L2) deletes everything. There is already a separate `test_compact_after_delete_all`, so partial-delete coverage existed only in the shadowed case and was not running. Renamed it to `test_compact_after_delete_partial`; the surviving definition is untouched. **`python_client/utils/util_pymilvus.py` — `add_vector_field`** Defined twice with different signatures: ```python def add_vector_field(entities, is_normal=False): # line 455 — shadowed ... entities.append(field) return entities def add_vector_field(nb, dimension=default_dim): # line 527 — wins field_name = gen_unique_str() field = {...} # built, then discarded return field_name # returns only the name ``` Removed the second — both the broken one and the one that was winning. Grepped the whole repo: **neither definition has a caller**; every `add_vector_field` hit is something else (`test_milvus_client_add_vector_field` test methods, the `Op.add_vector_field` enum member in `chaos/checker.py`, a `Checker` method of the same name). Dead-code removal, not an API change. **Net effect on what executes:** `test_delete_vector_default` ×4 (**L0**) and `test_compact_after_delete_partial` ×2 (L3) start running; nothing stops running; the 3 negative cases keep running under the new class name. Computed by parsing each file before and after and resolving shadowing the way Python does, not by reading the diff. F811 is now clean across `tests/` with no per-file suppressions. ### Star imports expanded, F403/F405/F821 re-enabled 33 of 307 files used `from utils.util_pymilvus import *` and friends. Inside those, pyflakes cannot tell a typo from a star-imported name — it downgrades everything unresolved to F405, and **F821 never fires**. The `NameError` fixed in commit 3 was only catchable because that file happens not to use a star import. The 379 F405 hits were only **58 distinct names**. All are now explicit, resolved by walking the star modules in source order (later import wins, matching Python). Names that `util_pymilvus` merely re-exports come from their real source now — `DataType` from `pymilvus`, `np` from `numpy`, `log` from `utils.util_log` — rather than being laundered through it. Turning F821 on immediately caught **two real undefined names**, both in `rate_limit/test_rate_limit.py` (L3, so it has never run in CI): ```python for collection_num in range(collectionRateLimit): # no such name # ^ the parametrize arg is `collection_rate_limit`, and it is # a string ("10"/"500"), so range() needs int(). # The same test uses the correct name 30 lines below. mil.uninstall(release_name, namespace=NAMESPACE) # no such name # ^ MilvusOperator.uninstall has exactly this signature, and other suites do the # same under `mic`/`milvus_op` — the instantiation was just missing. ``` Both fixed. `test_milvus_client_data_integrity_new.py` needed 15 `noqa` markers instead: its `prepare_data` fixture injects those names via `globals()[field] = ...`, which no static analysis can follow. That block has other problems — a `float is None` check against the builtin, an always-true `if "is null" or "IS NULL" in expression`, leftover `binbin_debug1` logging — but rewriting fixture logic does not belong here; noted in #52116. ### 13 assertion gaps closed Of ~80 F841 hits, most are harmless scratch values. 13 were a call whose result was bound and then never checked. Highlights: - `test_milvus_client_database.py`: two tests apply a non-existent property key (`data.replica.number`, note `data.` not `database.`), describe the database, and drop it without checking anything. Their docstrings say `actual: run successfully` / `nothing changed` — the intent was to prove nothing changed. Now asserted. - `test_milvus_client_alias.py`: two assertions were commented out, **and they were commented out for a reason** — `list_aliases()[0]` returns a dict, so `assert alias in aliases` would have tested dict keys. Rewritten against `[0]["aliases"]` (how the rest of the file does it) and re-enabled. In the skipped `alias_default` test the calls were also missing the required `collection_name` argument. The remaining ~65 F841 stay, and the rule stays off. ### E402, E712, E722 also enabled **E722** (37 sites): mechanical `except:` → `except Exception:`. Checked the two ways this could change behaviour, both empty: no bare-except try-block can raise `SystemExit`/`KeyboardInterrupt`, and **none calls `pytest.skip`/`fail`/`xfail`**. That second one matters — pytest's `Skipped` derives from `BaseException`, so a bare except silently eats it while `except Exception` would not. 0 of 37, so behaviour is unchanged. **E402** (5 sites): imports that were just in the wrong place — `import re` sat 70 lines into `check/param_check.py`, and the two `resource_group` modules imported `rich` after their module-level constants. Nothing to do with `sys.path`, which is what an earlier version of this description wrongly claimed. **E712** (5 sites), gone through one at a time rather than bulk-rewritten: | site | value | change | |---|---|---| | `test_milvus_client_partition.py` | `has_partition()` | → `is False` — stricter than `not result`, which would pass on `None` | | `test_restart.py` | JSON status flag | → `is True` | | `test_restart.py` | local `break_flag` | → plain truth test, it is a real bool | | `test_query.py` | wrapper `check_result` | → `is True` | | `test_milvus_client_ttl.py` | `hit.get(bool_field)` | **kept as `== False` + noqa** | That last one is the reason this rule cannot be bulk-fixed: `.get()` returns `None` for a missing field, so `not ...` would pass exactly where the assertion should fail, and `is False` would break on a numpy bool. ### What ruff.toml still disables | rule | n | reason | |---|---|---| | F841 | ~65 | tests deliberately keep call results; the 13 that were genuine assertion gaps are fixed above | | UP031 | 95 | pure style rewrite that catches no bugs, and 27 sites use `% (...)` tuples where it is not equivalent | Everything else pyflakes/pycodestyle checks is on, including `F401`, `F811` and `F821`, so new code cannot introduce dead, duplicate or undefined names. Tracked in #52116. ### Verification Not just "ruff is green" — the mechanical passes were checked for semantic drift: - **Format pass:** parsed all 307 files before and after and compared ASTs with string constants normalized away → **zero structural differences**. The only AST-visible deltas are 146 string-constant changes, every one whitespace-only (docstring indentation / trailing whitespace); zero non-whitespace string changes. - **Import removals:** 142 imports were dropped. For each, checked whether the bound name still appears anywhere in its file. 115 have no remaining reference at all. The 27 that still matched textually were each read by hand and are all comments (`# pdb.set_trace()`, the commented-out checker dict in `test_load_with_checker.py`), docstring prose (`"input random collection name"`, `"Tuple[List[Dict]]"`), string literals (`"Collection ID"`, the `"config"` dict key), a different binding (`from time import sleep`), or attribute access through another module (`ct.CaseLabel`, `np.random`). **No real reference was removed.** - **Python version:** the pyupgrade rewrites emit `datetime.UTC` (3.11+) and `X | None` (3.10+). `tests/.python-version` is 3.12, `tests/scripts/python-env.sh` defaults to 3.12, and `tests/docker/Dockerfile` is `python:3.12-bullseye`. - **`compileall`** over all 307 files under Python 3.12 passes. - No external reference to `TestDeleteVector`, `test_compact_after_delete`, or `add_vector_field` exists anywhere in the repo (`*.py`, `*.sh`, `*.yaml`, `*.json`). ### Conflict surface Of the 100 currently-open PRs, 9 touch `tests/*.py` and only **2** overlap with this change: #51903 (3 of its 5 files) and #51856 (1 file). The other 7 touch files that were already format-clean. Worth merging promptly to keep that number small. `.pre-commit-config.yaml` needs no change — it already runs with `--config=tests/ruff.toml`, so local hooks and CI stay in sync. --------- Signed-off-by: xiaofanluan <xf@hjjaq.com> Co-authored-by: xiaofanluan <xf@hjjaq.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2677 lines
113 KiB
Python
2677 lines
113 KiB
Python
import random
|
|
import time
|
|
|
|
import numpy as np
|
|
import pandas as pd
|
|
import pytest
|
|
from base.client_base import TestcaseBase
|
|
from common import common_func as cf
|
|
from common import common_type as ct
|
|
from common.common_type import CaseLabel, CheckTasks
|
|
from pymilvus.grpc_gen.common_pb2 import SegmentState
|
|
from utils.util_log import test_log as log
|
|
|
|
prefix = "delete"
|
|
half_nb = ct.default_nb // 2
|
|
tmp_nb = 3000
|
|
tmp_expr = f"{ct.default_int64_field_name} in {[0]}"
|
|
query_res_tmp_expr = [{f"{ct.default_int64_field_name}": 0}]
|
|
query_tmp_expr_str = [{f"{ct.default_string_field_name}": "0"}]
|
|
exp_res = "exp_res"
|
|
default_string_expr = 'varchar in [ "0"]'
|
|
default_invalid_string_exp = "varchar >= 0"
|
|
index_name1 = cf.gen_unique_str("float")
|
|
index_name2 = cf.gen_unique_str("varchar")
|
|
default_search_params = ct.default_search_params
|
|
|
|
|
|
class TestDeleteParams(TestcaseBase):
|
|
"""
|
|
Test case of delete interface
|
|
def delete(expr, partition_name=None, timeout=None, **kwargs)
|
|
return MutationResult
|
|
Only the `in` operator is supported in the expr
|
|
"""
|
|
|
|
@pytest.mark.tags(CaseLabel.L0)
|
|
@pytest.mark.parametrize("is_binary", [False, True])
|
|
def test_delete_entities(self, is_binary):
|
|
"""
|
|
target: test delete data from collection
|
|
method: 1.create and insert nb with flush
|
|
2.load collection
|
|
3.delete half of nb
|
|
4.query with deleted ids
|
|
expected: Query result is empty
|
|
"""
|
|
# init collection with default_nb default data
|
|
collection_w, _, _, ids = self.init_collection_general(prefix, insert_data=True, is_binary=is_binary)[0:4]
|
|
expr = f"{ct.default_int64_field_name} in {ids[:half_nb]}"
|
|
|
|
# delete half of data
|
|
del_res = collection_w.delete(expr)[0]
|
|
assert del_res.delete_count == half_nb
|
|
# This flush will not persist the deleted ids, just delay the time to ensure that queryNode consumes deleteMsg
|
|
collection_w.num_entities
|
|
|
|
# query with deleted ids
|
|
collection_w.query(expr, check_task=CheckTasks.check_query_empty)
|
|
|
|
@pytest.mark.tags(CaseLabel.L0)
|
|
@pytest.mark.parametrize("is_binary", [False, True])
|
|
def test_delete_entities_with_range(self, is_binary):
|
|
"""
|
|
target: test delete data from collection
|
|
method: 1.create and insert nb with flush
|
|
2.load collection
|
|
3.delete half of nb
|
|
4.query with deleted ids
|
|
expected: Query result is empty
|
|
"""
|
|
# init collection with default_nb default data
|
|
collection_w, _, _, ids = self.init_collection_general(
|
|
prefix, insert_data=True, auto_id=True, is_binary=is_binary
|
|
)[0:4]
|
|
expr = f"{ct.default_int64_field_name} < {ids[half_nb]}"
|
|
|
|
# delete half of data
|
|
del_res = collection_w.delete(expr)[0]
|
|
assert del_res.delete_count == half_nb
|
|
# This flush will not persist the deleted ids, just delay the time to ensure that queryNode consumes deleteMsg
|
|
collection_w.num_entities
|
|
|
|
# query with deleted ids
|
|
collection_w.query(expr, check_task=CheckTasks.check_query_empty)
|
|
|
|
@pytest.mark.tags(CaseLabel.L2)
|
|
def test_delete_without_connection(self):
|
|
"""
|
|
target: test delete without connect
|
|
method: delete after remove connection
|
|
expected: raise exception
|
|
"""
|
|
# init collection with tmp_nb default data
|
|
collection_w = self.init_collection_general(prefix, nb=tmp_nb, insert_data=True)[0]
|
|
|
|
# remove connection and delete
|
|
self.connection_wrap.remove_connection(ct.default_alias)
|
|
res_list, _ = self.connection_wrap.list_connections()
|
|
assert ct.default_alias not in res_list
|
|
error = {ct.err_code: 1, ct.err_msg: "should create connection first"}
|
|
collection_w.delete(expr=tmp_expr, check_task=CheckTasks.err_res, check_items=error)
|
|
|
|
# Not Milvus Exception
|
|
@pytest.mark.tags(CaseLabel.L2)
|
|
def test_delete_expr_none(self):
|
|
"""
|
|
target: test delete with None expr
|
|
method: delete with None expr
|
|
expected: raise exception
|
|
"""
|
|
# init collection with tmp_nb default data
|
|
collection_w = self.init_collection_general(prefix, nb=tmp_nb, insert_data=True)[0]
|
|
error = {ct.err_code: 999, ct.err_msg: "Illegal str variables: {'filter': None}, expect non-empty str"}
|
|
collection_w.delete(expr=None, check_task=CheckTasks.err_res, check_items=error)
|
|
|
|
@pytest.mark.tags(CaseLabel.L2)
|
|
@pytest.mark.parametrize("expr", [1, [], ()])
|
|
def test_delete_expr_non_string(self, expr):
|
|
"""
|
|
target: test delete with non-string expression
|
|
method: delete with non-string expr
|
|
expected: raise exception
|
|
"""
|
|
# init collection with tmp_nb default data
|
|
collection_w = self.init_collection_general(prefix, nb=tmp_nb, insert_data=True)[0]
|
|
error = {
|
|
ct.err_code: 999,
|
|
ct.err_msg: "Illegal str variables: {'filter': %s}, expect non-empty str" % str(expr),
|
|
}
|
|
collection_w.delete(expr, check_task=CheckTasks.err_res, check_items=error)
|
|
|
|
@pytest.mark.tags(CaseLabel.L2)
|
|
@pytest.mark.parametrize("expr", ["12-s", "中文"])
|
|
def test_delete_invalid_expr_string(self, expr):
|
|
"""
|
|
target: test delete with invalid string expr
|
|
method: delete with invalid string
|
|
expected: Raise exception
|
|
"""
|
|
# init collection with tmp_nb default data
|
|
collection_w = self.init_collection_general(prefix, nb=tmp_nb, insert_data=True)[0]
|
|
error = {ct.err_code: 1100, ct.err_msg: f"failed to create delete plan: cannot parse expression: {expr}"}
|
|
collection_w.delete(expr, check_task=CheckTasks.err_res, check_items=error)
|
|
|
|
@pytest.mark.tags(CaseLabel.L2)
|
|
def test_delete_expr_empty_value(self):
|
|
"""
|
|
target: test delete with empty array expr
|
|
method: delete with expr: "id in []"
|
|
expected: assert num entities
|
|
"""
|
|
# init collection with tmp_nb default data
|
|
collection_w = self.init_collection_general(prefix, nb=tmp_nb, insert_data=True)[0]
|
|
expr = f"{ct.default_int64_field_name} in {[]}"
|
|
|
|
# delete empty entities
|
|
collection_w.delete(expr)
|
|
|
|
@pytest.mark.tags(CaseLabel.L1)
|
|
def test_delete_expr_single(self):
|
|
"""
|
|
target: test delete with one value
|
|
method: delete with expr: "id in [0]"
|
|
expected: Describe num entities by one
|
|
"""
|
|
# init collection with tmp_nb default data
|
|
collection_w = self.init_collection_general(prefix, nb=tmp_nb, insert_data=True)[0]
|
|
expr = f"{ct.default_int64_field_name} in {[0]}"
|
|
del_res, _ = collection_w.delete(expr)
|
|
assert del_res.delete_count == 1
|
|
collection_w.query(expr, check_task=CheckTasks.check_query_empty)
|
|
|
|
@pytest.mark.tags(CaseLabel.L1)
|
|
def test_delete_expr_all_values(self):
|
|
"""
|
|
target: test delete with all values
|
|
method: delete with expr: "id in [all]"
|
|
expected: num entities unchanged and deleted data will not be queried
|
|
"""
|
|
# init collection with default_nb default data
|
|
collection_w, _, _, ids = self.init_collection_general(prefix, insert_data=True)[0:4]
|
|
expr = f"{ct.default_int64_field_name} in {ids}"
|
|
del_res, _ = collection_w.delete(expr)
|
|
|
|
# assert results
|
|
assert del_res.delete_count == ct.default_nb
|
|
assert collection_w.num_entities == ct.default_nb
|
|
|
|
collection_w.query(expr, check_task=CheckTasks.check_query_empty)
|
|
|
|
@pytest.mark.tags(CaseLabel.L1)
|
|
def test_delete_expr_with_vector(self):
|
|
"""
|
|
target: test delete with vector field
|
|
method: delete with expr vector field
|
|
expected: raise exception
|
|
"""
|
|
collection_w = self.init_collection_general(
|
|
prefix, nb=tmp_nb, insert_data=True, is_all_data_type=True, is_index=True
|
|
)[0]
|
|
expr = f"{ct.default_float_vec_field_name} in [[0.1]]"
|
|
error = {ct.err_code: 1100, ct.err_msg: f"failed to create delete plan: cannot parse expression: {expr}"}
|
|
|
|
collection_w.delete(expr, check_task=CheckTasks.err_res, check_items=error)
|
|
|
|
@pytest.mark.tags(CaseLabel.L2)
|
|
def test_delete_not_existed_values(self):
|
|
"""
|
|
target: test delete not existed values
|
|
method: delete data not in the collection
|
|
expected: No exception
|
|
"""
|
|
# init collection with tmp_nb default data
|
|
collection_w = self.init_collection_general(prefix, nb=tmp_nb, insert_data=True)[0]
|
|
|
|
# No exception
|
|
expr = f"{ct.default_int64_field_name} in {[tmp_nb]}"
|
|
collection_w.delete(expr=expr)
|
|
collection_w.query(
|
|
tmp_expr,
|
|
check_task=CheckTasks.check_query_results,
|
|
check_items={"exp_res": query_res_tmp_expr, "pk_name": collection_w.primary_field.name},
|
|
)
|
|
|
|
@pytest.mark.tags(CaseLabel.L2)
|
|
def test_delete_part_not_existed_values(self):
|
|
"""
|
|
target: test delete part non-existed values
|
|
method: delete ids which part not existed
|
|
expected: delete existed id, ignore non-existed id
|
|
"""
|
|
# init collection with tmp_nb default data
|
|
collection_w = self.init_collection_general(prefix, nb=tmp_nb, insert_data=True)[0]
|
|
expr = f"{ct.default_int64_field_name} in {[0, tmp_nb]}"
|
|
collection_w.delete(expr=expr)
|
|
collection_w.query(expr, check_task=CheckTasks.check_query_empty)
|
|
|
|
@pytest.mark.tags(CaseLabel.L2)
|
|
def test_delete_expr_inconsistent_values(self):
|
|
"""
|
|
target: test delete with inconsistent type values
|
|
method: delete with non-int64 type values
|
|
expected: raise exception
|
|
"""
|
|
# init collection with tmp_nb default data
|
|
collection_w = self.init_collection_general(prefix, nb=tmp_nb, insert_data=True)[0]
|
|
expr = f"{ct.default_int64_field_name} in {[0.0, 1.0]}"
|
|
|
|
# Bad exception message
|
|
error = {ct.err_code: 1100, ct.err_msg: "failed to create delete plan: cannot parse expression"}
|
|
collection_w.delete(expr=expr, check_task=CheckTasks.err_res, check_items=error)
|
|
|
|
@pytest.mark.tags(CaseLabel.L2)
|
|
def test_delete_expr_mix_values(self):
|
|
"""
|
|
target: test delete with mix type values
|
|
method: delete with int64 and float values
|
|
expected: raise exception
|
|
"""
|
|
# init collection with tmp_nb default data
|
|
collection_w = self.init_collection_general(prefix, nb=tmp_nb, insert_data=True)[0]
|
|
expr = f"{ct.default_int64_field_name} in {[0, 1.0]}"
|
|
|
|
# Bad exception message
|
|
error = {ct.err_code: 1100, ct.err_msg: "failed to create delete plan: cannot parse expression"}
|
|
collection_w.delete(expr=expr, check_task=CheckTasks.err_res, check_items=error)
|
|
|
|
@pytest.mark.tags(CaseLabel.L0)
|
|
def test_delete_partition(self):
|
|
"""
|
|
target: test delete from partition
|
|
method: delete with partition names
|
|
expected: verify partition entities are deleted
|
|
"""
|
|
# init collection and partition
|
|
collection_w = self.init_collection_wrap(name=cf.gen_unique_str(prefix))
|
|
partition_w = self.init_partition_wrap(collection_wrap=collection_w)
|
|
|
|
# load collection and insert data to partition
|
|
collection_w.create_index(ct.default_float_vec_field_name, index_params=ct.default_flat_index)
|
|
collection_w.load()
|
|
df = cf.gen_default_dataframe_data(tmp_nb)
|
|
partition_w.insert(df)
|
|
|
|
# delete ids from partition
|
|
del_res, _ = collection_w.delete(tmp_expr, partition_name=partition_w.name)
|
|
assert del_res.delete_count == 1
|
|
|
|
# query with deleted id and query with existed id
|
|
collection_w.query(tmp_expr, check_task=CheckTasks.check_query_empty, partition_names=[partition_w.name])
|
|
res = df.iloc[1:2, :1].to_dict("records")
|
|
collection_w.query(
|
|
f"{ct.default_int64_field_name} in [1]",
|
|
check_task=CheckTasks.check_query_results,
|
|
check_items={"exp_res": res, "pk_name": collection_w.primary_field.name},
|
|
)
|
|
|
|
@pytest.mark.tags(CaseLabel.L2)
|
|
def test_delete_default_partition(self):
|
|
"""
|
|
target: test delete from default partition
|
|
method: delete with partition name "_default"
|
|
expected: assert delete successfully
|
|
"""
|
|
# create, insert with flush, load collection
|
|
collection_w = self.init_collection_general(prefix, nb=tmp_nb, insert_data=True)[0]
|
|
del_res, _ = collection_w.delete(tmp_expr, partition_name=ct.default_partition_name)
|
|
assert del_res.delete_count == 1
|
|
collection_w.num_entities
|
|
collection_w.query(tmp_expr, check_task=CheckTasks.check_query_empty)
|
|
|
|
@pytest.mark.parametrize("partition_name", [1, [], {}, ()])
|
|
@pytest.mark.tags(CaseLabel.L2)
|
|
def test_delete_non_string_partition_name(self, partition_name):
|
|
"""
|
|
target: test delete with non-string partition name
|
|
method: delete with non-string partition name
|
|
expected: Raise exception
|
|
"""
|
|
# create, insert with flush, load collection
|
|
collection_w = self.init_collection_general(prefix, nb=tmp_nb, insert_data=True)[0]
|
|
|
|
error = {
|
|
ct.err_code: 0,
|
|
ct.err_msg: "Illegal nullable str variables: {'partition_name': %s}, "
|
|
"expect None or non-empty str" % str(partition_name),
|
|
}
|
|
collection_w.delete(tmp_expr, partition_name=partition_name, check_task=CheckTasks.err_res, check_items=error)
|
|
|
|
|
|
class TestDeleteOperation(TestcaseBase):
|
|
"""
|
|
******************************************************************
|
|
The following cases are used to test delete interface operations
|
|
******************************************************************
|
|
"""
|
|
|
|
@pytest.mark.tags(CaseLabel.L2)
|
|
def test_delete_from_empty_collection(self):
|
|
"""
|
|
target: test delete entities from an empty collection
|
|
method: create a collection and delete entities
|
|
expected: No exception
|
|
"""
|
|
c_name = cf.gen_unique_str(prefix)
|
|
collection_w = self.init_collection_wrap(name=c_name)
|
|
collection_w.delete(tmp_expr)[0]
|
|
# todo assert del_res.delete_count == 0
|
|
|
|
@pytest.mark.tags(CaseLabel.L1)
|
|
def test_delete_entities_repeatedly(self):
|
|
"""
|
|
target: test delete entities twice
|
|
method: delete with same expr twice
|
|
expected: No exception for second deletion
|
|
"""
|
|
# init collection with nb default data
|
|
collection_w = self.init_collection_general(prefix, nb=tmp_nb, insert_data=True)[0]
|
|
|
|
# assert delete successfully and no exception
|
|
collection_w.delete(expr=tmp_expr)
|
|
collection_w.num_entities
|
|
collection_w.query(tmp_expr, check_task=CheckTasks.check_query_empty)
|
|
collection_w.delete(expr=tmp_expr)
|
|
|
|
@pytest.mark.tags(CaseLabel.L1)
|
|
def test_delete_after_index(self):
|
|
"""
|
|
target: test delete after creating index
|
|
method: 1.insert, flush, load
|
|
2.create index and re-load
|
|
3.delete entities
|
|
4.search
|
|
expected: assert index and deleted id not in search result
|
|
"""
|
|
# create collection, insert tmp_nb, flush and load
|
|
collection_w, vectors = self.init_collection_general(prefix, insert_data=True, is_index=False)[0:2]
|
|
|
|
# create index
|
|
index_params = {"index_type": "IVF_SQ8", "metric_type": "COSINE", "params": {"nlist": 64}}
|
|
collection_w.create_index(ct.default_float_vec_field_name, index_params)
|
|
assert collection_w.has_index()[0]
|
|
collection_w.release()
|
|
collection_w.load()
|
|
# delete entity
|
|
collection_w.delete(tmp_expr)
|
|
collection_w.query(tmp_expr, check_task=CheckTasks.check_query_empty)
|
|
assert collection_w.has_index()[0]
|
|
|
|
# search with id 0 vectors
|
|
search_res, _ = collection_w.search(
|
|
[vectors[0][ct.default_float_vec_field_name][0]],
|
|
ct.default_float_vec_field_name,
|
|
ct.default_search_params,
|
|
ct.default_limit,
|
|
)
|
|
assert 0 not in search_res[0].ids
|
|
|
|
@pytest.mark.tags(CaseLabel.L1)
|
|
def test_delete_and_index(self):
|
|
"""
|
|
target: test delete and create index
|
|
method: 1.insert
|
|
2.delete half
|
|
3.flush and create index
|
|
4.search
|
|
expected: Empty search result
|
|
"""
|
|
# init collection and insert data without flush
|
|
collection_w = self.init_collection_wrap(name=cf.gen_unique_str(prefix))
|
|
df = cf.gen_default_dataframe_data()
|
|
insert_res, _ = collection_w.insert(df)
|
|
|
|
# delete half and flush
|
|
expr = f"{ct.default_int64_field_name} in {insert_res.primary_keys[: ct.default_nb // 2]}"
|
|
del_res, _ = collection_w.delete(expr)
|
|
assert collection_w.num_entities in [ct.default_nb, ct.default_nb // 2]
|
|
|
|
# create index
|
|
index_params = {"index_type": "IVF_SQ8", "metric_type": "COSINE", "params": {"nlist": 64}}
|
|
collection_w.create_index(ct.default_float_vec_field_name, index_params)
|
|
assert collection_w.has_index()[0]
|
|
|
|
collection_w.load()
|
|
search_res, _ = collection_w.search(
|
|
[df[ct.default_float_vec_field_name][0]],
|
|
ct.default_float_vec_field_name,
|
|
ct.default_search_params,
|
|
ct.default_limit,
|
|
)
|
|
# assert search results not contains deleted ids
|
|
inter = set(insert_res.primary_keys[: ct.default_nb // 2]).intersection(set(search_res[0].ids))
|
|
assert len(inter) == 0
|
|
|
|
@pytest.mark.tags(CaseLabel.L1)
|
|
def test_delete_query_ids_both_L0_segment_and_WAL(self):
|
|
"""
|
|
target: test query that delete records from both L0 segment and WAL
|
|
method: 1.create and insert
|
|
2.delete id 0 and flush
|
|
3.load and query id 0
|
|
4.insert new id and delete the id
|
|
5.query id 0 and new id
|
|
expected: Empty query result
|
|
"""
|
|
# init collection and insert data without flush
|
|
collection_w = self.init_collection_wrap(name=cf.gen_unique_str(prefix))
|
|
df = cf.gen_default_dataframe_data(tmp_nb)
|
|
collection_w.insert(df)
|
|
|
|
# delete id 0 and flush
|
|
del_res, _ = collection_w.delete(tmp_expr)
|
|
assert del_res.delete_count == 1
|
|
assert collection_w.num_entities == tmp_nb
|
|
|
|
# load and query id 0, delete records from L0 segment
|
|
collection_w.create_index(ct.default_float_vec_field_name, index_params=ct.default_flat_index)
|
|
collection_w.load()
|
|
collection_w.query(tmp_expr, check_task=CheckTasks.check_query_empty)
|
|
|
|
# insert id tmp_nb and delete id 0 and tmp_nb, delete records from WAL
|
|
df_new = cf.gen_default_dataframe_data(nb=1, start=tmp_nb)
|
|
collection_w.insert(df_new)
|
|
collection_w.delete(expr=f"{ct.default_int64_field_name} in {[tmp_nb]}")
|
|
|
|
# query with id 0 and tmp_nb
|
|
collection_w.query(
|
|
expr=f"{ct.default_int64_field_name} in {[0, tmp_nb]}", check_task=CheckTasks.check_query_empty
|
|
)
|
|
|
|
# insert deleted ids and query last two rows
|
|
df_same = cf.gen_default_dataframe_data(tmp_nb + 1)
|
|
collection_w.insert(df_same)
|
|
res = df_same.iloc[-2:, [0, 1, -1]].to_dict("records")
|
|
collection_w.query(
|
|
expr=f"{ct.default_int64_field_name} >= {tmp_nb - 1}",
|
|
output_fields=[ct.default_float_vec_field_name, ct.default_float_field_name],
|
|
check_task=CheckTasks.check_query_results,
|
|
check_items={"exp_res": res, "with_vec": True, "pk_name": collection_w.primary_field.name},
|
|
)
|
|
|
|
@pytest.mark.tags(CaseLabel.L1)
|
|
def test_delete_query_delta_logs(self):
|
|
"""
|
|
target: test delete and query which delete records comes from delta logs(L1 delta)
|
|
method: 1. insert -> flush
|
|
2. delete and flush 11 twice -> gen more than 10 L0 segment binlog -> L0 compaction
|
|
3. load and query
|
|
expected: gets empty
|
|
"""
|
|
L0_binlog_num_compaction = 10
|
|
# init collection and insert data without flush
|
|
collection_w = self.init_collection_wrap(name=cf.gen_unique_str(prefix))
|
|
df = cf.gen_default_dataframe_data(tmp_nb)
|
|
collection_w.insert(df)
|
|
|
|
# delete id 0 and flush
|
|
for i in range(L0_binlog_num_compaction + 1):
|
|
delete_expr = f"{ct.default_int64_field_name} in {[i, i + 1]}"
|
|
del_res, _ = collection_w.delete(delete_expr)
|
|
assert del_res.delete_count == 2
|
|
collection_w.flush()
|
|
|
|
# load and query id 0, delete records from L0 segment
|
|
collection_w.create_index(ct.default_float_vec_field_name, index_params=ct.default_flat_index)
|
|
collection_w.load()
|
|
query_expr = f"{ct.default_int64_field_name} < {L0_binlog_num_compaction + 2}"
|
|
collection_w.query(query_expr, check_task=CheckTasks.check_query_empty)
|
|
|
|
# insert deleted ids and query last two rows
|
|
df_same = cf.gen_default_dataframe_data(L0_binlog_num_compaction + 2)
|
|
collection_w.insert(df_same)
|
|
res = df_same.iloc[:, [0, 1, -1]].to_dict("records")
|
|
collection_w.query(
|
|
expr=f"{ct.default_int64_field_name} < {L0_binlog_num_compaction + 2}",
|
|
output_fields=[ct.default_float_vec_field_name, ct.default_float_field_name],
|
|
check_task=CheckTasks.check_query_results,
|
|
check_items={"exp_res": res, "with_vec": True, "pk_name": collection_w.primary_field.name},
|
|
)
|
|
|
|
@pytest.mark.tags(CaseLabel.L1)
|
|
def test_delete_search(self):
|
|
"""
|
|
target: test delete and search
|
|
method: search entities after it was deleted
|
|
expected: deleted entity is not in the search result
|
|
"""
|
|
# init collection with nb default data
|
|
collection_w, _, _, ids = self.init_collection_general(prefix, insert_data=True)[0:4]
|
|
entity, _ = collection_w.query(tmp_expr, output_fields=["*"])
|
|
search_res, _ = collection_w.search(
|
|
[entity[0][ct.default_float_vec_field_name]],
|
|
ct.default_float_vec_field_name,
|
|
ct.default_search_params,
|
|
ct.default_limit,
|
|
)
|
|
# assert search results contains entity
|
|
assert 0 in search_res[0].ids
|
|
|
|
expr = f"{ct.default_int64_field_name} in {ids[: ct.default_nb // 2]}"
|
|
collection_w.delete(expr)
|
|
search_res_2, _ = collection_w.search(
|
|
[entity[0][ct.default_float_vec_field_name]],
|
|
ct.default_float_vec_field_name,
|
|
ct.default_search_params,
|
|
ct.default_limit,
|
|
)
|
|
# assert search result is not equal to entity
|
|
inter = set(ids[: ct.default_nb // 2]).intersection(set(search_res_2[0].ids))
|
|
# Using bounded staleness, we could still search the "deleted" entities,
|
|
# since the search requests arrived query nodes earlier than query nodes consume the delete requests.
|
|
assert len(inter) == 0
|
|
|
|
@pytest.mark.tags(CaseLabel.L2)
|
|
def test_delete_search_rename_collection(self):
|
|
"""
|
|
target: test delete and search in the renamed collection
|
|
method: search entities after it was deleted
|
|
expected: deleted entity is not in the search result
|
|
"""
|
|
# init collection with nb default data
|
|
collection_w, _, _, ids = self.init_collection_general(prefix, insert_data=True)[0:4]
|
|
entity, _ = collection_w.query(tmp_expr, output_fields=[ct.default_float_vec_field_name])
|
|
search_res, _ = collection_w.search(
|
|
[entity[0][ct.default_float_vec_field_name]],
|
|
ct.default_float_vec_field_name,
|
|
ct.default_search_params,
|
|
ct.default_limit,
|
|
)
|
|
# assert search results contains entity
|
|
assert 0 in search_res[0].ids
|
|
# rename collection
|
|
old_collection_name = collection_w.name
|
|
new_collection_name = cf.gen_unique_str(prefix + "new")
|
|
self.utility_wrap.rename_collection(old_collection_name, new_collection_name)
|
|
collection_w = self.init_collection_wrap(name=new_collection_name)
|
|
# delete entities
|
|
expr = f"{ct.default_int64_field_name} in {ids[: ct.default_nb // 2]}"
|
|
collection_w.delete(expr)
|
|
search_res_2, _ = collection_w.search(
|
|
[entity[0][ct.default_float_vec_field_name]],
|
|
ct.default_float_vec_field_name,
|
|
ct.default_search_params,
|
|
ct.default_limit,
|
|
)
|
|
# assert search result is not equal to entity
|
|
inter = set(ids[: ct.default_nb // 2]).intersection(set(search_res_2[0].ids))
|
|
# Using bounded staleness, we could still search the "deleted" entities,
|
|
# since the search requests arrived query nodes earlier than query nodes consume the delete requests.
|
|
assert len(inter) == 0
|
|
|
|
@pytest.mark.tags(CaseLabel.L1)
|
|
def test_delete_duplicate_primary_keys(self):
|
|
"""
|
|
target: test delete from duplicate primary keys
|
|
method: 1.insert data with dup ids
|
|
2.delete with repeated or not values
|
|
expected: currently only delete one entity, query get one entity
|
|
todo delete all entities
|
|
"""
|
|
collection_w = self.init_collection_wrap(name=cf.gen_unique_str(prefix))
|
|
df = cf.gen_default_dataframe_data(nb=tmp_nb)
|
|
df[ct.default_int64_field_name] = 0
|
|
collection_w.insert(df)
|
|
assert collection_w.num_entities == tmp_nb
|
|
del_res, _ = collection_w.delete(tmp_expr)
|
|
collection_w.create_index(ct.default_float_vec_field_name, index_params=ct.default_flat_index)
|
|
collection_w.load()
|
|
|
|
# Just one query res and search res, because de-dup
|
|
res, _ = collection_w.query(tmp_expr, output_fields=["*"])
|
|
assert len(res) == 0
|
|
|
|
search_res, _ = collection_w.search(
|
|
[df[ct.default_float_vec_field_name][1]],
|
|
ct.default_float_vec_field_name,
|
|
ct.default_search_params,
|
|
ct.default_limit,
|
|
output_fields=[ct.default_int64_field_name, ct.default_float_field_name],
|
|
)
|
|
assert len(search_res) == 1
|
|
|
|
@pytest.mark.tags(CaseLabel.L2)
|
|
def test_delete_empty_partition(self):
|
|
"""
|
|
target: test delete empty partition
|
|
method: delete from an empty partition
|
|
expected: No exception
|
|
"""
|
|
# init collection and partition
|
|
collection_w = self.init_collection_wrap(name=cf.gen_unique_str(prefix))
|
|
partition_w = self.init_partition_wrap(collection_wrap=collection_w)
|
|
|
|
collection_w.delete(tmp_expr, partition_name=partition_w.name)
|
|
|
|
@pytest.mark.tags(CaseLabel.L2)
|
|
def test_delete_not_existed_partition(self):
|
|
"""
|
|
target: test delete from an not existed partition
|
|
method: delete from an fake partition
|
|
expected: raise exception
|
|
"""
|
|
# init collection with tmp_nb data
|
|
collection_w = self.init_collection_general(prefix, nb=tmp_nb, insert_data=True)[0]
|
|
|
|
# raise exception
|
|
error = {
|
|
ct.err_code: 999,
|
|
ct.err_msg: f"Failed to get partition id: partition not found[partition={ct.default_tag}]",
|
|
}
|
|
collection_w.delete(tmp_expr, partition_name=ct.default_tag, check_task=CheckTasks.err_res, check_items=error)
|
|
|
|
@pytest.mark.tags(CaseLabel.L1)
|
|
def test_delete_from_partition_with_another_ids(self):
|
|
"""
|
|
target: delete another partition entities from partition
|
|
method: 1.insert nb entities into two partitions in half
|
|
2.delete entities from partition_1 with partition_2 values
|
|
3.delete entities from partition_1 with partition_1 values
|
|
expected: Entities in partition_1 will be deleted
|
|
"""
|
|
half = tmp_nb // 2
|
|
# create, insert, flush, load
|
|
collection_w, partition_w, _, _ = self.insert_entities_into_two_partitions_in_half(half)
|
|
|
|
# delete entities from another partition
|
|
expr = f"{ct.default_int64_field_name} in {[0]}"
|
|
collection_w.delete(expr, partition_name=ct.default_partition_name)
|
|
collection_w.query(
|
|
expr,
|
|
check_task=CheckTasks.check_query_results,
|
|
check_items={"exp_res": query_res_tmp_expr, "pk_name": collection_w.primary_field.name},
|
|
)
|
|
|
|
# delete entities from own partition
|
|
collection_w.delete(expr, partition_name=partition_w.name)
|
|
collection_w.query(expr, check_task=CheckTasks.check_query_empty)
|
|
|
|
@pytest.mark.tags(CaseLabel.L1)
|
|
def test_delete_from_partitions_with_same_ids(self):
|
|
"""
|
|
target: test delete same ids from two partitions with same data
|
|
method: 1.insert same nb data into two partitions
|
|
2.delete same ids from partition_1
|
|
expected: The data only in partition_1 will be deleted
|
|
"""
|
|
# init collection and partition
|
|
collection_w = self.init_collection_wrap(name=cf.gen_unique_str(prefix))
|
|
partition_w = self.init_partition_wrap(collection_wrap=collection_w)
|
|
|
|
# insert same data into partition_w and default partition
|
|
df = cf.gen_default_dataframe_data(tmp_nb)
|
|
collection_w.insert(df)
|
|
partition_w.insert(df)
|
|
|
|
# delete same id 0 from default_partition, and query on it get empty result
|
|
collection_w.delete(tmp_expr, partition_name=ct.default_partition_name)
|
|
assert collection_w.num_entities == tmp_nb * 2
|
|
collection_w.create_index(ct.default_float_vec_field_name, index_params=ct.default_flat_index)
|
|
collection_w.load()
|
|
collection_w.query(
|
|
tmp_expr, partition_names=[ct.default_partition_name], check_task=CheckTasks.check_query_empty
|
|
)
|
|
|
|
# query on partition_w with id 0 and get an result
|
|
collection_w.query(
|
|
tmp_expr,
|
|
partition_names=[partition_w.name],
|
|
check_task=CheckTasks.check_query_results,
|
|
check_items={"exp_res": query_res_tmp_expr, "pk_name": collection_w.primary_field.name},
|
|
)
|
|
|
|
@pytest.mark.tags(CaseLabel.L0)
|
|
def test_delete_auto_id_collection(self):
|
|
"""
|
|
target: test delete from auto_id collection
|
|
method: delete entities from auto_id=true collection
|
|
expected: versify delete successfully
|
|
"""
|
|
# init an auto_id collection and insert tmp_nb data
|
|
collection_w, _, _, ids = self.init_collection_general(prefix, nb=tmp_nb, insert_data=True, auto_id=True)[0:4]
|
|
|
|
# delete with insert ids
|
|
expr = f"{ct.default_int64_field_name} in {[ids[0]]}"
|
|
res, _ = collection_w.delete(expr)
|
|
|
|
# verify delete result
|
|
assert res.delete_count == 1
|
|
collection_w.query(expr, check_task=CheckTasks.check_query_empty)
|
|
|
|
@pytest.mark.tags(CaseLabel.L2)
|
|
def test_delete_query_without_loading(self):
|
|
"""
|
|
target: test delete and query without loading
|
|
method: 1.insert and flush data
|
|
2.delete ids
|
|
3.query without loading
|
|
expected: Raise exception
|
|
"""
|
|
# create collection, insert data without flush
|
|
collection_w = self.init_collection_wrap(name=cf.gen_unique_str(prefix))
|
|
df = cf.gen_default_dataframe_data(tmp_nb)
|
|
collection_w.insert(df)
|
|
assert collection_w.num_entities == tmp_nb
|
|
|
|
# delete
|
|
res = collection_w.delete(tmp_expr)[0]
|
|
assert res.delete_count == 1
|
|
|
|
# query without loading and raise exception
|
|
error = {ct.err_code: 65535, ct.err_msg: "collection not loaded"}
|
|
collection_w.query(expr=tmp_expr, check_task=CheckTasks.err_res, check_items=error)
|
|
|
|
@pytest.mark.tags(CaseLabel.L1)
|
|
def test_delete_sealed_segment_without_flush(self):
|
|
"""
|
|
target: test delete without flush
|
|
method: 1.insert and flush data
|
|
2.delete ids from collection and no flush
|
|
3.load and query with id
|
|
expected: No query result
|
|
"""
|
|
# create collection, insert data without flush
|
|
collection_w = self.init_collection_wrap(name=cf.gen_unique_str(prefix))
|
|
df = cf.gen_default_dataframe_data(tmp_nb)
|
|
collection_w.insert(df)
|
|
assert collection_w.num_entities == tmp_nb
|
|
|
|
# delete
|
|
del_res, _ = collection_w.delete(tmp_expr)
|
|
assert del_res.delete_count == 1
|
|
collection_w.flush()
|
|
|
|
# load and query with id -> delete records from L0 segment or WAL
|
|
collection_w.create_index(ct.default_float_vec_field_name, index_params=ct.default_flat_index)
|
|
collection_w.load()
|
|
collection_w.query(tmp_expr, check_task=CheckTasks.check_query_empty)
|
|
|
|
# insert deleted id and query
|
|
df_same = cf.gen_default_dataframe_data(1)
|
|
collection_w.insert(df_same)
|
|
res = df_same.iloc[:, [0, 1, -1]].to_dict("records")
|
|
collection_w.query(
|
|
expr=tmp_expr,
|
|
output_fields=[ct.default_float_vec_field_name, ct.default_float_field_name],
|
|
check_task=CheckTasks.check_query_results,
|
|
check_items={"exp_res": res, "with_vec": True, "pk_name": collection_w.primary_field.name},
|
|
)
|
|
|
|
@pytest.mark.tags(CaseLabel.L1)
|
|
def test_delete_growing_data_channel_delete(self):
|
|
"""
|
|
target: test delete entities from growing segment, and channel deleteMsg
|
|
method: 1.create collection
|
|
2.load collection
|
|
3.insert data and delete ids
|
|
4.query deleted ids
|
|
expected: No query result
|
|
"""
|
|
# create collection
|
|
collection_w = self.init_collection_wrap(name=cf.gen_unique_str(prefix))
|
|
# load collection and the queryNode watch the insertChannel
|
|
collection_w.create_index(ct.default_float_vec_field_name, index_params=ct.default_flat_index)
|
|
collection_w.load()
|
|
# insert data
|
|
df = cf.gen_default_dataframe_data(tmp_nb)
|
|
collection_w.insert(df)
|
|
# delete id 0
|
|
del_res = collection_w.delete(tmp_expr)[0]
|
|
assert del_res.delete_count == 1
|
|
# query id 0
|
|
collection_w.query(tmp_expr, check_task=CheckTasks.check_query_empty)
|
|
|
|
@pytest.mark.tags(CaseLabel.L1)
|
|
def test_delete_sealed_data_channel_delete(self):
|
|
"""
|
|
target: test delete sealed data and get deleteMsg from insertChannel
|
|
method: 1.create, insert and flush data
|
|
2.load collection
|
|
3.delete id without flush
|
|
4.query deleted ids (queryNode get deleted ids from channel not persistence)
|
|
expected: Delete successfully and no query result
|
|
"""
|
|
# create collection and insert flush data
|
|
collection_w = self.init_collection_wrap(name=cf.gen_unique_str(prefix))
|
|
df = cf.gen_default_dataframe_data(tmp_nb)
|
|
collection_w.insert(df)
|
|
assert collection_w.num_entities == tmp_nb
|
|
|
|
# load collection and queryNode subscribe channel
|
|
collection_w.create_index(ct.default_float_vec_field_name, index_params=ct.default_flat_index)
|
|
collection_w.load()
|
|
|
|
# delete ids and query
|
|
collection_w.delete(tmp_expr)
|
|
collection_w.query(tmp_expr, check_task=CheckTasks.check_query_empty)
|
|
|
|
@pytest.mark.tags(CaseLabel.L1)
|
|
def test_delete_sealed_segment_with_flush(self):
|
|
"""
|
|
target: test delete data from sealed segment and flush delta log
|
|
method: 1.create and insert and flush data
|
|
2.delete entities and flush (insert and flush)
|
|
3.load collection (load data and delta log)
|
|
4.query deleted ids
|
|
expected: No query result
|
|
"""
|
|
# create collection
|
|
collection_w = self.init_collection_wrap(name=cf.gen_unique_str(prefix))
|
|
# insert and flush data
|
|
df = cf.gen_default_dataframe_data(tmp_nb)
|
|
collection_w.insert(df)
|
|
# assert collection_w.num_entities == tmp_nb
|
|
|
|
# delete id 0
|
|
del_res = collection_w.delete(tmp_expr)[0]
|
|
assert del_res.delete_count == 1
|
|
|
|
# insert data and flush data and delete ids.
|
|
# if no insert, datanode will not really flush delete ids
|
|
collection_w.insert(cf.gen_default_dataframe_data(nb=1, start=tmp_nb))
|
|
log.info(f"Collection num entities: {collection_w.num_entities}")
|
|
|
|
# load and query id 0
|
|
collection_w.create_index(ct.default_float_vec_field_name, index_params=ct.default_flat_index)
|
|
collection_w.load()
|
|
collection_w.query(tmp_expr, check_task=CheckTasks.check_query_empty)
|
|
|
|
@pytest.mark.tags(CaseLabel.L2)
|
|
def test_delete_sealed_segment_with_twice_flush(self):
|
|
"""
|
|
target: test delete data from sealed segment and flush delta log
|
|
method: 1.create and insert and flush data
|
|
2.delete entities and flush (insert and flush)
|
|
3.load collection (load data and delta log)
|
|
4.query deleted ids
|
|
expected: No query result
|
|
"""
|
|
# create collection
|
|
collection_w = self.init_collection_wrap(name=cf.gen_unique_str(prefix))
|
|
# insert and flush data
|
|
df = cf.gen_default_dataframe_data(tmp_nb)
|
|
collection_w.insert(df)
|
|
assert collection_w.num_entities == tmp_nb
|
|
|
|
# delete id 0 and flush
|
|
del_res = collection_w.delete(tmp_expr)[0]
|
|
assert del_res.delete_count == 1
|
|
collection_w.insert(cf.gen_default_dataframe_data(nb=1, start=tmp_nb))
|
|
log.info(collection_w.num_entities)
|
|
# load and query id 0
|
|
collection_w.create_index(ct.default_float_vec_field_name, index_params=ct.default_flat_index)
|
|
collection_w.load()
|
|
collection_w.query(tmp_expr, check_task=CheckTasks.check_query_empty)
|
|
|
|
@pytest.mark.tags(CaseLabel.L1)
|
|
def test_delete_sealed_data_sealed_delete(self):
|
|
"""
|
|
target: test delete with sealed data and sealed delete request
|
|
method: 1.create, insert
|
|
2.delete and flush (will flush data and delete)
|
|
3.load and query
|
|
expected: Empty query result
|
|
"""
|
|
# create collection
|
|
collection_w = self.init_collection_wrap(name=cf.gen_unique_str(prefix))
|
|
# insert without flush
|
|
df = cf.gen_default_dataframe_data(tmp_nb)
|
|
collection_w.insert(df)
|
|
|
|
# delete id 0 and flush
|
|
del_res = collection_w.delete(tmp_expr)[0]
|
|
assert del_res.delete_count == 1
|
|
assert collection_w.num_entities == tmp_nb
|
|
|
|
# load and query id 0
|
|
collection_w.create_index(ct.default_float_vec_field_name, index_params=ct.default_flat_index)
|
|
collection_w.load()
|
|
collection_w.query(tmp_expr, check_task=CheckTasks.check_query_empty)
|
|
|
|
@pytest.mark.tags(CaseLabel.L2)
|
|
@pytest.mark.parametrize("to_query", [True, False])
|
|
@pytest.mark.parametrize("to_flush", [True, False])
|
|
def test_delete_insert_same_id_growing(self, to_query, to_flush):
|
|
"""
|
|
target: test insert same id entity after delete from growing data
|
|
method: 1.create and load
|
|
2.insert entities and no flush
|
|
3.delete id 0 entity
|
|
4.insert new entity with same id
|
|
5.query with the id
|
|
expected: Verify that the query gets the newly inserted entity
|
|
"""
|
|
# init collection and insert data without flush
|
|
collection_w = self.init_collection_wrap(name=cf.gen_unique_str(prefix))
|
|
collection_w.create_index(ct.default_float_vec_field_name, index_params=ct.default_flat_index)
|
|
collection_w.load()
|
|
df = cf.gen_default_dataframe_data(tmp_nb)
|
|
collection_w.insert(df)
|
|
|
|
# delete
|
|
del_expr = f"{ct.default_int64_field_name} in [0, 1, 3, 5]"
|
|
del_res, _ = collection_w.delete(del_expr)
|
|
log.debug(f"to_query:{to_query}")
|
|
if to_query:
|
|
collection_w.query(del_expr, check_task=CheckTasks.check_query_empty)
|
|
|
|
# insert entity with primary key 0, new scalar field data, new vector data
|
|
df_new = cf.gen_default_dataframe_data(4, start=tmp_nb)
|
|
df_new[ct.default_int64_field_name] = [0, 1, 3, 5]
|
|
collection_w.insert(df_new)
|
|
if to_flush:
|
|
log.debug(collection_w.num_entities)
|
|
|
|
# query entity
|
|
res = df_new.iloc[:, [0, 1, -1]].to_dict("records")
|
|
collection_w.query(
|
|
del_expr,
|
|
output_fields=[ct.default_float_vec_field_name, ct.default_float_field_name],
|
|
check_task=CheckTasks.check_query_results,
|
|
check_items={"exp_res": res, "with_vec": True, "pk_name": collection_w.primary_field.name},
|
|
)
|
|
search_res, _ = collection_w.search(
|
|
data=[df_new[ct.default_float_vec_field_name][0]],
|
|
anns_field=ct.default_float_vec_field_name,
|
|
param=default_search_params,
|
|
limit=1,
|
|
)
|
|
assert search_res[0][0].id == 0
|
|
|
|
@pytest.mark.tags(CaseLabel.L1)
|
|
@pytest.mark.parametrize("to_query", [True, False])
|
|
def test_delete_insert_same_id_sealed(self, to_query):
|
|
"""
|
|
target: test insert same id entity after delete from sealed data
|
|
method: 1.create and insert with flush
|
|
2.load and query with the id
|
|
3.delte the id entity
|
|
4.insert new entity with the same id and flush
|
|
5.query the id
|
|
expected: Verify that the query gets the newly inserted entity
|
|
"""
|
|
# init collection and insert data without flush
|
|
collection_w = self.init_collection_wrap(name=cf.gen_unique_str(prefix))
|
|
|
|
# insert
|
|
nb = 1000
|
|
df = cf.gen_default_dataframe_data(nb)
|
|
collection_w.insert(df)
|
|
log.debug(collection_w.num_entities)
|
|
|
|
# load and query
|
|
collection_w.create_index(ct.default_float_vec_field_name, index_params=ct.default_flat_index)
|
|
collection_w.load()
|
|
res = df.iloc[:1, :1].to_dict("records")
|
|
collection_w.search(
|
|
data=[df[ct.default_float_vec_field_name][0]],
|
|
anns_field=ct.default_float_vec_field_name,
|
|
param=default_search_params,
|
|
limit=1,
|
|
)
|
|
collection_w.query(
|
|
tmp_expr,
|
|
check_task=CheckTasks.check_query_results,
|
|
check_items={"exp_res": res, "pk_name": collection_w.primary_field.name},
|
|
)
|
|
|
|
# delete
|
|
collection_w.delete(tmp_expr)
|
|
if to_query:
|
|
collection_w.query(tmp_expr, check_task=CheckTasks.check_query_empty)
|
|
|
|
# re-insert same pk, new scalar field data, new vector data
|
|
df_new = cf.gen_default_dataframe_data(nb=1, start=nb)
|
|
df_new[ct.default_int64_field_name] = [0]
|
|
collection_w.insert(df_new)
|
|
log.debug(collection_w.num_entities)
|
|
|
|
# re-query
|
|
res = df_new.iloc[[0], [0, 1, -1]].to_dict("records")
|
|
collection_w.query(
|
|
tmp_expr,
|
|
output_fields=[ct.default_float_vec_field_name, ct.default_float_field_name],
|
|
check_task=CheckTasks.check_query_results,
|
|
check_items={"exp_res": res, "with_vec": True, "pk_name": collection_w.primary_field.name},
|
|
)
|
|
search_res, _ = collection_w.search(
|
|
data=[df_new[ct.default_float_vec_field_name][0]],
|
|
anns_field=ct.default_float_vec_field_name,
|
|
param=default_search_params,
|
|
limit=1,
|
|
)
|
|
assert search_res[0][0].id == 0
|
|
|
|
@pytest.mark.tags(CaseLabel.L2)
|
|
def test_delete_entity_loop(self):
|
|
"""
|
|
target: test delete all entities one by one in a loop
|
|
method: delete data one by one for a loop
|
|
expected: No exception
|
|
"""
|
|
# init an auto_id collection and insert tmp_nb data, flush and load
|
|
collection_w, _, _, ids = self.init_collection_general(prefix, nb=tmp_nb, insert_data=True, auto_id=True)[0:4]
|
|
|
|
for del_id in ids:
|
|
expr = f"{ct.default_int64_field_name} in {[del_id]}"
|
|
res = collection_w.delete(expr)[0]
|
|
assert res.delete_count == 1
|
|
|
|
# query with all ids
|
|
expr = f"{ct.default_int64_field_name} in {ids}"
|
|
collection_w.query(expr, check_task=CheckTasks.check_query_empty)
|
|
|
|
@pytest.mark.tags(CaseLabel.L2)
|
|
def test_delete_flush_loop(self):
|
|
"""
|
|
target: test delete and flush in a loop
|
|
method: in a loop, delete batch and flush, until delete all entities
|
|
expected: No exception
|
|
"""
|
|
# init an auto_id collection and insert tmp_nb data
|
|
collection_w, _, _, ids = self.init_collection_general(prefix, nb=tmp_nb, insert_data=True, auto_id=True)[0:4]
|
|
|
|
batch = 10
|
|
for i in range(tmp_nb // batch):
|
|
expr = f"{ct.default_int64_field_name} in {ids[i * batch : (i + 1) * batch]}"
|
|
res, _ = collection_w.delete(expr)
|
|
assert res.delete_count == batch
|
|
|
|
# query with all ids
|
|
expr = f"{ct.default_int64_field_name} in {ids}"
|
|
collection_w.query(expr, check_task=CheckTasks.check_query_empty)
|
|
|
|
@pytest.mark.tags(CaseLabel.L2)
|
|
@pytest.mark.parametrize("to_flush_data", [True, False])
|
|
@pytest.mark.parametrize("to_flush_delete", [True, False])
|
|
def test_delete_merge_same_id_channel_and_sealed(self, to_flush_data, to_flush_delete):
|
|
"""
|
|
target: test merge same delete ids from channel and sealed
|
|
method: 1.create, insert
|
|
2.delete id and flush (data and deleted become sealed)
|
|
3.load and query (verify delete successfully)
|
|
4.insert entity with deleted id
|
|
5.delete id
|
|
6.query with id
|
|
expected: Empty query result
|
|
"""
|
|
# init collection and insert data without flush
|
|
collection_w = self.init_collection_wrap(name=cf.gen_unique_str(prefix), shards_num=1)
|
|
df = cf.gen_default_dataframe_data(tmp_nb)
|
|
collection_w.insert(df)
|
|
|
|
# delete id 0 and flush
|
|
del_res, _ = collection_w.delete(tmp_expr)
|
|
assert del_res.delete_count == 1
|
|
assert collection_w.num_entities == tmp_nb
|
|
|
|
# load and query id 0
|
|
collection_w.create_index(ct.default_float_vec_field_name, index_params=ct.default_flat_index)
|
|
collection_w.load()
|
|
collection_w.query(tmp_expr, check_task=CheckTasks.check_query_empty)
|
|
|
|
# insert new entity with same id 0 and query
|
|
df_new = cf.gen_default_dataframe_data(1)
|
|
collection_w.insert(df_new)
|
|
if to_flush_data:
|
|
log.debug(collection_w.num_entities)
|
|
collection_w.query(
|
|
tmp_expr,
|
|
output_fields=[ct.default_float_vec_field_name],
|
|
check_task=CheckTasks.check_query_results,
|
|
check_items={
|
|
"exp_res": df_new.iloc[[0], [0, 4]].to_dict("records"),
|
|
"with_vec": True,
|
|
"pk_name": collection_w.primary_field.name,
|
|
},
|
|
)
|
|
|
|
collection_w.delete(tmp_expr)
|
|
if to_flush_delete:
|
|
log.debug(collection_w.num_entities)
|
|
collection_w.query(tmp_expr, check_task=CheckTasks.check_query_empty)
|
|
|
|
@pytest.mark.tags(CaseLabel.L2)
|
|
def test_delete_merge_ids_channel_and_sealed(self):
|
|
"""
|
|
target: test merge deleted ids come from both channel and sealed
|
|
method: 1.create, insert ids [0, tmp_nb) with shard_num=1
|
|
2.delete id 0 and flush
|
|
3.load and query with id 0
|
|
4.delete id 1 (merge same segment deleted ids 0 and 1)
|
|
5.query with id 0 and 1
|
|
expected: Empty query result
|
|
"""
|
|
# init collection and insert data without flush
|
|
collection_w = self.init_collection_wrap(name=cf.gen_unique_str(prefix), shards_num=1)
|
|
df = cf.gen_default_dataframe_data(tmp_nb)
|
|
collection_w.insert(df)
|
|
|
|
# delete id 0 and flush
|
|
del_res, _ = collection_w.delete(tmp_expr)
|
|
assert del_res.delete_count == 1
|
|
assert collection_w.num_entities == tmp_nb
|
|
|
|
# load and query id 0
|
|
collection_w.create_index(ct.default_float_vec_field_name, index_params=ct.default_flat_index)
|
|
collection_w.load()
|
|
collection_w.query(tmp_expr, check_task=CheckTasks.check_query_empty)
|
|
|
|
# delete id 1 and query id 0 and 1
|
|
collection_w.delete(expr=f"{ct.default_int64_field_name} in {[1]}")
|
|
collection_w.query(expr=f"{ct.default_int64_field_name} in {[0, 1]}", check_task=CheckTasks.check_query_empty)
|
|
|
|
@pytest.mark.tags(CaseLabel.L1)
|
|
def test_delete_query_after_handoff(self):
|
|
"""
|
|
target: test search after delete and handoff
|
|
method: 1.create and load collection
|
|
2.insert entities and delete id 0
|
|
3.flush entities
|
|
4.query deleted id after handoff completed
|
|
expected: Delete successfully, query get empty result
|
|
"""
|
|
# init collection and load
|
|
collection_w = self.init_collection_wrap(name=cf.gen_unique_str(prefix), shards_num=1)
|
|
index_params = {"index_type": "IVF_SQ8", "metric_type": "L2", "params": {"nlist": 64}}
|
|
collection_w.create_index(ct.default_float_vec_field_name, index_params)
|
|
collection_w.load()
|
|
|
|
# insert data and delete id 0
|
|
df = cf.gen_default_dataframe_data(tmp_nb)
|
|
collection_w.insert(df)
|
|
del_res, _ = collection_w.delete(tmp_expr)
|
|
|
|
# flush
|
|
assert collection_w.num_entities == tmp_nb
|
|
|
|
# wait for the handoff to complete
|
|
while True:
|
|
time.sleep(0.5)
|
|
segment_infos = self.utility_wrap.get_query_segment_info(collection_w.name)[0]
|
|
if len(segment_infos) > 0 and segment_infos[0].state == SegmentState.Sealed:
|
|
break
|
|
# query deleted id
|
|
collection_w.query(tmp_expr, check_task=CheckTasks.check_query_empty)
|
|
|
|
@pytest.mark.tags(CaseLabel.L2)
|
|
@pytest.mark.skip(reason="TODO")
|
|
def test_delete_multi_threading(self):
|
|
"""
|
|
target: test delete multi threading
|
|
method: delete multi threading
|
|
expected: delete successfully
|
|
"""
|
|
pass
|
|
|
|
@pytest.mark.tags(CaseLabel.L1)
|
|
def test_delete_insert_multi(self):
|
|
"""
|
|
target: test delete after multi insert
|
|
method: 1.create
|
|
2.insert multi times, no flush
|
|
3.load
|
|
3.delete even number
|
|
4.search and query
|
|
expected: Verify result
|
|
"""
|
|
# create collection, insert multi times, each with tmp_nb entities
|
|
collection_w = self.init_collection_wrap(name=cf.gen_unique_str(prefix))
|
|
multi = 3
|
|
for i in range(multi):
|
|
start = i * tmp_nb
|
|
df = cf.gen_default_dataframe_data(tmp_nb, start=start)
|
|
collection_w.insert(df)
|
|
|
|
# delete even numbers
|
|
ids = [i for i in range(0, tmp_nb * multi, 2)]
|
|
expr = f"{ct.default_int64_field_name} in {ids}"
|
|
collection_w.delete(expr)
|
|
|
|
collection_w.create_index(ct.default_float_vec_field_name, index_params=ct.default_flat_index)
|
|
collection_w.load()
|
|
collection_w.query(expr, check_task=CheckTasks.check_query_empty)
|
|
search_res, _ = collection_w.search(
|
|
cf.gen_vectors(ct.default_nq, ct.default_dim),
|
|
ct.default_float_vec_field_name,
|
|
ct.default_search_params,
|
|
ct.default_limit,
|
|
)
|
|
for res_id in search_res[0].ids:
|
|
assert res_id not in ids
|
|
|
|
@pytest.mark.tags(CaseLabel.L3)
|
|
def test_delete_sealed_only(self):
|
|
"""
|
|
target: test delete sealed-only
|
|
method: 1.deploy sealed-only: two dmlChannel and three queryNodes
|
|
2.create and insert with flush
|
|
3.load
|
|
4.delete all data
|
|
5.query
|
|
expected:
|
|
"""
|
|
# init collection and insert data without flush
|
|
collection_w = self.init_collection_wrap(name=cf.gen_unique_str(prefix), shards_num=2)
|
|
# insert 3000 entities into 3 segments
|
|
segment_num = 3
|
|
segment_per_count = 2000
|
|
ids = []
|
|
for i in range(segment_num):
|
|
df = cf.gen_default_dataframe_data(nb=segment_per_count, start=(i * segment_per_count))
|
|
res, _ = collection_w.insert(df)
|
|
assert collection_w.num_entities == (i + 1) * segment_per_count
|
|
ids.extend(res.primary_keys)
|
|
|
|
collection_w.create_index(ct.default_float_vec_field_name, index_params=ct.default_flat_index)
|
|
collection_w.load()
|
|
|
|
expr = f"{ct.default_int64_field_name} in {ids}"
|
|
collection_w.delete(expr)
|
|
|
|
collection_w.query(expr, check_task=CheckTasks.check_query_empty)
|
|
|
|
@pytest.mark.tags(CaseLabel.L2)
|
|
@pytest.mark.skip("issue #26820")
|
|
@pytest.mark.parametrize("consistency_level", ["Bounded", "Session", "Eventually"])
|
|
def test_delete_flush_query_consistency_not_strong(self, consistency_level):
|
|
"""
|
|
target: test delete, flush and query with Consistency level not strong
|
|
method: 1.delete ids
|
|
2.flush
|
|
3.query with Consistency level not strong
|
|
expected: query successfully
|
|
"""
|
|
# init collection
|
|
collection_w = self.init_collection_general(prefix, True)[0]
|
|
|
|
# delete and flush
|
|
delete_ids = [i for i in range(ct.default_nb // 2)]
|
|
delete_expr = f"{ct.default_int64_field_name} in {delete_ids}"
|
|
res = collection_w.delete(delete_expr)[0]
|
|
assert res.delete_count == ct.default_nb // 2
|
|
collection_w.flush()
|
|
|
|
# query with Consistency level not strong
|
|
collection_w.query(
|
|
expr=delete_expr, consistency_level=consistency_level, check_task=CheckTasks.check_query_empty
|
|
)
|
|
|
|
|
|
class TestDeleteString(TestcaseBase):
|
|
"""
|
|
Test case of delete interface with string
|
|
"""
|
|
|
|
@pytest.mark.tags(CaseLabel.L1)
|
|
def test_delete_entities_repeatedly_with_string(self):
|
|
"""
|
|
target: test delete entities twice with string expr
|
|
method: delete with same expr twice
|
|
expected: No exception for second deletion
|
|
"""
|
|
# init collection with nb default data
|
|
collection_w = self.init_collection_general(
|
|
prefix, nb=tmp_nb, insert_data=True, primary_field=ct.default_string_field_name
|
|
)[0]
|
|
|
|
# assert delete successfully and no exception
|
|
collection_w.delete(expr=default_string_expr)
|
|
collection_w.num_entities
|
|
collection_w.query(default_string_expr, check_task=CheckTasks.check_query_empty)
|
|
collection_w.delete(expr=default_string_expr)
|
|
|
|
@pytest.mark.tags(CaseLabel.L1)
|
|
def test_delete_all_index_with_string(self):
|
|
"""
|
|
target: test delete after creating index
|
|
method: 1.create collection , insert data, primary_field is string field
|
|
2.create string and float index ,delete entities, query
|
|
3.search
|
|
expected: assert index and deleted id not in search result
|
|
"""
|
|
# create collection, insert tmp_nb, flush and load
|
|
collection_w, vectors = self.init_collection_general(
|
|
prefix, insert_data=True, is_index=False, primary_field=ct.default_string_field_name
|
|
)[0:2]
|
|
|
|
# create index
|
|
index_params_one = {"index_type": "IVF_SQ8", "metric_type": "COSINE", "params": {"nlist": 64}}
|
|
collection_w.create_index(ct.default_float_vec_field_name, index_params_one, index_name=index_name1)
|
|
index_params_two = {}
|
|
collection_w.create_index(ct.default_string_field_name, index_params=index_params_two, index_name=index_name2)
|
|
assert collection_w.has_index(index_name=index_name2)
|
|
|
|
collection_w.release()
|
|
collection_w.load()
|
|
# delete entity
|
|
collection_w.delete(default_string_expr)
|
|
collection_w.query(default_string_expr, check_task=CheckTasks.check_query_empty)
|
|
assert collection_w.has_index(index_name=index_name2)
|
|
|
|
# search with id 0 vectors
|
|
search_res, _ = collection_w.search(
|
|
[vectors[0][ct.default_float_vec_field_name][0]],
|
|
ct.default_float_vec_field_name,
|
|
ct.default_search_params,
|
|
ct.default_limit,
|
|
)
|
|
assert "0" not in search_res[0].ids
|
|
|
|
@pytest.mark.tags(CaseLabel.L1)
|
|
def test_delete_and_index_with_string(self):
|
|
"""
|
|
target: test delete and create index
|
|
method: 1.create, insert, string field is primary
|
|
2.delete half
|
|
3.flush and create index
|
|
4.search
|
|
expected: Empty search result
|
|
"""
|
|
# init collection and insert data without flush
|
|
schema = cf.gen_string_pk_default_collection_schema()
|
|
collection_w = self.init_collection_wrap(name=cf.gen_unique_str(prefix), schema=schema)
|
|
df = cf.gen_default_dataframe_data()
|
|
insert_res, _ = collection_w.insert(df)
|
|
|
|
# delete half and flush
|
|
expr = f"{ct.default_string_field_name} in {insert_res.primary_keys[: ct.default_nb // 2]}"
|
|
expr = expr.replace("'", '"')
|
|
collection_w.delete(expr)
|
|
collection_w.flush()
|
|
|
|
# create index
|
|
index_params = {"index_type": "IVF_SQ8", "metric_type": "COSINE", "params": {"nlist": 64}}
|
|
collection_w.create_index(ct.default_float_vec_field_name, index_params)
|
|
assert collection_w.has_index()[0]
|
|
|
|
collection_w.load()
|
|
res = collection_w.query(expr="", output_fields=["count(*)"])[0]
|
|
assert res[0]["count(*)"] == ct.default_nb // 2
|
|
search_res, _ = collection_w.search(
|
|
[df[ct.default_float_vec_field_name][0]],
|
|
ct.default_float_vec_field_name,
|
|
ct.default_search_params,
|
|
ct.default_limit,
|
|
)
|
|
# assert search results not contains deleted ids
|
|
inter = set(insert_res.primary_keys[: ct.default_nb // 2]).intersection(set(search_res[0].ids))
|
|
assert len(inter) == 0, "assert no deleted ids in search results"
|
|
|
|
@pytest.mark.tags(CaseLabel.L1)
|
|
def test_delete_query_ids_both_L0_segment_and_WAL_with_string(self):
|
|
"""
|
|
target: test query that delete ids from both channel and sealed
|
|
method: 1.create and insert, string field is primary
|
|
2.delete id 0 and flush
|
|
3.load and query id 0
|
|
4.insert new id and delete the id
|
|
5.query id 0 and new id
|
|
expected: Empty query result
|
|
"""
|
|
# init collection and insert data without flush
|
|
schema = cf.gen_string_pk_default_collection_schema()
|
|
collection_w = self.init_collection_wrap(name=cf.gen_unique_str(prefix), schema=schema)
|
|
df = cf.gen_default_dataframe_data(tmp_nb)
|
|
collection_w.insert(df)
|
|
|
|
# delete id 0 and flush
|
|
del_res, _ = collection_w.delete(default_string_expr)
|
|
assert del_res.delete_count == 1
|
|
assert collection_w.num_entities == tmp_nb
|
|
|
|
# load and query id 0
|
|
collection_w.create_index(ct.default_float_vec_field_name, index_params=ct.default_flat_index)
|
|
collection_w.load()
|
|
collection_w.query(default_string_expr, check_task=CheckTasks.check_query_empty)
|
|
|
|
# insert id tmp_nb and delete id 0 and tmp_nb
|
|
df_new = cf.gen_default_dataframe_data(nb=1, start=tmp_nb)
|
|
collection_w.insert(df_new)
|
|
collection_w.delete(expr=f'{ct.default_string_field_name} in ["tmp_nb"]')
|
|
|
|
# query with id 0 and tmp_nb
|
|
collection_w.query(
|
|
expr=f'{ct.default_string_field_name} in ["0", "tmp_nb"]', check_task=CheckTasks.check_query_empty
|
|
)
|
|
|
|
# insert deleted id and query
|
|
df_same = cf.gen_default_dataframe_data(1)
|
|
collection_w.insert(df_same)
|
|
res = df_same.iloc[:, [2, -1]].to_dict("records")
|
|
collection_w.query(
|
|
expr=default_string_expr,
|
|
output_fields=[ct.default_float_vec_field_name],
|
|
check_task=CheckTasks.check_query_results,
|
|
check_items={"exp_res": res, "with_vec": True, "pk_name": collection_w.primary_field.name},
|
|
)
|
|
|
|
@pytest.mark.tags(CaseLabel.L1)
|
|
def test_delete_search_with_string(self):
|
|
"""
|
|
target: test delete and search
|
|
method: search entities after it was deleted, string field is primary
|
|
expected: deleted entity is not in the search result
|
|
"""
|
|
# init collection with nb default data
|
|
collection_w, _, _, ids = self.init_collection_general(
|
|
prefix, insert_data=True, primary_field=ct.default_string_field_name
|
|
)[0:4]
|
|
entity, _ = collection_w.query(default_string_expr, output_fields=["*"])
|
|
search_res, _ = collection_w.search(
|
|
[entity[0][ct.default_float_vec_field_name]],
|
|
ct.default_float_vec_field_name,
|
|
ct.default_search_params,
|
|
ct.default_limit,
|
|
)
|
|
# assert search results contains entity
|
|
assert "0" in search_res[0].ids
|
|
|
|
expr = f"{ct.default_string_field_name} in {ids[: ct.default_nb // 2]}"
|
|
expr = expr.replace("'", '"')
|
|
collection_w.delete(expr)
|
|
search_res_2, _ = collection_w.search(
|
|
[entity[0][ct.default_float_vec_field_name]],
|
|
ct.default_float_vec_field_name,
|
|
ct.default_search_params,
|
|
ct.default_limit,
|
|
)
|
|
# assert search result is not equal to entity
|
|
inter = set(ids[: ct.default_nb // 2]).intersection(set(search_res_2[0].ids))
|
|
# Using bounded staleness, we could still search the "deleted" entities,
|
|
# since the search requests arrived query nodes earlier than query nodes consume the delete requests.
|
|
assert len(inter) == 0
|
|
|
|
@pytest.mark.tags(CaseLabel.L1)
|
|
def test_delete_expr_repeated_values_with_string(self):
|
|
"""
|
|
target: test delete with repeated values
|
|
method: 1.insert data with unique primary keys, string field is primary
|
|
2.delete with repeated values: 'id in [0, 0]'
|
|
expected: delete one entity
|
|
"""
|
|
# init collection with nb default data
|
|
collection_w = self.init_collection_general(
|
|
prefix, nb=tmp_nb, insert_data=True, primary_field=ct.default_string_field_name
|
|
)[0]
|
|
expr = f'{ct.default_string_field_name} in ["0", "0", "0"]'
|
|
del_res, _ = collection_w.delete(expr)
|
|
assert del_res.delete_count == 1
|
|
collection_w.num_entities
|
|
collection_w.query(expr, check_task=CheckTasks.check_query_empty)
|
|
|
|
@pytest.mark.tags(CaseLabel.L1)
|
|
def test_delete_duplicate_primary_keys_with_string(self):
|
|
"""
|
|
target: test delete from duplicate primary keys
|
|
method: 1.insert data with dup ids, string field is primary
|
|
2.delete with repeated or not values
|
|
expected: currently only delete one entity, query get one entity
|
|
todo delete all entities
|
|
"""
|
|
schema = cf.gen_string_pk_default_collection_schema()
|
|
collection_w = self.init_collection_wrap(name=cf.gen_unique_str(prefix), schema=schema)
|
|
df = cf.gen_default_dataframe_data(nb=tmp_nb)
|
|
df[ct.default_string_field_name] = "0"
|
|
collection_w.insert(df)
|
|
assert collection_w.num_entities == tmp_nb
|
|
del_res, _ = collection_w.delete(default_string_expr)
|
|
collection_w.create_index(ct.default_float_vec_field_name, index_params=ct.default_flat_index)
|
|
collection_w.load()
|
|
|
|
# Just one query res and search res, because de-dup
|
|
res, _ = collection_w.query(default_string_expr, output_fields=["*"])
|
|
assert len(res) == 0
|
|
|
|
search_res, _ = collection_w.search(
|
|
[df[ct.default_float_vec_field_name][1]],
|
|
ct.default_float_vec_field_name,
|
|
ct.default_search_params,
|
|
ct.default_limit,
|
|
output_fields=[ct.default_int64_field_name, ct.default_float_field_name, ct.default_string_field_name],
|
|
)
|
|
assert len(search_res) == 1
|
|
|
|
@pytest.mark.tags(CaseLabel.L1)
|
|
def test_delete_from_partitions_with_same_ids_of_string(self):
|
|
"""
|
|
target: test delete same ids from two partitions with same data
|
|
method: 1.insert same nb data into two partitions
|
|
2.delete same ids from partition_1
|
|
expected: The data only in partition_1 will be deleted
|
|
"""
|
|
# init collection and partition
|
|
schema = cf.gen_string_pk_default_collection_schema()
|
|
collection_w = self.init_collection_wrap(name=cf.gen_unique_str(prefix), schema=schema)
|
|
partition_w = self.init_partition_wrap(collection_wrap=collection_w)
|
|
|
|
# insert same data into partition_w and default partition
|
|
df = cf.gen_default_dataframe_data(tmp_nb)
|
|
collection_w.insert(df)
|
|
partition_w.insert(df)
|
|
|
|
# delete same id 0 from default_partition, and query on it get empty result
|
|
collection_w.delete(default_string_expr, partition_name=ct.default_partition_name)
|
|
assert collection_w.num_entities == tmp_nb * 2
|
|
collection_w.create_index(ct.default_float_vec_field_name, index_params=ct.default_flat_index)
|
|
collection_w.load()
|
|
collection_w.query(
|
|
default_string_expr, partition_names=[ct.default_partition_name], check_task=CheckTasks.check_query_empty
|
|
)
|
|
|
|
# query on partition_w with id 0 and get an result
|
|
collection_w.query(
|
|
default_string_expr,
|
|
partition_names=[partition_w.name],
|
|
check_task=CheckTasks.check_query_results,
|
|
check_items={"exp_res": query_tmp_expr_str, "pk_name": collection_w.primary_field.name},
|
|
)
|
|
|
|
@pytest.mark.tags(CaseLabel.L1)
|
|
def test_delete_sealed_segment_without_flush_with_string(self):
|
|
"""
|
|
target: test delete without flush
|
|
method: 1.insert and flush data
|
|
2.delete ids from collection and no flush
|
|
3.load and query with id
|
|
expected: No query result
|
|
"""
|
|
# create collection, insert data without flush
|
|
schema = cf.gen_string_pk_default_collection_schema()
|
|
collection_w = self.init_collection_wrap(name=cf.gen_unique_str(prefix), schema=schema)
|
|
df = cf.gen_default_dataframe_data(tmp_nb)
|
|
collection_w.insert(df)
|
|
assert collection_w.num_entities == tmp_nb
|
|
|
|
# delete
|
|
del_res, _ = collection_w.delete(default_string_expr)
|
|
assert del_res.delete_count == 1
|
|
collection_w.flush()
|
|
|
|
# load and query with id
|
|
collection_w.create_index(ct.default_float_vec_field_name, index_params=ct.default_flat_index)
|
|
collection_w.load()
|
|
collection_w.query(default_string_expr, check_task=CheckTasks.check_query_empty)
|
|
|
|
# insert deleted id and query
|
|
df_same = cf.gen_default_dataframe_data(1)
|
|
collection_w.insert(df_same)
|
|
res = df_same.iloc[:, [2, -1]].to_dict("records")
|
|
collection_w.query(
|
|
expr=default_string_expr,
|
|
output_fields=[ct.default_float_vec_field_name],
|
|
check_task=CheckTasks.check_query_results,
|
|
check_items={"exp_res": res, "with_vec": True, "pk_name": collection_w.primary_field.name},
|
|
)
|
|
|
|
@pytest.mark.tags(CaseLabel.L1)
|
|
def test_delete_growing_data_channel_delete_with_string(self):
|
|
"""
|
|
target: test delete entities from growing segment, and channel deleteMsg
|
|
method: 1.create collection, string field is primary
|
|
2.load collection
|
|
3.insert data and delete ids
|
|
4.query deleted ids
|
|
expected: No query result
|
|
"""
|
|
# create collection
|
|
schema = cf.gen_string_pk_default_collection_schema()
|
|
collection_w = self.init_collection_wrap(name=cf.gen_unique_str(prefix), schema=schema)
|
|
# load collection and the queryNode watch the insertChannel
|
|
collection_w.create_index(ct.default_float_vec_field_name, index_params=ct.default_flat_index)
|
|
collection_w.load()
|
|
# insert data
|
|
df = cf.gen_default_dataframe_data(tmp_nb)
|
|
collection_w.insert(df)
|
|
# delete id 0
|
|
del_res = collection_w.delete(default_string_expr)[0]
|
|
assert del_res.delete_count == 1
|
|
# query id 0
|
|
collection_w.query(default_string_expr, check_task=CheckTasks.check_query_empty)
|
|
|
|
@pytest.mark.tags(CaseLabel.L1)
|
|
def test_delete_sealed_data_channel_delete_with_string(self):
|
|
"""
|
|
target: test delete sealed data and get deleteMsg from insertChannel
|
|
method: 1.create, insert and flush data, string field is primary
|
|
2.load collection
|
|
3.delete id without flush
|
|
4.query deleted ids (queryNode get deleted ids from channel not persistence)
|
|
expected: Delete successfully and no query result
|
|
"""
|
|
# create collection and insert flush data
|
|
schema = cf.gen_string_pk_default_collection_schema()
|
|
collection_w = self.init_collection_wrap(name=cf.gen_unique_str(prefix), schema=schema)
|
|
df = cf.gen_default_dataframe_data(tmp_nb)
|
|
collection_w.insert(df)
|
|
assert collection_w.num_entities == tmp_nb
|
|
|
|
# load collection and queryNode subscribe channel
|
|
collection_w.create_index(ct.default_float_vec_field_name, index_params=ct.default_flat_index)
|
|
collection_w.load()
|
|
|
|
# delete ids and query
|
|
collection_w.delete(default_string_expr)
|
|
collection_w.query(default_string_expr, check_task=CheckTasks.check_query_empty)
|
|
|
|
@pytest.mark.tags(CaseLabel.L2)
|
|
def test_delete_sealed_segment_with_flush_string(self):
|
|
"""
|
|
target: test delete data from sealed segment and flush delta log
|
|
method: 1.create and insert and flush data, string field is primary
|
|
2.delete entities and flush (insert and flush)
|
|
3.load collection (load data and delta log)
|
|
4.query deleted ids
|
|
expected: No query result
|
|
"""
|
|
# create collection
|
|
schema = cf.gen_string_pk_default_collection_schema()
|
|
collection_w = self.init_collection_wrap(name=cf.gen_unique_str(prefix), schema=schema)
|
|
# insert and flush data
|
|
df = cf.gen_default_dataframe_data(tmp_nb)
|
|
collection_w.insert(df)
|
|
assert collection_w.num_entities == tmp_nb
|
|
|
|
# delete id 0 and flush
|
|
del_res = collection_w.delete(default_string_expr)[0]
|
|
assert del_res.delete_count == 1
|
|
collection_w.insert(cf.gen_default_dataframe_data(nb=1, start=tmp_nb))
|
|
log.info(collection_w.num_entities)
|
|
# load and query id 0
|
|
collection_w.create_index(ct.default_float_vec_field_name, index_params=ct.default_flat_index)
|
|
collection_w.load()
|
|
collection_w.query(default_string_expr, check_task=CheckTasks.check_query_empty)
|
|
|
|
@pytest.mark.tags(CaseLabel.L1)
|
|
def test_delete_sealed_data_sealed_delete_string(self):
|
|
"""
|
|
target: test delete with sealed data and sealed delete request
|
|
method: 1.create, insert, string field is primary
|
|
2.delete and flush (will flush data and delete)
|
|
3.load and query
|
|
expected: Empty query result
|
|
"""
|
|
# create collection
|
|
schema = cf.gen_string_pk_default_collection_schema()
|
|
collection_w = self.init_collection_wrap(name=cf.gen_unique_str(prefix), schema=schema)
|
|
# insert without flush
|
|
df = cf.gen_default_dataframe_data(tmp_nb)
|
|
collection_w.insert(df)
|
|
|
|
# delete id 0 and flush
|
|
del_res = collection_w.delete(default_string_expr)[0]
|
|
assert del_res.delete_count == 1
|
|
assert collection_w.num_entities == tmp_nb
|
|
|
|
# load and query id 0
|
|
collection_w.create_index(ct.default_float_vec_field_name, index_params=ct.default_flat_index)
|
|
collection_w.load()
|
|
collection_w.query(default_string_expr, check_task=CheckTasks.check_query_empty)
|
|
|
|
@pytest.mark.tags(CaseLabel.L2)
|
|
def test_delete_entity_loop_with_string(self):
|
|
"""
|
|
target: test delete all entities one by one in a loop
|
|
method: delete data one by one for a loop
|
|
expected: No exception
|
|
"""
|
|
# init an auto_id collection and insert tmp_nb data, flush and load
|
|
collection_w, _, _, ids = self.init_collection_general(
|
|
prefix, nb=tmp_nb, insert_data=True, primary_field=ct.default_string_field_name
|
|
)[0:4]
|
|
|
|
for del_id in ids:
|
|
expr = f"{ct.default_string_field_name} in {[del_id]}"
|
|
expr = expr.replace("'", '"')
|
|
res = collection_w.delete(expr)[0]
|
|
assert res.delete_count == 1
|
|
|
|
# query with all ids
|
|
expr = f"{ct.default_string_field_name} in {ids}"
|
|
expr = expr.replace("'", '"')
|
|
collection_w.query(expr, check_task=CheckTasks.check_query_empty)
|
|
|
|
@pytest.mark.tags(CaseLabel.L2)
|
|
def test_delete_flush_loop_with_string(self):
|
|
"""
|
|
target: test delete and flush in a loop
|
|
method: in a loop, delete batch and flush, until delete all entities
|
|
expected: No exception
|
|
"""
|
|
# init an auto_id collection and insert tmp_nb data
|
|
collection_w, _, _, ids = self.init_collection_general(
|
|
prefix, nb=tmp_nb, insert_data=True, primary_field=ct.default_string_field_name
|
|
)[0:4]
|
|
|
|
batch = 10
|
|
for i in range(tmp_nb // batch):
|
|
expr = f"{ct.default_string_field_name} in {ids[i * batch : (i + 1) * batch]}"
|
|
expr = expr.replace("'", '"')
|
|
res, _ = collection_w.delete(expr)
|
|
assert res.delete_count == batch
|
|
|
|
# query with all ids
|
|
expr = f"{ct.default_string_field_name} in {ids}"
|
|
expr = expr.replace("'", '"')
|
|
collection_w.query(expr, check_task=CheckTasks.check_query_empty)
|
|
|
|
@pytest.mark.tags(CaseLabel.L2)
|
|
@pytest.mark.parametrize("to_flush_data", [True, False])
|
|
@pytest.mark.parametrize("to_flush_delete", [True, False])
|
|
def test_delete_merge_same_id_channel_and_sealed_string(self, to_flush_data, to_flush_delete):
|
|
"""
|
|
target: test merge same delete ids from channel and sealed
|
|
method: 1.create, insert, string field is primary
|
|
2.delete id and flush (data and deleted become sealed)
|
|
3.load and query (verify delete successfully)
|
|
4.insert entity with deleted id
|
|
5.delete id
|
|
6.query with id
|
|
expected: Empty query result
|
|
"""
|
|
# init collection and insert data without flush
|
|
schema = cf.gen_string_pk_default_collection_schema()
|
|
collection_w = self.init_collection_wrap(name=cf.gen_unique_str(prefix), schema=schema, shards_num=1)
|
|
df = cf.gen_default_dataframe_data(tmp_nb)
|
|
collection_w.insert(df)
|
|
|
|
# delete id 0 and flush
|
|
del_res, _ = collection_w.delete(default_string_expr)
|
|
assert del_res.delete_count == 1
|
|
assert collection_w.num_entities == tmp_nb
|
|
|
|
# load and query id 0
|
|
collection_w.create_index(ct.default_float_vec_field_name, index_params=ct.default_flat_index)
|
|
collection_w.load()
|
|
collection_w.query(default_string_expr, check_task=CheckTasks.check_query_empty)
|
|
|
|
# insert new entity with same id 0 and query
|
|
df_new = cf.gen_default_dataframe_data(1)
|
|
collection_w.insert(df_new)
|
|
if to_flush_data:
|
|
log.debug(collection_w.num_entities)
|
|
collection_w.query(
|
|
default_string_expr,
|
|
output_fields=[ct.default_float_vec_field_name],
|
|
check_task=CheckTasks.check_query_results,
|
|
check_items={
|
|
"exp_res": df_new.iloc[[0], [2, 4]].to_dict("records"),
|
|
"pk_name": collection_w.primary_field.name,
|
|
"with_vec": True,
|
|
},
|
|
)
|
|
|
|
collection_w.delete(default_string_expr)
|
|
if to_flush_delete:
|
|
log.debug(collection_w.num_entities)
|
|
collection_w.query(default_string_expr, check_task=CheckTasks.check_query_empty)
|
|
|
|
@pytest.mark.tags(CaseLabel.L2)
|
|
def test_delete_merge_ids_channel_and_sealed_string(self):
|
|
"""
|
|
target: test merge deleted ids come from both channel and sealed
|
|
method: 1.create, insert ids [0, tmp_nb) with shard_num=1, string field is primary
|
|
2.delete id 0 and flush
|
|
3.load and query with id 0
|
|
4.delete id 1 (merge same segment deleted ids 0 and 1)
|
|
5.query with id 0 and 1
|
|
expected: Empty query result
|
|
"""
|
|
# init collection and insert data without flush
|
|
schema = cf.gen_string_pk_default_collection_schema()
|
|
collection_w = self.init_collection_wrap(name=cf.gen_unique_str(prefix), schema=schema, shards_num=1)
|
|
df = cf.gen_default_dataframe_data(tmp_nb)
|
|
collection_w.insert(df)
|
|
|
|
# delete id 0 and flush
|
|
del_res, _ = collection_w.delete(default_string_expr)
|
|
assert del_res.delete_count == 1
|
|
assert collection_w.num_entities == tmp_nb
|
|
|
|
# load and query id 0
|
|
collection_w.create_index(ct.default_float_vec_field_name, index_params=ct.default_flat_index)
|
|
collection_w.load()
|
|
collection_w.query(default_string_expr, check_task=CheckTasks.check_query_empty)
|
|
|
|
# delete id 1 and query id 0 and 1
|
|
collection_w.delete(expr=f'{ct.default_string_field_name} in ["1"]')
|
|
collection_w.query(
|
|
expr=f'{ct.default_string_field_name} in ["0", "1"]', check_task=CheckTasks.check_query_empty
|
|
)
|
|
|
|
@pytest.mark.tags(CaseLabel.L1)
|
|
def test_delete_query_after_handoff_with_string(self):
|
|
"""
|
|
target: test search after delete and handoff
|
|
method: 1.create and load collection, string field is primary
|
|
2.insert entities and delete id 0
|
|
3.flush entities
|
|
4.query deleted id after handoff completed
|
|
expected: Delete successfully, query get empty result
|
|
"""
|
|
# init collection and load
|
|
schema = cf.gen_string_pk_default_collection_schema()
|
|
collection_w = self.init_collection_wrap(name=cf.gen_unique_str(prefix), schema=schema, shards_num=1)
|
|
index_params = {"index_type": "IVF_SQ8", "metric_type": "L2", "params": {"nlist": 64}}
|
|
collection_w.create_index(ct.default_float_vec_field_name, index_params)
|
|
collection_w.load()
|
|
|
|
# insert data and delete id 0
|
|
df = cf.gen_default_dataframe_data(tmp_nb)
|
|
collection_w.insert(df)
|
|
del_res, _ = collection_w.delete(default_string_expr)
|
|
|
|
# flush
|
|
assert collection_w.num_entities == tmp_nb
|
|
|
|
# wait for the handoff to complete
|
|
while True:
|
|
time.sleep(0.5)
|
|
segment_infos = self.utility_wrap.get_query_segment_info(collection_w.name)[0]
|
|
if len(segment_infos) > 0 and segment_infos[0].state == SegmentState.Sealed:
|
|
break
|
|
# query deleted id
|
|
collection_w.query(default_string_expr, check_task=CheckTasks.check_query_empty)
|
|
|
|
@pytest.mark.tags(CaseLabel.L1)
|
|
def test_delete_insert_multi_with_string(self):
|
|
"""
|
|
target: test delete after multi insert with string
|
|
method: 1.create a collection with string field is primary
|
|
2.insert multi times, no flush
|
|
3.load
|
|
3.delete even number
|
|
4.search and query
|
|
expected: Verify result
|
|
"""
|
|
# create collection, insert multi times, each with tmp_nb entities
|
|
schema = cf.gen_string_pk_default_collection_schema()
|
|
collection_w = self.init_collection_wrap(cf.gen_unique_str(prefix), schema=schema)
|
|
multi = 3
|
|
for i in range(multi):
|
|
start = i * tmp_nb
|
|
df = cf.gen_default_dataframe_data(tmp_nb, start=start)
|
|
collection_w.insert(df)
|
|
|
|
# delete even numbers
|
|
ids = [str(i) for i in range(0, tmp_nb * multi, 2)]
|
|
expr = f"{ct.default_string_field_name} in {ids}"
|
|
expr = expr.replace("'", '"')
|
|
collection_w.delete(expr)
|
|
|
|
collection_w.create_index(ct.default_float_vec_field_name, index_params=ct.default_flat_index)
|
|
collection_w.load()
|
|
collection_w.query(expr, check_task=CheckTasks.check_query_empty)
|
|
search_res, _ = collection_w.search(
|
|
cf.gen_vectors(ct.default_nq, ct.default_dim),
|
|
ct.default_float_vec_field_name,
|
|
ct.default_search_params,
|
|
ct.default_limit,
|
|
)
|
|
for res_id in search_res[0].ids:
|
|
assert res_id not in ids
|
|
|
|
@pytest.mark.tags(CaseLabel.L0)
|
|
def test_delete_invalid_expr(self):
|
|
"""
|
|
target: test delete data with string expr
|
|
method: 1.create collection, insert data and collection.load()
|
|
2.collection delete with invalid expr
|
|
3.query expr
|
|
expected: Raise exception
|
|
"""
|
|
collection_w = self.init_collection_general(
|
|
prefix, nb=tmp_nb, insert_data=True, primary_field=ct.default_string_field_name
|
|
)[0]
|
|
collection_w.load()
|
|
error = {
|
|
ct.err_code: 1100,
|
|
ct.err_msg: f"failed to create delete plan: cannot parse expression: {default_invalid_string_exp}: "
|
|
f"comparisons between VarChar and Int64 are not supported: query plan failed: invalid parameter",
|
|
}
|
|
collection_w.delete(expr=default_invalid_string_exp, check_task=CheckTasks.err_res, check_items=error)
|
|
|
|
@pytest.mark.tags(CaseLabel.L1)
|
|
@pytest.mark.parametrize("to_query", [True, False])
|
|
# @pytest.mark.parametrize("enable_dynamic_field", [True, False])
|
|
def test_delete_insert_same_id_sealed_string(self, to_query):
|
|
"""
|
|
target: test insert same id entity after delete from sealed data
|
|
method: 1.create and insert with flush, string is pk field
|
|
2.load and query with the id
|
|
3.delete the id entity
|
|
4.insert new entity with the same id and flush
|
|
5.query the id
|
|
expected: Verify that the query gets the newly inserted entity
|
|
"""
|
|
# init collection and insert data without flush
|
|
schema = cf.gen_string_pk_default_collection_schema()
|
|
collection_w = self.init_collection_wrap(cf.gen_unique_str(prefix), schema=schema)
|
|
|
|
# insert
|
|
df = cf.gen_default_dataframe_data(1000)
|
|
collection_w.insert(df)
|
|
log.debug(collection_w.num_entities)
|
|
|
|
# load and query
|
|
collection_w.create_index(ct.default_float_vec_field_name, index_params=ct.default_flat_index)
|
|
collection_w.load()
|
|
res = df.iloc[:1, 2:3].to_dict("records")
|
|
collection_w.search(
|
|
data=[df[ct.default_float_vec_field_name][0]],
|
|
anns_field=ct.default_float_vec_field_name,
|
|
param=default_search_params,
|
|
limit=1,
|
|
)
|
|
collection_w.query(
|
|
default_string_expr,
|
|
check_task=CheckTasks.check_query_results,
|
|
check_items={"exp_res": res, "pk_name": collection_w.primary_field.name},
|
|
)
|
|
|
|
# delete
|
|
collection_w.delete(default_string_expr)
|
|
if to_query:
|
|
collection_w.query(default_string_expr, check_task=CheckTasks.check_query_empty)
|
|
|
|
# re-insert
|
|
df_new = cf.gen_default_dataframe_data(nb=1)
|
|
collection_w.insert(df_new)
|
|
log.debug(collection_w.num_entities)
|
|
|
|
# re-query
|
|
res = df_new.iloc[[0], [2, 4]].to_dict("records")
|
|
collection_w.query(
|
|
default_string_expr,
|
|
output_fields=[ct.default_float_vec_field_name],
|
|
check_task=CheckTasks.check_query_results,
|
|
check_items={"exp_res": res, "pk_name": collection_w.primary_field.name, "with_vec": True},
|
|
)
|
|
collection_w.search(
|
|
data=[df_new[ct.default_float_vec_field_name][0]],
|
|
anns_field=ct.default_float_vec_field_name,
|
|
param=default_search_params,
|
|
limit=1,
|
|
)
|
|
|
|
@pytest.mark.tags(CaseLabel.L1)
|
|
def test_delete_with_string_field_is_empty(self):
|
|
"""
|
|
target: test delete with string field is empty
|
|
method: 1.string field is PK, insert empty data
|
|
2.delete ids from collection
|
|
3.load and query with id
|
|
expected: No query result
|
|
"""
|
|
# create collection, insert data without flush
|
|
schema = cf.gen_string_pk_default_collection_schema()
|
|
collection_w = self.init_collection_wrap(name=cf.gen_unique_str(prefix), schema=schema)
|
|
|
|
nb = 3000
|
|
df = cf.gen_default_list_data(nb)
|
|
df[2] = ["" for _ in range(nb)]
|
|
|
|
collection_w.insert(df)
|
|
collection_w.flush()
|
|
collection_w.create_index(ct.default_float_vec_field_name, index_params=ct.default_flat_index)
|
|
collection_w.load()
|
|
assert collection_w.num_entities == nb
|
|
|
|
# delete
|
|
string_expr = 'varchar in ["", ""]'
|
|
del_res, _ = collection_w.delete(string_expr)
|
|
assert del_res.delete_count == 1
|
|
|
|
# load and query with id
|
|
collection_w.load()
|
|
collection_w.query(string_expr, check_task=CheckTasks.check_query_empty)
|
|
|
|
|
|
class TestDeleteComplexExpr(TestcaseBase):
|
|
"""
|
|
Test case of delete interface with complex expr
|
|
"""
|
|
|
|
@pytest.mark.tags(CaseLabel.L0)
|
|
@pytest.mark.parametrize("expressions", cf.gen_normal_expressions_and_templates()[1:])
|
|
@pytest.mark.parametrize("enable_dynamic_field", [True, False])
|
|
def test_delete_normal_expressions(self, expressions, enable_dynamic_field):
|
|
"""
|
|
target: test delete entities using normal expression
|
|
method: delete using normal expression
|
|
expected: delete successfully
|
|
"""
|
|
# init collection with nb default data
|
|
collection_w, _vectors, _, insert_ids = self.init_collection_general(
|
|
prefix, True, enable_dynamic_field=enable_dynamic_field
|
|
)[0:4]
|
|
|
|
# filter result with expression in collection
|
|
_vectors = _vectors[0]
|
|
expression = expressions[0].replace("&&", "and").replace("||", "or")
|
|
filter_ids = []
|
|
for i, _id in enumerate(insert_ids):
|
|
if enable_dynamic_field:
|
|
int64 = _vectors[i][ct.default_int64_field_name]
|
|
float = _vectors[i][ct.default_float_field_name]
|
|
else:
|
|
int64 = _vectors.int64[i]
|
|
float = _vectors.float[i]
|
|
if not expression or eval(expression):
|
|
filter_ids.append(_id)
|
|
|
|
# delete with expressions
|
|
res = collection_w.delete(expression)[0]
|
|
assert res.delete_count == len(filter_ids)
|
|
|
|
# query to check
|
|
collection_w.query(f"int64 in {filter_ids}", check_task=CheckTasks.check_query_empty)
|
|
|
|
@pytest.mark.tags(CaseLabel.L0)
|
|
@pytest.mark.parametrize("expressions", cf.gen_normal_expressions_and_templates()[1:])
|
|
@pytest.mark.parametrize("enable_dynamic_field", [True, False])
|
|
def test_delete_normal_expressions_templates(self, expressions, enable_dynamic_field):
|
|
"""
|
|
target: test delete entities using normal expression
|
|
method: delete using normal expression
|
|
expected: delete successfully
|
|
"""
|
|
# init collection with nb default data
|
|
collection_w, _vectors, _, insert_ids = self.init_collection_general(
|
|
prefix, True, enable_dynamic_field=enable_dynamic_field
|
|
)[0:4]
|
|
|
|
# filter result with expression in collection
|
|
_vectors = _vectors[0]
|
|
expr = expressions[0].replace("&&", "and").replace("||", "or")
|
|
filter_ids = []
|
|
for i, _id in enumerate(insert_ids):
|
|
if enable_dynamic_field:
|
|
int64 = _vectors[i][ct.default_int64_field_name]
|
|
float = _vectors[i][ct.default_float_field_name]
|
|
else:
|
|
int64 = _vectors.int64[i]
|
|
float = _vectors.float[i]
|
|
if not expr or eval(expr):
|
|
filter_ids.append(_id)
|
|
|
|
# delete with expressions templates
|
|
expr = cf.get_expr_from_template(expressions[1]).replace("&&", "and").replace("||", "or")
|
|
expr_params = cf.get_expr_params_from_template(expressions[1])
|
|
res = collection_w.delete(expr=expr, expr_params=expr_params)[0]
|
|
assert res.delete_count == len(filter_ids)
|
|
|
|
# query to check
|
|
collection_w.query(f"int64 in {filter_ids}", check_task=CheckTasks.check_query_empty)
|
|
|
|
@pytest.mark.tags(CaseLabel.L2)
|
|
@pytest.mark.parametrize("expressions", cf.gen_array_field_expressions_and_templates())
|
|
@pytest.mark.parametrize("enable_dynamic_field", [True, False])
|
|
def test_delete_array_expressions(self, expressions, enable_dynamic_field):
|
|
"""
|
|
target: test delete entities using normal expression
|
|
method: delete using normal expression
|
|
expected: delete successfully
|
|
"""
|
|
# 1. create a collection
|
|
nb = ct.default_nb
|
|
schema = cf.gen_array_collection_schema()
|
|
collection_w = self.init_collection_wrap(schema=schema, enable_dynamic_field=enable_dynamic_field)
|
|
|
|
# 2. insert data
|
|
array_length = 100
|
|
data = []
|
|
for i in range(nb):
|
|
arr = {
|
|
ct.default_int64_field_name: i,
|
|
ct.default_float_vec_field_name: cf.gen_vectors(1, ct.default_dim)[0],
|
|
ct.default_int32_array_field_name: [np.int32(i) for i in range(array_length)],
|
|
ct.default_float_array_field_name: [np.float32(i) for i in range(array_length)],
|
|
ct.default_string_array_field_name: [str(i) for i in range(array_length)],
|
|
}
|
|
data.append(arr)
|
|
collection_w.insert(data)
|
|
collection_w.flush()
|
|
collection_w.create_index(ct.default_float_vec_field_name, ct.default_flat_index)
|
|
collection_w.load()
|
|
|
|
# 3. filter result with expression in collection
|
|
expr = expressions[0].replace("&&", "and").replace("||", "or")
|
|
filter_ids = []
|
|
for i in range(nb):
|
|
int32_array = data[i][ct.default_int32_array_field_name]
|
|
float_array = data[i][ct.default_float_array_field_name]
|
|
string_array = data[i][ct.default_string_array_field_name]
|
|
if not expr or eval(expr):
|
|
filter_ids.append(i)
|
|
|
|
# 4. delete by array expression
|
|
res = collection_w.delete(expr)[0]
|
|
assert res.delete_count == len(filter_ids)
|
|
|
|
# 5. query to check
|
|
collection_w.query(expr, check_task=CheckTasks.check_query_empty)
|
|
|
|
@pytest.mark.tags(CaseLabel.L2)
|
|
@pytest.mark.parametrize("expressions", cf.gen_array_field_expressions_and_templates())
|
|
@pytest.mark.parametrize("enable_dynamic_field", [True, False])
|
|
def test_delete_array_expressions_templates(self, expressions, enable_dynamic_field):
|
|
"""
|
|
target: test delete entities using normal expression
|
|
method: delete using normal expression
|
|
expected: delete successfully
|
|
"""
|
|
# 1. create a collection
|
|
nb = ct.default_nb
|
|
schema = cf.gen_array_collection_schema()
|
|
collection_w = self.init_collection_wrap(schema=schema, enable_dynamic_field=enable_dynamic_field)
|
|
|
|
# 2. insert data
|
|
array_length = 100
|
|
data = []
|
|
for i in range(nb):
|
|
arr = {
|
|
ct.default_int64_field_name: i,
|
|
ct.default_float_vec_field_name: cf.gen_vectors(1, ct.default_dim)[0],
|
|
ct.default_int32_array_field_name: [np.int32(i) for i in range(array_length)],
|
|
ct.default_float_array_field_name: [np.float32(i) for i in range(array_length)],
|
|
ct.default_string_array_field_name: [str(i) for i in range(array_length)],
|
|
}
|
|
data.append(arr)
|
|
collection_w.insert(data)
|
|
collection_w.flush()
|
|
collection_w.create_index(ct.default_float_vec_field_name, ct.default_flat_index)
|
|
collection_w.load()
|
|
|
|
# 3. filter result with expression in collection
|
|
expr = expressions[0].replace("&&", "and").replace("||", "or")
|
|
filter_ids = []
|
|
for i in range(nb):
|
|
int32_array = data[i][ct.default_int32_array_field_name]
|
|
float_array = data[i][ct.default_float_array_field_name]
|
|
string_array = data[i][ct.default_string_array_field_name]
|
|
if not expr or eval(expr):
|
|
filter_ids.append(i)
|
|
|
|
# 4. delete by array expression
|
|
expr = cf.get_expr_from_template(expressions[1]).replace("&&", "and").replace("||", "or")
|
|
expr_params = cf.get_expr_params_from_template(expressions[1])
|
|
res = collection_w.delete(expr=expr, expr_params=expr_params)[0]
|
|
assert res.delete_count == len(filter_ids)
|
|
|
|
# 5. query to check
|
|
collection_w.query(expr=expr, expr_params=expr_params, check_task=CheckTasks.check_query_empty)
|
|
|
|
@pytest.mark.tags(CaseLabel.L1)
|
|
@pytest.mark.parametrize("field_name", ["varchar", "json_field['string']", "NewStr"])
|
|
@pytest.mark.parametrize("like", ["like", "LIKE"])
|
|
@pytest.mark.parametrize("enable_dynamic_field", [True, False])
|
|
def test_delete_string_expressions_like(self, field_name, like, enable_dynamic_field):
|
|
"""
|
|
target: test delete expr like
|
|
method: delete using expression like
|
|
expected: delete successfully
|
|
"""
|
|
if field_name == "NewStr" and enable_dynamic_field is False:
|
|
pytest.skip("only support when enable_dynamic_filed == True")
|
|
# init collection with nb default data
|
|
nb = 1000
|
|
collection_w, _vectors, _, insert_ids = self.init_collection_general(
|
|
prefix, False, enable_dynamic_field=enable_dynamic_field
|
|
)[0:4]
|
|
|
|
# insert
|
|
string_list = [cf.gen_str_by_length() for _ in range(nb)]
|
|
if enable_dynamic_field:
|
|
data = cf.gen_default_rows_data(nb)
|
|
for i in range(nb):
|
|
data[i][ct.default_json_field_name] = {"string": string_list[i]}
|
|
data[i]["NewStr"] = string_list[i]
|
|
data[i][ct.default_string_field_name] = string_list[i]
|
|
else:
|
|
data = cf.gen_default_dataframe_data(nb)
|
|
data[ct.default_json_field_name] = [{"string": string_list[i]} for i in range(nb)]
|
|
data[ct.default_string_field_name] = string_list
|
|
collection_w.insert(data)
|
|
collection_w.flush()
|
|
collection_w.load()
|
|
|
|
# delete with expressions
|
|
deleted_str = [s for s in string_list if s.startswith("a")]
|
|
expression = f"{field_name} {like} 'a%'"
|
|
res = collection_w.delete(expression)[0]
|
|
assert res.delete_count == len(deleted_str)
|
|
|
|
# query to check
|
|
collection_w.load()
|
|
collection_w.query(
|
|
"int64 >= 0",
|
|
output_fields=["count(*)"],
|
|
check_task=CheckTasks.check_query_results,
|
|
check_items={"count(*)": nb - len(deleted_str)},
|
|
)
|
|
|
|
@pytest.mark.tags(CaseLabel.L2)
|
|
def test_delete_expr_empty_string(self):
|
|
"""
|
|
target: test delete with expr empty
|
|
method: delete with expr=""
|
|
expected: raise exception
|
|
"""
|
|
# init collection with nb default data
|
|
collection_w = self.init_collection_general(prefix, True)[0]
|
|
|
|
# delete
|
|
error = {ct.err_code: 1, ct.err_msg: "Illegal str variables: {'filter': ''}, expect non-empty str"}
|
|
collection_w.delete(expr="", check_task=CheckTasks.err_res, check_items=error)
|
|
|
|
@pytest.mark.tags(CaseLabel.L2)
|
|
def test_delete_complex_expr_before_load(self):
|
|
"""
|
|
target: test delete before load
|
|
method: delete with any complex expr
|
|
expected: raise exception
|
|
"""
|
|
# init collection with nb default data
|
|
collection_w = self.init_collection_general(prefix, False)[0]
|
|
collection_w.release()
|
|
|
|
# delete
|
|
error = {ct.err_code: 101, ct.err_msg: "collection not loaded"}
|
|
collection_w.delete(expr="int64 >= 0", check_task=CheckTasks.err_res, check_items=error)
|
|
|
|
@pytest.mark.tags(CaseLabel.L1)
|
|
@pytest.mark.parametrize("expr_prefix", ["json_contains", "JSON_CONTAINS"])
|
|
@pytest.mark.parametrize("field_name", ["json_field['list']", "list"])
|
|
@pytest.mark.parametrize("enable_dynamic_field", [True, False])
|
|
def test_delete_expr_json_contains_base(self, expr_prefix, field_name, enable_dynamic_field):
|
|
"""
|
|
target: test delete expr using json_contains
|
|
method: delete using expression using json_contains
|
|
expected: delete successfully
|
|
"""
|
|
if field_name == "list" and enable_dynamic_field is False:
|
|
pytest.skip("only support when enable_dynamic_filed == True")
|
|
# init collection with nb default data
|
|
collection_w = self.init_collection_general(prefix, False, enable_dynamic_field=enable_dynamic_field)[0]
|
|
|
|
# insert
|
|
listMix = [[i, i + 2] for i in range(ct.default_nb)] # only int
|
|
if enable_dynamic_field:
|
|
data = cf.gen_default_rows_data()
|
|
for i in range(ct.default_nb):
|
|
data[i][ct.default_json_field_name] = {"list": listMix[i]}
|
|
data[i]["list"] = listMix[i]
|
|
else:
|
|
data = cf.gen_default_dataframe_data()
|
|
data[ct.default_json_field_name] = [{"list": listMix[i]} for i in range(ct.default_nb)]
|
|
collection_w.insert(data)
|
|
collection_w.flush()
|
|
collection_w.load()
|
|
|
|
# delete with expressions
|
|
delete_ids = random.randint(2, ct.default_nb - 2)
|
|
expression = f"{expr_prefix}({field_name}, {delete_ids})"
|
|
res = collection_w.delete(expression)[0]
|
|
exp_ids = cf.assert_json_contains(expression, listMix)
|
|
assert res.delete_count == len(exp_ids)
|
|
|
|
# query to check
|
|
collection_w.query(expression, check_task=CheckTasks.check_query_empty)
|
|
|
|
@pytest.mark.tags(CaseLabel.L2)
|
|
@pytest.mark.parametrize(
|
|
"expr_prefix", ["json_contains_all", "JSON_CONTAINS_ALL", "json_contains_any", "JSON_CONTAINS_ANY"]
|
|
)
|
|
@pytest.mark.parametrize("field_name", ["json_field['list']", "list"])
|
|
@pytest.mark.parametrize("enable_dynamic_field", [True, False])
|
|
def test_delete_expr_json_contains_all(self, expr_prefix, field_name, enable_dynamic_field):
|
|
"""
|
|
target: test delete expr using json_contains
|
|
method: delete using expression using json_contains
|
|
expected: delete successfully
|
|
"""
|
|
if field_name == "list" and enable_dynamic_field is False:
|
|
pytest.skip("only support when enable_dynamic_filed == True")
|
|
# init collection with nb default data
|
|
collection_w = self.init_collection_general(prefix, False, enable_dynamic_field=enable_dynamic_field)[0]
|
|
|
|
# insert
|
|
listMix = [
|
|
[i, i * 0.00001, bool(i % 2), [i, str(i)]] for i in range(ct.default_nb)
|
|
] # mix int, float, list, bool
|
|
if enable_dynamic_field:
|
|
data = cf.gen_default_rows_data()
|
|
for i in range(ct.default_nb):
|
|
data[i][ct.default_json_field_name] = {"list": listMix[i]}
|
|
data[i]["list"] = listMix[i]
|
|
else:
|
|
data = cf.gen_default_dataframe_data()
|
|
data[ct.default_json_field_name] = [{"list": listMix[i]} for i in range(ct.default_nb)]
|
|
collection_w.insert(data)
|
|
collection_w.flush()
|
|
collection_w.load()
|
|
|
|
# delete with expressions
|
|
ids = random.randint(0, ct.default_nb)
|
|
delete_ids = [bool(ids % 2), ids]
|
|
expression = f"{expr_prefix}({field_name}, {delete_ids})"
|
|
res = collection_w.delete(expression)[0]
|
|
exp_ids = cf.assert_json_contains(expression, listMix)
|
|
assert res.delete_count == len(exp_ids)
|
|
|
|
# query to check
|
|
collection_w.query(expression, check_task=CheckTasks.check_query_empty)
|
|
|
|
@pytest.mark.tags(CaseLabel.L2)
|
|
@pytest.mark.parametrize(
|
|
"expressions",
|
|
cf.gen_field_compare_expressions(["int64_1", "json_field['int'][0]"], ["int64_2", "json_field['int'][1]"]),
|
|
)
|
|
def test_delete_expr_compare_two_variables(self, expressions):
|
|
"""
|
|
target: test delete expr using 2 variables
|
|
method: delete with expressions using compare 2 variables
|
|
expected: delete successfully
|
|
"""
|
|
# init collection with nb default data
|
|
nb = 1000
|
|
dim = 32
|
|
fields = [
|
|
cf.gen_int64_field("int64_1"),
|
|
cf.gen_int64_field("int64_2"),
|
|
cf.gen_json_field("json_field"),
|
|
cf.gen_float_vec_field("float_vector", dim=dim),
|
|
]
|
|
schema = cf.gen_collection_schema(fields=fields, primary_field="int64_1")
|
|
collection_w = self.init_collection_wrap(schema=schema)
|
|
|
|
# insert
|
|
int64_1_values = [i for i in range(nb)]
|
|
int64_2_values = [random.randint(0, nb) for _ in range(nb)]
|
|
vectors = cf.gen_vectors(nb, dim)
|
|
json_values = [[i, int64_2_values[i]] for i in range(nb)]
|
|
data = pd.DataFrame(
|
|
{
|
|
"int64_1": int64_1_values,
|
|
"int64_2": int64_2_values,
|
|
"json_field": [{"int": json_values[i]} for i in range(nb)],
|
|
"float_vector": vectors,
|
|
}
|
|
)
|
|
collection_w.insert(data)
|
|
collection_w.flush()
|
|
collection_w.create_index("float_vector")
|
|
collection_w.load()
|
|
|
|
# delete with expressions
|
|
error = {ct.err_code: 1100, ct.err_msg: f"failed to create delete plan: cannot parse expression: {expressions}"}
|
|
collection_w.delete(expressions, check_task=CheckTasks.err_res, check_items=error)
|
|
|
|
@pytest.mark.tags(CaseLabel.L2)
|
|
@pytest.mark.parametrize("expressions", cf.gen_json_field_expressions_and_templates())
|
|
@pytest.mark.parametrize("enable_dynamic_field", [True, False])
|
|
def test_delete_expr_json_field(self, expressions, enable_dynamic_field):
|
|
"""
|
|
target: test delete entities using normal expression
|
|
method: delete using normal expression
|
|
expected: delete successfully
|
|
"""
|
|
# init collection with nb default data
|
|
collection_w, _vectors, _, insert_ids = self.init_collection_general(
|
|
prefix, True, enable_dynamic_field=enable_dynamic_field
|
|
)[0:4]
|
|
|
|
# filter result with expression in collection
|
|
_vectors = _vectors[0]
|
|
expr = expressions[0].replace("&&", "and").replace("||", "or")
|
|
filter_ids = []
|
|
json_field = {}
|
|
for i, _id in enumerate(insert_ids):
|
|
if enable_dynamic_field:
|
|
json_field["number"] = _vectors[i][ct.default_json_field_name]["number"]
|
|
json_field["float"] = _vectors[i][ct.default_json_field_name]["float"]
|
|
else:
|
|
json_field["number"] = _vectors[ct.default_json_field_name][i]["number"]
|
|
json_field["float"] = _vectors[ct.default_json_field_name][i]["float"]
|
|
if not expr or eval(expr):
|
|
filter_ids.append(_id)
|
|
|
|
# delete with expressions
|
|
res = collection_w.delete(expr)[0]
|
|
assert res.delete_count == len(filter_ids)
|
|
|
|
# query to check
|
|
collection_w.query(f"int64 in {filter_ids}", check_task=CheckTasks.check_query_empty)
|
|
|
|
@pytest.mark.tags(CaseLabel.L2)
|
|
@pytest.mark.parametrize("expressions", cf.gen_json_field_expressions_and_templates())
|
|
@pytest.mark.parametrize("enable_dynamic_field", [True, False])
|
|
def test_delete_expr_templtes_json_field(self, expressions, enable_dynamic_field):
|
|
"""
|
|
target: test delete entities using normal expression
|
|
method: delete using normal expression
|
|
expected: delete successfully
|
|
"""
|
|
# init collection with nb default data
|
|
collection_w, _vectors, _, insert_ids = self.init_collection_general(
|
|
prefix, True, enable_dynamic_field=enable_dynamic_field
|
|
)[0:4]
|
|
|
|
# filter result with expression in collection
|
|
_vectors = _vectors[0]
|
|
expr = expressions[0].replace("&&", "and").replace("||", "or")
|
|
filter_ids = []
|
|
json_field = {}
|
|
for i, _id in enumerate(insert_ids):
|
|
if enable_dynamic_field:
|
|
json_field["number"] = _vectors[i][ct.default_json_field_name]["number"]
|
|
json_field["float"] = _vectors[i][ct.default_json_field_name]["float"]
|
|
else:
|
|
json_field["number"] = _vectors[ct.default_json_field_name][i]["number"]
|
|
json_field["float"] = _vectors[ct.default_json_field_name][i]["float"]
|
|
if not expr or eval(expr):
|
|
filter_ids.append(_id)
|
|
|
|
# delete with expressions template
|
|
expr = cf.get_expr_from_template(expressions[1]).replace("&&", "and").replace("||", "or")
|
|
expr_params = cf.get_expr_params_from_template(expressions[1])
|
|
res = collection_w.delete(expr=expr, expr_params=expr_params)[0]
|
|
assert res.delete_count == len(filter_ids)
|
|
|
|
# query to check
|
|
collection_w.query(f"int64 in {filter_ids}", check_task=CheckTasks.check_query_empty)
|
|
|
|
@pytest.mark.tags(CaseLabel.L2)
|
|
@pytest.mark.parametrize(
|
|
"normal_expressions, json_expressions",
|
|
zip(cf.gen_normal_expressions_and_templates()[1:4], cf.gen_json_field_expressions_and_templates()[6:9]),
|
|
)
|
|
@pytest.mark.parametrize("enable_dynamic_field", [True, False])
|
|
def test_delete_expr_complex_mixed(self, normal_expressions, json_expressions, enable_dynamic_field):
|
|
"""
|
|
target: test delete entities using normal expression
|
|
method: delete using normal expression
|
|
expected: delete successfully
|
|
"""
|
|
# init collection with nb default data
|
|
collection_w, _vectors, _, insert_ids = self.init_collection_general(
|
|
prefix, True, enable_dynamic_field=enable_dynamic_field
|
|
)[0:4]
|
|
|
|
# filter result with expression in collection
|
|
expr = normal_expressions[0] + " and " + json_expressions[0]
|
|
_vectors = _vectors[0]
|
|
expr = expr.replace("&&", "and").replace("||", "or")
|
|
filter_ids = []
|
|
json_field = {}
|
|
for i, _id in enumerate(insert_ids):
|
|
if enable_dynamic_field:
|
|
json_field["number"] = _vectors[i][ct.default_json_field_name]["number"]
|
|
json_field["float"] = _vectors[i][ct.default_json_field_name]["float"]
|
|
int64 = _vectors[i][ct.default_int64_field_name]
|
|
float = _vectors[i][ct.default_float_field_name]
|
|
else:
|
|
json_field["number"] = _vectors[ct.default_json_field_name][i]["number"]
|
|
json_field["float"] = _vectors[ct.default_json_field_name][i]["float"]
|
|
int64 = _vectors.int64[i]
|
|
float = _vectors.float[i]
|
|
if not expr or eval(expr):
|
|
filter_ids.append(_id)
|
|
|
|
# delete with expressions and template mixed
|
|
json_expr = cf.get_expr_from_template(json_expressions[1]).replace("&&", "and").replace("||", "or")
|
|
expr = normal_expressions[0] + " and " + json_expr
|
|
json_expr_params = cf.get_expr_params_from_template(json_expressions[1])
|
|
res = collection_w.delete(expr=expr, expr_params=json_expr_params)[0]
|
|
assert res.delete_count == len(filter_ids)
|
|
|
|
# query to check
|
|
collection_w.query(f"int64 in {filter_ids}", check_task=CheckTasks.check_query_empty)
|
|
|
|
@pytest.mark.tags(CaseLabel.L1)
|
|
@pytest.mark.parametrize(
|
|
"expression", cf.gen_normal_string_expressions(["varchar", "json_field['string']", "NewStr"])
|
|
)
|
|
@pytest.mark.parametrize("enable_dynamic_field", [True, False])
|
|
def test_delete_string_expressions_normal(self, expression, enable_dynamic_field):
|
|
"""
|
|
target: test delete expr like
|
|
method: delete using expression like
|
|
expected: delete successfully
|
|
"""
|
|
if "NewStr" in expression and enable_dynamic_field is False:
|
|
pytest.skip("only support when enable_dynamic_filed == True")
|
|
# init collection with nb default data
|
|
nb = 1000
|
|
collection_w, _vectors, _, insert_ids = self.init_collection_general(
|
|
prefix, False, enable_dynamic_field=enable_dynamic_field
|
|
)[0:4]
|
|
|
|
# insert
|
|
if enable_dynamic_field:
|
|
data = cf.gen_default_rows_data(nb)
|
|
for i in range(nb):
|
|
data[i][ct.default_json_field_name] = {"string": str(i)}
|
|
data[i]["NewStr"] = str(i)
|
|
else:
|
|
data = cf.gen_default_dataframe_data(nb)
|
|
data[ct.default_json_field_name] = [{"string": str(i)} for i in range(nb)]
|
|
collection_w.insert(data)
|
|
collection_w.flush()
|
|
collection_w.load()
|
|
|
|
# calculate the result
|
|
_vectors = data
|
|
expression = expression.replace("&&", "and").replace("||", "or")
|
|
filter_ids = []
|
|
json_field = {}
|
|
for i in range(nb):
|
|
if enable_dynamic_field:
|
|
json_field["string"] = _vectors[i][ct.default_json_field_name]["string"]
|
|
varchar = _vectors[i][ct.default_string_field_name]
|
|
NewStr = _vectors[i]["NewStr"]
|
|
else:
|
|
json_field["string"] = _vectors[ct.default_json_field_name][i]["string"]
|
|
varchar = _vectors.varchar[i]
|
|
if not expression or eval(expression):
|
|
filter_ids.append(i)
|
|
|
|
# delete with expressions
|
|
res = collection_w.delete(expression)[0]
|
|
assert res.delete_count == len(filter_ids)
|
|
|
|
# query to check
|
|
collection_w.load()
|
|
collection_w.query(
|
|
"int64 >= 0",
|
|
output_fields=["count(*)"],
|
|
check_task=CheckTasks.check_query_results,
|
|
check_items={"count(*)": nb - len(filter_ids)},
|
|
)
|
|
|
|
|
|
class TestCollectionSearchNoneAndDefaultData(TestcaseBase):
|
|
"""
|
|
Test case of delete interface with None data
|
|
"""
|
|
|
|
@pytest.fixture(scope="function", params=[0, 0.5, 1])
|
|
def null_data_percent(self, request):
|
|
yield request.param
|
|
|
|
@pytest.mark.tags(CaseLabel.L1)
|
|
def test_delete_search_with_none_data(self, null_data_percent):
|
|
"""
|
|
target: test delete and search when there is None data
|
|
method: search entities after it was deleted
|
|
expected: deleted entity is not in the search result
|
|
"""
|
|
# init collection with nb default data
|
|
collection_w, _, _, ids = self.init_collection_general(
|
|
prefix,
|
|
insert_data=True,
|
|
nullable_fields={ct.default_float_field_name: null_data_percent},
|
|
default_value_fields={ct.default_string_field_name: "data"},
|
|
)[0:4]
|
|
entity, _ = collection_w.query(tmp_expr, output_fields=["*"])
|
|
search_res, _ = collection_w.search(
|
|
[entity[0][ct.default_float_vec_field_name]],
|
|
ct.default_float_vec_field_name,
|
|
ct.default_search_params,
|
|
ct.default_limit,
|
|
)
|
|
# assert search results contains entity
|
|
assert 0 in search_res[0].ids
|
|
|
|
expr = f"{ct.default_int64_field_name} in {ids[: ct.default_nb // 2]}"
|
|
collection_w.delete(expr)
|
|
search_res_2, _ = collection_w.search(
|
|
[entity[0][ct.default_float_vec_field_name]],
|
|
ct.default_float_vec_field_name,
|
|
ct.default_search_params,
|
|
ct.default_limit,
|
|
)
|
|
# assert search result is not equal to entity
|
|
inter = set(ids[: ct.default_nb // 2]).intersection(set(search_res_2[0].ids))
|
|
# Using bounded staleness, we could still search the "deleted" entities,
|
|
# since the search requests arrived query nodes earlier than query nodes consume the delete requests.
|
|
assert len(inter) == 0
|
|
|
|
@pytest.mark.tags(CaseLabel.L2)
|
|
def test_delete_entities_repeatedly_with_string_none_data(self, null_data_percent):
|
|
"""
|
|
target: test delete entities twice with string expr
|
|
method: delete with same expr twice
|
|
expected: No exception for second deletion
|
|
"""
|
|
# init collection with nb default data
|
|
collection_w = self.init_collection_general(
|
|
prefix,
|
|
nb=tmp_nb,
|
|
insert_data=True,
|
|
primary_field=ct.default_string_field_name,
|
|
nullable_fields={ct.default_float_field_name: null_data_percent},
|
|
default_value_fields={ct.default_int64_field_name: 100},
|
|
)[0]
|
|
|
|
# assert delete successfully and no exception
|
|
collection_w.delete(expr=default_string_expr)
|
|
collection_w.num_entities
|
|
collection_w.query(default_string_expr, check_task=CheckTasks.check_query_empty)
|
|
collection_w.delete(expr=default_string_expr)
|
|
|
|
@pytest.mark.tags(CaseLabel.L1)
|
|
@pytest.mark.skip(reason="waiting for the expr code part to be merged")
|
|
def test_delete_entities_repeatedly_with_expr_on_none_fields(self, null_data_percent):
|
|
"""
|
|
target: test delete entities twice with string expr
|
|
method: delete with same expr twice
|
|
expected: No exception for second deletion
|
|
"""
|
|
# init collection with nb default data
|
|
collection_w = self.init_collection_general(
|
|
prefix,
|
|
nb=tmp_nb,
|
|
insert_data=True,
|
|
primary_field=ct.default_string_field_name,
|
|
nullable_fields={ct.default_float_field_name: null_data_percent},
|
|
default_value_fields={ct.default_int64_field_name: 100},
|
|
)[0]
|
|
|
|
# assert delete successfully and no exception
|
|
collection_w.delete(expr=default_string_expr)
|
|
collection_w.num_entities
|
|
collection_w.query(default_string_expr, check_task=CheckTasks.check_query_empty)
|
|
collection_w.delete(expr=default_string_expr)
|