Skip to content

Commit 63ca540

Browse files
committed
chore(stack): re-vendor EQL v3 bundle; move type domains to public.*
Adopt the latest EQL v3 SQL bundle and realign the v3 DSL so the SDK's emitted domain names byte-match the installed bundle (a mismatch fails CREATE TABLE/cast resolution at runtime, only caught by live-PG tests). - Re-vendor packages/stack/__tests__/fixtures/eql-v3/cipherstash-encrypt-v3.sql - Type domains eql_v3.* -> public.* (user-column domains now live in public so app tables survive EQL uninstall); eql_v3.bool -> public.boolean - Index-term constructors hmac_256/ore_block_256/bloom_filter moved to eql_v3_internal in live-test SQL; operator extractors (eq_term/ord_term/ match_term) stay in eql_v3 - Remap eqlType strings + catalog keys + type-level/unit assertions Verified: test:types 60/60, schema-v3 unit 162/162, no regressions. Live-PG contract suite runs in CI.
1 parent 89b903f commit 63ca540

12 files changed

Lines changed: 32622 additions & 19055 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@cipherstash/stack': patch
3+
---
4+
5+
Re-vendor the EQL v3 SQL bundle and align the v3 DSL to it: encrypted type domains now live in the `public` schema (`public.text`, `public.integer`, …) rather than `eql_v3`, and the boolean domain is `public.boolean` (was `eql_v3.bool`). The `eql_v3` schema now holds only the operator-backing functions, and the index-term constructors (`hmac_256`, `ore_block_256`, `bloom_filter`) moved to `eql_v3_internal`. This keeps the SDK's emitted domain names byte-matched to the installed bundle so `CREATE TABLE`/cast resolution succeeds.

packages/stack/__tests__/fixtures/eql-v3/cipherstash-encrypt-v3.sql

Lines changed: 32468 additions & 18906 deletions
Large diffs are not rendered by default.

packages/stack/__tests__/helpers/eql-v3.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,13 @@ const eqlV3SqlPath = resolve(
1414
// The sentinel must be a type that exists ONLY in the currently vendored
1515
// bundle, so a database still carrying an older EQL v3 install is detected as
1616
// stale and reinstalled (the bundle's leading DROP SCHEMA … CASCADE replaces
17-
// the old install wholesale). eql_v3.timestamp is new in the current bundle
17+
// the old install wholesale). public.timestamp is new in the current bundle
1818
// (the timestamptz → timestamp rename, encrypt-query-language@2e64ca73); the
19-
// previous sentinel, eql_v3.text_search, exists in both generations and would
19+
// previous sentinel, public.text_search, exists in both generations and would
2020
// leave a stale install in place.
2121
async function hasCurrentEqlV3(sql: postgres.Sql): Promise<boolean> {
2222
const [row] = await sql<{ installed: boolean }[]>`
23-
SELECT to_regtype('eql_v3.timestamp') IS NOT NULL AS installed
23+
SELECT to_regtype('public.timestamp') IS NOT NULL AS installed
2424
`
2525
return row?.installed ?? false
2626
}
@@ -49,7 +49,7 @@ export async function installEqlV3IfNeeded(sql: postgres.Sql): Promise<void> {
4949
await reserved.unsafe(eqlV3Sql)
5050

5151
if (!(await hasCurrentEqlV3(reserved))) {
52-
throw new Error('EQL v3 installation did not create eql_v3.timestamp')
52+
throw new Error('EQL v3 installation did not create public.timestamp')
5353
}
5454
} finally {
5555
await reserved`SELECT pg_advisory_unlock(${EQL_V3_ADVISORY_LOCK_ID})`

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

Lines changed: 24 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ async function insertRow(label: string, email: string): Promise<number> {
6363

6464
const [inserted] = await sql<{ id: number }[]>`
6565
INSERT INTO protect_ci_v3_text_search (email, label, test_run_id)
66-
VALUES (${sql.json(encrypted)}::eql_v3.text_search, ${label}, ${TEST_RUN_ID})
66+
VALUES (${sql.json(encrypted)}::public.text_search, ${label}, ${TEST_RUN_ID})
6767
RETURNING id
6868
`
6969

@@ -98,7 +98,7 @@ beforeAll(async () => {
9898
await sql`
9999
CREATE TABLE IF NOT EXISTS protect_ci_v3_text_search (
100100
id INTEGER PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
101-
email eql_v3.text_search NOT NULL,
101+
email public.text_search NOT NULL,
102102
label TEXT NOT NULL,
103103
test_run_id TEXT NOT NULL
104104
)
@@ -107,9 +107,9 @@ beforeAll(async () => {
107107
await sql`
108108
CREATE TABLE IF NOT EXISTS protect_ci_v3_typed_domains (
109109
id INTEGER PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
110-
age eql_v3.integer_ord NOT NULL,
111-
nickname eql_v3.text_eq NOT NULL,
112-
active eql_v3.bool NOT NULL,
110+
age public.integer_ord NOT NULL,
111+
nickname public.text_eq NOT NULL,
112+
active public.boolean NOT NULL,
113113
test_run_id TEXT NOT NULL
114114
)
115115
`
@@ -143,7 +143,7 @@ afterAll(async () => {
143143
}, 30000)
144144

145145
describeLivePg('eql_v3 text_search postgres integration', () => {
146-
it('round-trips an encrypted value through an eql_v3.text_search column', async () => {
146+
it('round-trips an encrypted value through an public.text_search column', async () => {
147147
const id = await insertRow('roundtrip', 'roundtrip@example.com')
148148

149149
const [row] = await sql<InsertedRow[]>`
@@ -156,38 +156,38 @@ describeLivePg('eql_v3 text_search postgres integration', () => {
156156
await expect(decryptRow(row)).resolves.toBe('roundtrip@example.com')
157157
}, 30000)
158158

159-
it('queries equality terms with eql_v3.eq_term and eql_v3.hmac_256', async () => {
159+
it('queries equality terms with eql_v3.eq_term and eql_v3_internal.hmac_256', async () => {
160160
const ids = await seedRows()
161161
const equalityTerm = await encryptQueryTerm('grace@example.com', 'equality')
162162

163163
const rows = await sql<InsertedRow[]>`
164164
SELECT id, email::jsonb AS email, label
165165
FROM protect_ci_v3_text_search
166166
WHERE test_run_id = ${TEST_RUN_ID}
167-
AND eql_v3.eq_term(email) = eql_v3.hmac_256(${sql.json(equalityTerm)}::jsonb)
167+
AND eql_v3.eq_term(email) = eql_v3_internal.hmac_256(${sql.json(equalityTerm)}::jsonb)
168168
ORDER BY id
169169
`
170170

171171
expect(rows.map((row) => row.id)).toEqual([ids.grace])
172172
await expect(decryptRow(rows[0])).resolves.toBe('grace@example.com')
173173
}, 30000)
174174

175-
it('queries free-text terms with eql_v3.match_term and eql_v3.bloom_filter', async () => {
175+
it('queries free-text terms with eql_v3.match_term and eql_v3_internal.bloom_filter', async () => {
176176
await seedRows()
177177
const matchTerm = await encryptQueryTerm('example.com', 'freeTextSearch')
178178

179179
const rows = await sql<InsertedRow[]>`
180180
SELECT id, email::jsonb AS email, label
181181
FROM protect_ci_v3_text_search
182182
WHERE test_run_id = ${TEST_RUN_ID}
183-
AND eql_v3.match_term(email) @> eql_v3.bloom_filter(${sql.json(matchTerm)}::jsonb)
183+
AND eql_v3.match_term(email) @> eql_v3_internal.bloom_filter(${sql.json(matchTerm)}::jsonb)
184184
ORDER BY label
185185
`
186186

187187
expect(rows.map((row) => row.label)).toEqual(['ada', 'grace'])
188188
}, 30000)
189189

190-
it('queries range terms with eql_v3.ord_term and eql_v3.ore_block_256', async () => {
190+
it('queries range terms with eql_v3.ord_term and eql_v3_internal.ore_block_256', async () => {
191191
await seedRows()
192192
const lower = await encryptQueryTerm('grace@example.com', 'orderAndRange')
193193
const upper = await encryptQueryTerm('zora@example.org', 'orderAndRange')
@@ -196,8 +196,8 @@ describeLivePg('eql_v3 text_search postgres integration', () => {
196196
SELECT id, email::jsonb AS email, label
197197
FROM protect_ci_v3_text_search
198198
WHERE test_run_id = ${TEST_RUN_ID}
199-
AND eql_v3.ord_term(email) >= eql_v3.ore_block_256(${sql.json(lower)}::jsonb)
200-
AND eql_v3.ord_term(email) <= eql_v3.ore_block_256(${sql.json(upper)}::jsonb)
199+
AND eql_v3.ord_term(email) >= eql_v3_internal.ore_block_256(${sql.json(lower)}::jsonb)
200+
AND eql_v3.ord_term(email) <= eql_v3_internal.ore_block_256(${sql.json(upper)}::jsonb)
201201
ORDER BY eql_v3.ord_term(email)
202202
`
203203

@@ -242,14 +242,14 @@ describeLivePg('eql_v3 text_search postgres integration', () => {
242242
)
243243
}, 30000)
244244

245-
it('rejects query-only payloads cast as eql_v3.text_search values', async () => {
245+
it('rejects query-only payloads cast as public.text_search values', async () => {
246246
const equalityTerm = await encryptQueryTerm('ada@example.com', 'equality')
247247

248248
await expect(
249249
sql`
250250
INSERT INTO protect_ci_v3_text_search (email, label, test_run_id)
251251
VALUES (
252-
${sql.json(equalityTerm)}::eql_v3.text_search,
252+
${sql.json(equalityTerm)}::public.text_search,
253253
'query-only',
254254
${TEST_RUN_ID}
255255
)
@@ -280,9 +280,9 @@ describeLivePg('eql_v3 text_search postgres integration', () => {
280280
const [inserted] = await sql<{ id: number }[]>`
281281
INSERT INTO protect_ci_v3_typed_domains (age, nickname, active, test_run_id)
282282
VALUES (
283-
${sql.json(age as postgres.JSONValue)}::eql_v3.integer_ord,
284-
${sql.json(nickname as postgres.JSONValue)}::eql_v3.text_eq,
285-
${sql.json(active as postgres.JSONValue)}::eql_v3.bool,
283+
${sql.json(age as postgres.JSONValue)}::public.integer_ord,
284+
${sql.json(nickname as postgres.JSONValue)}::public.text_eq,
285+
${sql.json(active as postgres.JSONValue)}::public.boolean,
286286
${TEST_RUN_ID}
287287
)
288288
RETURNING id
@@ -300,7 +300,7 @@ describeLivePg('eql_v3 text_search postgres integration', () => {
300300
SELECT id
301301
FROM protect_ci_v3_typed_domains
302302
WHERE test_run_id = ${TEST_RUN_ID}
303-
AND eql_v3.ord_term(age) >= eql_v3.ore_block_256(${sql.json(ageTerm)}::jsonb)
303+
AND eql_v3.ord_term(age) >= eql_v3_internal.ore_block_256(${sql.json(ageTerm)}::jsonb)
304304
`
305305

306306
expect(rows.map((row) => row.id)).toContain(inserted.id)
@@ -333,9 +333,9 @@ describeLivePg('eql_v3 text_search postgres integration', () => {
333333
const [row] = await sql<{ id: number }[]>`
334334
INSERT INTO protect_ci_v3_typed_domains (age, nickname, active, test_run_id)
335335
VALUES (
336-
${sql.json(ageCt)}::eql_v3.integer_ord,
337-
${sql.json(nick)}::eql_v3.text_eq,
338-
${sql.json(act)}::eql_v3.bool,
336+
${sql.json(ageCt)}::public.integer_ord,
337+
${sql.json(nick)}::public.text_eq,
338+
${sql.json(act)}::public.boolean,
339339
${TEST_RUN_ID}
340340
)
341341
RETURNING id
@@ -363,7 +363,7 @@ describeLivePg('eql_v3 text_search postgres integration', () => {
363363
SELECT id
364364
FROM protect_ci_v3_typed_domains
365365
WHERE test_run_id = ${TEST_RUN_ID}
366-
AND eql_v3.ord_term(age) = eql_v3.ore_block_256(${sql.json(equalityTerm)}::jsonb)
366+
AND eql_v3.ord_term(age) = eql_v3_internal.ore_block_256(${sql.json(equalityTerm)}::jsonb)
367367
ORDER BY id
368368
`
369369
// Exactly the age=37 row — not the 30 or 42 rows.
@@ -383,7 +383,7 @@ describeLivePg('eql_v3 text_search postgres integration', () => {
383383
SELECT id
384384
FROM protect_ci_v3_typed_domains
385385
WHERE test_run_id = ${TEST_RUN_ID}
386-
AND eql_v3.ord_term(age) = eql_v3.ore_block_256(${sql.json(missTerm)}::jsonb)
386+
AND eql_v3.ord_term(age) = eql_v3_internal.ore_block_256(${sql.json(missTerm)}::jsonb)
387387
`
388388
expect(none).toHaveLength(0)
389389
}, 30000)

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

Lines changed: 20 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -158,27 +158,27 @@ describe('eql_v3 text_match column', () => {
158158

159159
describe('eql_v3 concrete type names', () => {
160160
it('preserves public factory names while exposing concrete Postgres domain names', () => {
161-
expect(types.Integer('n').getEqlType()).toBe('eql_v3.integer')
162-
expect(types.IntegerEq('n').getEqlType()).toBe('eql_v3.integer_eq')
163-
expect(types.IntegerOrdOre('n').getEqlType()).toBe('eql_v3.integer_ord_ore')
164-
expect(types.IntegerOrd('n').getEqlType()).toBe('eql_v3.integer_ord')
161+
expect(types.Integer('n').getEqlType()).toBe('public.integer')
162+
expect(types.IntegerEq('n').getEqlType()).toBe('public.integer_eq')
163+
expect(types.IntegerOrdOre('n').getEqlType()).toBe('public.integer_ord_ore')
164+
expect(types.IntegerOrd('n').getEqlType()).toBe('public.integer_ord')
165165

166-
expect(types.Smallint('n').getEqlType()).toBe('eql_v3.smallint')
167-
expect(types.SmallintEq('n').getEqlType()).toBe('eql_v3.smallint_eq')
166+
expect(types.Smallint('n').getEqlType()).toBe('public.smallint')
167+
expect(types.SmallintEq('n').getEqlType()).toBe('public.smallint_eq')
168168
expect(types.SmallintOrdOre('n').getEqlType()).toBe(
169-
'eql_v3.smallint_ord_ore',
169+
'public.smallint_ord_ore',
170170
)
171-
expect(types.SmallintOrd('n').getEqlType()).toBe('eql_v3.smallint_ord')
172-
173-
expect(types.Boolean('b').getEqlType()).toBe('eql_v3.bool')
174-
expect(types.Real('n').getEqlType()).toBe('eql_v3.real')
175-
expect(types.RealEq('n').getEqlType()).toBe('eql_v3.real_eq')
176-
expect(types.RealOrdOre('n').getEqlType()).toBe('eql_v3.real_ord_ore')
177-
expect(types.RealOrd('n').getEqlType()).toBe('eql_v3.real_ord')
178-
expect(types.Double('n').getEqlType()).toBe('eql_v3.double')
179-
expect(types.DoubleEq('n').getEqlType()).toBe('eql_v3.double_eq')
180-
expect(types.DoubleOrdOre('n').getEqlType()).toBe('eql_v3.double_ord_ore')
181-
expect(types.DoubleOrd('n').getEqlType()).toBe('eql_v3.double_ord')
171+
expect(types.SmallintOrd('n').getEqlType()).toBe('public.smallint_ord')
172+
173+
expect(types.Boolean('b').getEqlType()).toBe('public.boolean')
174+
expect(types.Real('n').getEqlType()).toBe('public.real')
175+
expect(types.RealEq('n').getEqlType()).toBe('public.real_eq')
176+
expect(types.RealOrdOre('n').getEqlType()).toBe('public.real_ord_ore')
177+
expect(types.RealOrd('n').getEqlType()).toBe('public.real_ord')
178+
expect(types.Double('n').getEqlType()).toBe('public.double')
179+
expect(types.DoubleEq('n').getEqlType()).toBe('public.double_eq')
180+
expect(types.DoubleOrdOre('n').getEqlType()).toBe('public.double_ord_ore')
181+
expect(types.DoubleOrd('n').getEqlType()).toBe('public.double_ord')
182182
})
183183
})
184184

@@ -450,12 +450,12 @@ describe('eql_v3 equality via ORE on order-capable columns (regression)', () =>
450450
})
451451

452452
describe('eql_v3 text order domains carry the hm (unique) index (regression)', () => {
453-
// The `eql_v3.text_ord` and `eql_v3.text_ord_ore` SQL domains require BOTH
453+
// The `public.text_ord` and `public.text_ord_ore` SQL domains require BOTH
454454
// `hm` (HMAC) and `ob` (ORE) in the stored ciphertext: text equality is
455455
// HMAC-based (their `eql_v3.eq_term` extracts `hm`), unlike numeric/date order
456456
// domains which answer equality via `ob` and need only ORE. So text order
457457
// columns must emit `unique` (hm) IN ADDITION to `ore` (ob), or a real INSERT
458-
// fails with `value for domain eql_v3.text_ord_ore violates check constraint`.
458+
// fails with `value for domain public.text_ord_ore violates check constraint`.
459459
it.each([
460460
['text_ord_ore', types.TextOrdOre],
461461
['text_ord', types.TextOrd],

0 commit comments

Comments
 (0)