Skip to content

Release 2.6.1: catalog gap-fill + memory primitives - #17

Merged
CoderDayton merged 10 commits into
mainfrom
release/2.6.1
May 10, 2026
Merged

Release 2.6.1: catalog gap-fill + memory primitives#17
CoderDayton merged 10 commits into
mainfrom
release/2.6.1

Conversation

@CoderDayton

Copy link
Copy Markdown
Owner

Summary

  • Closes ten long-standing catalog gaps with a coherent set of additive primitives: pending vector buffer, atomic counter increments, weighted directed edges, append-only event feed, TTL with delete/callback sweep, threshold-driven rebuild scheduler, SAVEPOINT-backed transactions, Mongo-style range/set filters across search and edges/events, multi-process write safety (busy_timeout, FK cascade), and async wrappers for all of the above.
  • No public API breaks; existing 2.6.0 databases upgrade transparently — new tables are created on first open.
  • Drops the unused sqlite-vec dependency and the v1.x auto-migration path that was already non-functional.
  • Hardens 2.6.1 boundaries against NaN/inf and silent edge cases; collapses async run_in_executor boilerplate behind a _run helper.
  • Lefthook now enforces ruff format / ruff / mypy / bandit / pytest at every commit and the full pytest-cov suite at every push.
  • Docs reorganized: comprehensive feature list moved to docs/Features.md; README slimmed to highlights + quickstart; docs/examples.md rewritten with a v2.6.1 Memory primitives section. New scripts/exercise_async_collection.py smoke-runner walks the entire async surface.

Test plan

  • uv run ruff format --check — 85 files already formatted
  • uv run ruff check — all checks passed
  • uv run mypy src — no issues in 21 source files
  • uv run bandit -r src -ll -c .bandit — 0 medium/high findings
  • uv run pytest tests/ -q — 767 passed, 2 skipped
  • uv run pytest tests/ --cov=src/simplevecdb — 90% coverage (pre-push hook)
  • uv run python scripts/exercise_async_collection.py — 52/52 async surface calls pass

Vector apps built on 2.6.0 kept reimplementing the same primitives outside
the library — pending-write buffers, edge tables, JSON counter increments,
TTL sweepers, change feeds, range filters. 2.6.1 brings them into the
catalog as a coherent, additive set with no public API breaks; existing
databases upgrade transparently on first open.

What you can now do without leaving the library:

- Update a vector in place: collection.update_embedding(id, vec) buffers
  the change in a transactional overlay and flushes to HNSW on demand,
  replacing the remove+re-add churn pattern.
- Wrap several mutations atomically: with db.transaction(): ... opens a
  SAVEPOINT around catalog writes; usearch effects are buffered and
  applied only on commit, with collection.tx() as the single-collection
  shorthand.
- Walk a graph alongside the index: collection.edges supports add/get/
  update/delete with weight, bonus, hits, last_touch as real columns;
  numeric deltas (dweight=+0.02, dhits=+1) compile to a single atomic
  UPDATE and stack safely under contention.
- Increment counters atomically: collection.increment_metadata(id,
  {"hits": 1, "drift": 0.02}) chains json_set + json_extract in one
  statement; safe under WAL with concurrent writers.
- Filter by range: similarity_search, keyword_search, hybrid_search,
  edges.get_edges, and events.read all accept Mongo-style operator
  dicts ($eq $ne $gt $gte $lt $lte $in $nin $exists $between) plus
  tuple shorthand ("range", lo, hi).
- Subscribe to changes: collection.events.read(since=, kind=) and
  .subscribe(...) expose an append-only feed populated automatically by
  every mutating method; cross-process visibility comes from WAL.
- Expire docs by clock: collection.ttl.set(id, seconds=, on_expire=)
  with an opt-in background sweeper.
- Defer rebuilds: collection.maintenance.rebuild_if_needed(...) gates a
  full rebuild_index() behind pending / tombstone / wall-time thresholds.
- Run multi-process: PRAGMA busy_timeout=5000 and foreign_keys=ON at
  every connection-open site reduce DatabaseLockedError pressure and
  cascade-delete aux rows on doc deletion.
The sqlite-vec package was never imported and the v1->v2 migration code
could not have worked without loading the extension anyway. Removes the
dependency, MigrationRequiredError, VectorDB.check_migration, the
auto_migrate flag, the catalog legacy helpers, and their tests.
- Drop sqlite-vec dep + v1 migration path from changelog (mirrored to
  docs/CHANGELOG.md).
- Correct the db.transaction() atomicity claim: SQL writes roll back
  via SAVEPOINT, but coarse vector mutations (add_texts/delete) do not;
  point users at update_embedding + pending.flush() for commit-gated
  vector changes.
- Note that the events table is intentionally FK-less so the audit
  trail survives doc deletions.
- _DBTransaction.__exit__ now logs at ERROR and re-raises when the
  outermost conn.commit() fails (was silently swallowed at DEBUG).
- Cap filter $in/$nin lists at 999 items to stay below the universally
  safe SQLITE_MAX_VARIABLE_NUMBER.
- Document the _table_name validation invariant in CatalogManager.
- Drop the dead self-import in _CollectionTransaction.

