Skip to content

Commit 17857ee

Browse files
committed
feat(stack): re-enable EQL v3 bigint domain family on protect-ffi 0.28
Native bigint round-tripping landed in protect-ffi 0.28, so restore the bigint domains that skip-v3-bigint deferred: - columns.ts: BIGINT/BIGINT_EQ/BIGINT_ORD/BIGINT_ORD_ORE definitions (concrete domain eql_v3.int8* per the SQL fixture, castAs 'bigint'), EncryptedBigint*Column classes, AnyEncryptedV3Column union, and PlaintextKind/PlaintextFromKind mapping 'bigint' -> JS bigint. - types.ts / index.ts: types.Bigint* factories and barrel exports. - v3-matrix/catalog.ts: four int8 rows with i64-bound samples (max/min/0 and a value past Number.MAX_SAFE_INTEGER); samples widened to bigint. Drizzle operator coverage is catalog-driven, so it picks these up too. - schema-v3.test-d: flip the "bigint not public" negative test to assert the factories now exist. - encryption/v3.ts: bigint needs no decrypt reconstruction (FFI returns a real JS bigint on 0.28). - drop .changeset/skip-v3-bigint.md; add re-enable changeset. i64 bounds are enforced at the protect-ffi boundary. *_ord_ope is out of scope (CIP-3403).
1 parent 57e4483 commit 17857ee

8 files changed

Lines changed: 104 additions & 17 deletions

File tree

.changeset/eql-v3-bigint.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@cipherstash/stack': minor
3+
---
4+
5+
Re-enable the EQL v3 bigint domain family now that native bigint round-tripping landed in `@cipherstash/protect-ffi` 0.28. Adds the `types.Bigint` / `types.BigintEq` / `types.BigintOrd` / `types.BigintOrdOre` factories (concrete Postgres domains `eql_v3.int8`, `eql_v3.int8_eq`, `eql_v3.int8_ord`, `eql_v3.int8_ord_ore`), their `EncryptedBigint*Column` classes, and full test-matrix + Drizzle operator coverage. Bigint columns take and decrypt to a JS `bigint`; i64 bounds (`-2^63 … 2^63 - 1`) are enforced at the protect-ffi boundary.

.changeset/skip-v3-bigint.md

Lines changed: 0 additions & 5 deletions
This file was deleted.

