Skip to content

fix(pgvector,supabase): negated ranges, zero vectors, and metric detection - #58

Merged
PunGrumpy merged 6 commits into
mainfrom
fix/postgres-query-correctness
Oct 18, 2026
Merged

PunGrumpy merged 6 commits into
mainfrom
fix/postgres-query-correctness

Conversation

@PunGrumpy

Copy link
Copy Markdown
Owner

Stacked on #57 (which sits on #55 and #54). Review the last six commits, from test(pgvector,supabase): cover negated ranges, zero vectors, opclasses.

Background

Three Postgres query defects gave wrong results with no error, all reproduced on PGlite (Postgres 18.3, pgvector 0.8.1). Each lives in both the pgvector adapter and the SQL functions the Supabase adapter calls, so they ship together and Supabase users re-run sql/supabase.sql once.

  1. not around a range filter skipped records that lack the field. The range leaf was (jsonb_typeof(path) = 'number' AND path > value); for a missing field that's NULL, and NOT NULL is still NULL. not(gt("year", 2000)) skipped records without a year, and delete({ filter: not(gt("year", 2000)) }) left them behind. The provider differences page says pgvector and Supabase match them.
  2. A cosine zero vector ranked first. Its score is 1 - NaN, and the outer ORDER BY hits.score DESC added for the HNSW iterative scan puts NaN above every number. The inner ORDER BY distance and the plain statement put it last. A zero vector is what a failed embedding call usually returns.
  3. The metric was read from the wrong index. The lookup matched an operator class name anywhere in any index definition of the table (the SQL with like, where _ is a wildcard), so a euclidean table named x_vector_cosine_ops was read as cosine from its primary key.

Summary

  • The range leaf starts with path IS NOT NULL, in the TypeScript compiler and in vecstore_filter_sql.
  • The wrapped query selects the distance next to the score and the outer query sorts by it, ascending for every metric. The plain statement is byte-for-byte unchanged. vecstore_query does the same.
  • The metric lookup matches (embedding vector_cosine_ops) and friends exactly; the SQL uses strpos.
  • The compiled SQL on the filters page and the Match and Range landing cards show the new leaf.
  • Patch changeset, which says to re-run sql/supabase.sql.

Verification

  • 12 new test cases: three negated-range rows in the filter differential corpus (checked against expected ids, not just against each other), two conformance cases run on both PGlite suites, the opclass-named euclidean table on both adapters, and the zero vector on the wrapped path, a plain-path control, and Supabase.
  • 11 of them fail on the test commit and pass with the fixes; the plain-path control passes throughout.
  • bun run test twice in random order: 554 pass, 0 fail. bun run check and bun run typecheck are clean.
  • A script compiles the Match and Range filters with both compilers and compares them with the landing strings (whitespace ignored): four mismatches before, four matches after.
  • Not run against a real Supabase project or Postgres 15 to 17.
  • The four landing outputs grow by one to three lines, and the demo card has a fixed height, so please glance at the Match and Range panes for pgvector and Supabase in the Vercel preview.

Checklist

  • Tests have been added or updated
  • Documentation has been added or updated
  • A changeset has been added for vecstore-sdk (run bun run changeset in the project root)
  • I have reviewed this pull request myself

@PunGrumpy PunGrumpy added the bug Something isn't working label Oct 18, 2026
@vercel

vercel Bot commented Oct 18, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
vecstore-sdk Ready Ready Preview Oct 18, 2026 9:47am UTC

@changeset-bot

changeset-bot Bot commented Oct 18, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 8d8fe98

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
vecstore-sdk Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@coderabbitai

coderabbitai Bot commented Oct 18, 2026 •

Copy link
Copy Markdown

Warning

Review limit reached

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Next included review available in 27 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

Learn how review limits work.

Review configuration:

⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: eee075e4-fe56-4cb9-bfbd-1902343e2619
📥 Commits

Reviewing files that changed from the base of the PR and between 83c8002 and 8d8fe98.

📒 Files selected for processing: 13
  • .changeset/postgres-query-correctness.md
  • apps/web/content/docs/core/filters.mdx
  • apps/web/lib/landing-content.ts
  • packages/vecstore-sdk/sql/supabase.sql
  • packages/vecstore-sdk/src/filter/pgvector.ts
  • packages/vecstore-sdk/src/pgvector/index.ts
  • packages/vecstore-sdk/test/adapters/pgvector.test.ts
  • packages/vecstore-sdk/test/filter/pgvector.test.ts
  • packages/vecstore-sdk/test/live/conformance.ts
  • packages/vecstore-sdk/test/postgres/filter-differential.test.ts
  • packages/vecstore-sdk/test/postgres/pglite.ts
  • packages/vecstore-sdk/test/postgres/pgvector.test.ts
  • packages/vecstore-sdk/test/postgres/supabase.test.ts
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@pkg-pr-new

pkg-pr-new Bot commented Oct 18, 2026 •

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/vecstore-sdk@58

commit: 8d8fe98

Add tests for three query defects that give wrong results without an
error. not around a range filter skips a record that lacks the field,
a cosine zero vector ranks first once an HNSW query is wrapped in an
outer sort by score, and a euclidean table whose name contains an
operator class name is read as that metric.

The negated range cases run on both PGlite suites and in the filter
differential corpus. A plain-path zero vector test is a control. 11 of
the new tests fail at this commit and pass once the adapters are fixed.
A range leaf compiled to a jsonb_typeof check and a comparison. With the
field missing, the path is NULL, so the leaf was NULL and NOT of it was
NULL too, which dropped the record. not(gt("year", 2000)) skipped
records without a year, and delete by that filter left them behind,
against the docs.

The leaf now starts with an IS NOT NULL check on the path, so a missing
field makes it false and NOT of it true. The TypeScript compiler and
vecstore_filter_sql change together, and the SQL text expectations that
pin the leaf follow.
An HNSW query is wrapped so an outer sort restores the order after an
iterative scan. That sort used the score, descending for cosine and dot.
A zero vector has a cosine distance of NaN, so its score is NaN, and
Postgres sorts NaN above every number. The zero vector came first,
where the inner sort by distance and the plain statement put it last.

The wrapped statement now selects the distance next to the score and
the outer query sorts by it, ascending for every metric. The plain
statement is unchanged. vecstore_query does the same, and the
SCORE_ORDERS table and the score_order variable are gone.
The adapter and vecstore_metric read the metric by looking for an
operator class name anywhere in an index definition of the table. Every
definition contains the table name, so a euclidean table named
x_vector_cosine_ops was read as cosine from its primary key. The SQL
used like, where the underscore is also a wildcard.

Both now match the exact text of the embedding column's index, such as
(embedding vector_cosine_ops), and the SQL uses strpos so nothing is
read as a pattern.
The range leaf now starts with an IS NOT NULL check on the path. Update
the compiled SQL on the filters page and the pgvector and Supabase
outputs of the Match and Range demo cards to show it.
@PunGrumpy
PunGrumpy force-pushed the fix/postgres-query-correctness branch from 3bae98f to 8d8fe98 Compare October 18, 2026 09:46
@PunGrumpy
PunGrumpy merged commit a4daf39 into main Oct 18, 2026
13 of 14 checks passed

This branch was successfully deployed

1 active deployment
Preview — 8d8fe98d Deployed Oct 18, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant