Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .changeset/eql-v3-bundle-from-package.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
'@cipherstash/stack': patch
---

Source the EQL v3 install bundle from `@cipherstash/eql@3.0.0-alpha.3` instead of a hand-vendored 43k-line SQL fixture committed to the test tree. The package publishes its SQL and its TypeScript wire types from the same `eql-bindings` commit, so the bundle is now pinned to a released EQL version rather than tracked by convention.

Test-and-tooling only — `@cipherstash/eql` is a `devDependency` and no public API changes.

The staleness check in the v3 install helper now compares `eql_v3.version()` against the pinned release instead of probing for a hand-picked sentinel domain. The previous sentinel (`public.timestamp`) exists in both the old and new bundles, so it would have reported a stale install as current and left the suite silently running the wrong SQL.
10 changes: 8 additions & 2 deletions docs/eql-v3-ord-term-ordering-defect.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,14 @@ ORE-correct — and they are correct regardless of superuser.

## Why this happens on Supabase

File: `packages/stack/__tests__/fixtures/eql-v3/cipherstash-encrypt-v3.sql`
(the vendored bundle — do not hand-edit; it is regenerated on re-vendor).
File: the EQL v3 install bundle shipped by `@cipherstash/eql`, at
`node_modules/@cipherstash/eql/dist/sql/cipherstash-encrypt.sql` (or via
`installSqlPath()` from `@cipherstash/eql/sql`). Generated upstream — never
hand-edit; bump the pinned package version instead.

Line numbers below were recorded against the bundle vendored at the time of
writing and are indicative only; the released bundle orders its source files
differently. Locate the definitions by name.

1. `eql_v3.ord_term(a public.text_search)` (and every `*_ord` domain, e.g.
`public.integer_ord` at line 18117) returns the **composite** type
Expand Down
43,339 changes: 0 additions & 43,339 deletions packages/stack/__tests__/fixtures/eql-v3/cipherstash-encrypt-v3.sql

This file was deleted.

66 changes: 39 additions & 27 deletions packages/stack/__tests__/helpers/eql-v3.ts
Original file line number Diff line number Diff line change
@@ -1,33 +1,40 @@
import { readFile } from 'node:fs/promises'
import { dirname, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
import { readInstallSql, releaseManifest } from '@cipherstash/eql/sql'
import type postgres from 'postgres'

const EQL_V3_ADVISORY_LOCK_ID = 3_733_003

const helperDir = dirname(fileURLToPath(import.meta.url))
const eqlV3SqlPath = resolve(
helperDir,
'../fixtures/eql-v3/cipherstash-encrypt-v3.sql',
)

// The sentinel must be a type that exists ONLY in the currently vendored
// bundle, so a database still carrying an older EQL v3 install is detected as
// stale and reinstalled (the bundle's leading DROP SCHEMA … CASCADE replaces
// the old install wholesale). public.timestamp is new in the current bundle
// (the timestamptz → timestamp rename, encrypt-query-language@2e64ca73); the
// previous sentinel, public.text_search, exists in both generations and would
// leave a stale install in place.
async function hasCurrentEqlV3(sql: postgres.Sql): Promise<boolean> {
const [row] = await sql<{ installed: boolean }[]>`
SELECT to_regtype('public.timestamp') IS NOT NULL AS installed
// Staleness is decided by asking the database which EQL release it is running
// and comparing that to the release the pinned @cipherstash/eql ships. Earlier
// revisions probed for a sentinel type (public.text_search, then
// public.timestamp) that was hand-picked to exist only in the newest bundle.
// Every such sentinel decays: the next bundle keeps the type, the check starts
// reporting "current" against a stale install, and the suite silently exercises
// the wrong SQL. eql_v3.version() carries the release identity itself, so this
// needs no maintenance when the pin moves.
//
// The probe has to be its own statement. Postgres resolves function references
// while parsing a statement, before any branch of it runs, so guarding the call
// with CASE WHEN to_regprocedure(...) IS NULL in the same statement still raises
// "schema eql_v3 does not exist" against a database with no EQL v3 — the guard
// only ever succeeds when it is not needed. to_regprocedure() takes the name as
// a string, so on its own it yields NULL rather than raising, and a database
// with no EQL v3 — or with a bundle predating eql_v3.version() — reads as stale
// and gets installed.
async function readEqlV3Version(sql: postgres.Sql): Promise<string | null> {
const [probe] = await sql<{ present: boolean }[]>`
SELECT to_regprocedure('eql_v3.version()') IS NOT NULL AS present
`
return row?.installed ?? false
if (!probe?.present) return null

const [row] = await sql<
{ version: string }[]
>`SELECT eql_v3.version() AS version`
return row?.version ?? null
}

/**
* Install the generated EQL v3 SQL bundle only when the target database does
* not already expose the current bundle's sentinel type (see hasCurrentEqlV3).
* Install the EQL v3 SQL bundle shipped by @cipherstash/eql only when the
* target database is not already running that exact release.
*
* The bundle starts with DROP SCHEMA IF EXISTS eql_v3 CASCADE, so callers must
* never run it unconditionally against a shared test database.
Expand All @@ -43,13 +50,18 @@ export async function installEqlV3IfNeeded(sql: postgres.Sql): Promise<void> {
await reserved`SELECT pg_advisory_lock(${EQL_V3_ADVISORY_LOCK_ID})`

try {
if (await hasCurrentEqlV3(reserved)) return
if ((await readEqlV3Version(reserved)) === releaseManifest.eqlVersion)
return

const eqlV3Sql = await readFile(eqlV3SqlPath, 'utf8')
await reserved.unsafe(eqlV3Sql)
// Sent as one multi-statement string: the bundle is ~43k lines and a
// statement-at-a-time client would pay a round-trip per CREATE FUNCTION.
await reserved.unsafe(readInstallSql())

if (!(await hasCurrentEqlV3(reserved))) {
throw new Error('EQL v3 installation did not create public.timestamp')
const installed = await readEqlV3Version(reserved)
if (installed !== releaseManifest.eqlVersion) {
throw new Error(
`EQL v3 installation did not yield the expected release: wanted ${releaseManifest.eqlVersion}, got ${installed ?? 'no eql_v3.version()'}`,
)
}
} finally {
await reserved`SELECT pg_advisory_unlock(${EQL_V3_ADVISORY_LOCK_ID})`
Expand Down
1 change: 1 addition & 0 deletions packages/stack/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,7 @@
"release": "tsup"
},
"devDependencies": {
"@cipherstash/eql": "3.0.0-alpha.3",
"@clack/prompts": "^1.4.0",
"@supabase/supabase-js": "^2.105.4",
"@types/uuid": "^11.0.0",
Expand Down
3 changes: 2 additions & 1 deletion packages/stack/scripts/install-eql-v3.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ config({ path: '.env.development.local', quiet: true })
config({ path: '.env.development', quiet: true })
config({ path: '.env', quiet: true })

import { releaseManifest } from '@cipherstash/eql/sql'
import postgres from 'postgres'
import { installEqlV3IfNeeded } from '../__tests__/helpers/eql-v3'

Expand All @@ -21,7 +22,7 @@ const sql = postgres(process.env.DATABASE_URL, { prepare: false })

try {
await installEqlV3IfNeeded(sql)
console.log('eql_v3 (current bundle) is installed')
console.log(`eql_v3 ${releaseManifest.eqlVersion} is installed`)
} finally {
await sql.end()
}
8 changes: 8 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions pnpm-workspace.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,13 @@ blockExoticSubdeps: true
# - @cipherstash/auth* CipherStash-published auth strategies (NAPI +
# WASM-inline variant); also tracked in lockstep
# with protect-ffi for the WASM path.
# - @cipherstash/eql CipherStash-published EQL SQL bundle + wire types,
# generated from the same eql-bindings commit as the
# payload format protect-ffi emits; bumped in lockstep.
minimumReleaseAgeExclude:
- '@prisma-next/*'
- '@cipherstash/protect-ffi'
- '@cipherstash/protect-ffi-*'
- '@cipherstash/auth'
- '@cipherstash/auth-*'
- '@cipherstash/eql'
Loading