Skip to content

Filter.by_ref(...) reuse crashes: shared mutable target chain causes AssertionError in _target_path() #2143

Description

@VANDRANKI

Describe the bug

Reusing a Filter.by_ref(...) (or .by_ref_multi_target(...)) builder to construct two different property filters on the same referenced collection raises an uncaught AssertionError the second time a terminal filter method (.equal(), .greater_than(), .contains_any(), etc.) is called on it.

Root cause

_FilterByRef.by_property() (and by_id(), by_creation_time(), by_update_time(), by_ref_count()) all pass the same, shared self.__target object into the new _FilterByProperty/_FilterById/etc. instance instead of a copy:

https://github.com/weaviate/weaviate-python-client/blob/main/weaviate/collections/classes/filters.py#L599-L601

def by_property(self, name: str, length: bool = False) -> _FilterByProperty:
    """Define a filter based on a property to be used when querying and deleting from a collection."""
    return _FilterByProperty(prop=name, length=length, target=self.__target)

When a terminal method (e.g. .equal()) is later called, _FilterBase._target_path() mutates that shared target object in place:

https://github.com/weaviate/weaviate-python-client/blob/main/weaviate/collections/classes/filters.py#L157-L170

def _target_path(self) -> _FilterTargets:
    if self._target is None:
        return self._property

    # get last element in chain
    target = self._target
    while target.target is not None:
        assert isinstance(target.target, _MultiTargetRef) or isinstance(
            target.target, _SingleTargetRef
        )
        target = target.target

    target.target = self._property   # <-- mutates the shared _SingleTargetRef/_MultiTargetRef
    return self._target

The first call sets target.target on the shared _SingleTargetRef/_MultiTargetRef instance to a plain string (the property name, e.g. "name"). If the same _FilterByRef object is then used to build a second property filter (e.g. .by_property("rating")), _target_path() walks the chain again, finds target.target is no longer None (it's now the string left over from the first filter), and the assert isinstance(target.target, _MultiTargetRef) or isinstance(target.target, _SingleTargetRef) fails because a plain string is neither, raising AssertionError.

Reproduction

import weaviate
from weaviate.classes.query import Filter

print("weaviate-client version:", weaviate.__version__)

# Build a reference filter builder once, then try to compose two different
# property conditions against the same reference (e.g. to AND them together
# later) -- a natural pattern given the library supports chaining
# Filter.by_ref(...).by_property(...) directly.
ref = Filter.by_ref("hasCategory")

print("Building first filter (name == 'Electronics') ...")
f1 = ref.by_property("name").equal("Electronics")
print("  OK:", f1.target, f1.operator, f1.value)

print("Building second filter (rating > 4) on the SAME ref object ...")
f2 = ref.by_property("rating").greater_than(4)
print("  OK:", f2.target, f2.operator, f2.value)

Output

weaviate-client version: 4.23.0
Building first filter (name == 'Electronics') ...
  OK: link_on='hasCategory' target='name' _Operator.EQUAL Electronics
Building second filter (rating > 4) on the SAME ref object ...
Traceback (most recent call last):
  File "repro_filter_bug.py", line 31, in <module>
    f2 = ref.by_property("rating").greater_than(4)
  File ".../weaviate/collections/classes/filters.py", line 274, in greater_than
    target=self._target_path(),
           ~~~~~~~~~~~~~~~~~^^
  File ".../weaviate/collections/classes/filters.py", line 164, in _target_path
    assert isinstance(target.target, _MultiTargetRef) or isinstance(
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
        target.target, _SingleTargetRef
        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    )
    ^
AssertionError

As a sanity check, calling Filter.by_ref("hasCategory") twice (a fresh builder object for each property filter) works fine and produces the expected combined filter -- so the bug is specifically about reusing one _FilterByRef instance, not about referencing the same link_on string:

f1 = Filter.by_ref("hasCategory").by_property("name").equal("Electronics")
f2 = Filter.by_ref("hasCategory").by_property("rating").greater_than(4)
combined = f1 & f2   # works fine

Expected behavior

Filter.by_ref(...) should be safely reusable to build multiple, independent property/id/count/time filters against the same reference target (this is exactly the ergonomic use case the chaining API seems designed to support), without raising an AssertionError and without silently corrupting the target chain of previously-built filter objects.

Actual behavior

The first terminal filter built from a _FilterByRef instance permanently mutates the shared target-ref object. Any subsequent filter built from the same _FilterByRef instance raises AssertionError inside _target_path().

Environment

  • weaviate-client version: 4.23.0 (installed fresh from PyPI)
  • Python 3.13.3
  • Verified against the current main branch of this repo: fetched weaviate/collections/classes/filters.py fresh via raw.githubusercontent.com/weaviate/weaviate-python-client/main/... and confirmed it is byte-identical (module content, aside from line endings) to both the locally cloned main and the installed 4.23.0 package where the crash was reproduced above. The bug is present on current main.

Suggested fix

_FilterByRef.by_property() / by_id() / by_creation_time() / by_update_time() / by_ref_count() should pass a deep copy of self.__target (or otherwise avoid sharing mutable state), so that each derived filter builder gets its own independent target chain instead of mutating a shared one.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions