Skip to content

Linear-time inserts, Turso v0.8.0-pre.13 vector parity, browser vector gate - #73

Merged
Marc-André Moreau (mamoreau-devolutions) merged 3 commits into
masterfrom
claude/ahtola-vector-search-74db66
Sep 26, 2026
Merged

Marc-André Moreau (mamoreau-devolutions) merged 3 commits into
masterfrom
claude/ahtola-vector-search-74db66

Conversation

@mamoreau-devolutions

Copy link
Copy Markdown
Contributor

Summary

Vector search parity. Against Turso's Rust engine, Ahtola's vector support was already complete: the vector functions, the serialized formats, and an exact USING vector index for ORDER BY vector_distance_*(…) LIMIT k. This PR moves the turso-src pin to v0.8.0-pre.13 (64b8ef574) and ports the one behavioral change in that release:

  • Dense float32/float64 vector_distance_cos follows b9414023d: two zero vectors are at distance 0, and a zero dot product is at distance 1, decided before dividing. This is Turso's pure-Rust fallback, which Turso runs on wasm, so native and browser results stay identical.
  • The rest of the core/vector delta is simsimd build plumbing. turso/vector.sqltest is byte-identical to pre.7.
  • Upstream's index-method OFFSET fix (78395e704, #7523) needed no engine change here. Its FTS case is added as a test.
  • The libSQL-only features from Turso's vector-search blog post (F32_BLOB(N) typing, libsql_vector_idx, vector_top_k, DiskANN) do not exist in the Rust engine through pre.13, so they are out of scope.

Insert performance. Every statement did O(table) work, so row-at-a-time inserts were quadratic:

  • Catalog clones deep-copied every row of every table on every statement. Row and rowid lists are now shared copy-on-write in 1,024-element chunks (CowChunkedList).
  • Two sites patched stored rows in place: incremental blob writes and sqlite_sequence renames. They now replace the row instead, because stored row arrays are shared with clones.
  • PrepareInsert and ValidateRowids rebuilt a HashSet of every rowid per statement. Rowids are now cached per table as an immutable set plus their maximum, keyed on row-store lineage, revision and count, and extended by inserts.
  • INSERT validation checks NOT NULL and non-unique index keys only on the new rows, as SQLite does. It skips rescanning a rowid alias that ValidateRowids just proved unique, and validates through a view instead of copying the table. Unique constraints are still checked against every row.
Workload Before After
64-dim vectors with an index, one row per INSERT, 20k rows 9,120 µs/row 117 µs/row (87 at 40k)
Integer rows in one transaction, 20k rows 2,236 µs/row 27 µs/row (12 at 40k)

One visible behavior change from the cosine port: float32 vectors whose squared norm underflows to zero, while their dot product doesn't, now report +∞ instead of 1. ManagedVectorIndexReviewRegressionTests.CosineRecallSurvivesScalarNormCollapse is updated for that. It still requires the index and the scan to agree, and now also compares at full-table LIMIT.

Browser. samples/BrowserWasmConsumer now builds a USING vector index over OPFS, reopens it, and requires the planner to choose it and to return the same rows as the unindexed scan. The expected status in scripts/Invoke-BrowserPackageConsumer.ps1 is extended with vector-search=195-1987-643.

Testing

  • Full managed suite on net10.0: 8,246 passed, 0 failed. Core builds with zero warnings on net8.0, net9.0 and net10.0, with the AOT/trim analyzers on.
  • New tests: CowChunkedListTests (randomized differential against List<T> with clones), RowStoreCopyOnWriteTests (clone isolation, failed-statement atomicity, AUTOINCREMENT rename rollback, blob write rolled back to a savepoint), cosine zero-norm and zero-dot cases, and FTS ranked LIMIT/OFFSET.
  • The browser consumer app was run in Chromium through project references and produced the exact expected PASS string. The Playwright gate itself was not run locally.
  • validate-project-closure passes. format-check reports only pre-existing issues in lines this PR doesn't touch.

🤖 Generated with Claude Code

…r gate

Bump the turso-src pin to v0.8.0-pre.13 (64b8ef574) and port its vector
changes. The dense float32/float64 cosine rule follows b9414023d: two
zero vectors are at distance 0, and a zero dot product is at distance 1,
decided before dividing. That is Turso's pure-Rust fallback, the code
path Turso runs on wasm. The rest of the core/vector delta is simsimd
build plumbing, and turso/vector.sqltest is byte-identical to pre.7.
Upstream's index-method OFFSET fix (78395e704) needed no engine change;
its FTS case is added as a test.

Remove the O(table) work every statement did, which made row-at-a-time
inserts quadratic:
- Catalog clones share row and rowid lists copy-on-write in 1024-element
  chunks (CowChunkedList) instead of deep-copying every row of every
  table. The two sites that patched stored rows in place (incremental
  blob writes, sqlite_sequence renames) now replace the row.
- Rowids are cached per table as an immutable set with their maximum,
  keyed on row-store lineage, revision and count, and extended by
  inserts rather than rebuilt by PrepareInsert and ValidateRowids.
- INSERT validation checks NOT NULL and non-unique index keys on the new
  rows only, skips rescanning a rowid alias that ValidateRowids just
  proved unique, and validates through a view instead of copying the
  table.

64-dim vector inserts into an indexed table, one row per statement, go
from 9.1 ms/row at 20k rows to 87 us/row at 40k rows.

The browser package consumer now builds a USING vector index over OPFS,
reopens it, and requires the planner to choose it and to match the scan.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…r-search-74db66

# Conflicts:
#	scripts/Invoke-BrowserPackageConsumer.ps1
The cross-process replica race tests poll for the worker's started,
blocked and completed files with File.Exists and then read them. The
worker wrote them with File.WriteAllText, which creates the file before
writing it, so a read in between saw an empty or still-locked file
("being used by another process"). The assertion then threw inside the
fault-injection callback, and the worker's Dispose masked it with an
exit-code failure. This showed up intermittently on the Windows coverage
leg (ColdOpenRollbackDiscardsCommittedReplacementGenerationWal,
ColdOpenRollbackDiscardsReplacementWalRecreatedAfterQuarantineCrash).

Each signal is now written to a staging file and renamed into place.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@mamoreau-devolutions
Marc-André Moreau (mamoreau-devolutions) merged commit 34256d4 into master Sep 26, 2026
11 checks passed
@mamoreau-devolutions
Marc-André Moreau (mamoreau-devolutions) deleted the claude/ahtola-vector-search-74db66 branch September 26, 2026 13:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant