Skip to content

[BUG] SqliteVecIndex.delete() builds one OR chain and fails above 995 deletions #1850

Open

Description

@SWSAmor

Bug Description

SqliteVecIndex.delete() (repository/sqlite_vec_index.py:255-280) puts every chunk it deletes into a
single UPDATE … WHERE project_id = :project_id AND (<p0> OR <p1> OR …) RETURNING id statement, one
4-term predicate per chunk and no batching. SQLite's expression-depth limit
(SQLITE_MAX_EXPR_DEPTH, 1000) caps the chain, so a call with more than 995 deletions fails with
Expression tree is too large (maximum depth 1000). It is called once per entity
(repository/search_repository_base.py:862, 874), so one entity with enough stale chunks aborts
the whole operation.

We hit it during a full bm reindex --embeddings (on 0.23.2, where the same method lived at
sqlite_vec_index.py:226): 1 537 pending chunk rows of an already-deleted entity, and the reindex of
that project aborted within 2 s. We removed the rows by hand after a backup, and the re-run passed. The
statement shape is unchanged in 0.24.1.

Steps To Reproduce

The statement shape alone is enough. With the SQLite BM 0.24.1 runs on here (3.53.1, from its own
interpreter):

import sqlite3
db = sqlite3.connect(":memory:")
db.execute("CREATE TABLE c (id INTEGER PRIMARY KEY, project_id, entity_id, chunk_key, source_hash, embedding_status)")
def run(n):
    preds = " OR ".join(f"(entity_id = :e{i} AND chunk_key = :k{i} AND source_hash = :h{i} "
                        "AND embedding_status = 'pending')" for i in range(n))
    params = {"p": 1} | {f"e{i}": 1 for i in range(n)} | {f"k{i}": "k" for i in range(n)} \
             | {f"h{i}": "h" for i in range(n)}
    db.execute(f"UPDATE c SET source_hash = source_hash WHERE project_id = :p AND ({preds}) RETURNING id",
               params).fetchall()
run(995)   # ok
run(996)   # sqlite3.OperationalError: Expression tree is too large (maximum depth 1000)

In BM: any entity with ≥ 996 chunk rows to delete in one delete() call. In our case these were the
leftover pending rows of a deleted entity.

Expected Behavior

Deleting any number of chunks succeeds.

Actual Behavior

OperationalError: Expression tree is too large (maximum depth 1000), and the reindex or sync of that
project aborts.

Environment

  • OS: macOS 27.0.1
  • Python version: 3.13.17
  • Basic Memory version: 0.24.1 (first seen on 0.23.2)
  • Installation method: Homebrew
  • Claude Desktop version (if applicable): n/a

Additional Context

Separate from #1581, which was the correlated delete_orphans scan (fixed in 0.24). The leftover
pending rows of a deleted entity that triggered it for us may be a bug of their own: the entity was
gone, but its chunk manifest was not.

Possible Solution

Batch the deletions, e.g. 500 per statement, or stage the keys in a temp table and JOIN instead of
an OR chain. The follow-up rowid IN (…) list is a flat list, not a tree, so it is not affected by
the depth limit, but batching it too keeps it under SQLITE_MAX_VARIABLE_NUMBER.

Activity

  1. MbappeWU commented on Oct 24, 2026

    @MbappeWU
    Contributor

    I'll take this: batch SqliteVecIndex.delete() so OR chains stay under SQLite's expression-depth limit, with a regression test above 995 deletions.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions