Skip to content

fix(rvf-sdk): ergonomic query count arg + clear create-on-existing error (#657, #658)#696

Open
ohdearquant wants to merge 1 commit into
ruvnet:mainfrom
ohdearquant:fix/rvf-sdk-query-create
Open

fix(rvf-sdk): ergonomic query count arg + clear create-on-existing error (#657, #658)#696
ohdearquant wants to merge 1 commit into
ruvnet:mainfrom
ohdearquant:fix/rvf-sdk-query-create

Conversation

@ohdearquant

Copy link
Copy Markdown
Contributor

PR authored by an AI agent on behalf of @ohdearquant.

Fixes #657 and #658 — two @ruvector/rvf JS SDK papercuts where a mistaken call
form leaked a misleading low-level error instead of a clear, actionable one. Both
fixes live in the SDK layer (database.ts / backend.ts); no native rebuild.

#657query() options-object form

RvfDatabase.query(vector, k, options) accepted only a positional numeric k.
Passing an options object (query(vec, { k: 2 }), or {topK}/{limit}, as most
vector-DB JS APIs allow) put the object in the k slot, which crossed N-API and
threw Failed to convert napi value Object into rust type u32.

Now query() accepts both forms and validates before dispatch:

  • query(vec, 10) — positional (unchanged).
  • query(vec, { k: 10, efSearch: 200 }) — object form; topK and limit are
    accepted as aliases for k; the remaining fields pass through as query options.
  • A missing count, or a non-positive-integer count, throws a clear
    RvfError with the new RvfErrorCode.InvalidArgument instead of the N-API error.

#658create() on an existing path

create() on a path that already held a valid .rvf surfaced the native
FsyncFailed (durability) error — implying a disk/OS fault when the real cause was
just "the file already exists." NodeBackend.create() now checks first and throws a
clear RvfError with the new RvfErrorCode.FileExists, pointing at open() or a new
{ overwrite: true } option. overwrite removes the old file and its
.idmap.json sidecar before creating, so no stale string-id map leaks in.

Additive / compatible

  • Two new SDK-level (0xFF__) error codes: FileExists = 0xff04,
    InvalidArgument = 0xff05. No existing code changed.
  • New optional RvfOptions.overwrite (default false = current behavior).
  • query()'s positional-number form is unchanged; the object form is new surface.
  • dist/ regenerated with tsc (this package commits its build output; files
    publishes dist/ only). Stale src/*.js artifacts are not referenced by
    package.json and were left untouched.

Verification

scripts/smoke-query-create.mjs (also npm run smoke) exercises both fixes against
the built dist/. #657 runs with no native addon (mock backend via fromBackend);
#658 runs when a native/stub addon is resolvable.

  ok - #657 positional count query(v, 5) -> 5
  ok - #657 object {k:2} -> 2
  ok - #657 object {topK:3} -> 3
  ok - #657 object {limit:4} -> 4
  ok - #657 object {k, efSearch} forwards efSearch
  ok - #657 rejects no count with clear InvalidArgument (not napi error)
  ok - #657 rejects negative with clear InvalidArgument (not napi error)
  ok - #657 rejects non-integer with clear InvalidArgument (not napi error)
  ok - #657 rejects zero with clear InvalidArgument (not napi error)
  ok - #658 create() on existing path -> FileExists (clear, not FsyncFailed)
  ok - #658 create({overwrite:true}) clears old file + sidecar and proceeds

All 11 checks passed.

tsc --noEmit (strict) is clean. Note: #659 (ingestBatch metadata) is a related but
separate change touching the native ingest/query path — not included here.

…ror (ruvnet#657, ruvnet#658)

ruvnet#657: RvfDatabase.query() accepted only a positional numeric count; passing an
options object ({k}/{topK}/{limit}) put the object in the count slot and leaked a
low-level "Failed to convert napi value Object into rust type u32" error. query()
now accepts both forms and validates the count, throwing a clear InvalidArgument
(new SDK code) for a missing/non-positive-integer count before crossing N-API.

ruvnet#658: NodeBackend.create() on a path that already held a file surfaced the native
FsyncFailed durability error, sending debugging the wrong way. It now checks for an
existing file first and throws a clear FileExists (new SDK code) pointing at open()
or { overwrite: true }; overwrite removes the old file and its .idmap.json sidecar
before creating.

dist/ regenerated via tsc. scripts/smoke-query-create.mjs verifies both against the
built output (11 checks, no native addon required for ruvnet#657).

Co-Authored-By: Leo <noreply@khive.ai>
@ruvnet

ruvnet commented Jul 17, 2026

Copy link
Copy Markdown
Owner

Reviewed this (independently, not just the PR description — checked out the diff and traced the actual code paths). Found a blocker that needs a rebase before this can merge:

Hard error-code collision. This PR adds FileExists = 0xff04 in errors.ts, but PR #708 (merged today) already assigned MetadataNotSupported = 0xff04 to that exact slot for an unrelated error (rejecting unsupported per-vector metadata on ingest). InvalidArgument = 0xff05 is currently free on main, so only FileExists collides. Two semantically unrelated errors sharing a numeric code means err.code === 0xff04 / RvfErrorCode[0xff04] reverse-mapping become ambiguous for callers. GitHub already reports this PR as CONFLICTING/DIRTY against current main, consistent with this.

Suggest rebasing onto current main and moving FileExists to the next free slot (0xff08, since #697 — a sibling PR touching the same enum — reserves 0xff06/0xff07).

Two non-blocking things worth a look while rebasing:

  • {topK: 5, limit: 10} (both aliases set) resolves via k ?? topK ?? limit, so topK silently wins over limit — works, but undocumented; a one-line JSDoc note would help.
  • The overwrite: true path in create() does two separate rmSync calls (main file, then .idmap.json sidecar) gated on fs.existsSync(path) only. If the sidecar exists but the main file doesn't (e.g. from an earlier interrupted create), the existence check never fires and a stale sidecar can survive a fresh create(), desyncing ID mappings. Also, if the first rmSync succeeds but the second throws, you're left with the original file destroyed and no new file in a half-cleaned state. Might be worth a single guarded cleanup path.

Happy to help rebase this if useful — just didn't want to push to your branch without asking first.

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.

rvf: RvfDatabase.query() throws misleading napi type error on options-object call form ({k}/{topK}/{limit})

2 participants