Skip to content

Feat/prisma next eql v3#655

Open
calvinbrewer wants to merge 21 commits into
mainfrom
feat/prisma-next-eql-v3
Open

Feat/prisma next eql v3#655
calvinbrewer wants to merge 21 commits into
mainfrom
feat/prisma-next-eql-v3

Conversation

@calvinbrewer

@calvinbrewer calvinbrewer commented Jul 15, 2026

Copy link
Copy Markdown
Contributor

Summary by CodeRabbit

  • New Features

    • Added EQL v3 support for Prisma Next with concrete, capability-specific encrypted column constructors.
    • Added v3 query operators for equality, ranges, text-token search, ordering, and encrypted JSON containment.
    • Added first-class BigInt support, including values beyond JavaScript’s safe integer range.
    • Added a dedicated v3 setup entry point and package export.
    • Added EQL v3 bundle installation through migrations.
  • Bug Fixes

    • Storage-only boolean fields now reject unsupported query operators with clear errors.
    • Improved schema validation prevents mismatched encryption domains.
  • Documentation

    • Updated Prisma examples and guides for EQL v3 behavior, setup, migrations, and supported operations.

@calvinbrewer
calvinbrewer requested a review from a team as a code owner July 15, 2026 12:47
@changeset-bot

changeset-bot Bot commented Jul 15, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 4c7abac

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

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

Copy link
Copy Markdown

Important

Review skipped

Too many files!

This PR contains 126 files, which is 26 over the limit of 100.

To get a review, narrow the scope:
• coderabbit review --type committed # exclude uncommitted changes
• coderabbit review --dir # limit to a subdirectory
• coderabbit review --base # compare against a closer base

Upgrade to a paid plan to raise the limit.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 9a36c8e4-e990-4dd3-a32b-8ae73ee6ccb6

📥 Commits

Reviewing files that changed from the base of the PR and between a0f5ee0 and 4c7abac.

📒 Files selected for processing (129)
  • .changeset/eql-migration-command.md
  • .changeset/eql-v3-prisma-next.md
  • .changeset/prisma-example-eql-v3.md
  • .github/workflows/integration-prisma-next.yml
  • .github/workflows/tests.yml
  • AGENTS.md
  • examples/prisma/README.md
  • examples/prisma/migrations/app/20260709T1034_initial/migration.json
  • examples/prisma/migrations/app/20260709T1034_initial/migration.ts
  • examples/prisma/migrations/app/20260709T1034_initial/ops.json
  • examples/prisma/migrations/app/20260714T2142_initial/end-contract.d.ts
  • examples/prisma/migrations/app/20260714T2142_initial/end-contract.json
  • examples/prisma/migrations/app/20260714T2142_initial/migration.json
  • examples/prisma/migrations/app/20260714T2142_initial/migration.ts
  • examples/prisma/migrations/app/20260714T2142_initial/ops.json
  • examples/prisma/migrations/cipherstash/20260601T0100_install_eql_v3_bundle/migration.json
  • examples/prisma/migrations/cipherstash/20260601T0100_install_eql_v3_bundle/ops.json
  • examples/prisma/migrations/cipherstash/refs/head.json
  • examples/prisma/prisma/schema.prisma
  • examples/prisma/src/db.ts
  • examples/prisma/src/index.ts
  • examples/prisma/src/prisma/contract.d.ts
  • examples/prisma/src/prisma/contract.json
  • examples/prisma/test/e2e/README.md
  • examples/prisma/test/e2e/bigint.e2e.test.ts
  • examples/prisma/test/e2e/bool.e2e.test.ts
  • examples/prisma/test/e2e/date.e2e.test.ts
  • examples/prisma/test/e2e/global-setup.ts
  • examples/prisma/test/e2e/json.e2e.test.ts
  • examples/prisma/test/e2e/mixed.e2e.test.ts
  • examples/prisma/test/e2e/num.e2e.test.ts
  • examples/prisma/test/e2e/str-range.e2e.test.ts
  • packages/cli/src/bin/main.ts
  • packages/cli/src/cli/registry.ts
  • packages/cli/src/commands/db/__tests__/find-generated-migration.test.ts
  • packages/cli/src/commands/db/install.ts
  • packages/cli/src/commands/eql/__tests__/migration.test.ts
  • packages/cli/src/commands/eql/migration.ts
  • packages/cli/src/commands/init/lib/setup-prompt.ts
  • packages/cli/src/commands/init/utils.ts
  • packages/cli/src/messages.ts
  • packages/cli/tests/e2e/command-help.e2e.test.ts
  • packages/cli/tests/e2e/smoke.e2e.test.ts
  • packages/prisma-next/README.md
  • packages/prisma-next/integration/adapter.ts
  • packages/prisma-next/integration/families.integration.test.ts
  • packages/prisma-next/integration/vitest.config.ts
  • packages/prisma-next/migrations/20260601T0100_install_eql_v3_bundle/end-contract.d.ts
  • packages/prisma-next/migrations/20260601T0100_install_eql_v3_bundle/end-contract.json
  • packages/prisma-next/migrations/20260601T0100_install_eql_v3_bundle/migration.json
  • packages/prisma-next/migrations/20260601T0100_install_eql_v3_bundle/migration.ts
  • packages/prisma-next/migrations/20260601T0100_install_eql_v3_bundle/ops.json
  • packages/prisma-next/migrations/refs/head.json
  • packages/prisma-next/package.json
  • packages/prisma-next/src/contract-authoring.ts
  • packages/prisma-next/src/exports/column-types.ts
  • packages/prisma-next/src/exports/control.ts
  • packages/prisma-next/src/exports/runtime.ts
  • packages/prisma-next/src/exports/stack.ts
  • packages/prisma-next/src/exports/v3.ts
  • packages/prisma-next/src/extension-metadata/codec-metadata.ts
  • packages/prisma-next/src/extension-metadata/constants-v3.ts
  • packages/prisma-next/src/extension-metadata/descriptor-meta.ts
  • packages/prisma-next/src/middleware/bulk-encrypt.ts
  • packages/prisma-next/src/migration/cipherstash-codec-v3.ts
  • packages/prisma-next/src/migration/eql-bundle-v3.ts
  • packages/prisma-next/src/stack/from-stack-v3.ts
  • packages/prisma-next/src/stack/from-stack.ts
  • packages/prisma-next/src/types/codec-types.ts
  • packages/prisma-next/src/types/operation-types.ts
  • packages/prisma-next/src/v3/barrel.ts
  • packages/prisma-next/src/v3/bulk-encrypt-v3.ts
  • packages/prisma-next/src/v3/catalog.ts
  • packages/prisma-next/src/v3/codec-runtime-v3.ts
  • packages/prisma-next/src/v3/derive-schemas-v3.ts
  • packages/prisma-next/src/v3/envelope-number.ts
  • packages/prisma-next/src/v3/from-stack-v3-validate.ts
  • packages/prisma-next/src/v3/operators-v3.ts
  • packages/prisma-next/src/v3/query-term.ts
  • packages/prisma-next/src/v3/runtime-v3.ts
  • packages/prisma-next/src/v3/sdk-adapter-v3.ts
  • packages/prisma-next/src/v3/wire-v3.ts
  • packages/prisma-next/test/authoring.test.ts
  • packages/prisma-next/test/bulk-encrypt-middleware.helpers.ts
  • packages/prisma-next/test/bulk-encrypt-middleware.test.ts
  • packages/prisma-next/test/bundling-isolation.test.ts
  • packages/prisma-next/test/column-types.test.ts
  • packages/prisma-next/test/descriptor.test.ts
  • packages/prisma-next/test/equality-trait-removal.test.ts
  • packages/prisma-next/test/live/bigint-live-pg.test.ts
  • packages/prisma-next/test/live/boolean-storage-live-pg.test.ts
  • packages/prisma-next/test/live/bulk-encrypt-live-pg.test.ts
  • packages/prisma-next/test/live/helpers/eql-v3.ts
  • packages/prisma-next/test/live/helpers/harness.ts
  • packages/prisma-next/test/live/helpers/live-gate.ts
  • packages/prisma-next/test/live/migration-apply-live-pg.test.ts
  • packages/prisma-next/test/live/operators-live-pg.test.ts
  • packages/prisma-next/test/live/operators-null-live-pg.test.ts
  • packages/prisma-next/test/live/side-by-side-clients-live-pg.test.ts
  • packages/prisma-next/test/operation-types.types.test-d.ts
  • packages/prisma-next/test/psl-interpretation-numeric.test.ts
  • packages/prisma-next/test/psl-interpretation-other-types.test.ts
  • packages/prisma-next/test/psl-interpretation.test.ts
  • packages/prisma-next/test/v3/bulk-encrypt-v3.test.ts
  • packages/prisma-next/test/v3/catalog-invariants.test.ts
  • packages/prisma-next/test/v3/catalog.test.ts
  • packages/prisma-next/test/v3/codec-runtime-v3.test.ts
  • packages/prisma-next/test/v3/codec-types-v3.test-d.ts
  • packages/prisma-next/test/v3/column-types.test-d.ts
  • packages/prisma-next/test/v3/constants-v3.test.ts
  • packages/prisma-next/test/v3/derive-schemas-v3.test.ts
  • packages/prisma-next/test/v3/envelope-number.test.ts
  • packages/prisma-next/test/v3/from-stack-divergence-v3.test.ts
  • packages/prisma-next/test/v3/from-stack-v3.test.ts
  • packages/prisma-next/test/v3/migration-v3.test.ts
  • packages/prisma-next/test/v3/operator-gating-v3.test.ts
  • packages/prisma-next/test/v3/operator-lowering-v3-equality.test.ts
  • packages/prisma-next/test/v3/operator-lowering-v3-json.test.ts
  • packages/prisma-next/test/v3/operator-lowering-v3-order-range.test.ts
  • packages/prisma-next/test/v3/operator-lowering-v3-text-search.test.ts
  • packages/prisma-next/test/v3/operator-lowering-v3.helpers.ts
  • packages/prisma-next/test/v3/properties.test.ts
  • packages/prisma-next/test/v3/runtime-v3.test.ts
  • packages/prisma-next/test/v3/sdk-adapter-v3.test.ts
  • packages/prisma-next/test/v3/wire-v3.test.ts
  • packages/prisma-next/tsconfig.json
  • packages/prisma-next/tsup.config.ts
  • packages/prisma-next/vitest.config.ts
  • packages/test-kit/src/adapter.ts

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:

  • Review on demand using usage pricing
📝 Walkthrough

Walkthrough

This PR adds the EQL v3 Prisma-next surface: concrete per-domain constructors, v3 contracts and runtime wiring, capability-gated operators, JSONB wire handling, BigInt support, a pinned bundle migration, updated examples, and extensive unit, type-level, property, and live Postgres tests.

Changes

EQL v3 Prisma-next

Layer / File(s) Summary
Contracts, migrations, and example wiring
.changeset/*, examples/prisma/*, packages/prisma-next/migrations/*
The example and generated contracts migrate from EQL v2 codecs to concrete public.eql_v3_* domains, with v2 and v3 bundle baselines.
Domain catalog and authoring contracts
packages/prisma-next/src/contract-authoring.ts, src/exports/column-types.ts, src/v3/catalog.ts, src/extension-metadata/*
V3 constructors and column factories are derived from the domain registry; legacy configurable constructors become explicit *V2 aliases.
V3 stack initialization and runtime
packages/prisma-next/src/stack/*, src/v3/*, src/exports/v3.ts
Adds cipherstashFromStackV3, schema derivation and validation, v3 envelopes, JSONB wire conversion, runtime descriptors, SDK adaptation, and bulk-encryption middleware.
Operators and type dispatch
packages/prisma-next/src/types/*, src/v3/operators-v3.ts
Adds capability-gated equality, range, free-text, JSON containment, and encrypted ordering operators using eql_v3.* SQL forms.
Validation and integration coverage
packages/prisma-next/test/*
Adds authoring, type-level, runtime, property-based, bundling, migration, and live Postgres coverage for the v3 surface.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related issues

Possibly related PRs

  • cipherstash/stack#444 — Shares Prisma-next authoring, column factory, and middleware surfaces extended here for EQL v3.
  • cipherstash/stack#472 — Provides README command execution coverage affected by the example’s EQL v3 README changes.

Suggested reviewers: coderdan

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is related to the main change: Prisma Next EQL v3 support. It is broad, but still identifies the feature area and version.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/prisma-next-eql-v3

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 18

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
packages/prisma-next/test/bundling-isolation.test.ts (1)

243-250: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Reject impure chunks even when their metadata fingerprint matches.

A chunk containing every required marker is allowlisted even if it also contains SDK, migration, or wire-plane code. Add a negative forbidden-marker check before accepting it.

Proposed purity check
 function isAllowedSharedChunk(chunk: string): boolean {
   if (!SHARED_CHUNK_PATTERN.test(chunk)) {
     return false
   }
   const body = readChunk(chunk).body
+  const forbidden = [
+    ...CONTROL_FORBIDDEN,
+    ...RUNTIME_FORBIDDEN,
+    ...V2_WIRE_MARKERS,
+    ...V3_WIRE_MARKERS,
+  ]
+  if (forbidden.some((marker) => body.includes(marker))) {
+    return false
+  }
   return ALLOWED_SHARED_CHUNK_MARKER_SETS.some((markers) =>
     markers.every((marker) => body.includes(marker)),
   )
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/prisma-next/test/bundling-isolation.test.ts` around lines 243 - 250,
The isAllowedSharedChunk function currently allowlists chunks based only on
required markers. Add a forbidden-marker check after reading the chunk body and
reject the chunk if it contains SDK, migration, or wire-plane markers, before
evaluating ALLOWED_SHARED_CHUNK_MARKER_SETS; preserve the existing
required-marker validation for otherwise pure chunks.
examples/prisma/migrations/app/20260714T2142_initial/end-contract.json (1)

21-102: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Regenerate the contract with EQL v3 descriptors
domain and storage now reference cipherstash/eql-v3/*, but extensionPacks.cipherstash doesn’t declare matching descriptors for those codec IDs. Add the v3 descriptors (and keep the existing ones too if both are intentional).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@examples/prisma/migrations/app/20260714T2142_initial/end-contract.json`
around lines 21 - 102, Regenerate end-contract.json so
extensionPacks.cipherstash declares descriptors for every cipherstash/eql-v3
codec referenced by the contract, including the bigint, date, text_search,
boolean, json, and double descriptors shown in the model fields. Preserve
existing descriptor entries when both legacy and v3 codecs are intentional, and
ensure each descriptor’s codecId and capabilities match its referenced type.
🧹 Nitpick comments (3)
packages/prisma-next/test/v3/runtime-v3.test.ts (2)

97-106: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Verify SDK binding rather than codec object identity.

Distinct codec objects can still reference the same SDK or shared state. Invoke each codec’s decrypt/read path and assert that only its corresponding SDK mock is called.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/prisma-next/test/v3/runtime-v3.test.ts` around lines 97 - 106,
Replace the object-identity assertion in the
createCipherstashV3RuntimeDescriptor isolation test with behavioral
verification: create distinct SDK mocks, invoke each codec’s decrypt/read path,
and assert each invocation calls only its corresponding SDK mock. Keep the
per-descriptor setup and ensure cross-tenant SDK calls are not observed.

24-25: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Import the v3 surface through the public entry point. The ../../src/v3/* imports can miss a broken ./v3 export map or barrel; use ../../src/exports/v3 for createCipherstashV3RuntimeDescriptor and cipherstashV3QueryOperations instead.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/prisma-next/test/v3/runtime-v3.test.ts` around lines 24 - 25, Update
the imports in runtime-v3.test.ts to obtain createCipherstashV3RuntimeDescriptor
and cipherstashV3QueryOperations from the public ../../src/exports/v3 entry
point, removing the direct ../../src/v3/* imports so the test exercises the v3
export surface.

Source: Coding guidelines

packages/prisma-next/test/v3/bulk-encrypt-v3.test.ts (1)

26-34: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Exercise the new v3 public entry point instead of source internals.

Import publicly exported v3 symbols through ../../src/exports/v3 (or the package self-reference), retaining internal imports only for unavoidable fixture setup. This also tests the new export wiring.

As per coding guidelines, tests should prefer public APIs and avoid private internals.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/prisma-next/test/v3/bulk-encrypt-v3.test.ts` around lines 26 - 34,
Update the test imports for v3 symbols such as bulkEncryptMiddlewareV3,
EncryptedNumber, markV3QueryTerm, and v3ToDriver to use the public exports from
../../src/exports/v3 or the package self-reference. Retain direct internal
imports only for fixture setup symbols that are not publicly exported, and
ensure the test exercises the public entry-point wiring.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@examples/prisma/README.md`:
- Around line 7-14: Update the domain column in the table near
EncryptedTextSearch to use fully qualified v3 names by adding the public. schema
prefix to the remaining five rows: salary, accountId, birthday, emailVerified,
and preferences.

In `@examples/prisma/src/prisma/contract.d.ts`:
- Around line 60-78: Regenerate the extension-pack metadata in the contract,
including the sections around the field type declarations and lines 339-579, so
every codec referenced by the User fields uses the corresponding
cipherstash/eql-v3 codec instance and metadata. Replace remaining v2 codec
registrations, eql_v2_encrypted storage references, and v2 imports such as
EncryptedDouble with their generated v3 equivalents, while preserving the
existing contract structure.

In `@examples/prisma/src/prisma/contract.json`:
- Around line 21-28: Regenerate the CipherStash extension-pack metadata so
extensionPacks.cipherstash.types.codecTypes.codecInstances includes the active
cipherstash/eql-v3 codec IDs and types.storage maps them to eql_v3_encrypted
instead of the obsolete v2 entries. Ensure the generated contract is consistent
with the v3 codec IDs used by the active fields.

In `@examples/prisma/test/e2e/bool.e2e.test.ts`:
- Line 83: Update the capability probes in the bool e2e test, including the
emailVerified probe and the as-never call, to avoid type-erasing assertions. Use
a narrowly typed helper for the intentional runtime boundary, or add a focused
Biome suppression with a clear rationale if the assertions are required.

In `@packages/prisma-next/package.json`:
- Around line 63-66: Add the missing require field to the "./v3" export in the
package exports map, pointing to the appropriate compiled CommonJS artifact
while preserving the existing types and import entries.

In `@packages/prisma-next/src/exports/column-types.ts`:
- Around line 95-112: Update v3Authored so each returned factory invocation
creates and returns a fresh V3ColumnDescriptor, including independently cloned
nested typeParams and capabilities, rather than reusing the captured descriptor
object. Preserve the descriptor values derived from probe while ensuring
mutations to one result cannot affect later results, and add a test verifying
mutation isolation.

In `@packages/prisma-next/src/v3/bulk-encrypt-v3.ts`:
- Around line 82-84: Update the flow around stampRoutingKeysFromAst,
collectV3Targets, and the encryption handling at the referenced ranges so
routing is preserved per ParamRef rather than stored only on the shared envelope
instance. Ensure each parameter’s aliases receive independent routing metadata,
or reject conflicting reuse before encryption, so targets for different columns
cannot inherit the last routing stamp.

In `@packages/prisma-next/src/v3/operators-v3.ts`:
- Around line 677-684: Remove the cipherstashNotIlike operator from the operator
definitions, or replace its Bloom-filter negation with an exact
decrypt-and-post-filter implementation before exposing negative free-text
search. Do not retain the NOT eql_v3.contains predicate, and preserve the
existing positive free-text search behavior.

In `@packages/prisma-next/src/v3/sdk-adapter-v3.ts`:
- Around line 272-284: Update bulkDecrypt to validate that the unwrapped client
response contains exactly one entry per input ciphertext before mapping results.
If the response length differs from ciphertexts.length, throw an error instead
of returning a truncated or misaligned result; preserve the existing per-entry
error handling and plaintext conversion for valid responses.
- Around line 217-221: Add the repository-standard Biome suppression for
deliberate as never casts at both encryptQuery call sites in the SDK adapter,
preserving the existing rationale comments and behavior. Apply the suppression
only to these intentional assertions, or replace them with a typed adapter if
that is the established pattern.

In `@packages/prisma-next/src/v3/wire-v3.ts`:
- Around line 28-31: The v3ToDriver function can return undefined when
JSON.stringify receives an unsupported top-level value, violating its string |
null contract. Update the JSON.stringify result handling in v3ToDriver to
convert an undefined result to null while preserving serialized strings and the
existing null/undefined input behavior.

In `@packages/prisma-next/test/bundling-isolation.test.ts`:
- Around line 295-335: Update the bundling-isolation tests for the v3 and
middleware entries to scan every chunk returned by collectGraph(), not just each
entry body. Check the complete graphs against V2_WIRE_MARKERS and all six v2
codec factory symbols, including factories beyond createCipherstashStringCodec,
and fail with diagnostics identifying the offending chunks and markers.

In `@packages/prisma-next/test/live/bigint-live-pg.test.ts`:
- Around line 10-13: Load dotenv via a direct top-level import before all other
imports in packages/prisma-next/test/live/bigint-live-pg.test.ts (lines 10-13),
examples/prisma/test/e2e/str-range.e2e.test.ts (lines 18-26), and
packages/prisma-next/test/live/operators-live-pg.test.ts (lines 10-16), ensuring
environment variables are available before gates and database-dependent modules
initialize.

In `@packages/prisma-next/test/live/boolean-storage-live-pg.test.ts`:
- Around line 9-10: Load environment configuration before the live-test imports
by adding dotenv/config as the first import in
packages/prisma-next/test/live/boolean-storage-live-pg.test.ts (lines 9-10),
packages/prisma-next/test/live/migration-apply-live-pg.test.ts (lines 16-20),
and packages/prisma-next/test/live/side-by-side-clients-live-pg.test.ts (lines
17-20); no other changes are needed.

In `@packages/prisma-next/test/live/helpers/harness.ts`:
- Around line 208-224: Update the row-to-parameter construction in the rows.map
callback so every row is bound according to the canonical columnNames order
derived from the first row. Iterate columnNames after binding the row id,
retrieve each row’s corresponding cell, and append its value using that column’s
codec; preserve the existing AST references while handling missing columns
consistently with the expected query shape.

In `@packages/prisma-next/test/live/migration-apply-live-pg.test.ts`:
- Around line 66-83: Ensure the test setup around installEqlV3IfNeeded uses an
isolated fresh database or explicitly removes the pinned EQL v3 installation
before invoking it, so the helper cannot return early on a reused database.
Preserve the existing byte-for-byte assertions in the test and verify that the
customer-facing migration SQL is actually executed.

In `@packages/prisma-next/test/v3/bulk-encrypt-v3.test.ts`:
- Around line 285-304: Update the aborted-signal test for
bulkEncryptMiddlewareV3 so the beforeExecute rejection explicitly matches the
advertised RUNTIME.ABORTED code or message, rather than accepting any thrown
exception. Keep the existing createCtx signal setup and sdk.bulkEncryptCalls
empty assertion unchanged.

In `@packages/prisma-next/test/v3/operator-lowering-v3-json.test.ts`:
- Around line 89-96: Update the test case “rejects a null needle with an
isNull() hint” to assert that the thrown EncryptionOperatorError message
includes the promised isNull() guidance, while retaining the existing
error-class assertion.

---

Outside diff comments:
In `@examples/prisma/migrations/app/20260714T2142_initial/end-contract.json`:
- Around line 21-102: Regenerate end-contract.json so extensionPacks.cipherstash
declares descriptors for every cipherstash/eql-v3 codec referenced by the
contract, including the bigint, date, text_search, boolean, json, and double
descriptors shown in the model fields. Preserve existing descriptor entries when
both legacy and v3 codecs are intentional, and ensure each descriptor’s codecId
and capabilities match its referenced type.

In `@packages/prisma-next/test/bundling-isolation.test.ts`:
- Around line 243-250: The isAllowedSharedChunk function currently allowlists
chunks based only on required markers. Add a forbidden-marker check after
reading the chunk body and reject the chunk if it contains SDK, migration, or
wire-plane markers, before evaluating ALLOWED_SHARED_CHUNK_MARKER_SETS; preserve
the existing required-marker validation for otherwise pure chunks.

---

Nitpick comments:
In `@packages/prisma-next/test/v3/bulk-encrypt-v3.test.ts`:
- Around line 26-34: Update the test imports for v3 symbols such as
bulkEncryptMiddlewareV3, EncryptedNumber, markV3QueryTerm, and v3ToDriver to use
the public exports from ../../src/exports/v3 or the package self-reference.
Retain direct internal imports only for fixture setup symbols that are not
publicly exported, and ensure the test exercises the public entry-point wiring.

In `@packages/prisma-next/test/v3/runtime-v3.test.ts`:
- Around line 97-106: Replace the object-identity assertion in the
createCipherstashV3RuntimeDescriptor isolation test with behavioral
verification: create distinct SDK mocks, invoke each codec’s decrypt/read path,
and assert each invocation calls only its corresponding SDK mock. Keep the
per-descriptor setup and ensure cross-tenant SDK calls are not observed.
- Around line 24-25: Update the imports in runtime-v3.test.ts to obtain
createCipherstashV3RuntimeDescriptor and cipherstashV3QueryOperations from the
public ../../src/exports/v3 entry point, removing the direct ../../src/v3/*
imports so the test exercises the v3 export surface.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: efc246fe-e8f1-4c75-ad03-04303692ca88

📥 Commits

Reviewing files that changed from the base of the PR and between a8c9602 and a0f5ee0.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (108)
  • .changeset/eql-v3-prisma-next.md
  • .changeset/eql-v3-stack-domain-registry-exports.md
  • .changeset/prisma-example-eql-v3.md
  • AGENTS.md
  • examples/prisma/README.md
  • examples/prisma/migrations/app/20260709T1034_initial/migration.json
  • examples/prisma/migrations/app/20260709T1034_initial/migration.ts
  • examples/prisma/migrations/app/20260709T1034_initial/ops.json
  • examples/prisma/migrations/app/20260714T2142_initial/end-contract.d.ts
  • examples/prisma/migrations/app/20260714T2142_initial/end-contract.json
  • examples/prisma/migrations/app/20260714T2142_initial/migration.json
  • examples/prisma/migrations/app/20260714T2142_initial/migration.ts
  • examples/prisma/migrations/app/20260714T2142_initial/ops.json
  • examples/prisma/migrations/cipherstash/20260601T0100_install_eql_v3_bundle/migration.json
  • examples/prisma/migrations/cipherstash/20260601T0100_install_eql_v3_bundle/ops.json
  • examples/prisma/migrations/cipherstash/refs/head.json
  • examples/prisma/prisma/schema.prisma
  • examples/prisma/src/db.ts
  • examples/prisma/src/index.ts
  • examples/prisma/src/prisma/contract.d.ts
  • examples/prisma/src/prisma/contract.json
  • examples/prisma/test/e2e/README.md
  • examples/prisma/test/e2e/bigint.e2e.test.ts
  • examples/prisma/test/e2e/bool.e2e.test.ts
  • examples/prisma/test/e2e/date.e2e.test.ts
  • examples/prisma/test/e2e/global-setup.ts
  • examples/prisma/test/e2e/json.e2e.test.ts
  • examples/prisma/test/e2e/mixed.e2e.test.ts
  • examples/prisma/test/e2e/num.e2e.test.ts
  • examples/prisma/test/e2e/str-range.e2e.test.ts
  • packages/prisma-next/migrations/20260601T0100_install_eql_v3_bundle/end-contract.d.ts
  • packages/prisma-next/migrations/20260601T0100_install_eql_v3_bundle/end-contract.json
  • packages/prisma-next/migrations/20260601T0100_install_eql_v3_bundle/migration.json
  • packages/prisma-next/migrations/20260601T0100_install_eql_v3_bundle/migration.ts
  • packages/prisma-next/migrations/20260601T0100_install_eql_v3_bundle/ops.json
  • packages/prisma-next/migrations/refs/head.json
  • packages/prisma-next/package.json
  • packages/prisma-next/src/contract-authoring.ts
  • packages/prisma-next/src/exports/column-types.ts
  • packages/prisma-next/src/exports/control.ts
  • packages/prisma-next/src/exports/runtime.ts
  • packages/prisma-next/src/exports/stack.ts
  • packages/prisma-next/src/exports/v3.ts
  • packages/prisma-next/src/extension-metadata/constants-v3.ts
  • packages/prisma-next/src/middleware/bulk-encrypt.ts
  • packages/prisma-next/src/migration/cipherstash-codec-v3.ts
  • packages/prisma-next/src/migration/eql-bundle-v3.ts
  • packages/prisma-next/src/stack/from-stack-v3.ts
  • packages/prisma-next/src/stack/from-stack.ts
  • packages/prisma-next/src/types/codec-types.ts
  • packages/prisma-next/src/types/operation-types.ts
  • packages/prisma-next/src/v3/barrel.ts
  • packages/prisma-next/src/v3/bulk-encrypt-v3.ts
  • packages/prisma-next/src/v3/catalog.ts
  • packages/prisma-next/src/v3/codec-runtime-v3.ts
  • packages/prisma-next/src/v3/derive-schemas-v3.ts
  • packages/prisma-next/src/v3/envelope-number.ts
  • packages/prisma-next/src/v3/from-stack-v3-validate.ts
  • packages/prisma-next/src/v3/operators-v3.ts
  • packages/prisma-next/src/v3/query-term.ts
  • packages/prisma-next/src/v3/runtime-v3.ts
  • packages/prisma-next/src/v3/sdk-adapter-v3.ts
  • packages/prisma-next/src/v3/wire-v3.ts
  • packages/prisma-next/test/authoring.test.ts
  • packages/prisma-next/test/bulk-encrypt-middleware.helpers.ts
  • packages/prisma-next/test/bulk-encrypt-middleware.test.ts
  • packages/prisma-next/test/bundling-isolation.test.ts
  • packages/prisma-next/test/column-types.test.ts
  • packages/prisma-next/test/descriptor.test.ts
  • packages/prisma-next/test/equality-trait-removal.test.ts
  • packages/prisma-next/test/live/bigint-live-pg.test.ts
  • packages/prisma-next/test/live/boolean-storage-live-pg.test.ts
  • packages/prisma-next/test/live/bulk-encrypt-live-pg.test.ts
  • packages/prisma-next/test/live/helpers/eql-v3.ts
  • packages/prisma-next/test/live/helpers/harness.ts
  • packages/prisma-next/test/live/helpers/live-gate.ts
  • packages/prisma-next/test/live/migration-apply-live-pg.test.ts
  • packages/prisma-next/test/live/operators-live-pg.test.ts
  • packages/prisma-next/test/live/operators-null-live-pg.test.ts
  • packages/prisma-next/test/live/side-by-side-clients-live-pg.test.ts
  • packages/prisma-next/test/operation-types.types.test-d.ts
  • packages/prisma-next/test/psl-interpretation-numeric.test.ts
  • packages/prisma-next/test/psl-interpretation-other-types.test.ts
  • packages/prisma-next/test/psl-interpretation.test.ts
  • packages/prisma-next/test/v3/bulk-encrypt-v3.test.ts
  • packages/prisma-next/test/v3/catalog-invariants.test.ts
  • packages/prisma-next/test/v3/catalog.test.ts
  • packages/prisma-next/test/v3/codec-runtime-v3.test.ts
  • packages/prisma-next/test/v3/codec-types-v3.test-d.ts
  • packages/prisma-next/test/v3/column-types.test-d.ts
  • packages/prisma-next/test/v3/constants-v3.test.ts
  • packages/prisma-next/test/v3/derive-schemas-v3.test.ts
  • packages/prisma-next/test/v3/envelope-number.test.ts
  • packages/prisma-next/test/v3/from-stack-divergence-v3.test.ts
  • packages/prisma-next/test/v3/from-stack-v3.test.ts
  • packages/prisma-next/test/v3/migration-v3.test.ts
  • packages/prisma-next/test/v3/operator-gating-v3.test.ts
  • packages/prisma-next/test/v3/operator-lowering-v3-equality.test.ts
  • packages/prisma-next/test/v3/operator-lowering-v3-json.test.ts
  • packages/prisma-next/test/v3/operator-lowering-v3-order-range.test.ts
  • packages/prisma-next/test/v3/operator-lowering-v3-text-search.test.ts
  • packages/prisma-next/test/v3/operator-lowering-v3.helpers.ts
  • packages/prisma-next/test/v3/properties.test.ts
  • packages/prisma-next/test/v3/runtime-v3.test.ts
  • packages/prisma-next/test/v3/sdk-adapter-v3.test.ts
  • packages/prisma-next/test/v3/wire-v3.test.ts
  • packages/prisma-next/tsup.config.ts
  • packages/stack/src/eql/v3/index.ts
💤 Files with no reviewable changes (3)
  • examples/prisma/migrations/app/20260709T1034_initial/ops.json
  • examples/prisma/migrations/app/20260709T1034_initial/migration.json
  • examples/prisma/migrations/app/20260709T1034_initial/migration.ts

Comment thread examples/prisma/README.md Outdated
Comment thread examples/prisma/src/prisma/contract.d.ts
Comment thread examples/prisma/src/prisma/contract.json
Comment thread examples/prisma/test/e2e/bool.e2e.test.ts Outdated
Comment thread packages/prisma-next/package.json
Comment on lines +9 to +10
import type postgres from 'postgres'
import { afterAll, beforeAll, expect, it } from 'vitest'

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Load environment configuration before importing the live-test harness.

These tests use liveConnection() but do not import dotenv/config at the top.

  • packages/prisma-next/test/live/boolean-storage-live-pg.test.ts#L9-L10: add import 'dotenv/config' before other imports.
  • packages/prisma-next/test/live/migration-apply-live-pg.test.ts#L16-L20: add import 'dotenv/config' before other imports.
  • packages/prisma-next/test/live/side-by-side-clients-live-pg.test.ts#L17-L20: add import 'dotenv/config' before other imports.

As per coding guidelines, test files must import dotenv/config at the top when environment variables are required.

📍 Affects 3 files
  • packages/prisma-next/test/live/boolean-storage-live-pg.test.ts#L9-L10 (this comment)
  • packages/prisma-next/test/live/migration-apply-live-pg.test.ts#L16-L20
  • packages/prisma-next/test/live/side-by-side-clients-live-pg.test.ts#L17-L20
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/prisma-next/test/live/boolean-storage-live-pg.test.ts` around lines
9 - 10, Load environment configuration before the live-test imports by adding
dotenv/config as the first import in
packages/prisma-next/test/live/boolean-storage-live-pg.test.ts (lines 9-10),
packages/prisma-next/test/live/migration-apply-live-pg.test.ts (lines 16-20),
and packages/prisma-next/test/live/side-by-side-clients-live-pg.test.ts (lines
17-20); no other changes are needed.

Source: Coding guidelines

Comment on lines +208 to +224
const first = rows[0]
if (!first) return
const columnNames = ['id', ...Object.keys(first.cells)]

const params: unknown[] = []
const astRows = rows.map((row) => {
const astRow: Record<string, ParamRef> = {}
const idRef = ParamRef.of(row.id, { codec: { codecId: PG_TEXT } })
astRow.id = idRef
params.push(row.id)
for (const [column, cell] of Object.entries(row.cells)) {
const ref = ParamRef.of(cell.value, {
codec: { codecId: cell.codecId },
})
astRow[column] = ref
params.push(cell.value)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Bind every row using the canonical column order.

columnNames comes from the first row, but subsequent parameters follow each row’s own Object.entries order. A differently ordered or missing key can bind encrypted values to the wrong SQL columns and invalidate live-test results.

Proposed fix
   const first = rows[0]
   if (!first) return
-  const columnNames = ['id', ...Object.keys(first.cells)]
+  const cellColumns = Object.keys(first.cells)
+  const columnNames = ['id', ...cellColumns]

   const params: unknown[] = []
   const astRows = rows.map((row) => {
+    const rowColumns = Object.keys(row.cells)
+    if (
+      rowColumns.length !== cellColumns.length ||
+      cellColumns.some((column) => !(column in row.cells))
+    ) {
+      throw new Error('insertEncryptedRows: all rows must have identical columns')
+    }
+
     const astRow: Record<string, ParamRef> = {}
     const idRef = ParamRef.of(row.id, { codec: { codecId: PG_TEXT } })
     astRow.id = idRef
     params.push(row.id)
-    for (const [column, cell] of Object.entries(row.cells)) {
+    for (const column of cellColumns) {
+      const cell = row.cells[column]
+      if (!cell) throw new Error(`insertEncryptedRows: missing column ${column}`)
       const ref = ParamRef.of(cell.value, {
         codec: { codecId: cell.codecId },
       })
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const first = rows[0]
if (!first) return
const columnNames = ['id', ...Object.keys(first.cells)]
const params: unknown[] = []
const astRows = rows.map((row) => {
const astRow: Record<string, ParamRef> = {}
const idRef = ParamRef.of(row.id, { codec: { codecId: PG_TEXT } })
astRow.id = idRef
params.push(row.id)
for (const [column, cell] of Object.entries(row.cells)) {
const ref = ParamRef.of(cell.value, {
codec: { codecId: cell.codecId },
})
astRow[column] = ref
params.push(cell.value)
}
const first = rows[0]
if (!first) return
const cellColumns = Object.keys(first.cells)
const columnNames = ['id', ...cellColumns]
const params: unknown[] = []
const astRows = rows.map((row) => {
const rowColumns = Object.keys(row.cells)
if (
rowColumns.length !== cellColumns.length ||
cellColumns.some((column) => !(column in row.cells))
) {
throw new Error('insertEncryptedRows: all rows must have identical columns')
}
const astRow: Record<string, ParamRef> = {}
const idRef = ParamRef.of(row.id, { codec: { codecId: PG_TEXT } })
astRow.id = idRef
params.push(row.id)
for (const column of cellColumns) {
const cell = row.cells[column]
if (!cell) throw new Error(`insertEncryptedRows: missing column ${column}`)
const ref = ParamRef.of(cell.value, {
codec: { codecId: cell.codecId },
})
astRow[column] = ref
params.push(cell.value)
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/prisma-next/test/live/helpers/harness.ts` around lines 208 - 224,
Update the row-to-parameter construction in the rows.map callback so every row
is bound according to the canonical columnNames order derived from the first
row. Iterate columnNames after binding the row id, retrieve each row’s
corresponding cell, and append its value using that column’s codec; preserve the
existing AST references while handling missing columns consistently with the
expected query shape.

Comment on lines +66 to +83
beforeAll(async () => {
sql = liveConnection()
await installEqlV3IfNeeded(sql)
}, 240_000)

afterAll(async () => {
if (sql) await sql.end()
})

it('the applied SQL is byte-for-byte the shipped migration op, carrying the v3 invariant', () => {
const op = readOpsFixture()
expect(op.id).toBe('cipherstash.install-eql-v3-bundle')
expect(op.invariantId).toBe(CIPHERSTASH_V3_INVARIANTS.installBundle)
expect(op.operationClass).toBe('data')
expect(op.execute).toHaveLength(1)
// Byte identity: what installEqlV3IfNeeded just applied IS the
// migration bundle a customer's `prisma-next migrate` applies.
expect(op.execute[0]?.sql).toBe(readInstallSql())

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Ensure this test actually executes the migration SQL.

installEqlV3IfNeeded() returns early when the pinned version is already installed. On a reused live database, this test only compares fixture bytes and can pass without applying the customer-facing migration. Run this suite against an isolated fresh database, or explicitly establish a clean pre-install state before calling the helper.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/prisma-next/test/live/migration-apply-live-pg.test.ts` around lines
66 - 83, Ensure the test setup around installEqlV3IfNeeded uses an isolated
fresh database or explicitly removes the pinned EQL v3 installation before
invoking it, so the helper cannot return early on a reused database. Preserve
the existing byte-for-byte assertions in the test and verify that the
customer-facing migration SQL is actually executed.

Comment on lines +285 to +304
it('short-circuits with RUNTIME.ABORTED before any SDK call when already aborted', async () => {
const sdk = makeV3Sdk()
const middleware = bulkEncryptMiddlewareV3(sdk)
const plan = buildInsertPlan(
'user',
[{ email: EncryptedString.from('alice@example.com') }],
V3_TEXT_SEARCH,
)
const params = createSqlParamRefMutator(plan)
const controller = new AbortController()
controller.abort()

await expect(
middleware.beforeExecute?.(
plan,
createCtx({ signal: controller.signal }),
params,
),
).rejects.toThrow()
expect(sdk.bulkEncryptCalls).toEqual([])

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Assert the advertised RUNTIME.ABORTED failure.

rejects.toThrow() passes for any exception, including unrelated middleware regressions. Match the expected error code or message while retaining the no-SDK-call assertion.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/prisma-next/test/v3/bulk-encrypt-v3.test.ts` around lines 285 - 304,
Update the aborted-signal test for bulkEncryptMiddlewareV3 so the beforeExecute
rejection explicitly matches the advertised RUNTIME.ABORTED code or message,
rather than accepting any thrown exception. Keep the existing createCtx signal
setup and sdk.bulkEncryptCalls empty assertion unchanged.

Comment on lines +89 to +96
it('rejects a null needle with an isNull() hint', () => {
expect(() =>
callOperator(
getOperator('cipherstashJsonContains'),
columnAccessorV3(TABLE, 'payload', JSON_CODEC_ID),
null,
),
).toThrow(EncryptionOperatorError)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Assert the promised isNull() hint.

This test only verifies the error class, so the diagnostic named by the test can disappear unnoticed.

Proposed test adjustment
-    expect(() =>
+    const invoke = () =>
       callOperator(
         getOperator('cipherstashJsonContains'),
         columnAccessorV3(TABLE, 'payload', JSON_CODEC_ID),
         null,
-      ),
-    ).toThrow(EncryptionOperatorError)
+      )
+    expect(invoke).toThrow(EncryptionOperatorError)
+    expect(invoke).toThrow(/isNull\(\)/)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
it('rejects a null needle with an isNull() hint', () => {
expect(() =>
callOperator(
getOperator('cipherstashJsonContains'),
columnAccessorV3(TABLE, 'payload', JSON_CODEC_ID),
null,
),
).toThrow(EncryptionOperatorError)
it('rejects a null needle with an isNull() hint', () => {
const invoke = () =>
callOperator(
getOperator('cipherstashJsonContains'),
columnAccessorV3(TABLE, 'payload', JSON_CODEC_ID),
null,
)
expect(invoke).toThrow(EncryptionOperatorError)
expect(invoke).toThrow(/isNull\(\)/)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/prisma-next/test/v3/operator-lowering-v3-json.test.ts` around lines
89 - 96, Update the test case “rejects a null needle with an isNull() hint” to
assert that the thrown EncryptionOperatorError message includes the promised
isNull() guidance, while retaining the existing error-class assertion.

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

Adds first-class EQL v3 support to @cipherstash/prisma-next (plus the Prisma example app), introducing a v3-only entry point and runtime/codec/operator surface that targets public.eql_v3_* domain-backed JSONB wire format, along with migrations to install the EQL v3 bundle.

Changes:

  • Add @cipherstash/prisma-next/v3 entry point (cipherstashFromStackV3) with v3 runtime descriptor, codec catalog/derivation, query-term routing, and JSONB wire helpers.
  • Introduce v3 migrations/baseline install plumbing (@cipherstash/eql/sql sourced SQL baked into ops) and update control/runtime exports accordingly.
  • Convert the Prisma example + e2e harness to v3 domains/operators (including lossless bigint beyond Number.MAX_SAFE_INTEGER and v3 order-term sorting).

Reviewed changes

Copilot reviewed 103 out of 109 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
pnpm-lock.yaml Adds lockfile entries for v3 migration SQL source and live-test deps.
packages/stack/src/eql/v3/index.ts Re-exports v3 domain registry utilities for downstream adapters.
packages/prisma-next/tsup.config.ts Builds the new src/exports/v3.ts entry.
packages/prisma-next/test/v3/wire-v3.test.ts Unit tests for v3 JSONB wire encoding/decoding behavior.
packages/prisma-next/test/v3/runtime-v3.test.ts Pins v3 runtime descriptor identity, codecs, traits, and operation registration.
packages/prisma-next/test/v3/operator-lowering-v3-text-search.test.ts Tests v3 lowering for free-text token containment operators.
packages/prisma-next/test/v3/operator-lowering-v3-json.test.ts Tests v3 lowering + operand validation for encrypted JSON containment.
packages/prisma-next/test/v3/operator-gating-v3.test.ts Tests capability gating and v2/v3 descriptor co-registration collision behavior.
packages/prisma-next/test/v3/migration-v3.test.ts Asserts v3 baseline migration artifacts and invariant-only edge semantics.
packages/prisma-next/test/v3/from-stack-v3.test.ts Validates v3-only entry point rejection paths and schema divergence checks.
packages/prisma-next/test/v3/from-stack-divergence-v3.test.ts Tests exact-domain override divergence rules for v3 schemas.
packages/prisma-next/test/v3/envelope-number.test.ts Introduces/validates v3 EncryptedNumber envelope semantics and redaction.
packages/prisma-next/test/v3/derive-schemas-v3.test.ts Tests contract→v3 schema derivation via nativeType→factory mapping.
packages/prisma-next/test/v3/constants-v3.test.ts Pins v3 codec id sets, traits derivation, and extension identity constants.
packages/prisma-next/test/v3/column-types.test-d.ts Type-level drift guards for v3 column-type factory literal narrowing.
packages/prisma-next/test/v3/codec-types-v3.test-d.ts Type-level drift guards for v3 CodecTypes entries/traits and envelope outputs.
packages/prisma-next/test/v3/catalog.test.ts Tests derived v3 domain catalog invariants and factory mappings.
packages/prisma-next/test/v3/catalog-invariants.test.ts Pins non-structural v3 catalog invariants against stack domain registry.
packages/prisma-next/test/psl-interpretation-other-types.test.ts Renames PSL constructors to explicit *V2 forms for non-numeric v2 types.
packages/prisma-next/test/psl-interpretation-numeric.test.ts Renames PSL constructors to explicit *V2 forms for v2 numeric types.
packages/prisma-next/test/operation-types.types.test-d.ts Extends type-level operator dispatch tests to cover v3 marker-trait routing.
packages/prisma-next/test/live/operators-null-live-pg.test.ts Live-PG suite for v3 NULL handling and operator operand rejection behavior.
packages/prisma-next/test/live/migration-apply-live-pg.test.ts Live-PG suite asserting v3 bundle apply matches shipped migration bytes + postchecks.
packages/prisma-next/test/live/helpers/live-gate.ts Adds env/profile gating for v3 live PG suites.
packages/prisma-next/test/live/helpers/eql-v3.ts Adds idempotent, advisory-lock-guarded v3 install helper for live suites.
packages/prisma-next/test/live/bulk-encrypt-live-pg.test.ts Live-PG test for v3 bulk-encrypt middleware end-to-end behavior.
packages/prisma-next/test/live/boolean-storage-live-pg.test.ts Live-PG coverage for storage-only boolean domain and operator refusals.
packages/prisma-next/test/live/bigint-live-pg.test.ts Live-PG coverage for lossless bigint and ordering/range semantics.
packages/prisma-next/test/equality-trait-removal.test.ts Extends “no framework built-in traits” invariant checks to v3 descriptors.
packages/prisma-next/test/descriptor.test.ts Updates descriptor tests to include v3 baseline migration and head ref invariants.
packages/prisma-next/test/bulk-encrypt-middleware.test.ts Refactors v2 middleware tests to share harness with new v3 suite.
packages/prisma-next/test/bulk-encrypt-middleware.helpers.ts New shared harness utilities for bulk-encrypt middleware tests.
packages/prisma-next/src/v3/wire-v3.ts Implements v3 plain-JSONB wire encoding/decoding helpers.
packages/prisma-next/src/v3/runtime-v3.ts Adds v3 runtime extension descriptor composition (codecs + query ops).
packages/prisma-next/src/v3/query-term.ts Introduces the v3 query-term mark/inspection seam + v3 operator error type.
packages/prisma-next/src/v3/from-stack-v3-validate.ts Adds v3 schema override validation based on exact public.eql_v3_* domain identity.
packages/prisma-next/src/v3/envelope-number.ts Adds v3 EncryptedNumber envelope (sibling of v2 EncryptedDouble).
packages/prisma-next/src/v3/derive-schemas-v3.ts Derives v3 stack schemas from contract nativeType via catalog factories.
packages/prisma-next/src/v3/catalog.ts Derives v3 domain metadata/catalog from stack DOMAIN_REGISTRY.
packages/prisma-next/src/v3/barrel.ts Exposes v3 user-facing envelopes via v3 subtree barrel exports.
packages/prisma-next/src/types/operation-types.ts Adds v3 marker-trait-based type-level operator visibility rules.
packages/prisma-next/src/stack/from-stack.ts Documents that this entry point is v2-only and points to v3 sibling.
packages/prisma-next/src/stack/from-stack-v3.ts Adds v3-only one-call setup (cipherstashFromStackV3).
packages/prisma-next/src/migration/eql-bundle-v3.ts Re-exports v3 bundle SQL/manifest from @cipherstash/eql/sql for emit-time baking.
packages/prisma-next/src/migration/cipherstash-codec-v3.ts Adds v3 control-plane hooks (schema stripping only; no search-config ops).
packages/prisma-next/src/middleware/bulk-encrypt.ts Exports AST routing-key stamping helper for reuse by v3 middleware.
packages/prisma-next/src/exports/v3.ts New consolidated @cipherstash/prisma-next/v3 export surface.
packages/prisma-next/src/exports/stack.ts Adds v3 stack entry point exports alongside existing v2 stack exports.
packages/prisma-next/src/exports/runtime.ts Adds v3 runtime exports (descriptor, ops, wire helpers, envelopes).
packages/prisma-next/src/exports/control.ts Adds v3 baseline migration + hooks into the control descriptor.
packages/prisma-next/package.json Adds ./v3 export, live test script, and devDependencies for v3 emit/live suites.
packages/prisma-next/migrations/refs/head.json Adds v3 bundle invariant to head ref.
packages/prisma-next/migrations/20260601T0100_install_eql_v3_bundle/migration.ts Adds v3 baseline migration (invariant-only self-edge op).
packages/prisma-next/migrations/20260601T0100_install_eql_v3_bundle/migration.json Adds emitted metadata for the v3 baseline migration.
packages/prisma-next/migrations/20260601T0100_install_eql_v3_bundle/end-contract.json Adds emitted end-contract artifact for the v3 baseline migration.
packages/prisma-next/migrations/20260601T0100_install_eql_v3_bundle/end-contract.d.ts Adds emitted end-contract type artifact for the v3 baseline migration.
examples/prisma/test/e2e/str-range.e2e.test.ts Updates string-domain e2e coverage to v3 operators + semantics.
examples/prisma/test/e2e/README.md Updates e2e harness documentation for v3 behavior and removed v2 limitations.
examples/prisma/test/e2e/num.e2e.test.ts Updates numeric e2e coverage to v3 + EncryptedNumber and order-term sorting.
examples/prisma/test/e2e/global-setup.ts Updates e2e global setup docs/comments for v3 baselines and domains.
examples/prisma/test/e2e/date.e2e.test.ts Updates date e2e coverage to v3 and order-term sorting.
examples/prisma/test/e2e/bigint.e2e.test.ts Updates bigint e2e to assert lossless > safe-integer behavior on v3.
examples/prisma/src/prisma/contract.json Regenerates example contract for v3 codec ids, domains, and typeParams structure.
examples/prisma/src/db.ts Switches example wiring to cipherstashFromStackV3 via @cipherstash/prisma-next/v3.
examples/prisma/README.md Updates example documentation for v3 domain constructors and operator semantics.
examples/prisma/prisma/schema.prisma Updates Prisma schema to v3 per-domain constructors and revised operator expectations.
examples/prisma/migrations/cipherstash/refs/head.json Adds v3 bundle invariant to the example’s cipherstash space head ref.
examples/prisma/migrations/cipherstash/20260601T0100_install_eql_v3_bundle/migration.json Adds example cipherstash-space v3 baseline metadata.
examples/prisma/migrations/app/20260714T2142_initial/ops.json New initial app migration ops creating users against v3 domains.
examples/prisma/migrations/app/20260714T2142_initial/migration.ts New initial app migration authored against v3 domains + codec refs.
examples/prisma/migrations/app/20260714T2142_initial/migration.json New initial app migration metadata with updated storage hash.
examples/prisma/migrations/app/20260709T1034_initial/migration.ts Removes prior v2-era initial migration (included per-column search-config ops).
examples/prisma/migrations/app/20260709T1034_initial/migration.json Removes prior v2-era initial migration metadata.
AGENTS.md Updates repo layout note to reflect prisma-next now supports both v2 and v3 entry points.
.changeset/prisma-example-eql-v3.md Changeset for upgrading the example app to v3.
.changeset/eql-v3-stack-domain-registry-exports.md Changeset for new stack v3 domain registry exports.
.changeset/eql-v3-prisma-next.md Changeset describing prisma-next v3 surface (constructors, entry points, migrations).
Files not reviewed (1)
  • pnpm-lock.yaml: Generated file

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

Comment thread packages/prisma-next/src/v3/wire-v3.ts Outdated
Comment on lines +39 to +43
export function v3FromDriver<T>(value: string | object | null | undefined): T {
if (value === null || value === undefined) return value as T
if (typeof value === 'object') return value as T
return JSON.parse(value) as T
}

@coderdan coderdan 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.

Really strong PR — disciplined commit sequence, green CI including live integration, and several things done exactly right: the domain catalog derived from DOMAIN_REGISTRY rather than hand-maintained (a new domain in stack flows through with zero prisma-next edits), the mark/collect/route seam sending every predicate operand through encryptQuery as a ciphertext-free $n::eql_v3.query_<domain> term, ord_term/ord_term_ore ordering, the self-parenthesised between, and the empty-needle containment guard matching the other adapters. The review asks below are consistency items, not correctness problems.

Requested changes

1. Free-text search must be match, not ilike 🔴

cipherstashIlike/cipherstashNotIlike lowering to eql_v3.contains is the exact category error we removed from the Drizzle and Supabase v3 surfaces (#588, #617): an ilike name promises SQL pattern semantics over what is fuzzy, order/multiplicity-insensitive, one-sided bloom token matching. v3 is a new opt-in surface — there's no continuity to preserve. Please name the operator match (plain match if Prisma's extension mechanics allow it; if a prefix is unavoidable see #3 below). If you want an ilike compat alias, follow the Supabase shim's pattern: deprecated, one-time warning, approximate-semantics disclaimer.

Two guards are also missing and need to ship with whatever the operator is called:

  • Short-needle rejection — a needle below the tokenizer length blooms to nothing and can match every row. matchNeedleError in @cipherstash/stack/adapter-kit exists for exactly this; both other adapters use it.
  • Wildcard handling — an ilike-shaped surface invites '%foo%', and % gets tokenized as an ordinary character → silently wrong results. Strip leading/trailing % and reject interior %/_ (see likeNeedle in stack-supabase for the reference behavior).

2. Operator naming should line up with EQL, not the company

This is EQL — the naming should be informed by the eql_v3.* functions themselves (eq, neq, gt, gte, lt, lte, between, match, JSON contains) so the TS surface, the SQL it lowers to, and the EQL docs all use one vocabulary. cipherstashEq → the SQL reads eql_v3.eq(...); the prefix adds length without information. If the Prisma extension registry needs namespacing to avoid colliding with Prisma's native filter names, prefer an EQL-derived prefix (eqlEq, eqlMatch, …) over cipherstash*. The v2 surface keeps its existing names — this is about not carrying them onto the new surface.

3. Consume @cipherstash/stack/adapter-kit (and resolve the duplicated door)

This PR graduates DOMAIN_REGISTRY/factoryForDomain/stripDomainSchema to public eql/v3 while adapter-kit still exports the same symbols — two doors to the same registry, no pointer between them. adapter-kit is the seam built for first-party adapters (stack-drizzle and stack-supabase consume these exact symbols through it), and it now also carries the shared selector-path helpers and matchNeedleError this PR needs. Either import adapter-kit like the other adapters, or — if we decide graduation is right — do it properly in a coordinated change (pointer note in adapter-kit, migrate the other adapters' imports). Default to adapter-kit for this PR; the graduation can be its own discussion.

4. Adopt the shared test-kit family harness

The hand-rolled live suite is good, but it doesn't pin prisma-next against the same derived capability matrix and plaintext oracle as the other adapters. @cipherstash/test-kit's IntegrationAdapter + runFamilySuite gives you, per domain: the positive/negative op matrix derived from capabilities (flipping a capability flips a test), single-vs-bulk insert crossover, ordering assertions against the oracle, and absent/null handling — and every future extension to the kit then covers prisma-next for free. Implement a makePrismaNextAdapter() and run the family suites; keep your bespoke tests for the prisma-specific surfaces (middleware, codecs, envelope types) — that split is exactly how the other two adapters are organised.

5. Implement JSON selector querying the way Drizzle does (#623/#651)

The scope-out comment is honest, but the approach is already established — please add it here (or as an immediate follow-up PR if you prefer, with a tracking issue like #650):

  • Path handling: parseSelectorSegments / reconstructSelectorDocument / unsupportedLeafReason are in adapter-kit now — shared validation, don't re-implement.
  • Lowering: eql_v3.<op>(eql_v3.jsonb_path_query_first({{self}}, $sel::text), $needle::public.eql_v3_jsonb_entry) where $sel is the bare selector hash from encryptQuery(jsonPath, { queryType: 'searchableJson' }) and $needle is INTERIM a storage encryption of the reconstructed {path: value} document (its ste_vec entry carries the c + hm/op the comparison reads). Ciphertext-free RHS lands with cipherstash/protectjs-ffi#137 — carry the same interim comment Drizzle does.
  • Semantics to preserve: ne includes absent-path rows (OR … IS NULL), scalar-leaf guard (ordering arm for gt/lt), and note the array-leaf behavior (a scalar needle does not match an array at the path — pinned live in stack-supabase's suite).

Happy to pair on any of these — the drizzle operators.ts selector block and the test-kit adapter implementations in stack-drizzle/stack-supabase integration/adapter.ts are the reference points.

https://claude.ai/code/session_01MxTTPaPP16m6br7Hoab94w

calvinbrewer added a commit that referenced this pull request Jul 16, 2026
…he eql/v3 graduation

The branch had graduated DOMAIN_REGISTRY / factoryForDomain /
stripDomainSchema onto the public eql/v3 barrel while adapter-kit still
exported the same symbols — two doors to one registry. Revert the
graduation and import through adapter-kit, the seam the Drizzle and
Supabase adapters already consume (PR #655 review, item 3). Also make
v3Authored return a fresh descriptor per invocation so a caller
mutation cannot alter later contracts (review).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
calvinbrewer added a commit that referenced this pull request Jul 16, 2026
…eqlMatch

Rename the v3 query operations to the eql_v3.* function vocabulary they
lower to — eqlEq/eqlNeq/eqlIn/eqlNotIn/eqlGt/eqlGte/eqlLt/eqlLte/
eqlBetween/eqlNotBetween/eqlJsonContains, ordering via eqlAsc/eqlDesc.
The eql prefix (not bare eq/gt) is required because the framework's
native scope-field methods already claim eq/neq/gt/…, which lower to
plain SQL comparisons (PR #655 review, items 1+2). The v2 surface keeps
its cipherstash* names; the sets are now disjoint.

Free-text search is eqlMatch, not ilike: eql_v3.contains is fuzzy bloom
token matching, not SQL pattern semantics. Two guards run before
encryption: wildcard normalisation (leading/trailing % stripped,
interior % or any _ rejected) and the shared adapter-kit
matchNeedleError short-needle rejection. cipherstashNotIlike is removed
outright — negating a may-false-positive bloom test silently drops
matching rows.

Type-level dispatch mirrors the split: v3 codec entries carry only
cipherstash:v3-* marker traits, so eql* methods never surface on v2
columns and cipherstash* methods never surface on v3 columns.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
calvinbrewer added a commit that referenced this pull request Jul 16, 2026
- wire-v3: v3ToDriver coerces JSON.stringify's undefined branch to null;
  v3FromDriver gains overloads so a nullable input yields a nullable
  result instead of laundering null into T.
- sdk-adapter-v3: bulkDecrypt validates response cardinality (a
  truncated response would silently misalign plaintexts); the
  deliberate as-never bridges carry the repo's biome suppression.
- pack meta: extensionPacks.cipherstash now registers the 40 v3 codec
  metadata instances + storage rows (derived from the catalog, never
  hand-listed) and the EncryptedNumber type import, so consumer
  contracts describe the codecs their fields use.
- live suites: direct dotenv/config imports, canonical column-order
  binding in insertEncryptedRows (a differing key order could bind
  values to the wrong SQL columns), and migration-apply now uninstalls
  EQL v3 first so the customer-facing migration SQL genuinely executes
  on a reused database (test:live is serial now — the reinstall is
  destructive across files).
- bundling-isolation: entry checks scan the complete import graph for
  every v2 wire/codec-factory marker, not just entry bodies.

Verified live: 599 unit + 30 live-PG + 582 integration + 40 example-e2e
+ 7 README-walkthrough tests green against real ZeroKMS + Postgres.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
calvinbrewer added a commit that referenced this pull request Jul 16, 2026
Implement makePrismaNextAdapter() behind the shared
@cipherstash/test-kit IntegrationAdapter seam and run runFamilySuite
over every family — the same derived capability matrix, plaintext
oracle, single-vs-bulk insert crossover, ordering assertions, and
absent/null handling that pin the Drizzle and Supabase adapters (PR
#655 review, item 4). Queries lower through the real v3 operator
registry + postgres adapter + bulk-encrypt middleware; inserts run
one-row vs multi-row INSERT statements, prisma-next's two batching
shapes. New CI workflow mirrors the Drizzle job on both database
variants. 582 tests green against real ZeroKMS + live Postgres.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@calvinbrewer

Copy link
Copy Markdown
Contributor Author

Review feedback addressed — branch merged up to main (79 commits, needed because the review references the adapter-kit selector helpers and match guards that landed after this branch diverged), then five commits on top. Everything below was verified against live infrastructure: 599 unit + 30 live-PG + 582 integration + 40 example-e2e + 7 README-walkthrough tests green against real ZeroKMS + Postgres, plus 902 stack unit tests.

@coderdan's requested changes

1. Free-text search is match, not ilike — the operator is now eqlMatch (see item 2 for the prefix), lowering to eql_v3.contains as before but with both missing guards shipped: short-needle rejection via the shared matchNeedleError from @cipherstash/stack/adapter-kit (same guard as the other two adapters), and wildcard handling mirroring the Supabase likeNeedle semantics (leading/trailing % stripped, interior % or any _ rejected with a clear message). No ilike compat alias — v3 is a new opt-in surface. cipherstashNotIlike is removed outright (bloom negation false-negatives; also flagged by CodeRabbit), with a code comment explaining why no negated match may exist until a decrypt-and-post-filter path does.

2. EQL-derived operator naming — the v3 surface is now eqlEq / eqlNeq / eqlIn / eqlNotIn / eqlGt / eqlGte / eqlLt / eqlLte / eqlBetween / eqlNotBetween / eqlMatch / eqlJsonContains, ordering via eqlAsc / eqlDesc. The bare eq/match names weren't available: the framework's native scope-field methods already claim eq, neq, gt, gte, lt, lte, in, notIn, like — and those lower to plain SQL comparisons, exactly what must never attach to EQL ciphertexts — so this takes the review's EQL-derived-prefix fallback. The v2 surface keeps its cipherstash* names; the two sets are now disjoint, and type-level dispatch was reworked so eql* methods only surface on v3 columns and cipherstash* only on v2 (pinned in both directions in the type tests).

3. adapter-kit, not graduation — the branch's eql/v3 barrel graduation of DOMAIN_REGISTRY / factoryForDomain / stripDomainSchema is reverted (and its changeset removed); prisma-next now imports the registry, stripDomainSchema, and matchNeedleError through @cipherstash/stack/adapter-kit like the other first-party adapters. The graduation discussion can happen on its own if wanted.

4. test-kit family harnessmakePrismaNextAdapter() now implements the shared IntegrationAdapter and runFamilySuite runs over every family (packages/prisma-next/integration/), with a new CI workflow mirroring the Drizzle job on both database variants. Queries lower through the real operator registry + postgres adapter + bulk-encrypt middleware; single-vs-bulk crossover maps to prisma-next's two batching shapes (one-row vs multi-row INSERT — there is no encryptModel/bulkEncryptModels pair here, the middleware is the one encrypt path). 582 tests green locally against both a wiped and a reused database. Bespoke suites for the prisma-specific surfaces (middleware, codecs, envelopes) remain.

5. JSON selector querying — taken as the sanctioned follow-up: tracking issue #677 captures the full approach from the review (adapter-kit parseSelectorSegments/reconstructSelectorDocument/unsupportedLeafReason, the jsonb_path_query_first lowering with the interim storage-encrypted needle pending cipherstash/protectjs-ffi#137, ne absent-path semantics, scalar-leaf guard, array-leaf note). The prisma-next-specific work is expressing the two-param lowering (selector hash + needle through different encryption routes) in the ParamRef/middleware model — happy to pair on that PR.

Bot findings

Fixed: fully-qualified public.eql_v3_* domains in the example README; example contract regenerated (pnpm emit) so extensionPacks.cipherstash now carries all 40 v3 codec instances + storage rows + the EncryptedNumber import (pack meta derives them from the catalog, never hand-listed); v3Authored returns a fresh descriptor per invocation; bulkDecrypt cardinality check; v3ToDriver undefinednull + v3FromDriver nullable overloads (also the Copilot unsound-return finding); biome suppressions on the deliberate as never SDK bridges; type-erasing assertions removed from bool.e2e; direct dotenv/config imports in all flagged live/e2e files; canonical column-order binding in insertEncryptedRows; migration-apply now uninstalls EQL v3 first so the migration SQL genuinely executes on a reused DB (live suite is serial now — the reinstall is destructive across files); bundling-isolation scans complete import graphs for every v2 marker; RUNTIME.ABORTED asserted exactly; the isNull() hint asserted in the JSON lowering test.

Skipped with reason (inline replies on the threads): the ./v3 CJS require condition (the package and its framework deps are ESM-only throughout — a lone CJS entry would be unsatisfiable), and the bulk-encrypt routing-per-ParamRef concern (conflicting envelope reuse already throws at setHandleRoutingKey; added a v3 regression test pinning it).

🤖 Generated with Claude Code

…QL install)

First of the three PRs in #690: make migration-first, ORM-agnostic EQL v3 install
the front door, so every integration sources install SQL from one place (the CLI's
bundled variants) instead of vendoring its own. This is the pattern that keeps
Drizzle Supabase-safe; prisma-next's superuser-on-Supabase failure is fixed by the
stacked follow-ups (`--prisma` emitter + removing prisma-next's baked baseline),
which land on top of the prisma-next EQL v3 work (#655).

`stash eql migration --drizzle [--supabase] [--name] [--out] [--dry-run]`:
- Generates a Drizzle custom migration (via `drizzle-kit generate --custom`, then
  injects the SQL) carrying the bundled EQL **v3** install script + the
  `cs_migrations` tracking schema — one `drizzle-kit migrate` does everything
  `stash encrypt …` needs.
- `--supabase` appends the v3 role grants (`eql_v3` + `eql_v3_internal` →
  anon/authenticated/service_role), matching `stash eql install --supabase`.
- v3 only — no `--eql-version` (prisma-next never shipped v2).
- `--prisma` is registered but fails with a pointer to #690 until the stacked PR.

The SQL assembly (`buildEqlV3MigrationSql`) is a pure, unit-tested function; the
drizzle-kit scaffold/inject orchestration reuses the proven helpers from the v2
`install --drizzle` path (`findGeneratedMigration`, `cleanupMigrationFile`, now
exported). Registry + dispatch + help + `stash-cli`/`stash-drizzle` skills updated.

Tests: 4 new unit (bundle present, grants gated on --supabase, tracking schema),
544 CLI unit green, manifest resolves the command, biome clean. Changeset: stash minor.

Claude-Session: https://claude.ai/code/session_01MxTTPaPP16m6br7Hoab94w
coderdan and others added 8 commits July 17, 2026 23:17
Addresses the review on #691 — correctness, security, telemetry, and coverage:

- Run the PROJECT-LOCAL drizzle-kit (`pnpm exec` / `npx --no-install`), not the
  `pnpm dlx`/`yarn dlx` download-and-run form, so it resolves the project's own
  drizzle.config.ts + schema. New shared `execCommand`/`execArgv` in init/utils
  (setup-prompt's private copy folded in).
- Invoke via `spawnSync` with an argv array instead of `execSync` on a shell
  string — a `--name` with spaces or shell metacharacters is now one inert token
  (no word-splitting, no command injection). Plus a `[\w-]+` name guard.
- Pass `--out` through to `drizzle-kit generate` (always) so the flag actually
  steers where the migration is written, and our lookup can't miss it.
- Validation exits and every abort throw `CliExit(1)` instead of `process.exit`,
  so the telemetry `finally` runs and the branches are unit-testable.
- Guard `loadBundledEqlSql` (corrupt bundle → clean error, not a fatal trace),
  add the missing `p.intro`, and call `printNextSteps()` so the now-preferred
  install path isn't less helpful than `eql install`.
- HELP banner lists `eql migration`.

Tests: 14 migration unit (target selection incl. `--supabase`-alone, name guard,
dry-run, argv threading, non-zero drizzle-kit, write-failure cleanup, SQL order),
3 `findGeneratedMigration` unit (now public API), e2e smoke + `eql migration
--help`. 557 unit / 65 e2e green, code:check clean.

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

Codec ids are the DOMAIN_REGISTRY key verbatim (cipherstash/eql-v3/eql_v3_*@1)
per the GA eql_v3_* domain naming; json is a first-class exposed domain.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…t test

The closed union infers eqlType from getEqlType()'s return type — the
stack's bundled d.ts drops the private definition field's type, so the
EqlTypeForColumn infer-D path widens to public.${string} downstream.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…y gating

Add cipherstashV3QueryOperations() (src/v3/operators-v3.ts): the EQL v3
operator set registered by the v3 extension descriptor only (decision 1b
— same cipherstash* method names as v2, never co-registered; the flat
OperationRegistry collision is pinned by test).

Lowering matches the stack-drizzle v3 dialect byte-for-byte: operands
are ciphertext-free query terms cast to the column domain's
eql_v3.query_<domain> type (eql_v3.eq/neq/gt/gte/lt/lte/contains,
self-parenthesised (gte AND lte) range, OR-of-eq membership), JSON
containment is OPERATOR(public.@>) with the irregular
::eql_v3.query_jsonb cast (cipherstashJsonContains, empty-needle
guarded), and ordering extracts eql_v3.ord_term / ord_term_ore by the
domain's flavour via the free-standing cipherstashV3Asc/Desc helpers.

Operands are bound as pg/text@1 params (bare $N; the template supplies
the query cast — the column codec's storage-domain cast would fail the
query term's CHECK) and each envelope is routing-key stamped plus
marked with its queryType (markV3QueryTerm / v3QueryTermTypeOf), the
seam Task 7's SDK adapter uses to route WHERE operands through
encryptQuery instead of storage bulkEncrypt.

Every operator gates on the concrete domain's catalog capabilities and
throws the v3-owned EncryptionOperatorError naming column, domain,
operator, and missing capability. v3 codec descriptors are pinned to
cipherstash:*-only traits alongside the v2 equality-trait regression.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…tor + cipherstashFromStackV3

- deriveStackSchemasV3(contractJson): walks the 0.14 namespace envelope
  and maps each v3 column's public.eql_v3_* nativeType to its concrete
  @cipherstash/stack/eql/v3 factory (capabilities are intrinsic to the
  domain — no typeParams flags), preserving exact domain identity.
- createCipherstashV3Sdk(client, schemas): adapts the EncryptionV3
  TypedEncryptionClient (structural surface, no casts) to the framework
  CipherstashSdk. Consumes the Task-6 query-term seam: envelopes marked
  by markV3QueryTerm route through encryptQuery — one batch crossing
  per scalar flavour (stack-drizzle's inArray surface), single-call for
  searchableJson (stack-drizzle's JSON containment path) — while
  unmarked values take the storage bulkEncrypt path, position-stable.
- bulkEncryptMiddlewareV3 now collects pg/text@1-bound marked envelopes
  (WHERE operands), forwards the envelope itself through the SDK seam,
  and writes the returned term back as query-term JSONB text without
  stamping it into the envelope's storage-ciphertext slot.
- createCipherstashV3RuntimeDescriptor({ sdk }): the v3 extension
  descriptor under v3's own id/version (decision 1b — never
  co-registered with v2; shared method names collide by design).
- assertV3SchemasAgree: override validation on EXACT domain identity
  (integer_ord ≠ integer_ord_ore despite shared cast_as).
- cipherstashFromStackV3: the v3-only entry point — hard-errors on v2
  cipherstash codec ids and on contracts with no v3 columns; returns
  { extensions, middleware, encryptionClient } like the v2 factory.
- Query-term seam (markV3QueryTerm / v3QueryTermTypeOf +
  EncryptionOperatorError) extracted to src/v3/query-term.ts so the
  middleware/adapter don't import the operator registry; operators-v3
  re-exports it unchanged.
- Exports: v3 surface added to ./runtime and ./stack; new ./v3 subpath
  (ESM, matching the package's existing ESM-only exports map).
- bundling-isolation: allow the v3 catalog chunk (pure domain metadata)
  as a second cross-plane shared chunk alongside the v2 constants chunk.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
calvinbrewer and others added 11 commits July 17, 2026 23:25
…/eql

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ting

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Bundling isolation (test/bundling-isolation.test.ts):
- v3.js joins the entry existence check and is pinned against both the
  contract-space artefacts (RUNTIME_FORBIDDEN) and the v2 wire plane
- content-fingerprinted wire markers (encodeEqlV2EncryptedWire /
  makeCipherstashCellCodec vs CipherstashV3CellCodec /
  createV3CodecDescriptors / bulkEncryptMiddlewareV3): no code-split
  chunk may reference both wire planes, the v3 entry graph never
  reaches the v2 codec-runtime chunk, and middleware.js (v2-only
  entry) never loads the v3 wire

Live-PG suites (test/live/, self-skipping via describeLivePg when
CS creds — env vars or ~/.cipherstash profile — or DATABASE_URL are
absent; `pnpm test:live` convenience script added):
- helpers: live-gate (skip posture), eql-v3 (advisory-locked
  installEqlV3IfNeeded over the Task-8 migration source SQL), harness
  (real cipherstashFromStackV3 + real bulkEncryptMiddlewareV3 driven
  through real execution plans, operator SELECTs lowered by the real
  postgres adapter, envelope fromInternal().decrypt() read path)
- operators-live-pg: eq/ne/inArray, contains (ilike), gt/lte/between,
  date + timestamp ranges, ord_term ordering, JSON containment
  (@> with eql_v3.query_jsonb — ruled decision 2), per-family decrypt
- operators-null-live-pg: NULL cells round-trip, null operands rejected
- boolean-storage-live-pg: storage-only round-trip, operators refuse
- bigint-live-pg: lossless bigint beyond MAX_SAFE_INTEGER, eq/range/order
- migration-apply-live-pg: applied SQL is byte-identical to the shipped
  ops.json, invariant id, domains + eql_v3 schema, op postchecks, no
  add_search_config
- bulk-encrypt-live-pg: real EncryptionV3 through the real middleware,
  JSONB cells, ciphertext stamping, decrypt round-trip
- side-by-side-clients-live-pg: v2 and v3 clients coexisting in-process
  against one database, distinct extension identities and wires

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Also a stack changeset for the new eql/v3 barrel exports, and the
AGENTS.md repository-layout line updated for the v2/v3 split.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Gaps surfaced by converting the example app and driving the real CLI +
runtime rather than unit fixtures:

- CodecTypes/QueryOperationTypes cover all 40 v3 codec ids with
  trait-accurate operator visibility (cipherstash:v3-* marker traits;
  cipherstashJsonContains v3-only; storage-only domains surface no
  operator methods at the type level).
- The v3 runtime descriptor presents the pack id ('cipherstash') with
  v3's own version — the runtime matches contract extensionPacks by
  descriptor id, so the old distinct id failed startup with
  RUNTIME.MISSING_EXTENSION_PACK.
- Every v3 codec id registers a control-plane expandNativeType hook
  stripping the public. qualifier (the planner requires the hook for
  typeParams-carrying columns; bare names match introspected udt_name).
  No onFieldEvent — v3 emits zero add_search_config ops.
- The v3 bundle op is reclassified 'data': the integrity checker
  rejects self-edges without a data-class op.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Schema: every column is a concrete public.eql_v3_* domain
(EncryptedTextSearch / EncryptedDoubleOrd / EncryptedBigIntOrd /
EncryptedDateOrd / EncryptedBoolean / EncryptedJson); wiring via
cipherstashFromStackV3. Migrations regenerated from the v3 contract —
initial app migration carries zero add_search_config ops; the
cipherstash space ships both bundle baselines.

E2E suites adapted to the v3 surface and green from a wiped database
(7 files / 40 tests, live PG + ZeroKMS): equality/range/free-text
tokens, order-term sorting, JSON @> containment (positive, negative,
multi-key, nested, empty-needle rejection), lossless bigint beyond
MAX_SAFE_INTEGER, and eql_v3_boolean's storage-only operator refusal
pinned as a feature. README walkthrough e2e (repo-level) also green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…he eql/v3 graduation

The branch had graduated DOMAIN_REGISTRY / factoryForDomain /
stripDomainSchema onto the public eql/v3 barrel while adapter-kit still
exported the same symbols — two doors to one registry. Revert the
graduation and import through adapter-kit, the seam the Drizzle and
Supabase adapters already consume (PR #655 review, item 3). Also make
v3Authored return a fresh descriptor per invocation so a caller
mutation cannot alter later contracts (review).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…eqlMatch

Rename the v3 query operations to the eql_v3.* function vocabulary they
lower to — eqlEq/eqlNeq/eqlIn/eqlNotIn/eqlGt/eqlGte/eqlLt/eqlLte/
eqlBetween/eqlNotBetween/eqlJsonContains, ordering via eqlAsc/eqlDesc.
The eql prefix (not bare eq/gt) is required because the framework's
native scope-field methods already claim eq/neq/gt/…, which lower to
plain SQL comparisons (PR #655 review, items 1+2). The v2 surface keeps
its cipherstash* names; the sets are now disjoint.

Free-text search is eqlMatch, not ilike: eql_v3.contains is fuzzy bloom
token matching, not SQL pattern semantics. Two guards run before
encryption: wildcard normalisation (leading/trailing % stripped,
interior % or any _ rejected) and the shared adapter-kit
matchNeedleError short-needle rejection. cipherstashNotIlike is removed
outright — negating a may-false-positive bloom test silently drops
matching rows.

Type-level dispatch mirrors the split: v3 codec entries carry only
cipherstash:v3-* marker traits, so eql* methods never surface on v2
columns and cipherstash* methods never surface on v3 columns.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- wire-v3: v3ToDriver coerces JSON.stringify's undefined branch to null;
  v3FromDriver gains overloads so a nullable input yields a nullable
  result instead of laundering null into T.
- sdk-adapter-v3: bulkDecrypt validates response cardinality (a
  truncated response would silently misalign plaintexts); the
  deliberate as-never bridges carry the repo's biome suppression.
- pack meta: extensionPacks.cipherstash now registers the 40 v3 codec
  metadata instances + storage rows (derived from the catalog, never
  hand-listed) and the EncryptedNumber type import, so consumer
  contracts describe the codecs their fields use.
- live suites: direct dotenv/config imports, canonical column-order
  binding in insertEncryptedRows (a differing key order could bind
  values to the wrong SQL columns), and migration-apply now uninstalls
  EQL v3 first so the customer-facing migration SQL genuinely executes
  on a reused database (test:live is serial now — the reinstall is
  destructive across files).
- bundling-isolation: entry checks scan the complete import graph for
  every v2 wire/codec-factory marker, not just entry bodies.

Verified live: 599 unit + 30 live-PG + 582 integration + 40 example-e2e
+ 7 README-walkthrough tests green against real ZeroKMS + Postgres.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… contract

Rename every v3 operator call to the EQL-derived vocabulary (eqlEq,
eqlMatch, eqlAsc/eqlDesc, …), fully qualify the README's domain table
(public.eql_v3_*), describe free-text search as fuzzy bloom token
matching under its real needle semantics, drop the removed
cipherstashNotIlike coverage, add a direct dotenv/config import to the
str-range suite, and remove the type-erasing probe assertions in the
bool suite. pnpm emit regenerates contract.{json,d.ts} — the
extensionPacks.cipherstash block now carries the 40 v3 codec instances
and storage rows.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Implement makePrismaNextAdapter() behind the shared
@cipherstash/test-kit IntegrationAdapter seam and run runFamilySuite
over every family — the same derived capability matrix, plaintext
oracle, single-vs-bulk insert crossover, ordering assertions, and
absent/null handling that pin the Drizzle and Supabase adapters (PR
#655 review, item 4). Queries lower through the real v3 operator
registry + postgres adapter + bulk-encrypt middleware; inserts run
one-row vs multi-row INSERT statements, prisma-next's two batching
shapes. New CI workflow mirrors the Drizzle job on both database
variants. 582 tests green against real ZeroKMS + live Postgres.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coderdan
coderdan force-pushed the feat/prisma-next-eql-v3 branch from 06df347 to b6ff7c4 Compare July 17, 2026 13:26
@coderdan

Copy link
Copy Markdown
Contributor

Heads-up: I rebased this branch onto the stash eql migration PR (#691) as part of the migration-first EQL install plan (#690). Old tip 06df3474 → new tip b6ff7c43 — force-pushed with lease, no content conflicts (691 is CLI-only, this is prisma-next). If you have local work, reset to the new tip. The stacked --prisma emitter PR (which removes the baked baseline in favour of stash eql migration --prisma) comes next, on top of this.

@calvinbrewer

Copy link
Copy Markdown
Contributor Author

Branch note — what just happened with the history here

Timeline of this morning's conflict, for anyone following along:

  1. Yesterday's review round was pushed as 3f566679..06df3474 (merge of main + five commits), and one further commit for prisma-next: typecheck is red and not gated in CI — the type-level operator-capability tests aren't enforced #684 (the hermetic typecheck fix) existed locally but hadn't been pushed yet — it was blocked on a commit-signing hiccup.
  2. At 13:26 UTC today @coderdan force-pushed a rebase of the branch — the same content rebuilt onto the newer main (which had picked up the CLI/migration work), so every commit got a new SHA. I diffed the rewritten commits against the originals: the prisma-next / example / stack / test-kit trees are byte-identical, so nothing was lost — but the rewrite meant the not-yet-pushed prisma-next: typecheck is red and not gated in CI — the type-level operator-capability tests aren't enforced #684 commit now sat on top of obsolete history.
  3. A subsequent git pull then tried to merge the old and rewritten copies of the same history, which is where the tsconfig.json conflict came from (the prisma-next: typecheck is red and not gated in CI — the type-level operator-capability tests aren't enforced #684 paths block colliding with its own pre-prisma-next: typecheck is red and not gated in CI — the type-level operator-capability tests aren't enforced #684 ancestor).

Resolution: aborted the duplicate-history merge, reset the branch to the rebased remote, cherry-picked the #684 fix on top (clean apply), re-validated (typecheck green with and without a built packages/stack/dist; 599 unit tests green), and pushed as 4c7abac6.

Current CI on 4c7abac6: everything green — including both new prisma-next v3 integration legs (postgres + supabase), Drizzle v3 both legs, unit/e2e/README-walkthrough suites — except Supabase v3 integration (db=supabase), which was queue-cancelled by the shared integration-live-db-supabase concurrency group ("higher priority waiting request"), not a test failure; it's been re-run.

Coordination ask: if the branch needs rebasing again, a heads-up first please — a force-push while someone holds unpushed commits reproduces exactly this duplicate-history merge.

🤖 Generated with Claude Code

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.

3 participants