Skip to content

fix(vector): ORDER BY vec_distance_*() LIMIT k reaches the vector index, and CREATE VECTOR INDEX builds one - #474

Merged
MPCoreDeveloper merged 1 commit into
masterfrom
fix/vector-sql-optimizer
Oct 25, 2026
Merged

MPCoreDeveloper merged 1 commit into
masterfrom
fix/vector-sql-optimizer

Conversation

@MPCoreDeveloper

Copy link
Copy Markdown
Owner

Symptom

ORDER BY vec_distance_cosine(embedding, @q) LIMIT k and CREATE VECTOR INDEX both did nothing
observable: the plan fell back to a full scan, and the index statement returned without building an
index. Nothing threw, so both read as "the optimizer declined" rather than "the optimizer was never
reached".

Two independent breaks, not one

  1. The planner branch was a stub. SqlParser.TryExecuteVectorOptimized returned null
    unconditionally. e220461e had a working ~100-line implementation (argument extractor included);
    997103be replaced it with the stub and four unused parameters. VectorQueryOptimizer itself was
    intact — only the call path was gone.
  2. Nothing was ever wired to the parser. SqlParser.VectorQueryOptimizer was an internal static
    property that no code in the engine assigned, and no other assembly could assign it (the parser is
    internal). SharpCoreDB.VectorSearch only registers the optimizer with the DI container. Both
    consumers were null-conditional, so CREATE VECTOR INDEX was dead for the same reason the planner
    was — the whole SQL surface, not just the ranking branch.

Fixing only one of them leaves the symptom in place, which is why the existing VectorQueryOptimizer
unit tests stayed green throughout: they exercise the optimizer directly, never through Database.

What changed

  • Wiring: Database.GetSharedSqlParser resolves IVectorQueryOptimizer from the service provider
    (the same pattern IGraphRagProvider already used) and attaches it per parser instance via
    SetVectorQueryOptimizer. The duplicate new SqlParser(...) in the ExecuteCompiledQuery fallback
    now routes through GetSharedSqlParser, so there is one construction point instead of two.
  • The restored planner: TryExecuteVectorOptimized projects onto the SELECT list (SELECT *,
    aliases and the distance expression substituted), accepts an ORDER BY that names either the alias or
    the function, and refuses DESC and a non-empty WHERE — an ascending top-k cannot answer those, and
    returning the wrong shape silently is what made the original bug hard to see. Those two fall back to a
    scan.
  • Parameter binding: a float[] argument is sent unquoted as [1,0,0,0], and the argument splitter
    now tracks bracket and brace nesting, not just parentheses — a query vector is not a call argument.

Tests

New SqlVectorQueryTests go through AddVectorSupport() + Database + SQL, the only level where the
wiring is observable, covering: index built by CREATE VECTOR INDEX, ORDER BY vec_distance_cosine
with a literal and a parameter vector, alias form, SELECT * form, DESC and WHERE falling back to a
scan, and a query on an index that does not exist.

  • SharpCoreDB.VectorSearch.Tests: 255/255
  • SharpCoreDB.Tests: 2137/2137 (16 skipped)
  • 0 errors

Known limitation

An index is a snapshot taken at CREATE VECTOR INDEX. Inserts after it are not in the index until it is
rebuilt; the scanner path still sees them, so a stale index under-answers rather than mis-answers.

…ng in a dead branch

`CREATE VECTOR INDEX` and the `ORDER BY vec_distance_*(col, vector) LIMIT k`
planner branch were both permanently inactive, and restoring the deleted method
alone would have revived neither. `SqlParser.VectorQueryOptimizer` was an
`internal static` that no code in SharpCoreDB ever assigned and no other assembly
could assign (the parser is internal), while SharpCoreDB.VectorSearch registers
the optimizer with the DI container. So `VectorQueryOptimizer?.BuildIndex(...)`
in the DDL path and `VectorQueryOptimizer is not null` in the SELECT path were
no-ops in both directions: nothing built an index and nothing looked for one.

Wiring: Database.GetSharedSqlParser now resolves IVectorQueryOptimizer from the
service provider — the same pattern GRAPH_RAG already used — and attaches it via
SetVectorQueryOptimizer; the property is per parser instance instead of static.
The second SqlParser construction site (the compiled-query fallback) now goes
through GetSharedSqlParser, so falling back cannot produce a parser without the
extensions the first site attaches.

Restored and repaired: TryExecuteVectorOptimized was replaced by a stub returning
null in 997103b, after working in e220461. The restored code is not a snapshot
of it. The old version copied every column and appended a "distance" key whatever
the SELECT list said, and did so for DESC and WHERE queries too, so an optimized
query and an unoptimized one returned different shapes for the same SQL. Rows are
now projected onto the SELECT list (SELECT *, plain columns, aliases, and the
distance expression substituted from the key the optimizer injects), ORDER BY may
name either the alias or the function, and the two shapes an ascending top-k index
cannot answer — DESC and a WHERE clause — fall back to the scan instead of
returning a plausible wrong answer.

Found while testing: an unquoted vector literal (`[1,0,0,0]`, which is exactly
what ParameterBinder produces for a float[] parameter) lost its commas to the
argument splitter, leaving "[1" as the query vector. Brackets and braces now count
as nesting, so a bound float[] parameter works end to end.

Tests: tests/SharpCoreDB.VectorSearch.Tests/SqlVectorQueryTests.cs, seven
end-to-end cases through AddVectorSupport() + Database — the only level at which
the wiring is observable, which is why the existing VectorQueryOptimizer unit
tests stayed green throughout the outage. 255/255 in that project, 2137/2137 in
SharpCoreDB.Tests.

Known limitation, unchanged: an index is a snapshot of the table at CREATE VECTOR
INDEX; rows inserted afterwards are not in it until it is rebuilt.
@codecov

codecov Bot commented Oct 25, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 77.63158% with 34 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/SharpCoreDB/Services/SqlParser.DML.cs 78.98% 12 Missing and 17 partials ⚠️
...arpCoreDB/Database/Execution/Database.Execution.cs 72.72% 2 Missing and 1 partial ⚠️
src/SharpCoreDB/Services/SqlParser.DDL.cs 0.00% 0 Missing and 2 partials ⚠️

📢 Thoughts on this report? Let us know!

@sonarqubecloud

Copy link
Copy Markdown

@MPCoreDeveloper
MPCoreDeveloper merged commit 1146f97 into master Oct 25, 2026
16 checks passed
@MPCoreDeveloper
MPCoreDeveloper deleted the fix/vector-sql-optimizer branch October 25, 2026 10:20
MPCoreDeveloper added a commit that referenced this pull request Oct 25, 2026
Release 2.1.1: version bump, release notes and changelog (stacked on #474)
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