Adds two regression tests: filter-list cap and tx commit-failure
propagation.
Each AsyncVectorCollection / AsyncVectorDB method opened with the same
three-line pattern: get the running loop, dispatch to self._executor,
wrap the sync call in a lambda. Replace 41 of those with a private
_run(fn, *args, **kwargs) helper using functools.partial. Behaviour
preserved (still uses self._executor, not asyncio.to_thread's global
default pool).
Verified gaps on the new public APIs:

- $between now rejects lo>hi (silent empty result -> ValueError).
- update_embedding rejects non-finite vector elements before they reach
  the pending buffer and corrupt distance math on flush.
- ttl.set validates seconds/expires_at finiteness and constrains
  on_expire to {"delete","callback"}.
- ttl.start_background validates interval>0 and finite (was a busy loop
  on 0/negative/NaN).
- ttl.stop_background no longer drops the thread handle when join times
  out, so the next start_background can't spawn a duplicate sweeper.
- ttl.sweep escalates the index-remove failure log from DEBUG to
  WARNING -- catalog/HNSW divergence is a correctness issue users want
  to see, not a debug-only event.
- Edge add/upsert/update_edge reject NaN/inf for weight/bonus and the
  numeric deltas; otherwise the column silently traps NaN and every
  later range filter returns wrong results.
lefthook now runs the same gate set on pre-commit and pre-push;
pre-commit autofixes (ruff format, ruff check --fix), pre-push runs
the formatter in --check mode so a push can't regress what was clean
at commit time.

Tree changes required to make the new gates pass:

- Apply `ruff format` across the repo (45 files, whitespace and
  trailing-comma normalization only).
- Rename the local `expires_at` rebinding inside `_TTLNamespace.set`
  to `resolved`; mypy could not narrow the original `float | None`
  parameter through the chained guards introduced by the previous
  fortification commit.
Adds 33 tests (26 -> 59 in this file). Splits roughly into two halves:

Coverage gap fillers for APIs that shipped with 2.6.1 but had no
direct tests:
- counters.get default and missing-row paths
- $exists operator on present/absent metadata keys
- events.last_seq, events.prune semantics, events.subscribe yield

Boundary guards introduced by the fortification commit:
- update_embedding rejects NaN, inf, and non-1-D vectors
- $between rejects lo>hi, non-finite bounds, wrong arity, plus
  the inclusive-range happy path and the ("range", lo, hi) shorthand
- edges add/upsert/update_edge reject NaN/inf for weight, bonus,
  dweight, dbonus
- ttl.set rejects neither/both of seconds/expires_at, invalid
  on_expire, and non-finite seconds or expires_at
- ttl.start_background rejects zero, negative, and NaN intervals
- ttl.stop_background lets a clean stop be followed by a fresh start
Walks every AsyncVectorCollection / AsyncVectorDB wrapper in one run —
CRUD, search variants, hierarchy, edges, counters, pending vectors,
TTL, events, maintenance, clustering, multi-collection helpers, and
async context lifecycle. Reports per-call pass/fail so a single broken
API doesn't mask the rest. Intended as a manual smoke runner for the
2.6.1 'advanced memory' surface, not part of the pytest suite.

Run: uv run python scripts/exercise_async_collection.py
@CoderDayton CoderDayton self-assigned this May 10, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: af03c2b683

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/simplevecdb/engine/catalog.py Outdated
Comment thread src/simplevecdb/engine/catalog.py Outdated
@CoderDayton

Copy link
Copy Markdown
Owner Author

Both P1 review comments addressed in e2b50c1.

Numeric filter type guard (catalog.py:_build_operator_clauses)
$gt, $gte, $lt, $lte, $between now compile to a clause gated on json_type(metadata, ?) IN ('integer','real') before applying CAST(... AS REAL). Without the guard, a row with {"score": "oops"} would coerce to 0.0 and spuriously match {"$lt": 1} on SQL paths (get_documents, FTS, edges metadata) — disagreeing with the Python-side _matches_filter, which already rejected non-numeric values.

Atomic TTL sweep (catalog.sweep_ttl)
Replaced the SELECT-then-DELETE pair with a single DELETE … RETURNING doc_id, on_expire inside one write transaction. A concurrent set_ttl extending the deadline (or clear_ttl) between the previous two-step read+write window would still see docs deleted off the stale read; the atomic claim eliminates that race.

Regression coverage (tests/unit/test_v26_1_features.py)

  • TestNumericFilterTypeGuard — verifies $lt, $gt, $between skip string/missing values, and that SQL pre-filter (get_documents) and Python post-filter (similarity_search) agree on the same rows.
  • TestTTLSweepAtomic — verifies basic sweep behaviour, plus that a TTL renewal or clear between snapshotting cutoff_t0 and calling sweep(now=cutoff_t0) correctly skips the row.

Full lefthook gauntlet (ruff format/check, mypy, bandit, pytest-cov) green on push: 774 passed, 2 skipped, 90% coverage.

@CoderDayton
CoderDayton merged commit fd40aba into main May 10, 2026
9 checks passed
@CoderDayton
CoderDayton deleted the release/2.6.1 branch May 10, 2026 16:35
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