Skip to content

Commit e566f93

Browse files
committed
feat(prisma-next): source EQL v3 install SQL from @cipherstash/eql at runtime
Baking the ~1.7 MB install bundle into the v3 baseline migration's ops.json coupled every @cipherstash/eql upgrade to a prisma-next release: a patch or minor bump meant re-emitting the migration and cutting a new adapter version. Now @cipherstash/eql is a runtime dependency (^3.0.0). The committed v3 baseline op carries a sentinel placeholder; the descriptor (control.ts) injects readInstallSql() from the installed @cipherstash/eql at build time via withRuntimeEqlSql(). A patch/minor now flows through npm resolution with no re-emit and no adapter release — mirroring how the stash CLI already sources v3 SQL (#692). Safe because the v3 baseline is an invariant-only self-edge (from === to): the install SQL never contributes to the contract-space hash, and contractSpaceFromJson passes ops through with no integrity check on their contents (verifyMigrationHash runs only on the disk read path for the user's repo, never on the in-memory extension descriptor). The apply planner routes on the cipherstash:install-eql-v3-bundle-v1 invariant, not SQL content. ops.json drops from 1,717,297 → 1,219 bytes. No user-facing change — EQL still installs via `prisma-next migration apply`. 600 tests green; build + biome clean. Claude-Session: https://claude.ai/code/session_01MxTTPaPP16m6br7Hoab94w
1 parent 0811330 commit e566f93

9 files changed

Lines changed: 147 additions & 40 deletions

File tree

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
---
2+
'@cipherstash/prisma-next': minor
3+
---
4+
5+
Source the EQL v3 install SQL from `@cipherstash/eql` at runtime instead of
6+
baking it into the baseline migration.
7+
8+
`@cipherstash/eql` is now a runtime dependency (`^3.0.0`). The v3 baseline
9+
migration no longer embeds the ~1.7 MB install bundle in its `ops.json`; the
10+
committed op carries a placeholder, and the extension descriptor injects
11+
`readInstallSql()` from the installed `@cipherstash/eql` when it is built. As a
12+
result, a `@cipherstash/eql` patch or minor release flows through to
13+
`prisma-next migration apply` via normal dependency resolution — it no longer
14+
requires re-emitting the migration and cutting a new `@cipherstash/prisma-next`
15+
release. This mirrors how the `stash` CLI already sources the v3 SQL.
16+
17+
No change to user-facing behaviour: EQL still installs as part of
18+
`prisma-next migration apply`. Safe because the v3 baseline is an
19+
invariant-only self-edge — the install SQL never contributes to the
20+
contract-space hash.

packages/prisma-next/migrations/20260601T0100_install_eql_v3_bundle/migration.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,5 @@
55
"cipherstash:install-eql-v3-bundle-v1"
66
],
77
"createdAt": "2026-07-14T20:10:24.325Z",
8-
"migrationHash": "sha256:d1ca5a987ab10eb85c2fe05700134b31f17f694d2d5b3df3969e902610b1d9cf"
8+
"migrationHash": "sha256:55f261c28b9fe8feee21270a652d758a5c90ccbe7aac9cc79ac50662b956160b"
99
}

