Feat/prisma next eql v3#655
Conversation
🦋 Changeset detectedLatest commit: 4c7abac The changes in this PR will be included in the next version bump. This PR includes changesets to release 11 packages
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 |
|
Important Review skippedToo many files! This PR contains 126 files, which is 26 over the limit of 100. To get a review, narrow the scope: Upgrade to a paid plan to raise the limit. ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (129)
You can disable this status message by setting the Use the checkbox below for a quick retry:
📝 WalkthroughWalkthroughThis 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. ChangesEQL v3 Prisma-next
Estimated code review effort: 5 (Critical) | ~120 minutes Possibly related issues
Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
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 winReject 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 winRegenerate the contract with EQL v3 descriptors
domainandstoragenow referencecipherstash/eql-v3/*, butextensionPacks.cipherstashdoesn’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 winVerify 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 winImport the v3 surface through the public entry point. The
../../src/v3/*imports can miss a broken./v3export map or barrel; use../../src/exports/v3forcreateCipherstashV3RuntimeDescriptorandcipherstashV3QueryOperationsinstead.🤖 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 winExercise 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
⛔ Files ignored due to path filters (1)
pnpm-lock.yamlis 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.mdAGENTS.mdexamples/prisma/README.mdexamples/prisma/migrations/app/20260709T1034_initial/migration.jsonexamples/prisma/migrations/app/20260709T1034_initial/migration.tsexamples/prisma/migrations/app/20260709T1034_initial/ops.jsonexamples/prisma/migrations/app/20260714T2142_initial/end-contract.d.tsexamples/prisma/migrations/app/20260714T2142_initial/end-contract.jsonexamples/prisma/migrations/app/20260714T2142_initial/migration.jsonexamples/prisma/migrations/app/20260714T2142_initial/migration.tsexamples/prisma/migrations/app/20260714T2142_initial/ops.jsonexamples/prisma/migrations/cipherstash/20260601T0100_install_eql_v3_bundle/migration.jsonexamples/prisma/migrations/cipherstash/20260601T0100_install_eql_v3_bundle/ops.jsonexamples/prisma/migrations/cipherstash/refs/head.jsonexamples/prisma/prisma/schema.prismaexamples/prisma/src/db.tsexamples/prisma/src/index.tsexamples/prisma/src/prisma/contract.d.tsexamples/prisma/src/prisma/contract.jsonexamples/prisma/test/e2e/README.mdexamples/prisma/test/e2e/bigint.e2e.test.tsexamples/prisma/test/e2e/bool.e2e.test.tsexamples/prisma/test/e2e/date.e2e.test.tsexamples/prisma/test/e2e/global-setup.tsexamples/prisma/test/e2e/json.e2e.test.tsexamples/prisma/test/e2e/mixed.e2e.test.tsexamples/prisma/test/e2e/num.e2e.test.tsexamples/prisma/test/e2e/str-range.e2e.test.tspackages/prisma-next/migrations/20260601T0100_install_eql_v3_bundle/end-contract.d.tspackages/prisma-next/migrations/20260601T0100_install_eql_v3_bundle/end-contract.jsonpackages/prisma-next/migrations/20260601T0100_install_eql_v3_bundle/migration.jsonpackages/prisma-next/migrations/20260601T0100_install_eql_v3_bundle/migration.tspackages/prisma-next/migrations/20260601T0100_install_eql_v3_bundle/ops.jsonpackages/prisma-next/migrations/refs/head.jsonpackages/prisma-next/package.jsonpackages/prisma-next/src/contract-authoring.tspackages/prisma-next/src/exports/column-types.tspackages/prisma-next/src/exports/control.tspackages/prisma-next/src/exports/runtime.tspackages/prisma-next/src/exports/stack.tspackages/prisma-next/src/exports/v3.tspackages/prisma-next/src/extension-metadata/constants-v3.tspackages/prisma-next/src/middleware/bulk-encrypt.tspackages/prisma-next/src/migration/cipherstash-codec-v3.tspackages/prisma-next/src/migration/eql-bundle-v3.tspackages/prisma-next/src/stack/from-stack-v3.tspackages/prisma-next/src/stack/from-stack.tspackages/prisma-next/src/types/codec-types.tspackages/prisma-next/src/types/operation-types.tspackages/prisma-next/src/v3/barrel.tspackages/prisma-next/src/v3/bulk-encrypt-v3.tspackages/prisma-next/src/v3/catalog.tspackages/prisma-next/src/v3/codec-runtime-v3.tspackages/prisma-next/src/v3/derive-schemas-v3.tspackages/prisma-next/src/v3/envelope-number.tspackages/prisma-next/src/v3/from-stack-v3-validate.tspackages/prisma-next/src/v3/operators-v3.tspackages/prisma-next/src/v3/query-term.tspackages/prisma-next/src/v3/runtime-v3.tspackages/prisma-next/src/v3/sdk-adapter-v3.tspackages/prisma-next/src/v3/wire-v3.tspackages/prisma-next/test/authoring.test.tspackages/prisma-next/test/bulk-encrypt-middleware.helpers.tspackages/prisma-next/test/bulk-encrypt-middleware.test.tspackages/prisma-next/test/bundling-isolation.test.tspackages/prisma-next/test/column-types.test.tspackages/prisma-next/test/descriptor.test.tspackages/prisma-next/test/equality-trait-removal.test.tspackages/prisma-next/test/live/bigint-live-pg.test.tspackages/prisma-next/test/live/boolean-storage-live-pg.test.tspackages/prisma-next/test/live/bulk-encrypt-live-pg.test.tspackages/prisma-next/test/live/helpers/eql-v3.tspackages/prisma-next/test/live/helpers/harness.tspackages/prisma-next/test/live/helpers/live-gate.tspackages/prisma-next/test/live/migration-apply-live-pg.test.tspackages/prisma-next/test/live/operators-live-pg.test.tspackages/prisma-next/test/live/operators-null-live-pg.test.tspackages/prisma-next/test/live/side-by-side-clients-live-pg.test.tspackages/prisma-next/test/operation-types.types.test-d.tspackages/prisma-next/test/psl-interpretation-numeric.test.tspackages/prisma-next/test/psl-interpretation-other-types.test.tspackages/prisma-next/test/psl-interpretation.test.tspackages/prisma-next/test/v3/bulk-encrypt-v3.test.tspackages/prisma-next/test/v3/catalog-invariants.test.tspackages/prisma-next/test/v3/catalog.test.tspackages/prisma-next/test/v3/codec-runtime-v3.test.tspackages/prisma-next/test/v3/codec-types-v3.test-d.tspackages/prisma-next/test/v3/column-types.test-d.tspackages/prisma-next/test/v3/constants-v3.test.tspackages/prisma-next/test/v3/derive-schemas-v3.test.tspackages/prisma-next/test/v3/envelope-number.test.tspackages/prisma-next/test/v3/from-stack-divergence-v3.test.tspackages/prisma-next/test/v3/from-stack-v3.test.tspackages/prisma-next/test/v3/migration-v3.test.tspackages/prisma-next/test/v3/operator-gating-v3.test.tspackages/prisma-next/test/v3/operator-lowering-v3-equality.test.tspackages/prisma-next/test/v3/operator-lowering-v3-json.test.tspackages/prisma-next/test/v3/operator-lowering-v3-order-range.test.tspackages/prisma-next/test/v3/operator-lowering-v3-text-search.test.tspackages/prisma-next/test/v3/operator-lowering-v3.helpers.tspackages/prisma-next/test/v3/properties.test.tspackages/prisma-next/test/v3/runtime-v3.test.tspackages/prisma-next/test/v3/sdk-adapter-v3.test.tspackages/prisma-next/test/v3/wire-v3.test.tspackages/prisma-next/tsup.config.tspackages/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
| import type postgres from 'postgres' | ||
| import { afterAll, beforeAll, expect, it } from 'vitest' |
There was a problem hiding this comment.
🩺 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: addimport 'dotenv/config'before other imports.packages/prisma-next/test/live/migration-apply-live-pg.test.ts#L16-L20: addimport 'dotenv/config'before other imports.packages/prisma-next/test/live/side-by-side-clients-live-pg.test.ts#L17-L20: addimport '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-L20packages/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
| 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) | ||
| } |
There was a problem hiding this comment.
🎯 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.
| 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.
| 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()) |
There was a problem hiding this comment.
🎯 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.
| 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([]) |
There was a problem hiding this comment.
🎯 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.
| it('rejects a null needle with an isNull() hint', () => { | ||
| expect(() => | ||
| callOperator( | ||
| getOperator('cipherstashJsonContains'), | ||
| columnAccessorV3(TABLE, 'payload', JSON_CODEC_ID), | ||
| null, | ||
| ), | ||
| ).toThrow(EncryptionOperatorError) |
There was a problem hiding this comment.
🎯 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.
| 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.
There was a problem hiding this comment.
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/v3entry point (cipherstashFromStackV3) with v3 runtime descriptor, codec catalog/derivation, query-term routing, and JSONB wire helpers. - Introduce v3 migrations/baseline install plumbing (
@cipherstash/eql/sqlsourced SQL baked into ops) and update control/runtime exports accordingly. - Convert the Prisma example + e2e harness to v3 domains/operators (including lossless
bigintbeyondNumber.MAX_SAFE_INTEGERand 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.
| 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
left a comment
There was a problem hiding this comment.
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.
matchNeedleErrorin@cipherstash/stack/adapter-kitexists 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%/_(seelikeNeedlein 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/unsupportedLeafReasonare 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$selis the bare selector hash fromencryptQuery(jsonPath, { queryType: 'searchableJson' })and$needleis INTERIM a storage encryption of the reconstructed{path: value}document (its ste_vec entry carries thec+hm/opthe comparison reads). Ciphertext-free RHS lands with cipherstash/protectjs-ffi#137 — carry the same interim comment Drizzle does. - Semantics to preserve:
neincludes absent-path rows (OR … IS NULL), scalar-leaf guard (orderingarm 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.
…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>
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>
|
Review feedback addressed — branch merged up to @coderdan's requested changes1. Free-text search is 2. EQL-derived operator naming — the v3 surface is now 3. adapter-kit, not graduation — the branch's 4. test-kit family harness — 5. JSON selector querying — taken as the sanctioned follow-up: tracking issue #677 captures the full approach from the review (adapter-kit Bot findingsFixed: fully-qualified Skipped with reason (inline replies on the threads): the 🤖 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
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>
…/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>
06df347 to
b6ff7c4
Compare
|
Heads-up: I rebased this branch onto the |
|
Branch note — what just happened with the history here Timeline of this morning's conflict, for anyone following along:
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 Current CI on 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 |
Summary by CodeRabbit
New Features
Bug Fixes
Documentation