Skip to content

feat(stack-drizzle): EQL v3 JSON selector-with-constraint querying#651

Draft
coderdan wants to merge 1 commit into
mainfrom
feat/eql-v3-json-selector
Draft

feat(stack-drizzle): EQL v3 JSON selector-with-constraint querying#651
coderdan wants to merge 1 commit into
mainfrom
feat/eql-v3-json-selector

Conversation

@coderdan

Copy link
Copy Markdown
Contributor

Adds JSONPath selector-with-constraint querying for EQL v3 encrypted JSON to the Drizzle integration. Refs #623. Draft — pending CI (integration is live-DB only).

What it adds

ops.selector(col, '$.path') returns comparison methods bound to a JSONPath into a types.Json column:

await db.select().from(users)
  .where(await ops.selector(users.doc, '$.age').gt(21))   // col->'$.age' > 21

Methods: eq, ne, gt, gte, lt, lte. It complements the existing contains (@>) operator; its unique power is ordering at a path (gt/gte/lt/lte), which containment cannot express. (Equality-at-a-path is also expressible via contains(col, {path: value}), but selector(...).eq is more direct.)

How it works (verified against the v3 bundle)

Emits eql_v3.<op>(eql_v3."->"(col, '<sel>'), eql_v3."->"('<needle>'::public.eql_v3_json, '<sel>')):

  • Selector (<sel>): encryptQuery(path, searchableJson) — a string needle infers a ste_vec_selector term → the bare HMAC selector hash, bound as the text arg of eql_v3."->".
  • Comparison entry (RHS): a storage encrypt of the reconstructed {path: value} document. The eql_v3_jsonb_entry domain CHECK requires ciphertext c, so a ciphertext-free query term can't be the RHS — hence a storage encryption, from which the same selector extracts the entry.

Scope decisions

  • Core @cipherstash/stack: no change. The selector term + comparison entry come from the existing encryptQuery/encrypt on types.Json.
  • The operator factory's client requirement gained encrypt (storage) alongside encryptQuery — needed only for the selector RHS.
  • v1 supports dot-notation object paths ($.a, $.a.b); array-index/wildcard paths are rejected with a clear error (a follow-up).
  • Supabase is a separate follow-up (EQL v3 JSON on Supabase: encrypted containment + JSONPath selector querying #650) — it has no encrypted-JSON surface at all and is blocked by PostgREST's inability to cast a filter value; that needs a live-PostgREST probe first.

Verification

  • Builds clean; 357 Drizzle unit tests pass; Biome clean.
  • Live round-trip coverage in integration/json-selector.integration.test.ts (equality, ordering gt/lt/gte, empty result, array-path rejection). These run in CI only (need a live EQL-v3 Postgres).

Verified in CI, not locally — watch these

  1. Selector normalization match — the encryptQuery(path) hash must equal the storage doc's ste_vec entry selector for the same path.
  2. Ordering at a selector — the gt/lt tests assume stored ste_vec entries carry the op (CLLW-OPE) ordering term extractable via ->; if entries carry only hm, ordering needs a bundle-side adjustment.
  3. Bare-string selector shape under the v3 client.

Remaining before ready

  • CI green on the integration suite.
  • skills/stash-drizzle + skills/stash-encryption docs for the selector pattern (holding until CI confirms the API shape).

Refs #623 · follow-up #650
https://claude.ai/code/session_01MxTTPaPP16m6br7Hoab94w

Add ops.selector(col, '$.path') to the v3 Drizzle integration (#623): comparison
methods (eq/ne/gt/gte/lt/lte) bound to a JSONPath into a types.Json column,
emitting `eql_v3.<op>(eql_v3."->"(col,'<sel>'), eql_v3."->"('<needle>'::eql_v3_json,'<sel>'))`.
Its unique power over `contains` is ORDERING at a path (col->'$.age' > 21).

- Selector hash from encryptQuery(path, searchableJson) (string needle → ste_vec_selector).
- Comparison entry from a STORAGE encrypt of {path: value} — its ciphertext `c`
  is required by the eql_v3_jsonb_entry domain (a query term can't carry it).
- One new dialect fn (selectorEntry); reuses equality/comparison for the compare.
- v1: dot-notation object paths; array/wildcard rejected with a clear error.

Core needs no change. Supabase is a follow-up (#650) — blocked by PostgREST's
inability to cast a filter value.

Refs #623
Claude-Session: https://claude.ai/code/session_01MxTTPaPP16m6br7Hoab94w
@coderabbitai

coderabbitai Bot commented Jul 14, 2026

Copy link
Copy Markdown

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 5de90791-4cfd-4016-8d9e-2a5f27fc4c18

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/eql-v3-json-selector

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.

@changeset-bot

changeset-bot Bot commented Jul 14, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 59b994e

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

This PR includes changesets to release 3 packages
Name Type
@cipherstash/stack-drizzle Minor
@cipherstash/bench Patch
@cipherstash/basic-example 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

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