packages/prisma-next/migrations/20260601T0100_install_eql_v3_bundle/migration.ts

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,14 @@
22
/**
33
* CipherStash v3 baseline migration — install the EQL v3 bundle.
44
*
5-
* The install SQL is sourced at EMIT time from `@cipherstash/eql/sql`
6-
* (see `../../src/migration/eql-bundle-v3.ts` — the same source the
7-
* stack's `installEqlV3IfNeeded` uses) and baked into `ops.json`
8-
* byte-for-byte. The bundle creates the 40 `public.eql_v3_*` storage
9-
* domains, the `eql_v3` operator-function schema (`eql_v3.eq`,
10-
* `eql_v3.ord_term`, …), the `eql_v3.query_*` operand domains, and the
11-
* `eql_v3_internal` helper schema.
5+
* The install SQL is NOT baked into `ops.json`. The committed op carries
6+
* `RUNTIME_EQL_SQL_SENTINEL`; `src/exports/control.ts` injects
7+
* `readInstallSql()` from the installed `@cipherstash/eql` at descriptor-build
8+
* time (see `../../src/migration/eql-bundle-v3.ts` `withRuntimeEqlSql`), so an
9+
* EQL patch/minor flows through npm with no re-emit or re-release. The bundle
10+
* creates the 40 `public.eql_v3_*` storage domains, the `eql_v3`
11+
* operator-function schema (`eql_v3.eq`, `eql_v3.ord_term`, …), the
12+
* `eql_v3.query_*` operand domains, and the `eql_v3_internal` helper schema.
1213
*
1314
* This is the SECOND migration in the cipherstash contract space, and
1415
* it is an **invariant-only edge**: the v3 bundle declares no
@@ -32,7 +33,7 @@ import {
3233
} from '@prisma-next/target-postgres/migration'
3334
import { CIPHERSTASH_V3_INVARIANTS } from '../../src/extension-metadata/constants-v3'
3435
import {
35-
readInstallSql,
36+
RUNTIME_EQL_SQL_SENTINEL,
3637
releaseManifest,
3738
} from '../../src/migration/eql-bundle-v3'
3839

@@ -67,7 +68,15 @@ export default class M extends Migration {
6768
invariantId: CIPHERSTASH_V3_INVARIANTS.installBundle,
6869
target: { id: 'postgres' },
6970
precheck: [],
70-
execute: [{ description: INSTALL_LABEL, sql: readInstallSql() }],
71+
// Placeholder only — the real install SQL is injected at descriptor
72+
// build time from the installed `@cipherstash/eql` (see
73+
// `../../src/migration/eql-bundle-v3.ts` `withRuntimeEqlSql`), so the
74+
// ~1.7 MB bundle is NOT baked into `ops.json` and an EQL patch/minor
75+
// needs no re-emit or re-release. Safe because this is an
76+
// invariant-only self-edge: the SQL never moves the contract hash.
77+
execute: [
78+
{ description: INSTALL_LABEL, sql: RUNTIME_EQL_SQL_SENTINEL },
79+
],
7180
postcheck: [
7281
{
7382
description: 'verify the "eql_v3" operator schema exists',

packages/prisma-next/migrations/20260601T0100_install_eql_v3_bundle/ops.json

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

packages/prisma-next/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,7 @@
8585
"clean": "rm -rf dist coverage .tmp-output"
8686
},
8787
"dependencies": {
88+
"@cipherstash/eql": "^3.0.0",
8889
"@cipherstash/stack": "workspace:*",
8990
"@prisma-next/contract": "0.14.0",
9091
"@prisma-next/family-sql": "0.14.0",
@@ -99,7 +100,6 @@
99100
"arktype": "^2.2.3"
100101
},
101102
"devDependencies": {
102-
"@cipherstash/eql": "3.0.0",
103103
"@cipherstash/test-kit": "workspace:*",
104104
"@prisma-next/adapter-postgres": "0.14.0",
105105
"@prisma-next/cli": "0.14.0",

packages/prisma-next/src/exports/control.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ import {
6666
cipherstashStringCodecHooks,
6767
} from '../migration/cipherstash-codec'
6868
import { cipherstashV3CodecControlHooks } from '../migration/cipherstash-codec-v3'
69+
import { withRuntimeEqlSql } from '../migration/eql-bundle-v3'
6970

7071
const cipherstashContractSpace = contractSpaceFromJson<Contract<SqlStorage>>({
7172
contractJson,
@@ -86,7 +87,12 @@ const cipherstashContractSpace = contractSpaceFromJson<Contract<SqlStorage>>({
8687
{
8788
dirName: CIPHERSTASH_V3_BASELINE_MIGRATION_NAME,
8889
metadata: v3BaselineMetadata,
89-
ops: v3BaselineOps,
90+
// The committed `ops.json` carries a placeholder in place of the ~1.7 MB
91+
// install SQL; inject it here from the installed `@cipherstash/eql` so an
92+
// EQL patch/minor flows through without re-emitting the migration. Safe
93+
// because this is an invariant-only self-edge — the SQL never touches the
94+
// contract hash and `contractSpaceFromJson` does not integrity-check ops.
95+
ops: withRuntimeEqlSql(v3BaselineOps),
9096
},
9197
],
9298
headRef,
Lines changed: 69 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,73 @@
11
/**
2-
* CipherStash EQL v3 install SQL, sourced from `@cipherstash/eql/sql` — the
3-
* same source the stack install script (`packages/stack/scripts/install-eql-v3.ts`
4-
* / `installEqlV3IfNeeded`) uses. `readInstallSql()` returns the full bundle
5-
* that creates the `public.eql_v3_*` domains and the `eql_v3.*` operator
6-
* functions; `releaseManifest.eqlVersion` identifies the pinned release.
2+
* CipherStash EQL v3 install SQL, sourced from `@cipherstash/eql/sql` at
3+
* RUNTIME — the same source the stack install script (`installEqlV3IfNeeded`)
4+
* and the CLI installer (`readV3InstallSql`) use. `readInstallSql()` returns
5+
* the full bundle that creates the `public.eql_v3_*` domains and the `eql_v3.*`
6+
* operator functions; `releaseManifest.eqlVersion` identifies the pinned
7+
* release.
78
*
8-
* The SQL is read at migration EMIT time and baked into the v3 baseline
9-
* migration's `ops.json`, so `@cipherstash/eql` stays a devDependency of this
10-
* package (not a runtime dependency of the published artefact). Bumping the
11-
* pinned `@cipherstash/eql` version requires re-running the maintainer emit
12-
* loop for `migrations/20260601T0100_install_eql_v3_bundle/` (see that
13-
* migration's header comment) and re-running `test/descriptor.test.ts` /
14-
* `test/v3/migration-v3.test.ts`.
9+
* ## Runtime-sourced, NOT baked
1510
*
16-
* Hash impact: like the v2 bundle (see `./eql-bundle.ts`), the SQL lives in
17-
* the migration op's `execute[]`, NOT in `contract.json` — the v3 bundle
18-
* declares no contract-space storage, so the edge is invariant-only and
19-
* `headRef.hash` does not move.
11+
* The v3 baseline migration does **not** embed the ~1.7 MB install SQL in its
12+
* `ops.json`. The committed op carries {@link RUNTIME_EQL_SQL_SENTINEL}, and
13+
* `src/exports/control.ts` swaps in `readInstallSql()` from the INSTALLED
14+
* `@cipherstash/eql` when it builds the descriptor (see {@link withRuntimeEqlSql}).
15+
* So `@cipherstash/eql` is a runtime dependency, and a patch/minor bump flows
16+
* through npm resolution — no re-emit of the migration, no re-release of this
17+
* package. (Baking it in coupled every EQL upgrade to a prisma-next release.)
18+
*
19+
* Why this is sound: the v3 baseline is an INVARIANT-ONLY self-edge
20+
* (`from === to`; the bundle declares no contract-space storage — unlike the v2
21+
* bundle's `eql_v2_configuration`). The install SQL therefore never contributes
22+
* to the contract-space hash, and `contractSpaceFromJson` passes the ops through
23+
* with no integrity check on their contents (`verifyMigrationHash` runs only on
24+
* the disk read path for the user's own repo, never on the in-memory extension
25+
* descriptor). Swapping the SQL at descriptor-construction time is invisible to
26+
* the planner, which routes purely on the `cipherstash:install-eql-v3-bundle-v1`
27+
* invariant.
28+
*/
29+
import { readInstallSql, releaseManifest } from '@cipherstash/eql/sql'
30+
31+
export { readInstallSql, releaseManifest }
32+
33+
/** The install op's `id` in the v3 baseline `ops.json`. */
34+
export const V3_INSTALL_OP_ID = 'cipherstash.install-eql-v3-bundle'
35+
36+
/**
37+
* Placeholder the committed v3 baseline op carries in `execute[].sql` in place
38+
* of the baked install SQL. It is replaced at runtime by {@link withRuntimeEqlSql};
39+
* if it ever reached a database directly it is an inert SQL comment.
40+
*/
41+
export const RUNTIME_EQL_SQL_SENTINEL =
42+
'-- EQL v3 install SQL is injected at runtime from @cipherstash/eql — see packages/prisma-next/src/migration/eql-bundle-v3.ts'
43+
44+
type OpLike = {
45+
readonly id?: unknown
46+
readonly execute?: ReadonlyArray<{
47+
readonly description?: unknown
48+
readonly sql?: unknown
49+
}>
50+
}
51+
52+
/**
53+
* Return `ops` with the v3 install op's placeholder SQL replaced by the install
54+
* SQL from the installed `@cipherstash/eql`. Every other op passes through
55+
* untouched. Called by the descriptor in `control.ts` so the applied SQL always
56+
* matches the resolved `@cipherstash/eql` version, not a baked snapshot.
2057
*/
21-
export { readInstallSql, releaseManifest } from '@cipherstash/eql/sql'
58+
export function withRuntimeEqlSql<T extends OpLike>(ops: readonly T[]): T[] {
59+
const sql = readInstallSql()
60+
return ops.map((op) =>
61+
op.id === V3_INSTALL_OP_ID
62+
? {
63+
...op,
64+
execute: [
65+
{
66+
description: String(op.execute?.[0]?.description ?? ''),
67+
sql,
68+
},
69+
],
70+
}
71+
: op,
72+
)
73+
}

packages/prisma-next/test/v3/migration-v3.test.ts

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@
22
* v3 baseline migration assertions — the on-disk emitted artefacts for
33
* `20260601T0100_install_eql_v3_bundle`.
44
*
5-
* The install SQL is sourced at EMIT time from `@cipherstash/eql/sql`
6-
* (the same source the stack's `installEqlV3IfNeeded` uses) and baked
7-
* into `ops.json` byte-for-byte. The edge is invariant-only: the v3
8-
* bundle creates `public.eql_v3_*` domains + `eql_v3.*` functions but
9-
* no contract-space storage, so `from === to === <v2 baseline head>`.
5+
* The install SQL is NOT baked into `ops.json`: the committed op carries a
6+
* placeholder, and the descriptor (`control.ts`) injects `readInstallSql()`
7+
* from the installed `@cipherstash/eql` at build time. The edge is
8+
* invariant-only: the v3 bundle creates `public.eql_v3_*` domains + `eql_v3.*`
9+
* functions but no contract-space storage, so `from === to === <v2 baseline head>`.
1010
*/
1111
import { readInstallSql, releaseManifest } from '@cipherstash/eql/sql'
1212
import { describe, expect, it } from 'vitest'
@@ -20,11 +20,13 @@ import v3Ops from '../../migrations/20260601T0100_install_eql_v3_bundle/ops.json
2020
type: 'json',
2121
}
2222
import headRef from '../../migrations/refs/head.json' with { type: 'json' }
23+
import cipherstashDescriptor from '../../src/exports/control'
2324
import { CIPHERSTASH_INVARIANTS } from '../../src/extension-metadata/constants'
2425
import {
2526
CIPHERSTASH_V3_BASELINE_MIGRATION_NAME,
2627
CIPHERSTASH_V3_INVARIANTS,
2728
} from '../../src/extension-metadata/constants-v3'
29+
import { RUNTIME_EQL_SQL_SENTINEL } from '../../src/migration/eql-bundle-v3'
2830

2931
describe('v3 baseline migration (20260601T0100_install_eql_v3_bundle)', () => {
3032
it('installs under the v3 invariant with a single data-class rawSql op', () => {
@@ -39,16 +41,34 @@ describe('v3 baseline migration (20260601T0100_install_eql_v3_bundle)', () => {
3941
expect(op.operationClass).toBe('data')
4042
})
4143

42-
it('inlines the @cipherstash/eql install SQL byte-for-byte through ops.json', () => {
44+
it('does NOT bake the install SQL into ops.json — it carries the runtime placeholder', () => {
4345
const op = (
4446
v3Ops as ReadonlyArray<{
4547
readonly execute?: ReadonlyArray<{ readonly sql: string }>
4648
}>
4749
)[0]!
48-
expect(op.execute?.[0]?.sql).toBe(readInstallSql())
50+
// The ~1.7 MB bundle must not be committed here — an EQL patch/minor should
51+
// not require re-emitting this file. The op carries the sentinel instead.
52+
expect(op.execute?.[0]?.sql).toBe(RUNTIME_EQL_SQL_SENTINEL)
53+
expect(op.execute?.[0]?.sql).not.toContain('CREATE')
54+
expect(JSON.stringify(v3Ops).length).toBeLessThan(5_000)
4955
expect(releaseManifest.eqlVersion).toBe('3.0.0')
5056
})
5157

58+
it('injects readInstallSql() from @cipherstash/eql into the descriptor at build time', () => {
59+
// control.ts swaps the placeholder for the install SQL of the INSTALLED
60+
// @cipherstash/eql, so the applied SQL always matches the resolved version.
61+
const v3Baseline = cipherstashDescriptor.contractSpace.migrations[1]!
62+
const op = (
63+
v3Baseline.ops as ReadonlyArray<{
64+
readonly id: string
65+
readonly execute?: ReadonlyArray<{ readonly sql: string }>
66+
}>
67+
).find((o) => o.id === 'cipherstash.install-eql-v3-bundle')!
68+
expect(op.execute?.[0]?.sql).toBe(readInstallSql())
69+
expect(op.execute?.[0]?.sql).toContain('EQL v3 schema creation')
70+
})
71+
5272
it('emits no add_search_config / remove_search_config ops', () => {
5373
const json = JSON.stringify(v3Ops)
5474
expect(json).not.toContain('add_search_config')

pnpm-lock.yaml

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)