Skip to content

Commit 70e9f19

Browse files
committed
fix(stack): probe eql_v3.version() in its own statement
CI failed on a fresh database with `PostgresError: schema "eql_v3" does not exist` (3F000) from hasCurrentEqlV3. Postgres resolves function references while parsing a statement, before any branch of it executes. So in SELECT CASE WHEN to_regprocedure('eql_v3.version()') IS NULL THEN NULL ELSE eql_v3.version() END the ELSE branch is resolved even when the guard would have short-circuited: the statement raises 3F000 when the eql_v3 schema is absent, and 42883 when the schema exists but the function does not. The guard only ever succeeded when it was not needed. to_regprocedure() itself is fine — it takes the name as a string and yields NULL rather than raising. Split the probe into its own statement and only then call eql_v3.version(), so the call is parsed after existence is known. Every local run had already installed EQL v3, so the empty-database path was never exercised — the case the previous commit message claimed was safe. Verified against a fresh ghcr.io/cipherstash/postgres-eql:17-2.3.1 (no eql_v3): v3 suites 237 passed; full packages/stack suite 1,055 passed / 0 failed. Also covers the schema-present-but-no-version() case: reinstalls rather than raising.
1 parent cfd46ee commit 70e9f19

1 file changed

Lines changed: 22 additions & 19 deletions

File tree

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

Lines changed: 22 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -12,17 +12,24 @@ const EQL_V3_ADVISORY_LOCK_ID = 3_733_003
1212
// the wrong SQL. eql_v3.version() carries the release identity itself, so this
1313
// needs no maintenance when the pin moves.
1414
//
15-
// to_regprocedure() yields NULL instead of raising when the function is absent,
16-
// so a database with no EQL v3 — or with a bundle predating eql_v3.version() —
17-
// reads as stale and gets installed rather than erroring.
18-
async function hasCurrentEqlV3(sql: postgres.Sql): Promise<boolean> {
19-
const [row] = await sql<{ version: string | null }[]>`
20-
SELECT CASE
21-
WHEN to_regprocedure('eql_v3.version()') IS NULL THEN NULL
22-
ELSE eql_v3.version()
23-
END AS version
15+
// The probe has to be its own statement. Postgres resolves function references
16+
// while parsing a statement, before any branch of it runs, so guarding the call
17+
// with CASE WHEN to_regprocedure(...) IS NULL in the same statement still raises
18+
// "schema eql_v3 does not exist" against a database with no EQL v3 — the guard
19+
// only ever succeeds when it is not needed. to_regprocedure() takes the name as
20+
// a string, so on its own it yields NULL rather than raising, and a database
21+
// with no EQL v3 — or with a bundle predating eql_v3.version() — reads as stale
22+
// and gets installed.
23+
async function readEqlV3Version(sql: postgres.Sql): Promise<string | null> {
24+
const [probe] = await sql<{ present: boolean }[]>`
25+
SELECT to_regprocedure('eql_v3.version()') IS NOT NULL AS present
2426
`
25-
return row?.version === releaseManifest.eqlVersion
27+
if (!probe?.present) return null
28+
29+
const [row] = await sql<
30+
{ version: string }[]
31+
>`SELECT eql_v3.version() AS version`
32+
return row?.version ?? null
2633
}
2734

2835
/**
@@ -43,21 +50,17 @@ export async function installEqlV3IfNeeded(sql: postgres.Sql): Promise<void> {
4350
await reserved`SELECT pg_advisory_lock(${EQL_V3_ADVISORY_LOCK_ID})`
4451

4552
try {
46-
if (await hasCurrentEqlV3(reserved)) return
53+
if ((await readEqlV3Version(reserved)) === releaseManifest.eqlVersion)
54+
return
4755

4856
// Sent as one multi-statement string: the bundle is ~43k lines and a
4957
// statement-at-a-time client would pay a round-trip per CREATE FUNCTION.
5058
await reserved.unsafe(readInstallSql())
5159

52-
if (!(await hasCurrentEqlV3(reserved))) {
53-
const [row] = await reserved<{ version: string | null }[]>`
54-
SELECT CASE
55-
WHEN to_regprocedure('eql_v3.version()') IS NULL THEN NULL
56-
ELSE eql_v3.version()
57-
END AS version
58-
`
60+
const installed = await readEqlV3Version(reserved)
61+
if (installed !== releaseManifest.eqlVersion) {
5962
throw new Error(
60-
`EQL v3 installation did not yield the expected release: wanted ${releaseManifest.eqlVersion}, got ${row?.version ?? 'no eql_v3.version()'}`,
63+
`EQL v3 installation did not yield the expected release: wanted ${releaseManifest.eqlVersion}, got ${installed ?? 'no eql_v3.version()'}`,
6164
)
6265
}
6366
} finally {

0 commit comments

Comments
 (0)