fix(sqlite): inline JSON paths for expression indexes#1487
Open
lalitkapoor wants to merge 2 commits intoTanStack:mainfrom
Open
fix(sqlite): inline JSON paths for expression indexes#1487lalitkapoor wants to merge 2 commits intoTanStack:mainfrom
lalitkapoor wants to merge 2 commits intoTanStack:mainfrom
Conversation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
🎯 Changes
Fixes a SQLite planner mismatch in
@tanstack/db-sqlite-persistence-core.This came from a real app query against persisted
threadMessagesdata:In the app, we first added the missing persisted filter index:
At that point, we expected SQLite to stop doing a full table scan for the filter.
The important expectation was not just “there is now an index on
threadId”. It was:threadIdis stored as literal-path SQLWHERE threadId = ?should compile to the same expression shapeSEARCH ... USING INDEX ...for the filterThe expected planner outcome after adding that index was:
SEARCH ... USING INDEX <threadId expression index>for theWHERE threadId = ?filterUSE TEMP B-TREE FOR ORDER BY, because the app query also sorts bycreatedAt DESC, id DESCand there is no composite persisted index for(threadId, createdAt, id)What the runtime query was actually compiled into was the full SQL shape below:
with params shaped like:
The persisted expression index for
threadIdhad already been normalized into literal-path SQL such as:json_extract(value, '$.threadId')So the runtime query and the persisted index were logically equivalent but not structurally identical from SQLite's perspective. SQLite expression-index matching is shape-sensitive here:
json_extract(value, ?)is not the same expression asjson_extract(value, '$.threadId')for planner matching purposes.That is why adding the missing
threadIdindex was necessary but not sufficient: the index existed, but the framework was still compiling the runtime predicate into a form that could not match that index.What we should have expected the runtime query to look like, in order to leverage the
threadIdexpression index, was the full query shape below. The important part is that thethreadIdexpression is compiled with literal JSON paths, while the actual thread ID remains bound:with params shaped like:
That full query shape would let SQLite match the filter expression against the persisted
threadIdexpression index. The remainingORDER BYcould still require a temp sort, which is fine and expected for this query shape.This PR fixes that by compiling runtime ref expressions with inlined JSON-path literals in
compileRefExpressionSql(...), while still keeping real filter values bound.After this change, the runtime SQL shape matches the persisted index expression shape, which allows SQLite to use the index for the filter path.
It also adds two regressions:
better-sqlite3uses the expression index for the emitted runtime SQL viaEXPLAIN QUERY PLAN✅ Checklist
pnpm test.🚀 Release Impact
Verification
Passed on the fixed tree:
pnpm exec vitest --run tests/sqlite-core-adapter-cli-runtime.test.tspnpm exec vitest --run tests/node-sqlite-core-adapter-contract.test.tspnpm --filter @tanstack/db-sqlite-persistence-core testNegative verification:
I temporarily reverted only
compileRefExpressionSql(...)locally and re-ran the targeted regressions.Without the fix:
json_extract(value, ?)instead of inlined JSON paths'$.threadId.__tanstack_db_persisted_type__','$.threadId.value', etc.) instead of keeping only the real filter value bound