Skip to content

Commit e244b8d

Browse files
committed
refactor(prisma-next): apply multi-agent review fixes (v3)
- Remove dead v3Dialect.orderBy() (unreachable — cipherstashAsc/Desc are v2-only); document that v3 ORDER BY sort is not yet wired (README + e2e header). - Fix stale eqlOperator docblock (now trait-dispatched + dialect-selected, not pinned to string@1 / eql_v2). - Reuse FLAG_DISPATCH in applyV3Index instead of a parallel if-chain. - Expand tests: full index/operator mismatch matrix, V3_ENVELOPE_COERCERS reject path, setHandleQueryType write-once-wins conflict, encryptQuery failure branch; tighten the sdk-adapter column assertion; add gt/lte e2e ord cases. - Align vendor-script + migration regen comments with the actual invocation.
1 parent 8c0b492 commit e244b8d

22 files changed

Lines changed: 260 additions & 99 deletions

examples/prisma/test/e2e/helpers/eql-v3-seed.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,3 @@ export const oracleBetween = (lo: string, hi: string) =>
3333
ids(v3Seed.filter((r) => r.label >= lo && r.label <= hi))
3434
export const oracleInArray = (targets: ReadonlyArray<string>) =>
3535
ids(v3Seed.filter((r) => targets.includes(r.label)))
36-
37-
// Ascending labels (text_ord) — for the order-by assertion.
38-
export const oracleAscLabels = () => [...v3Seed].map((r) => r.label).sort((a, b) => a.localeCompare(b))

examples/prisma/test/e2e/str-v3.e2e.test.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,10 @@
66
* index columns:
77
* - email (text_eq) — cipherstashEq / Ne / InArray / NotInArray
88
* - bio (text_match) — cipherstashIlike / NotIlike (containment)
9-
* - name (text_ord) — cipherstashLt/Lte/Gt/Gte/Between + asc/desc order
9+
* - name (text_ord) — cipherstashLt / Lte / Gt / Between (ORE comparison)
10+
*
11+
* (Ordered `ORDER BY` sort is not yet wired for v3 — see the package README — so
12+
* this matrix covers the range/comparison predicates only.)
1013
*
1114
* Gated on DATABASE_URL (set by global-setup once the harness Postgres + ZeroKMS
1215
* credentials are configured); skips cleanly otherwise.
@@ -18,8 +21,10 @@ import {
1821
oracleBetween,
1922
oracleContains,
2023
oracleEq,
24+
oracleGt,
2125
oracleInArray,
2226
oracleLt,
27+
oracleLte,
2328
oracleNe,
2429
v3Seed,
2530
} from './helpers/eql-v3-seed'
@@ -69,9 +74,13 @@ describe.skipIf(!dbUrl)('EQL v3 String e2e (live PG + eql_v3 + ZeroKMS)', () =>
6974
)
7075
})
7176

72-
it('lt / between on text_ord match the ord oracle', async () => {
77+
it('lt / lte / gt / between on text_ord match the ORE comparison oracles', async () => {
7378
const lt = await db.orm.UserV3.where((u) => u.name.cipherstashLt('banana')).all()
7479
expect(lt.map((r) => r.id).sort()).toEqual(oracleLt('banana'))
80+
const lte = await db.orm.UserV3.where((u) => u.name.cipherstashLte('banana')).all()
81+
expect(lte.map((r) => r.id).sort()).toEqual(oracleLte('banana'))
82+
const gt = await db.orm.UserV3.where((u) => u.name.cipherstashGt('banana')).all()
83+
expect(gt.map((r) => r.id).sort()).toEqual(oracleGt('banana'))
7584
const between = await db.orm.UserV3.where((u) => u.name.cipherstashBetween('aardvark', 'cherry')).all()
7685
expect(between.map((r) => r.id).sort()).toEqual(oracleBetween('aardvark', 'cherry'))
7786
})

packages/prisma-next/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ model Doc {
125125
| `orderAndRange` | `eql_v3.text_ord` | `cipherstashGt` / `cipherstashGte` / `cipherstashLt` / `cipherstashLte` / `cipherstashBetween` / `cipherstashNotBetween` |
126126
| `freeTextSearch` | `eql_v3.text_match`| `cipherstashIlike` / `cipherstashNotIlike` (containment) |
127127

128-
Applying an operator that needs a different index than the column declares (e.g. `cipherstashGt` on an `equality` column) is rejected with a clear `TypeError` at **query-build time** — a runtime guard, not compile-time gating (milestone-1 trade-off; per-index codec ids could restore compile-time gating later). The v3 baseline migration installs the `eql_v3` bundle alongside the v2 bundle; both `bulkEncryptMiddleware` and `bulkEncryptV3Middleware` register over the same SDK and ignore each other's columns.
128+
Ordered `ORDER BY` sort (`cipherstashAsc` / `cipherstashDesc`) is **not yet wired for v3** — those helpers currently accept v2 columns only. Applying an operator that needs a different index than the column declares (e.g. `cipherstashGt` on an `equality` column) is rejected with a clear `TypeError` at **query-build time** — a runtime guard, not compile-time gating (milestone-1 trade-off; per-index codec ids could restore compile-time gating later). The v3 baseline migration installs the `eql_v3` bundle alongside the v2 bundle; both `bulkEncryptMiddleware` and `bulkEncryptV3Middleware` register over the same SDK and ignore each other's columns.
129129

130130
## Authentication
131131

packages/prisma-next/migrations/20260601T0100_install_eql_v3_bundle/migration.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@
1212
* (applied via the codec hook's `expandNativeType`) encodes the index capability.
1313
*
1414
* Authoring loop: hand-edited; re-emit `ops.json` / `migration.json` after edits
15-
* via `node migration.ts`.
15+
* by running this file through a TypeScript loader (the imports below are
16+
* extensionless TS) — e.g. `pnpm dlx tsx migration.ts`.
1617
*/
1718
import { Migration, MigrationCLI, rawSql } from '@prisma-next/target-postgres/migration';
1819
import { CIPHERSTASH_INVARIANTS } from '../../src/extension-metadata/constants';

packages/prisma-next/scripts/vendor-eql-v3-install.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,8 @@ writeFileSync(
2727
`//\n` +
2828
`// This file is committed to source control so dev environments and offline\n` +
2929
`// builds work without network access. Regenerate with\n` +
30-
`// \`pnpm tsx scripts/vendor-eql-v3-install.ts\` after refreshing the fixture.\n` +
30+
`// \`node --experimental-strip-types scripts/vendor-eql-v3-install.ts\` after\n` +
31+
`// refreshing the fixture (see scripts/REFRESH_EQL_V3.md).\n` +
3132
`export const EQL_V3_INSTALL_VERSION = '${VERSION}' as const\n` +
3233
`export const EQL_V3_INSTALL_SQL: string = \`${escaped}\`\n`,
3334
)

packages/prisma-next/src/execution/codec-runtime.ts

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,18 @@
22
* Cipherstash storage codec runtimes — wrap each `Encrypted*` envelope
33
* at the SQL codec boundary.
44
*
5-
* Every cipherstash codec has identical encode/decode bodies (the
6-
* `eql_v2_encrypted` composite-literal wire format is determined by
7-
* the EQL type definition, not by the plaintext type). The shared body
8-
* lives in `./cell-codec-factory.ts`; the per-codec wrappers below
9-
* supply only the per-type discriminators (codec id, user-facing type
10-
* name, envelope `fromInternal` factory) and re-export the codec class
11-
* for backwards compatibility with consumers that imported it directly
12-
* from this module.
5+
* The six v2 cipherstash codecs share identical encode/decode bodies
6+
* (the `eql_v2_encrypted` composite-literal wire format is determined
7+
* by the EQL type definition, not by the plaintext type). The v3 string
8+
* codec (re-exported below from `./codec-v3.ts`) reuses the same shared
9+
* body but OVERRIDES `encodeWire`/`decodeWire` for the plain-jsonb v3
10+
* domain wire (`eql_v3.text*` columns are `CREATE DOMAIN … AS jsonb`,
11+
* not the v2 composite literal). The shared body lives in
12+
* `./cell-codec-factory.ts`; the per-codec wrappers below supply only
13+
* the per-type discriminators (codec id, user-facing type name, envelope
14+
* `fromInternal` factory) and re-export the codec class for backwards
15+
* compatibility with consumers that imported it directly from this
16+
* module.
1317
*
1418
* Mirrors the `makeCipherstashCodecHooks` pattern on the migration
1519
* plane (see `../migration/codec-hooks-factory.ts`) — same shape,

packages/prisma-next/src/execution/dialect.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,15 +15,13 @@ export interface SqlDialect {
1515
comparison(op: ComparisonOp): string
1616
range(): string
1717
match(op: MatchOp): string
18-
orderBy(): string
1918
}
2019

2120
export const v2Dialect: SqlDialect = {
2221
equality: (op) => (op === 'eq' ? 'eql_v2.eq({{self}}, {{arg0}})' : 'NOT eql_v2.eq({{self}}, {{arg0}})'),
2322
comparison: (op) => `eql_v2.${op}({{self}}, {{arg0}})`,
2423
range: () => 'eql_v2.gte({{self}}, {{arg0}}) AND eql_v2.lte({{self}}, {{arg1}})',
2524
match: (op) => `eql_v2.${op === 'like' ? 'ilike' : op}({{self}}, {{arg0}})`,
26-
orderBy: () => 'eql_v2.order_by({{self}})',
2725
}
2826

2927
const ORD_SYMBOL: Record<ComparisonOp, string> = { gt: '>', gte: '>=', lt: '<', lte: '<=' }
@@ -47,7 +45,6 @@ export const v3Dialect: SqlDialect = {
4745
'eql_v3.ord_term({{self}}) >= eql_v3.ore_block_u64_8_256({{arg0}}::jsonb) AND ' +
4846
'eql_v3.ord_term({{self}}) <= eql_v3.ore_block_u64_8_256({{arg1}}::jsonb)',
4947
match: () => 'eql_v3.match_term({{self}}) @> eql_v3.bloom_filter({{arg0}}::jsonb)',
50-
orderBy: () => 'eql_v3.ord_term({{self}})',
5148
}
5249

5350
// Route by codec id: a column's wire/operator family is fixed by its codec id,

packages/prisma-next/src/execution/operators.ts

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -323,16 +323,18 @@ function extractColumnRef(selfAst: AnyExpression): ColumnRef | undefined {
323323
}
324324

325325
/**
326-
* Build a single-codec cipherstash operator descriptor — the
327-
* original shape used by `cipherstashEq` / `cipherstashIlike`,
328-
* pinned to `cipherstash/string@1`. Multi-codec operators use
329-
* {@link envelopeOperator} with trait-based dispatch instead.
326+
* Build the single-ARG cipherstash operator descriptor used by
327+
* `cipherstashEq` / `cipherstashIlike`. Dispatches on the shared
328+
* `cipherstash:string` trait (carried by BOTH `cipherstash/string@1` and
329+
* `cipherstash/string-v3@1`, and only those), so it attaches to v2 AND v3 string
330+
* columns; the v2/v3 SQL split is chosen at `impl` time via
331+
* {@link dialectForCodecId}. Multi-codec operators use {@link envelopeOperator}.
330332
*
331-
* @param publicMethod - The user-facing method name on the column
332-
* accessor (e.g. `cipherstashEq`). Must not collide with any
333-
* framework- or adapter-shipped method name.
334-
* @param eqlFunction - The EQL function to lower to (`eq`, `ilike`).
335-
* Embedded into the SQL lowering template as `eql_v2.<eqlFunction>(...)`.
333+
* @param publicMethod - The user-facing method name on the column accessor (e.g.
334+
* `cipherstashEq`). Must not collide with any framework- or adapter-shipped name.
335+
* @param eqlFunction - Selects the dialect template (`equality('eq')` for `eq`,
336+
* `match('like')` for `ilike`), lowered to `eql_v2.*` or `eql_v3.*` per the
337+
* column's codec. Also selects the v3 `queryType` for the index/operator guard.
336338
*/
337339
function eqlOperator(publicMethod: string, eqlFunction: 'eq' | 'ilike'): SqlOperationDescriptor {
338340
// `eq` carries an equality index; `ilike` (free-text containment) carries the

packages/prisma-next/src/execution/parameterized.ts

Lines changed: 14 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,23 +10,26 @@
1010
* so each `createParameterizedCodecDescriptors(sdk)` call produces a
1111
* fresh descriptor list closed over the SDK so multi-tenant
1212
* deployments can compose multiple cipherstash extensions side-by-side
13-
* without cross-talk; and the cipherstash family ships six codecs
14-
* (one per encrypted column type) which all share the same
15-
* `eql_v2_encrypted` Postgres native type.
13+
* without cross-talk; and the cipherstash family ships SEVEN codecs.
14+
* The six v2 codecs (one per encrypted column type) all share the same
15+
* `eql_v2_encrypted` Postgres native type; the seventh — the EQL v3
16+
* string codec — uses its OWN `jsonb` param-cast type backed by the
17+
* per-column `eql_v3.text*` domains (NOT `eql_v2_encrypted`).
1618
*
1719
* Per-codec params shape (every flag defaults to `true` because
1820
* searchable encryption is the legitimate default for an extension
1921
* whose entire reason for existing is to make encrypted columns
2022
* queryable):
2123
*
22-
* | Codec | Params |
23-
* |---------------------|-------------------------------------|
24-
* | `cipherstash/string@1` | `{ equality, freeTextSearch, orderAndRange }` |
25-
* | `cipherstash/double@1` | `{ equality, orderAndRange }` |
26-
* | `cipherstash/bigint@1` | `{ equality, orderAndRange }` |
27-
* | `cipherstash/date@1` | `{ equality, orderAndRange }` |
28-
* | `cipherstash/boolean@1` | `{ equality }` |
29-
* | `cipherstash/json@1` | `{ searchableJson }` |
24+
* | Codec | Params |
25+
* |----------------------------|-------------------------------------|
26+
* | `cipherstash/string@1` | `{ equality, freeTextSearch, orderAndRange }` |
27+
* | `cipherstash/double@1` | `{ equality, orderAndRange }` |
28+
* | `cipherstash/bigint@1` | `{ equality, orderAndRange }` |
29+
* | `cipherstash/date@1` | `{ equality, orderAndRange }` |
30+
* | `cipherstash/boolean@1` | `{ equality }` |
31+
* | `cipherstash/json@1` | `{ searchableJson }` |
32+
* | `cipherstash/string-v3@1` | `{ index }` (single v3 index choice; native type `jsonb`/`eql_v3.text*`, NOT `eql_v2_encrypted`) |
3033
*
3134
* The codec runtimes are per-cell stateless across params on the write
3235
* side (encode reads ciphertext from the handle, independent of the

packages/prisma-next/src/execution/sdk.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ export interface CipherstashBulkEncryptQueryArgs {
8383
* implement these three methods directly.
8484
*/
8585
export interface CipherstashSdk {
86-
decrypt(args: CipherstashSingleDecryptArgs): Promise<string>;
86+
decrypt(args: CipherstashSingleDecryptArgs): Promise<unknown>;
8787
bulkEncrypt(args: CipherstashBulkEncryptArgs): Promise<ReadonlyArray<unknown>>;
8888
bulkDecrypt(args: CipherstashBulkDecryptArgs): Promise<ReadonlyArray<unknown>>;
8989
/**

0 commit comments

Comments
 (0)