packages/stack/__tests__/schema-v3.test-d.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import { describe, expectTypeOf, it } from 'vitest'
22
import { Encryption, type EncryptionClient } from '@/encryption'
33
import type {
4+
EncryptedBigintColumn,
5+
EncryptedBigintOrdColumn,
46
EncryptedTextSearchColumn,
57
InferEncrypted,
68
InferPlaintext,
@@ -21,9 +23,9 @@ describe('eql_v3 schema type inference', () => {
2123
expectTypeOf(col).toEqualTypeOf<EncryptedTextSearchColumn>()
2224
})
2325

24-
it('does not expose bigint factories until native bigint round-tripping lands', () => {
25-
// @ts-expect-error - bigint v3 domains are intentionally not public yet
26-
types.Bigint('large')
26+
it('exposes bigint factories (native round-tripping landed in protect-ffi 0.28)', () => {
27+
expectTypeOf(types.Bigint('large')).toEqualTypeOf<EncryptedBigintColumn>()
28+
expectTypeOf(types.BigintOrd('large')).toEqualTypeOf<EncryptedBigintOrdColumn>()
2729
})
2830

2931
it('encryptedTable exposes column builders as typed properties', () => {

packages/stack/__tests__/v3-matrix/catalog.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@ import type {
2323
QueryCapabilities,
2424
} from '@/eql/v3'
2525
import {
26+
EncryptedBigintColumn,
27+
EncryptedBigintEqColumn,
28+
EncryptedBigintOrdColumn,
29+
EncryptedBigintOrdOreColumn,
2630
EncryptedBooleanColumn,
2731
EncryptedDateColumn,
2832
EncryptedDateEqColumn,
@@ -99,7 +103,7 @@ export type DomainSpec = Readonly<{
99103
* tell `integer` from `double`, and a fractional value on an int-named domain is
100104
* untested territory (it would truncate against a real narrow PG column).
101105
*/
102-
samples: ReadonlyArray<string | number | boolean | Date>
106+
samples: ReadonlyArray<string | number | bigint | boolean | Date>
103107
/**
104108
* Values that MUST fail encryption. Number domains reject `NaN`/`±Infinity`
105109
* via a global guard; other domains omit this.
@@ -175,6 +179,14 @@ const TEXT_SEARCH_IDX: Indexes = {
175179
// smallint/integer, fractionals for real/double/numeric. See `DomainSpec.samples`.
176180
const SMALLINT_S = [0, -1, 32767, -32768] as const
177181
const INTEGER_S = [0, -42, 2147483647, -2147483648] as const
182+
// bigint (int8) plaintexts: i64 bounds, 0, and a value beyond
183+
// Number.MAX_SAFE_INTEGER (2^53 - 1) that would lose precision as a JS `number`.
184+
const BIGINT_S = [
185+
0n,
186+
9223372036854775807n,
187+
-9223372036854775808n,
188+
9007199254740993n,
189+
] as const
178190
const REAL_S = [0, 77.5, -117.25, 0.5] as const
179191
const DOUBLE_S = [0, -117.123456, 1e15, -1e15] as const
180192
const NUMERIC_S = [0, 12345.678, -42, -0.5] as const
@@ -220,6 +232,11 @@ export const V3_MATRIX = {
220232
'eql_v3.smallint_eq': { builder: types.SmallintEq, ColumnClass: EncryptedSmallintEqColumn, castAs: 'number', capabilities: EQ, indexes: UNIQUE_IDX, samples: SMALLINT_S, errorSamples: NUM_ERR },
221233
'eql_v3.smallint_ord_ore': { builder: types.SmallintOrdOre, ColumnClass: EncryptedSmallintOrdOreColumn, castAs: 'number', capabilities: ORD, indexes: ORE_IDX, samples: SMALLINT_S, errorSamples: NUM_ERR },
222234
'eql_v3.smallint_ord': { builder: types.SmallintOrd, ColumnClass: EncryptedSmallintOrdColumn, castAs: 'number', capabilities: ORD, indexes: ORE_IDX, samples: SMALLINT_S, errorSamples: NUM_ERR },
235+
// bigint (int8) — plaintext is a JS `bigint`; no errorSamples (bigint has no NaN/Infinity)
236+
'eql_v3.int8': { builder: types.Bigint, ColumnClass: EncryptedBigintColumn, castAs: 'bigint', capabilities: STORAGE, indexes: NONE, samples: BIGINT_S },
237+
'eql_v3.int8_eq': { builder: types.BigintEq, ColumnClass: EncryptedBigintEqColumn, castAs: 'bigint', capabilities: EQ, indexes: UNIQUE_IDX, samples: BIGINT_S },
238+
'eql_v3.int8_ord_ore': { builder: types.BigintOrdOre, ColumnClass: EncryptedBigintOrdOreColumn, castAs: 'bigint', capabilities: ORD, indexes: ORE_IDX, samples: BIGINT_S },
239+
'eql_v3.int8_ord': { builder: types.BigintOrd, ColumnClass: EncryptedBigintOrdColumn, castAs: 'bigint', capabilities: ORD, indexes: ORE_IDX, samples: BIGINT_S },
223240
// date
224241
'eql_v3.date': { builder: types.Date, ColumnClass: EncryptedDateColumn, castAs: 'date', capabilities: STORAGE, indexes: NONE, samples: DATE_S },
225242
'eql_v3.date_eq': { builder: types.DateEq, ColumnClass: EncryptedDateEqColumn, castAs: 'date', capabilities: EQ, indexes: UNIQUE_IDX, samples: DATE_S },

packages/stack/src/encryption/v3.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -128,8 +128,10 @@ export interface TypedEncryptionClient<S extends readonly AnyV3Table[]> {
128128
* row-invariant, but non-trivial to build — is derived once per call site,
129129
* not once per row on the bulk path.
130130
*
131-
* NOTE: `bigint` (int8) reconstruction is intentionally absent — int8 domains are
132-
* omitted from the v3 SDK until the native FFI supports lossless bigint I/O.
131+
* NOTE: `bigint` (int8) columns need NO reconstruction here — protect-ffi 0.28
132+
* marshals a JS `bigint` across the Neon boundary and returns a real JS `bigint`
133+
* on decrypt, which the model decrypt path passes through with no JSON
134+
* round-trip that could destroy it. Only date-like domains are rebuilt below.
133135
*/
134136
function rowReconstructor(
135137
table: AnyV3Table,

packages/stack/src/eql/v3/columns.ts

Lines changed: 52 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ export type DateLikeCast = (typeof DATE_LIKE_CASTS)[number]
4040

4141
/** The plaintext (TypeScript) kind a v3 domain decrypts to. A subset of the
4242
* SDK `CastAs` enum, restricted to the scalar kinds v3 domains actually use. */
43-
type PlaintextKind = 'string' | 'number' | 'boolean' | DateLikeCast
43+
type PlaintextKind = 'string' | 'number' | 'bigint' | 'boolean' | DateLikeCast
4444

4545
/**
4646
* The full, literal definition of a v3 domain. This is the LOAD-BEARING type:
@@ -152,6 +152,33 @@ export const SMALLINT_ORD = {
152152
capabilities: ORDER_AND_RANGE,
153153
} as const
154154

155+
// bigint (int8) domains. Plaintext is a JS `bigint` (always decrypts to
156+
// `bigint`); bounds are the full i64 range, enforced at the protect-ffi
157+
// boundary. Native round-tripping landed in protect-ffi 0.28 (`JsPlaintext`
158+
// marshals a JS `bigint` across the Neon boundary), so live encrypt/decrypt is
159+
// no longer gated (`*_ord_ope` variants are out of scope — CIP-3403). The
160+
// concrete domain is the SQL fixture's `int8` name, not `bigint`.
161+
export const BIGINT = {
162+
eqlType: 'eql_v3.int8',
163+
castAs: 'bigint',
164+
capabilities: STORAGE_ONLY,
165+
} as const
166+
export const BIGINT_EQ = {
167+
eqlType: 'eql_v3.int8_eq',
168+
castAs: 'bigint',
169+
capabilities: EQUALITY_ONLY,
170+
} as const
171+
export const BIGINT_ORD_ORE = {
172+
eqlType: 'eql_v3.int8_ord_ore',
173+
castAs: 'bigint',
174+
capabilities: ORDER_AND_RANGE,
175+
} as const
176+
export const BIGINT_ORD = {
177+
eqlType: 'eql_v3.int8_ord',
178+
castAs: 'bigint',
179+
capabilities: ORDER_AND_RANGE,
180+
} as const
181+
155182
export const DATE = {
156183
eqlType: 'eql_v3.date',
157184
castAs: 'date',
@@ -497,6 +524,19 @@ export class EncryptedSmallintOrdColumn extends EncryptedV3Column<
497524
typeof SMALLINT_ORD
498525
> {}
499526

527+
// bigint (int8) — native round-trips landed in protect-ffi 0.28; see the note
528+
// by the BIGINT domain definitions above.
529+
export class EncryptedBigintColumn extends EncryptedV3Column<typeof BIGINT> {}
530+
export class EncryptedBigintEqColumn extends EncryptedV3Column<
531+
typeof BIGINT_EQ
532+
> {}
533+
export class EncryptedBigintOrdOreColumn extends EncryptedV3Column<
534+
typeof BIGINT_ORD_ORE
535+
> {}
536+
export class EncryptedBigintOrdColumn extends EncryptedV3Column<
537+
typeof BIGINT_ORD
538+
> {}
539+
500540
// date
501541
export class EncryptedDateColumn extends EncryptedV3Column<typeof DATE> {}
502542
export class EncryptedDateEqColumn extends EncryptedV3Column<typeof DATE_EQ> {}
@@ -584,6 +624,10 @@ export type AnyEncryptedV3Column =
584624
| EncryptedSmallintEqColumn
585625
| EncryptedSmallintOrdOreColumn
586626
| EncryptedSmallintOrdColumn
627+
| EncryptedBigintColumn
628+
| EncryptedBigintEqColumn
629+
| EncryptedBigintOrdOreColumn
630+
| EncryptedBigintOrdColumn
587631
| EncryptedDateColumn
588632
| EncryptedDateEqColumn
589633
| EncryptedDateOrdOreColumn
@@ -625,11 +669,13 @@ type PlaintextFromKind<K extends PlaintextKind> = K extends 'string'
625669
? string
626670
: K extends 'number'
627671
? number
628-
: K extends 'boolean'
629-
? boolean
630-
: K extends DateLikeCast
631-
? Date
632-
: never
672+
: K extends 'bigint'
673+
? bigint
674+
: K extends 'boolean'
675+
? boolean
676+
: K extends DateLikeCast
677+
? Date
678+
: never
633679

634680
/**
635681
* The plaintext type for a single v3 column, read from the literal domain

packages/stack/src/eql/v3/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,10 @@ export type {
1818
} from './columns'
1919

2020
export {
21+
EncryptedBigintColumn,
22+
EncryptedBigintEqColumn,
23+
EncryptedBigintOrdColumn,
24+
EncryptedBigintOrdOreColumn,
2125
EncryptedBooleanColumn,
2226
EncryptedDateColumn,
2327
EncryptedDateEqColumn,

packages/stack/src/eql/v3/types.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
11
import {
2+
BIGINT,
3+
BIGINT_EQ,
4+
BIGINT_ORD,
5+
BIGINT_ORD_ORE,
26
BOOLEAN,
37
DATE,
48
DATE_EQ,
@@ -8,6 +12,10 @@ import {
812
DOUBLE_EQ,
913
DOUBLE_ORD,
1014
DOUBLE_ORD_ORE,
15+
EncryptedBigintColumn,
16+
EncryptedBigintEqColumn,
17+
EncryptedBigintOrdColumn,
18+
EncryptedBigintOrdOreColumn,
1119
EncryptedBooleanColumn,
1220
EncryptedDateColumn,
1321
EncryptedDateEqColumn,
@@ -117,6 +125,14 @@ export const types = {
117125
SmallintOrd: (name: string) =>
118126
new EncryptedSmallintOrdColumn(name, SMALLINT_ORD),
119127

128+
// bigint (int8) — plaintext is a JS `bigint` (i64-bounded at the protect-ffi
129+
// boundary); native round-tripping landed in protect-ffi 0.28
130+
Bigint: (name: string) => new EncryptedBigintColumn(name, BIGINT),
131+
BigintEq: (name: string) => new EncryptedBigintEqColumn(name, BIGINT_EQ),
132+
BigintOrdOre: (name: string) =>
133+
new EncryptedBigintOrdOreColumn(name, BIGINT_ORD_ORE),
134+
BigintOrd: (name: string) => new EncryptedBigintOrdColumn(name, BIGINT_ORD),
135+
120136
// date
121137
Date: (name: string) => new EncryptedDateColumn(name, DATE),
122138
DateEq: (name: string) => new EncryptedDateEqColumn(name, DATE_EQ),

0 commit comments

Comments
 (0)