Skip to content

feat(stack-drizzle): encrypt v3 query operands with encryptQuery (#622)#636

Merged
coderdan merged 3 commits into
feat/eql-v3-text-search-schemafrom
feat/eql-v3-drizzle-encrypt-query
Jul 13, 2026
Merged

feat(stack-drizzle): encrypt v3 query operands with encryptQuery (#622)#636
coderdan merged 3 commits into
feat/eql-v3-text-search-schemafrom
feat/eql-v3-drizzle-encrypt-query

Conversation

@coderdan

Copy link
Copy Markdown
Contributor

Targets the 1.0 candidate feat/eql-v3-text-search-schema.

Problem

The EQL v3 Drizzle operators encrypted every scalar/text WHERE operand with client.encrypt — a full storage envelope (including the ciphertext c), cast to ::jsonb. That is semantically wrong (a query is not a value to store) and wasteful (larger payloads). Only JSON containment used client.encryptQuery, which forced encryptQuery to be an optional capability with a runtime guard the JSON path alone exercised.

Change

Every operator (eq/ne/gt/gte/lt/lte/between/notBetween/inArray/notInArray/contains) now encrypts its operand with client.encryptQuery:

  • The term is ciphertext-free and cast to the column's eql_v3.query_<domain> type (derived by queryCastForDomain), reaching the bundle's (domain, query_<domain>) operator overloads (whose query_<domain> CHECK requires NOT (VALUE ? 'c')). The dialect no longer appends ::jsonb; the operator layer owns the cast.
  • encryptQuery is now a required capability of the operand client — the optional guard and the JSON-only branch are gone; scalar and JSON paths are unified.
  • inArray/notInArray encrypt the whole list in one batch encryptQuery crossing (position-stable; a wrong-length response is rejected, never silently truncated).
  • @cipherstash/stack: added a batch encryptQuery(terms) overload to TypedEncryptionClient (mirrors the nominal EncryptionClient) so createEncryptionOperatorsV3(await EncryptionV3(...)) type-checks with the batch path (the M1 .test-d guard).

Verification

  • Confirmed against the eql-3.0.0 bundle (cipherstash-encrypt-v3.sql) that every queryable domain has a query_<domain> type and (domain, query_<domain>) overloads, and that storage-only domains (boolean, bare base types) are gated out before any cast.
  • Confirmed via a live encryptQuery probe that a v3 column emits a composite term carrying all its configured index terms (TextSearch{hm,op,bf}, TextOrd{hm,op}, etc.), which is exactly what the multi-key query_text_search/query_text_ord CHECKs require — and that encrypt adds c while encryptQuery omits it.
  • Unit tests rewritten to assert query-term operands and ::eql_v3.query_<domain> casts (356 stack-drizzle tests, dialect + bigint + matrix). Type tests (M1) pass.
  • End-to-end operator behaviour against real Postgres + EQL v3 is covered by the Drizzle integration matrix (integration-drizzle.yml), which runs on this PR — that is the authoritative check that the narrowed terms satisfy the operators, as the issue asked.

Closes #622

https://claude.ai/code/session_01MxTTPaPP16m6br7Hoab94w

… encrypt (#622)

Every v3 Drizzle operator (eq/ne/gt/gte/lt/lte/between/notBetween/inArray/
notInArray/contains) previously encrypted its operand with client.encrypt — a
full storage envelope (with ciphertext `c`) cast to `::jsonb`. A WHERE operand
should be a query term, not a value to store.

- All operators now use client.encryptQuery, producing a ciphertext-free term
  cast to the column's `eql_v3.query_<domain>` type, reaching the bundle's
  `(domain, query_<domain>)` overloads. The dialect no longer adds `::jsonb`;
  the operator layer owns the query-domain cast (`queryCastForDomain`).
- encryptQuery is now a REQUIRED capability of the operand client (the optional
  guard and its only-JSON-uses-it branch are gone); scalar and JSON paths are
  unified.
- inArray/notInArray encrypt the whole list in one batch encryptQuery crossing
  (position-stable, wrong-length rejected), replacing the bulkEncrypt/
  bounded-concurrency path.
- stack: add a batch `encryptQuery(terms)` overload to TypedEncryptionClient
  (mirrors the nominal client) so the documented
  `createEncryptionOperatorsV3(await EncryptionV3(...))` usage type-checks with
  the batch path.
- Verified the query-domain mapping and the composite (hm/op/bf) query-term
  shape against the eql-3.0.0 bundle and a live encryptQuery probe; unit tests
  updated to assert query-term operands and `::eql_v3.query_<domain>` casts. The
  end-to-end operator behaviour is covered by the Drizzle integration matrix.

Closes #622

Claude-Session: https://claude.ai/code/session_01MxTTPaPP16m6br7Hoab94w
@coderdan coderdan requested a review from a team as a code owner July 13, 2026 06:43
@changeset-bot

changeset-bot Bot commented Jul 13, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 161f17b

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

This PR includes changesets to release 10 packages
Name Type
@cipherstash/stack-drizzle Minor
@cipherstash/stack Minor
stash Patch
@cipherstash/bench Patch
@cipherstash/basic-example Patch
@cipherstash/prisma-next Patch
@cipherstash/stack-supabase Patch
@cipherstash/test-kit Patch
@cipherstash/prisma-next-example Patch
@cipherstash/e2e 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 Jul 13, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

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: d7cf885f-0553-42b5-bca4-cd45a43933f9

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-drizzle-encrypt-query

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.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR updates the EQL v3 Drizzle integration so all query operands are encrypted as ciphertext-free query terms via encryptQuery (instead of full storage envelopes via encrypt), aligning operand semantics with the EQL v3 (domain, query_<domain>) operator overloads and reducing payload size.

Changes:

  • Switch v3 Drizzle operators (eq/ne/gt/gte/lt/lte/between/notBetween/inArray/notInArray/contains) to encrypt operands with encryptQuery, and cast operands to eql_v3.query_<domain> at the operator layer.
  • Add a batch encryptQuery(terms) overload to TypedEncryptionClient (stack v3 typing) to support single-crossing list encryption for inArray/notInArray.
  • Update Drizzle dialect rendering to stop appending ::jsonb, and rewrite unit/type tests to assert query-term operands and query_<domain> casts.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated no comments.

Show a summary per file
File Description
packages/stack/src/encryption/v3.ts Extends TypedEncryptionClient typing with batch encryptQuery(terms) and bridges overloads in typedClient.
packages/stack-drizzle/src/v3/sql-dialect.ts Removes dialect-level ::jsonb casts; assumes operands arrive pre-cast to query_<domain>.
packages/stack-drizzle/src/v3/operators.ts Migrates operand encryption to encryptQuery, adds query_<domain> casting logic, and batches list operands.
packages/stack-drizzle/tests/v3/sql-dialect.test.ts Updates expectations for dialect SQL output without ::jsonb.
packages/stack-drizzle/tests/v3/operators.test.ts Rewrites operator tests to assert query-term operands, casts, and batch list encryption behavior.
packages/stack-drizzle/tests/v3/operators.test-d.ts Updates type-level regression guards for the new encryptQuery-only operand client surface.
packages/stack-drizzle/tests/v3/bigint.test.ts Updates bigint operator tests to assert query-term casts and encryptQuery usage.
.changeset/eql-v3-drizzle-encrypt-query.md Changeset documenting the operand semantic shift and the added batch overload.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

…med overload

Replace the object-literal arrow + `as unknown as` bridge with a named function
carrying the two real overload signatures (single per-column form + batch
`terms[]` form). The compiler now checks the implementation against both
signatures directly, so the whole-value cast is gone; only the forwarded args
keep `as never`, consistent with the sibling encrypt/encryptModel wrappers.

Pure type-level restructuring — the runtime forward to the nominal client is
unchanged. Verified: stack + stack-drizzle build and test:types (incl. the M1
guard) pass, code:check clean.

Claude-Session: https://claude.ai/code/session_01MxTTPaPP16m6br7Hoab94w

@coderdan coderdan left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code review — no correctness bugs found

Ran an xhigh-effort recall pass over the diff. Verified against the eql-3.0.0 bundle that every derived eql_v3.query_<domain> cast type exists, every (domain, query_<domain>) overload (eq/neq/gt/gte/lt/lte/contains) exists, resolveIndexType answers equality on ordering-only columns, single/batch routing + the position-stable length check are correct, and lock-context/audit propagate on both paths. The mechanism is sound. Four docs/test/cleanup findings — three inline, one below (its file isn't in the diff).

🔴 skills/stash-drizzle/SKILL.md:790 — ships wrong guidance to customers

inArray / notInArray reject an empty list, and encrypt the whole list in a single bulkEncrypt crossing when the client exposes one.

This PR deletes the bulkEncrypt path — inArray/notInArray now always use a single required encryptQuery batch crossing. skills/ ships inside the stash npm tarball and is copied into customer repos, so this sentence becomes wrong guidance in someone else's codebase. AGENTS.md (“Agent Skills — these ship to customers”, checklist item 8) requires updating the affected skill in the same PR. It also means the PR needs a stash patch changeset for the skills change.

Suggested: “…encrypt the whole list in a single encryptQuery batch crossing.”

Comment thread packages/stack-drizzle/src/v3/operators.ts Outdated
Comment thread packages/stack-drizzle/src/v3/operators.ts
Comment thread packages/stack-drizzle/__tests__/v3/operators.test.ts Outdated
- operators.ts: drop the two dead regex branches in queryCastForDomain
  (`ord_ope`, `search_ore` are not real column-domain suffixes — ope ordering
  is `_ord`, text search is `_search`); comment now ties the suffixes to the
  column factories.
- operators.ts: fix stale docs left by the bulkEncrypt→encryptQuery move — the
  inArray/notInArray JSDoc and the inArrayOp comment no longer describe the
  removed bulkEncrypt path / deleted concurrency fallback.
- operators.test.ts: the encryptQuery double now routes to batch only when
  `opts === undefined && Array.isArray(...)`, matching the real client — an
  array-valued single query WITH opts (e.g. a searchableJson array needle) no
  longer diverges to the batch path in the double.
- stash-drizzle skill: correct the inArray/notInArray line (single encryptQuery
  batch crossing, not bulkEncrypt); + stash patch changeset (skills ship in the
  tarball).

Claude-Session: https://claude.ai/code/session_01MxTTPaPP16m6br7Hoab94w
@coderdan

Copy link
Copy Markdown
Contributor Author

All four review findings addressed in 161f17b:

  • skills/stash-drizzle/SKILL.md:790 — corrected to "encrypt the whole list in a single encryptQuery batch crossing" (the bulkEncrypt path is gone), and added a stash patch changeset (.changeset/stash-drizzle-skill-encrypt-query.md) since skills/ ships in the tarball.
  • operators.ts regex — dropped the dead ord_ope/search_ore branches; live suffixes verified against the column factories.
  • operators.ts stale docs — inArray/notInArray JSDoc + the inArrayOp comment no longer reference the removed bulkEncrypt/concurrency fallback.
  • operators.test.ts double — batch routing now gated on opts === undefined && Array.isArray(...), matching the real client so an array-with-opts needle can't diverge.

Verified: stack-drizzle build + 356 unit tests + test:types pass, code:check clean.

@coderdan coderdan merged commit 09ae9dd into feat/eql-v3-text-search-schema Jul 13, 2026
11 checks passed
@coderdan coderdan deleted the feat/eql-v3-drizzle-encrypt-query branch July 13, 2026 08:43
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.

2 participants