Add EQL v3 Drizzle support#565
Conversation
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
📝 WalkthroughWalkthroughThis PR adds a new EQL v3 Drizzle integration under ChangesEQL v3 Drizzle Integration
Estimated code review effort: 4 (Complex) | ~60 minutes Sequence Diagram(s)sequenceDiagram
participant App
participant createEncryptionOperatorsV3
participant EncryptionClient
participant v3Dialect
participant Postgres
App->>createEncryptionOperatorsV3: eq(column, value)
createEncryptionOperatorsV3->>createEncryptionOperatorsV3: resolve column context and requireIndex
createEncryptionOperatorsV3->>EncryptionClient: encryptQuery(value, queryType)
EncryptionClient-->>createEncryptionOperatorsV3: encrypted term
createEncryptionOperatorsV3->>v3Dialect: equality(term)
v3Dialect-->>createEncryptionOperatorsV3: SQL fragment
createEncryptionOperatorsV3-->>App: SQL
App->>Postgres: execute query with SQL
Possibly related issues
Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 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 |
🦋 Changeset detectedLatest commit: 9c82f50 The changes in this PR will be included in the next version bump. This PR includes changesets to release 6 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 |
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (3)
packages/stack/__tests__/drizzle-v3/column.test.ts (1)
11-18: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winTest reaches into Drizzle's internal column config instead of the public API.
(col as any).config.customTypeParams.dataType()asserts on an internal Drizzle builder shape rather than exercising a public entry point (e.g.,col.getSQLType(), which is Drizzle's documented API for the emitted SQL domain type). This couples the test to internalcustomTypeParams/configshape called out as fragile incolumn.ts.
As per coding guidelines, "prefer testing through the public API rather than private internals."♻️ Proposed fix using public API
it('sets dataType() to the concrete eql_v3 domain', () => { const col = makeEqlV3Column(v3Types.Int4Ord('age')) - // biome-ignore lint/suspicious/noExplicitAny: reading drizzle internals in test - expect((col as any).config.customTypeParams.dataType()).toBe( - 'eql_v3.int4_ord', - ) + expect((col as unknown as { getSQLType(): string }).getSQLType()).toBe( + 'eql_v3.int4_ord', + ) })🤖 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/stack/__tests__/drizzle-v3/column.test.ts` around lines 11 - 18, The `makeEqlV3Column` test is asserting against Drizzle internals via `(col as any).config.customTypeParams.dataType()`, so update the test to verify the emitted SQL domain through the public `getSQLType()` API on the column returned by `makeEqlV3Column` instead. Keep the test focused on the same `makeEqlV3Column`/`v3Types.Int4Ord` path, but remove the `any` cast and internal `config` access so it only depends on documented behavior.Source: Coding guidelines
packages/stack/src/eql/v3/drizzle/operators.ts (2)
188-200: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick winUnbounded concurrent encryption calls for large
inArray/notInArrayinputs.Each element triggers its own
equality()→encryptTerm()round trip, and all are fired concurrently viaPromise.allwith no batching or concurrency cap. A largevaluesarray could spike load on the encryption/KMS backend.Consider chunking or capping concurrency (e.g. via
p-limitor manual batching) for large arrays.🤖 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/stack/src/eql/v3/drizzle/operators.ts` around lines 188 - 200, The inArrayOp helper currently fans out one equality() call per value through Promise.all, which can overwhelm the encryption/KMS path for large arrays. Update inArrayOp to limit concurrency by batching or capping parallel equality() work (for example with a small worker pool or chunked awaits) while preserving the existing negate/or/and behavior and the empty-array true/false handling.
49-56: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueConsider Drizzle's
is()guard instead of blindanycasts for column internals.
drizzleTableOf/resolveContextreach into(column as any).table/.name. Drizzle-orm exposesis(value, Column)specifically to narrow such values safely withoutany, which would also guard against non-ColumnSQLWrapperinputs being misread.🤖 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/stack/src/eql/v3/drizzle/operators.ts` around lines 49 - 56, The `drizzleTableOf` and `resolveContext` helpers are reading `.table` and `.name` via `any` casts on `SQLWrapper`, which should be replaced with Drizzle’s `is()` guard for `Column` instead. Update these helpers to first narrow the input with `is(value, Column)` and only then access column internals, falling back safely for non-Column wrappers. Keep the changes localized to the `drizzleTableOf` and `resolveContext` logic so the context resolution remains type-safe without relying on blind casts.
🤖 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 `@docs/superpowers/specs/2026-07-06-eql-v3-drizzle-concrete-types-design.md`:
- Line 113: The fenced example blocks in this spec are unlabeled, which triggers
docs lint and hurts readability. Update both fenced blocks in the affected
sections to include a language tag using the existing markdown fences in this
document, and keep the content otherwise unchanged. Use the surrounding
fenced-example markup in the spec to locate the two bare fences that need the
same labeling fix.
In `@packages/stack/src/eql/v3/drizzle/operators.ts`:
- Line 47: The table cache in operators.ts is keyed by table name, which can
collide for pgSchema() tables that share names across schemas and merge
unrelated entries via the 'unknown' fallback. Update the cache used by the v3
operator logic to key by the table object itself, replacing the current
Map<string, AnyV3Table> with a WeakMap<PgTable, AnyV3Table>, and adjust the
lookup/insert logic in the table caching path so the AnyV3Table metadata stays
attached to the correct table instance.
In `@packages/stack/src/eql/v3/drizzle/schema-extraction.ts`:
- Around line 20-25: In schema-extraction.ts, the column identifier passed into
getEqlV3Column() is using the JS property key instead of the Drizzle column
name, which can rebuild v3 columns under the wrong name in the fallback path.
Update the loop that populates columns to prefer column.name when available and
only fall back to property if no Drizzle name exists, keeping the rest of the
logic in getEqlV3Column() unchanged.
---
Nitpick comments:
In `@packages/stack/__tests__/drizzle-v3/column.test.ts`:
- Around line 11-18: The `makeEqlV3Column` test is asserting against Drizzle
internals via `(col as any).config.customTypeParams.dataType()`, so update the
test to verify the emitted SQL domain through the public `getSQLType()` API on
the column returned by `makeEqlV3Column` instead. Keep the test focused on the
same `makeEqlV3Column`/`v3Types.Int4Ord` path, but remove the `any` cast and
internal `config` access so it only depends on documented behavior.
In `@packages/stack/src/eql/v3/drizzle/operators.ts`:
- Around line 188-200: The inArrayOp helper currently fans out one equality()
call per value through Promise.all, which can overwhelm the encryption/KMS path
for large arrays. Update inArrayOp to limit concurrency by batching or capping
parallel equality() work (for example with a small worker pool or chunked
awaits) while preserving the existing negate/or/and behavior and the empty-array
true/false handling.
- Around line 49-56: The `drizzleTableOf` and `resolveContext` helpers are
reading `.table` and `.name` via `any` casts on `SQLWrapper`, which should be
replaced with Drizzle’s `is()` guard for `Column` instead. Update these helpers
to first narrow the input with `is(value, Column)` and only then access column
internals, falling back safely for non-Column wrappers. Keep the changes
localized to the `drizzleTableOf` and `resolveContext` logic so the context
resolution remains type-safe without relying on blind casts.
🪄 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: c223fed7-0a1c-4838-90db-88844e2a9f9f
📒 Files selected for processing (20)
.changeset/eql-v3-drizzle.mddocs/superpowers/specs/2026-07-06-eql-v3-drizzle-concrete-types-design.mdpackages/stack/__tests__/drizzle-v3/codec.test.tspackages/stack/__tests__/drizzle-v3/column.test.tspackages/stack/__tests__/drizzle-v3/exports.test.tspackages/stack/__tests__/drizzle-v3/operators-live-pg.test.tspackages/stack/__tests__/drizzle-v3/operators.test.tspackages/stack/__tests__/drizzle-v3/schema-extraction.test.tspackages/stack/__tests__/drizzle-v3/sql-dialect.test.tspackages/stack/__tests__/drizzle-v3/types.test-d.tspackages/stack/__tests__/drizzle-v3/types.test.tspackages/stack/package.jsonpackages/stack/src/eql/v3/drizzle/codec.tspackages/stack/src/eql/v3/drizzle/column.tspackages/stack/src/eql/v3/drizzle/index.tspackages/stack/src/eql/v3/drizzle/operators.tspackages/stack/src/eql/v3/drizzle/schema-extraction.tspackages/stack/src/eql/v3/drizzle/sql-dialect.tspackages/stack/src/eql/v3/drizzle/types.tspackages/stack/tsup.config.ts
#565 ships the sibling integration but on the protect-ffi 0.26 / pre-rename-domain bundle line, while main (this module's base) is 0.27 + SQL-standard names with term constructors in eql_v3_internal and scalar encryptQuery unsupported — so its exact SQL and term-operand path do not transfer. Adopt its dialect shape instead: gating on build().indexes, equality-via-ORE fallback, encrypted ORDER BY (now in scope as a fragment builder), whereIn with bounded operand-encryption concurrency, and the no-silent-fallback error convention. Free-text match pinned to the two-arg eql_v3.contains form; sequencing no longer blocks on #565.
c43777e to
dbe1ec0
Compare
| it('sets dataType() to the concrete eql_v3 domain', () => { | ||
| const col = makeEqlV3Column(v3Types.IntegerOrd('age')) | ||
| const table = pgTable('users', { age: col }) | ||
|
|
||
| expect(table.age.getSQLType()).toBe('eql_v3.integer_ord') | ||
| }) |
There was a problem hiding this comment.
Is there any way to set the SEM specifier? i.e. distinguish between ORE and OPE variants? Can be a follow-up if not.
coderdan
left a comment
There was a problem hiding this comment.
Test coverage review — PR #565 (EQL v3 Drizzle integration)
This PR lands a large, well-covered v3 Drizzle integration (codec, columns, operators, schema extraction, dialect) plus a typedClient reconstructor refactor. Happy paths and most gating errors are thoroughly tested via the V3_MATRIX-driven suites. The gaps below are all new negative / fallback branches introduced by this diff that no test currently exercises — none are blocking.
Verdict: COMMENT — 5 coverage gaps flagged inline.
Out of scope (noted, not posted inline)
v3FromDriver(src/eql/v3/drizzle/codec.ts) callsJSON.parseon the driver string with no guard, so a malformed/corrupt jsonb value throws rather than surfacing a typed error. It's a thin wrapper and low priority, but there is no malformed-input case incodec.test.ts— worth a one-line test if you want the throw pinned.
No overflow beyond the cap.
| const skipUnlessJwt = (): boolean => { | ||
| if (!userJwt) { | ||
| console.log('Skipping lock-context operator test - no USER_JWT provided') | ||
| return true | ||
| } | ||
| return false | ||
| } |
There was a problem hiding this comment.
We'd probably miss this is no JWT set. In fact, I'm dubious that there is one set now.
coderdan
left a comment
There was a problem hiding this comment.
Looks great. A couple of items that should be addressed before merge. Also, the docs are almost non-existent. Should there be a section in the README and some typedoc?
Code review — EQL v3 Drizzle integration + bigint domainsReviewed at extra-high effort (multi-angle finder pass → verify → sweep) against Correctness / robustness1. 2. 3. Drizzle 4. Drizzle 5. Efficiency6. 7. Cleanup / conventions8. 9. A Linear issue ID appears in a source comment (and in the changeset) — 10. Drizzle table-name extraction via 11. 12. Dead Cleared during verification (not bugs): 🤖 Generated with Claude Code |
17857ee to
2f89186
Compare
2706592 to
db0c8ed
Compare
| expect(accounts.balance.getSQLType()).toBe('public.bigint_ord') | ||
| expect(accounts.ledgerId.getSQLType()).toBe('public.bigint_eq') | ||
| expect(accounts.archived.getSQLType()).toBe('public.bigint') |
There was a problem hiding this comment.
Type names will need to be updated when @freshtonic's stuff lands.
…ains) Adds the EQL v3 Drizzle integration at @cipherstash/stack/eql/v3/drizzle: drizzle-native column types, capability-checked operators emitting the two-argument eql_v3 SQL functions, and schema extraction for EncryptionV3. Replayed onto feat/eql-v3-bigint-restack, which already provides the FFI 0.28 concrete types, public.* domains, the bigint family, and the eqlVersion:3 fix. The adapter is namespace-agnostic (domains derived via getEqlType); tests updated to the base's public.<domain> naming while operator functions remain eql_v3.*. Superseded local work (concrete-types/bigint/FFI-0.28 re-vendor, redundant live guards, parallel matrix overhaul) dropped in favour of the base's versions. Original granular history retained in backup/eql-v3-drizzle-concrete-types-pre-ffi028-public-rebase.
typedClient() cached one row-reconstructor per schema table, keyed by the table OBJECT. AnyV3Table is structurally typed, so a table re-imported across modules, rebuilt after HMR, or re-derived via extractEncryptionSchemaV3 (the Drizzle flow) is a distinct object with the same name — object-identity keying returned a spurious 'unknown table' DecryptionError for that valid call. Key by table.tableName (the identity the FFI encrypt config and build() already use). Backward-compatible; adds tests for structural-match success and unknown-table failure. Note: affects only the v3 typedClient wrapper (in-repo, only the Drizzle adapter routes through it); isolated so it can be split to the base branch.
In v3 the concrete type defines capabilities, so the vestigial v2-style chainable .freeTextSearch(opts) tuner on EncryptedTextSearchColumn is obsolete. Remove the method, its matchOpts field, and the build() override that only cloned tuner opts; default match building is unchanged (the base build() already emits the default match config for the TextSearch type). The 'freeTextSearch' query-type/capability is untouched. Drops the now-obsolete tuner tests.
…, perf)
- A3: type the Drizzle customType around the stored Encrypted envelope, not
plaintext — inserts take pre-encrypted rows, select returns envelopes; the
codec is bigint-safe. Removes the as-never/Record casts on the insert path.
- M1: type createEncryptionOperatorsV3 structurally ({ encrypt }) so the
EncryptionV3() return value is accepted without a cast; add operators.test-d
guarding it (the typecheck surface that catches the regression).
- Add bigint drizzle coverage (unit + live round-trip).
- B6: memoize per-column context so inArray builds once, not per element.
- C10: extract a shared drizzle:Name helper. C8: document the intentional
v2/v3 EncryptionOperatorError fork. m3/m4: correct stale public.* comment and
error text. TSDoc for the factory and ~20 operators.
- README: document @cipherstash/stack/eql/v3/drizzle.
The identity/lock-context live suites soft-skip on a missing USER_JWT, which the CI coverage guard never asserted. Add a LIVE_LOCK_CONTEXT_ENABLED flag and a guard assertion, kept as it.skip until the USER_JWT CI secret is provisioned (flip to it.runIf(IN_CI) then) so CI does not fail on the not-yet-present secret.
db0c8ed to
dcae4a2
Compare
|
|
||
| ### EQL v3 Concrete-Type Columns (Drizzle) | ||
|
|
||
| The `@cipherstash/stack/eql/v3/drizzle` module targets EQL v3, where each encrypted column is a **concrete Postgres domain type** (`eql_v3.text_eq`, `eql_v3.integer_ord`, …). Instead of toggling capability flags, you pick a concrete type from the `types` namespace and its query capabilities are fixed by that choice. The v2 `@cipherstash/stack/drizzle` module above is unchanged — this is an additive export. |
There was a problem hiding this comment.
Flagging types need update
| // DEFERRED (follow-up): the CI `USER_JWT` secret is not yet provisioned, so | ||
| // enforcing this now would fail CI. Skipped until the secret exists — flip | ||
| // back to `it.runIf(IN_CI)` at that point. It still documents the real gap: | ||
| // the identity / lock-context live suites soft-skip on a missing USER_JWT, so | ||
| // once the secret lands this guard turns a silent whole-suite skip into a | ||
| // loud failure (as the CS_*/DATABASE_URL guards already do). |
There was a problem hiding this comment.
Should make sure there is an issue tracked
coderdan
left a comment
There was a problem hiding this comment.
A couple of test gaps flagged and we'll need to revise the public type names either here or in a follow-up. But this looks excellent!
freshtonic
left a comment
There was a problem hiding this comment.
Reviewed with a multi-angle pass (line-by-line, removed-behavior, cross-file tracing, reuse/simplification/efficiency/altitude), each candidate independently verified against the head commit and the installed drizzle-orm 0.45.2 / vendored EQL v3 SQL. The integration is solid: production wiring (exports map ↔ tsup entry ↔ barrel), extractEncryptionSchemaV3 → EncryptionV3 consistency, and the public.* domain / eql_v3.* function split all check out, and several scary-sounding candidates were refuted along the way (details at the end). Findings, most severe first:
1. packages/stack/src/eql/v3/drizzle/sql-dialect.ts:17 — v3Dialect.range emits an unparenthesized gte(...) AND lte(...) fragment, so ops.not(await ops.between(...)) silently returns wrong rows (CONFIRMED)
Drizzle's not renders not ${condition} with no parens (drizzle-orm 0.45.2 conditions.js:58-60), and this module re-exports it unchanged. NOT eql_v3.gte(col, x) AND eql_v3.lte(col, y) parses as (NOT gte) AND lte — for between(age, 25, 40) that's age < 25, not the intended complement. Your own notBetween proves the fragment is unsafe bare: it wraps in sql\NOT (...)`atoperators.ts:299. Tests cover not(contains(...))(an atomic call, safe) but nevernot(between(...)). Fix at the dialect level — parenthesize inside range` itself — so every composition is safe, rather than relying on each caller to wrap.
2. packages/stack/src/eql/v3/drizzle/operators.ts:328 — inArray/notInArray pay N FFI round-trips where one bulk call works (CONFIRMED)
Each value goes through a separate client.encrypt() crossing, four at a time (MAX_IN_ARRAY_CONCURRENCY). The v2 adapter already batches its inArray in a single call (src/drizzle/operators.ts encryptedInArray), and the v3 TypedEncryptionClient exposes bulkEncrypt(plaintexts, opts) with exactly the shape needed (N plaintexts, one {table, column}). Widening OperandEncryptionClient to include bulkEncrypt collapses an N=100 inArray from ~25 waves of FFI crossings to one.
3. __tests__/drizzle-v3/column.test.ts:12, operators.test.ts:57, operators-live-pg.test.ts:41 — three slug helpers still strip the old eql_v3. prefix (CONFIRMED)
eqlType values are now public.*, so the replace is a no-op and the live suite emits column identifiers like "public.integer_ord". Self-consistent (everything routes through the same no-op) so nothing fails, which is exactly why it slipped through — but the doc comments claim a bare integer_ord and the generated DDL is misleading. Same bug family exists in the sibling v3-matrix live tests. Suggest one shared slug in a common test helper so the next prefix change is a single edit.
4. packages/stack/src/eql/v3/drizzle/column.ts:26-27 — the "legacy" string key _eqlv3Column is redundant alongside the Symbol key (CONFIRMED)
This is brand-new code with no prior on-disk format. In drizzle-orm 0.45.2 the builder→column path passes config by reference (custom.js:12-16, column.js:5) and customTypeParams is never cloned, so the Symbol.for key always survives; Symbol.for is registry-global so CJS/ESM duality can't break it either, and no test exercises the string fallback. Every read/write maintaining two keys that can never legitimately diverge is pure drift surface — a single Symbol key suffices.
5. packages/stack/src/eql/v3/drizzle/sql-dialect.ts:9-25 — the eql_v3. function-schema prefix is hardcoded in all five dialect helpers
Domains are already centralized (derived from columns.ts via getEqlType()), but the invoked functions repeat the literal eql_v3. prefix five times, plus in test assertions. Given the domain rename that just swept this branch (eql_v3.* → public.*), a future function-schema move must hand-edit every site — and the unit tests assert the literal string while the live suite skips without credentials, so a half-migration ships green. One EQL_V3_FN prefix constant makes it a one-line change.
6. __tests__/live-coverage-guard.test.ts:97 — the USER_JWT guard is it.skip'd, so the new lock-context suite has zero effective CI coverage today
operators-lock-context-live-pg.test.ts soft-skips when USER_JWT is absent (the normal state until the secret is provisioned), and the guard designed to make that loud is disabled. Understood this is staged deliberately — flagging it so provisioning the secret and un-skipping doesn't slip; until then a lock-context regression ships undetected, which is the exact failure mode this guard file exists to prevent.
7. Changeset omission — the .freeTextSearch(opts) tuner removal isn't mentioned anywhere
The PR removes per-column match tuning from EncryptedTextSearchColumn (match index now always default config) along with the six tests covering its merge semantics. The code comments document the new behavior and there are no v3 callers, so the removal itself is fine — but neither eql-v3-drizzle.md nor the earlier text-search changeset records that tuning was dropped. Worth a line so the capability change is traceable.
8. packages/stack/src/eql/v3/drizzle/operators.ts:260 — the equality gate is inlined while every other operator uses requireIndex
equality() hand-rolls if (!ctx.indexes.unique && !ctx.indexes.ore) with its own message format; comparison/range/contains/orderTerm all route through requireIndex. Extending the helper to accept an index set keeps the "what grants equality" rule in one place and the diagnostics consistent — today a future change to gating semantics has to be made twice.
Checked and refuted, for the record: ops.eq on an ore-only IntegerOrd column is correct — Postgres selects the domain-specific eql_v3.eq(a public.integer_ord, b jsonb) overload which compares ord_terms (no HMAC needed), and the live matrix includes integer_ord in its equality domains and asserts the row comes back. The reconstructors Map in encryption/v3.ts can't collide on tableName (duplicate names throw in buildEncryptConfig at init), and rejecting unregistered tables in decryptModel is intentional and directly tested. Single-element inArray under not() is safe (atomic function-call term). The EncryptionOperatorError fork is explicitly documented as intentional.
The rebase moved the v3 catalog keys from eql_v3.* to public.*, but the drizzle-v3 test slug helper still stripped the 'eql_v3.' prefix — a no-op against public.* keys. It left every matrix column named 'public.<domain>' (with a dot) instead of '<domain>'. The mocked unit suites passed (they assert slug() against itself), but the LIVE operators-live-pg matrix failed at bulkEncryptModels with 'Cannot convert undefined or null to object' because protect-ffi cannot resolve a dotted column identifier. Strip '^public.' to match the passing v3-matrix suites (matrix-live-pg) and yield clean column names. Test-only; no source change.
…tion
v3Dialect.range emitted a bare `eql_v3.gte(..) AND eql_v3.lte(..)`. Drizzle's
`not` renders `not ${condition}` with no parentheses, and Postgres binds NOT
tighter than AND, so `ops.not(await ops.between(age, 25, 40))` ran as
`(NOT gte) AND lte` — i.e. `age < 25` — silently returning rows below the range
instead of its complement. `range` now emits a self-contained parenthesised
conjunction, so every composition is safe rather than each caller having to
wrap. `notBetween`'s manual `NOT (...)` wrapper becomes redundant.
inArray/notInArray encrypted one value per FFI crossing (4 at a time). They now
encrypt the whole list in a single `bulkEncrypt` call. `OperandEncryptionClient`
gains an optional `bulkEncrypt`, so `{ encrypt }`-only clients stay valid and
fall back to the existing concurrency pool. A bulk response whose length differs
from the input is rejected rather than truncated, which would otherwise widen an
inArray or narrow a notInArray.
That batching exposed a gap: EncryptOperation rejects NaN/±Infinity/out-of-int64
bigint client-side, BulkEncryptOperation did not — so routing operands through
the bulk path lost the guard for every bulkEncrypt caller. Validation now lives
in the shared `createEncryptPayloads`, restoring parity for both bulk classes.
Also: `requireIndex` takes a disjunction of index keys, so equality (unique OR
ore) routes through it like every other operator and gains the offending-domain
diagnostic; the redundant `_eqlv3Column` string key is dropped in favour of the
Symbol.for key that provably survives the builder→column path; the `eql_v3.`
function prefix is centralised behind EQL_V3_FN_SCHEMA; and the five copies of
the domain-slug helper collapse into one shared `eqlTypeSlug`.
Regression tests were written first and verified by mutation: reverting each fix
fails at least one test. The live `not(between)` proof anchors its bound at the
smaller seeded value — anchored at the larger one, the buggy `col < bound` and
the correct `col != bound` select the same row and the test is vacuous.
No changeset: the v3 Drizzle adapter is unreleased (CHANGELOG is at 0.18.0 with
no eql/v3), so these fixes fold into the pending eql-v3-drizzle changeset.
The bigint_ord/bigint_ord_ore domains are ORE-indexed, so their i64 bigint samples flow into comparePlain, which only handled Date/number/ string and threw "Unsupported ordered values" at runtime. Add a bigint branch (mirroring matrix-live-pg's oracle) and widen PlainValue.
Summary
Adds the EQL v3 Drizzle integration (
@cipherstash/stack/eql/v3/drizzle) plus hardened live test coverage, on top of the v3 typed client / schema DSL.typesnamespace whose columns are the semanticeql_v3.<domain>Postgres types.createEncryptionOperatorsV3— capability-checked operators (eq/ne/gt/gte/lt/lte/between/notBetween/contains/inArray/notInArray/asc/desc/and/or/not/isNull/isNotNull/exists/notExists) that emit the two-argumenteql_v3term-function SQL. The concrete column type drives which operators are legal (e.g.containsneeds a match index;asc/betweenneed order).extractEncryptionSchemaV3rebuilds the encrypt schema forEncryptionV3.The existing v2
@cipherstash/stack/drizzleintegration is unchanged. v3 free-text search is token containment, socontainsreplaces SQLlike/ilike(deliberately not exposed).Usage
Test Plan
cd packages/stack && pnpm biome check src/eql/v3/drizzle __tests__/{drizzle-v3,v3-matrix}cd packages/stack && pnpm vitest run __tests__/{drizzle-v3,v3-matrix}cd packages/stack && pnpm test:types && pnpm buildeql_v3) run whenDATABASE_URL+CS_*are set and self-install theeql_v3extension atbeforeAll. The Drizzle lock-context suite additionally needsUSER_JWTand soft-skips without it.Notes
REQUIRE_LIVE/REQUIRE_LIVE_PGguard turns a silent skip into a CI failure so the live matrix can't quietly go green.eql_v3.*function forms are interim (tracked by CIP-3402 / CIP-3423).Summary by CodeRabbit
New Features
Bug Fixes