Skip to content

fix: validate is_in/not_in receive a list in the filter builder - #683

Open
NishchayMahor wants to merge 1 commit into
pinecone-io:mainfrom
NishchayMahor:fix/filter-builder-require-list
Open

NishchayMahor wants to merge 1 commit into
pinecone-io:mainfrom
NishchayMahor:fix/filter-builder-require-list

Conversation

@NishchayMahor

@NishchayMahor NishchayMahor commented Jul 5, 2026

Copy link
Copy Markdown

What

In the metadata filter builder, Field.is_in() / Field.not_in() accept any value without checking it's a list. A common mistake — passing a bare value instead of a list — is silently turned into an invalid filter:

Field("genre").is_in("drama").to_dict()
# -> {"genre": {"$in": "drama"}}   ❌  (a bare string, not a list)

Because a string is iterable, nothing catches this client-side; the server rejects it later with an opaque 400 Invalid request, which is hard to trace back to the filter.

This is inconsistent with the numeric operators in the same class — gt / gte / lt / lte already validate their argument via _require_numeric and raise a clear TypeError.

Fix

Add a _require_list guard mirroring the existing _require_numeric, and call it from is_in / not_in (pinecone/utils/filter_builder.py). It accepts a list or tuple and raises a clear TypeError otherwise:

Field("genre").is_in("drama")
# TypeError: is_in requires a list of values, got str

Valid list/tuple usage is unchanged.

Testing

Added tests to TestFieldSetOperators in tests/unit/utils/test_filter_builder.py, mirroring the existing test_numeric_only_* cases (tuple accepted; is_in/not_in with a non-list raise TypeError matching "list"). Verified the behavior directly against the module — filter_builder.py is pure Python (list/tuple pass, str/int raise).


Note

Low Risk
Pure client-side validation in the optional filter builder; no API or server behavior changes beyond catching mistakes earlier.

Overview
Field.is_in() and Field.not_in() now validate their argument at build time via a new _require_list helper (same idea as _require_numeric on comparison operators). Passing a bare scalar (e.g. a string) no longer silently produces an invalid $in / $nin filter that fails later with a vague API error—it raises a TypeError with a clear message. list and tuple inputs still work as before.

Unit tests cover tuple acceptance and TypeError for non-list values on both operators.

Reviewed by Cursor Bugbot for commit 9bc017a. Bugbot is set up for automated code reviews on this repo. Configure here.

Field.is_in() and Field.not_in() accepted any value, so Field("genre").is_in("drama")
silently produced {"genre": {"$in": "drama"}} — an invalid filter the server
rejects with an opaque 400. This is inconsistent with gt/gte/lt/lte, which already
validate via _require_numeric. Add a matching _require_list guard (accepts list or
tuple) and call it from is_in/not_in, with unit tests mirroring the numeric ones.
@NishchayMahor
NishchayMahor force-pushed the fix/filter-builder-require-list branch from 47a999d to 9bc017a Compare September 22, 2026 07:30
@NishchayMahor

Copy link
Copy Markdown
Author

Rebased onto current main — the branch was 329 commits behind and had gone CONFLICTING.

The conflict was real content overlap rather than drift: 7871024e and ab75c775 rewrote the is_in / not_in docstrings into the house style with examples, while this branch had added the validation calls to the same two methods. I kept both — main's docstrings verbatim, plus the _require_list guard. The 21 doctests in the file pass, so the examples still hold.

behind_by=0 and MERGEABLE now. The change itself is unchanged: is_in / not_in raise TypeError when handed a bare string or number instead of a list, rather than silently building {"$in": "drama"}, which the API rejects later with a far less obvious error.

Verification on the rebased branch: tests/unit/utils/test_filter_builder.py is 22 passed, and with filter_builder.py reverted to main the two new tests fail — so they are still pinning the behaviour and not passing by accident. ruff check and ruff format --check are clean.

Force-pushed to rebase (old head 47a999d4, new 9bc017aa).

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant