1- import { readFile } from 'node:fs/promises'
2- import { dirname , resolve } from 'node:path'
3- import { fileURLToPath } from 'node:url'
1+ import { readInstallSql , releaseManifest } from '@cipherstash/eql/sql'
42import type postgres from 'postgres'
53
64const EQL_V3_ADVISORY_LOCK_ID = 3_733_003
75
8- const helperDir = dirname ( fileURLToPath ( import . meta. url ) )
9- const eqlV3SqlPath = resolve (
10- helperDir ,
11- '../fixtures/eql-v3/cipherstash-encrypt-v3.sql' ,
12- )
13-
14- // The sentinel must be a type that exists ONLY in the currently vendored
15- // bundle, so a database still carrying an older EQL v3 install is detected as
16- // stale and reinstalled (the bundle's leading DROP SCHEMA … CASCADE replaces
17- // the old install wholesale). public.timestamp is new in the current bundle
18- // (the timestamptz → timestamp rename, encrypt-query-language@2e64ca73); the
19- // previous sentinel, public.text_search, exists in both generations and would
20- // leave a stale install in place.
21- async function hasCurrentEqlV3 ( sql : postgres . Sql ) : Promise < boolean > {
22- const [ row ] = await sql < { installed : boolean } [ ] > `
23- SELECT to_regtype('public.timestamp') IS NOT NULL AS installed
6+ // Staleness is decided by asking the database which EQL release it is running
7+ // and comparing that to the release the pinned @cipherstash /eql ships. Earlier
8+ // revisions probed for a sentinel type (public.text_search, then
9+ // public.timestamp) that was hand-picked to exist only in the newest bundle.
10+ // Every such sentinel decays: the next bundle keeps the type, the check starts
11+ // reporting "current" against a stale install, and the suite silently exercises
12+ // the wrong SQL. eql_v3.version() carries the release identity itself, so this
13+ // needs no maintenance when the pin moves.
14+ //
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 ?. installed ?? false
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/**
29- * Install the generated EQL v3 SQL bundle only when the target database does
30- * not already expose the current bundle's sentinel type (see hasCurrentEqlV3) .
36+ * Install the EQL v3 SQL bundle shipped by @cipherstash/eql only when the
37+ * target database is not already running that exact release .
3138 *
3239 * The bundle starts with DROP SCHEMA IF EXISTS eql_v3 CASCADE, so callers must
3340 * never run it unconditionally against a shared test database.
@@ -43,13 +50,18 @@ 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
48- const eqlV3Sql = await readFile ( eqlV3SqlPath , 'utf8' )
49- await reserved . unsafe ( eqlV3Sql )
56+ // Sent as one multi-statement string: the bundle is ~43k lines and a
57+ // statement-at-a-time client would pay a round-trip per CREATE FUNCTION.
58+ await reserved . unsafe ( readInstallSql ( ) )
5059
51- if ( ! ( await hasCurrentEqlV3 ( reserved ) ) ) {
52- throw new Error ( 'EQL v3 installation did not create public.timestamp' )
60+ const installed = await readEqlV3Version ( reserved )
61+ if ( installed !== releaseManifest . eqlVersion ) {
62+ throw new Error (
63+ `EQL v3 installation did not yield the expected release: wanted ${ releaseManifest . eqlVersion } , got ${ installed ?? 'no eql_v3.version()' } ` ,
64+ )
5365 }
5466 } finally {
5567 await reserved `SELECT pg_advisory_unlock(${ EQL_V3_ADVISORY_LOCK_ID } )`
0 commit comments