Skip to content
Open
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-additive-install-304.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
---
'@cipherstash/prisma-next': patch
'@cipherstash/stack': patch
'stash': patch
---

Fix `prisma-next db init` failing with PN-RUN-3020 on fresh databases, and bump the pinned EQL bundle to 3.0.4.

The cipherstash space's EQL install migration is re-emitted with `additive` operation class (it only CREATEs its own schemas/domains/functions, and the genesis edge is not a self-edge, so the integrity checker accepts it) and now bakes the eql-3.0.4 bundle while carrying the upgrade invariants, so fresh-database `db init` — including Prisma Compute preview deploys — satisfies the head ref from a single all-additive edge. A new `data`-classed 3.0.4 upgrade self-edge covers databases installed at an older bundle via `migrate`. Consumers with a vendored `migrations/cipherstash/` should delete the space directory and re-run `prisma-next migration plan` to pick up the re-emitted artefacts.
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@
"from": null,
"to": "sha256:efd408cf8924b4d1805bf5acced8898114aa03cd46b465720179c82a4431d51e",
"providedInvariants": [
"cipherstash:install-eql-v3-bundle-v1"
"cipherstash:install-eql-v3-bundle-v1",
"cipherstash:upgrade-eql-v3-bundle-3.0.2-v1",
"cipherstash:upgrade-eql-v3-bundle-3.0.4-v1"
],
"createdAt": "2026-07-14T20:10:24.325Z",
"migrationHash": "sha256:2c8739076699b81bcf515f1f8ff23501ff1f2582b933cfd80c5fb5bcc3de9e12"
"migrationHash": "sha256:fc495f7f59e6d18ae8e3df594a38898263ca91f8f5fb5f625bff20d04a0d7223"
}

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"from": "sha256:efd408cf8924b4d1805bf5acced8898114aa03cd46b465720179c82a4431d51e",
"to": "sha256:efd408cf8924b4d1805bf5acced8898114aa03cd46b465720179c82a4431d51e",
"providedInvariants": [
"cipherstash:upgrade-eql-v3-bundle-3.0.4-v1"
],
"createdAt": "2026-07-28T10:44:32.390Z",
"migrationHash": "sha256:59e124d8a64d31a0f2b27ef9f5b3868f822ec12823aad71fee8e64b36f115bf4"
}

Large diffs are not rendered by default.

9 changes: 8 additions & 1 deletion examples/prisma/migrations/cipherstash/refs/head.json
Original file line number Diff line number Diff line change
@@ -1 +1,8 @@
{"hash":"sha256:efd408cf8924b4d1805bf5acced8898114aa03cd46b465720179c82a4431d51e","invariants":["cipherstash:install-eql-v3-bundle-v1","cipherstash:upgrade-eql-v3-bundle-3.0.2-v1"]}
{
"hash": "sha256:efd408cf8924b4d1805bf5acced8898114aa03cd46b465720179c82a4431d51e",
"invariants": [
"cipherstash:install-eql-v3-bundle-v1",
"cipherstash:upgrade-eql-v3-bundle-3.0.2-v1",
"cipherstash:upgrade-eql-v3-bundle-3.0.4-v1"
]
}
2 changes: 1 addition & 1 deletion packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@
},
"dependencies": {
"@cipherstash/auth": "catalog:repo",
"@cipherstash/eql": "3.0.2",
"@cipherstash/eql": "3.0.4",
"@cipherstash/migrate": "workspace:*",
"@clack/prompts": "1.7.0",
"dotenv": "17.4.2",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@
"from": null,
"to": "sha256:efd408cf8924b4d1805bf5acced8898114aa03cd46b465720179c82a4431d51e",
"providedInvariants": [
"cipherstash:install-eql-v3-bundle-v1"
"cipherstash:install-eql-v3-bundle-v1",
"cipherstash:upgrade-eql-v3-bundle-3.0.2-v1",
"cipherstash:upgrade-eql-v3-bundle-3.0.4-v1"
],
"createdAt": "2026-07-14T20:10:24.325Z",
"migrationHash": "sha256:2c8739076699b81bcf515f1f8ff23501ff1f2582b933cfd80c5fb5bcc3de9e12"
"migrationHash": "sha256:fc495f7f59e6d18ae8e3df594a38898263ca91f8f5fb5f625bff20d04a0d7223"
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@
* into consumer repos; a re-emit that changes them orphans every vendored
* copy. An EQL version bump ships as a NEW upgrade migration directory
* (see `20260720T0000_upgrade_eql_v3_3_0_2`), never as an edit here.
* (One authorized pre-GA re-emit happened on the 1.0 release branch: the
* install op was reclassified `data` → `additive` and the 3.0.2 invariant
* carrier was added so fresh-database `db init` passes its additive-only
* policy. RC consumers must delete `migrations/cipherstash/` and re-run
* `prisma-next migration plan` to pick up the re-emitted artefacts.)
*
* Authoring loop (pre-publication only): hand-edit, then re-emit
* `ops.json` / `migration.json` via
Expand Down Expand Up @@ -56,18 +61,25 @@ export default class M extends Migration {
rawSql({
id: 'cipherstash.install-eql-v3-bundle',
label: INSTALL_LABEL,
// `data`, not `additive`: this genesis edge moves NO contract
// storage (`from: null` → the empty-storage hash — the bundle
// declares no contract-space storage), and the aggregate
// integrity checker (`migration-tools` `check-integrity.ts`)
// rejects a no-storage-movement edge unless it carries a
// `data`-class op. Along the axis the checker classifies — does
// the op move the modeled contract shape? — `data` is the
// truthful answer: the bundle creates `public.eql_v3_*` domains
// and `eql_v3.*` functions the space contract deliberately does
// not model. The `migrate` policy allows all four classes, so
// apply behaviour is unchanged.
operationClass: 'data',
// `additive` (previously `data`): the bundle only CREATEs new
// objects — the `public.eql_v3_*` domains, the `eql_v3` /
// `eql_v3_internal` schemas and their functions — and touches no
// user data, so along the axis migration POLICIES classify
// (can this op damage existing data?) `additive` is the truthful
// answer. The practical stake: `db init` enforces an
// additive-only policy, and a `data` class here made every
// fresh-database init (e.g. Prisma Compute preview deploys)
// fail with PN-RUN-3020.
//
// The earlier `data` classification existed to satisfy the
// aggregate integrity checker's no-op self-edge rule
// (`sameSourceAndTarget`) — but that rule only fires when
// `from === to`, and this genesis edge runs `from: null` → the
// empty-storage hash, which the checker does NOT treat as a
// self-edge. Verified against `migration-tools`
// `check-integrity.ts` 0.16.0. (The 3.0.2 upgrade edge IS a true
// self-edge and keeps its `data` class for exactly that rule.)
operationClass: 'additive',
invariantId: CIPHERSTASH_V3_INVARIANTS.installBundle,
target: { id: 'postgres' },
precheck: [],
Expand Down Expand Up @@ -100,6 +112,52 @@ export default class M extends Migration {
},
],
}),
// Invariant carrier: the install op above already ships the pinned
// release's bundle (eql-3.0.2 — `readVerifiedInstallSql()` is digest-
// verified against the installed manifest), so a fresh database that
// walks this genesis edge IS at the pinned release (3.0.4, a
// superset of every earlier v3 surface, so the 3.0.2 invariant is
// honestly satisfied too). Declaring the upgrade invariants here
Comment on lines +115 to +120
// lets the shortest-path planner (`computeExtensionSpaceApplyPath` /
// `findPathWithDecision`) satisfy the head ref from this single
// all-additive edge, so fresh-database `db init` (additive-only
// policy) never needs to walk the `data`-classed upgrade self-edges.
// Databases installed at an older bundle still reach the pinned
// release through those upgrade edges via `migrate`, whose policy
// allows all classes.
rawSql({
id: 'cipherstash.install-provides-eql-v3-3-0-2',
label:
'EQL 3.0.2 invariant — provided by the install bundle above (no additional SQL)',
operationClass: 'additive',
invariantId: CIPHERSTASH_V3_INVARIANTS.upgradeBundle302,
target: { id: 'postgres' },
precheck: [],
execute: [],
postcheck: [
{
description:
'verify the eql_v3 operator schema exists (the bundle was installed by the preceding op)',
sql: "SELECT EXISTS (SELECT 1 FROM pg_namespace WHERE nspname = 'eql_v3')",
},
],
}),
rawSql({
id: 'cipherstash.install-provides-eql-v3-3-0-4',
label:
'EQL 3.0.4 invariant — provided by the install bundle above (no additional SQL)',
operationClass: 'additive',
invariantId: CIPHERSTASH_V3_INVARIANTS.upgradeBundle304,
target: { id: 'postgres' },
precheck: [],
execute: [],
postcheck: [
{
description: `verify eql_v3.version() reports ${releaseManifest.eqlVersion}`,
sql: `SELECT eql_v3.version() = '${releaseManifest.eqlVersion}'`,
},
],
}),
]
}
}
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"from": "sha256:efd408cf8924b4d1805bf5acced8898114aa03cd46b465720179c82a4431d51e",
"to": "sha256:efd408cf8924b4d1805bf5acced8898114aa03cd46b465720179c82a4431d51e",
"providedInvariants": [
"cipherstash:upgrade-eql-v3-bundle-3.0.4-v1"
],
"createdAt": "2026-07-28T10:44:32.390Z",
"migrationHash": "sha256:59e124d8a64d31a0f2b27ef9f5b3868f822ec12823aad71fee8e64b36f115bf4"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
#!/usr/bin/env -S node
/**
* Upgrade an existing EQL v3 installation to the pinned 3.0.4 bundle.
*
* This is a separate invariant-only edge from the original v3 baseline. A
* database that already recorded the baseline (and/or 3.0.2) invariant must
* still traverse this edge, otherwise changing the SQL behind those
* historical markers would leave the database on an older EQL surface.
* Fresh databases never walk it: the re-emitted install baseline bakes the
* 3.0.4 bundle and carries this invariant itself, so the shortest-path
* planner satisfies the head ref from the all-additive genesis edge — which
* is what keeps `db init` (additive-only policy) working.
*
* PUBLISHED — DO NOT RE-EMIT. This package's bytes are content-addressed
* into consumer repos (the frozen-hash pin in
* `test/v3/migration-v3.test.ts` fails on any change). A future EQL
* version ships as a NEW `migrations/<ts>_upgrade_eql_v3_<x>_<y>_<z>/`
* directory with a fresh invariant, modelled on this one — never as an
* edit here.
*/
import {
Migration,
MigrationCLI,
rawSql,
} from '@prisma-next/target-postgres/migration'
import { CIPHERSTASH_V3_INVARIANTS } from '../../src/extension-metadata/constants-v3'
import {
readVerifiedInstallSql,
releaseManifest,
} from '../../src/migration/eql-bundle-v3'

const UPGRADE_LABEL = `Upgrade EQL v3 bundle to eql-${releaseManifest.eqlVersion}`

export default class M extends Migration {
override describe() {
// Invariant-only self-edge on the v3-only contract space's
// empty-storage hash — the same `to` the baseline lands on.
return {
from: 'sha256:efd408cf8924b4d1805bf5acced8898114aa03cd46b465720179c82a4431d51e',
to: 'sha256:efd408cf8924b4d1805bf5acced8898114aa03cd46b465720179c82a4431d51e',
}
}

override get operations() {
return [
rawSql({
id: 'cipherstash.upgrade-eql-v3-bundle-3.0.4',
label: UPGRADE_LABEL,
// `data` is required here: this IS a true self-edge (from === to),
// and the aggregate integrity checker rejects a self-edge without a
// data-class op as a no-op. Only `migrate` (whose policy allows all
// classes) ever needs to walk it.
operationClass: 'data',
invariantId: CIPHERSTASH_V3_INVARIANTS.upgradeBundle304,
target: { id: 'postgres' },
precheck: [],
// The full install SQL, digest-verified at emit time (the EQL v3
// install bundle is idempotent-by-reinstall: it drops and
// recreates the `eql_v3` operator schema while preserving the
// `public.eql_v3_*` storage domains customer columns depend on).
execute: [
{ description: UPGRADE_LABEL, sql: readVerifiedInstallSql() },
],
postcheck: [
{
description: `verify eql_v3.version() reports ${releaseManifest.eqlVersion}`,
sql: `SELECT eql_v3.version() = '${releaseManifest.eqlVersion}'`,
},
{
description: 'verify the typed JSON query domain exists',
sql: "SELECT to_regtype('eql_v3.query_json') IS NOT NULL",
},
{
description: 'verify the typed text-match query domain exists',
sql: "SELECT to_regtype('eql_v3.query_text_match') IS NOT NULL",
},
],
}),
]
}
}

MigrationCLI.run(import.meta.url, M)

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion packages/prisma-next/migrations/refs/head.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"hash": "sha256:efd408cf8924b4d1805bf5acced8898114aa03cd46b465720179c82a4431d51e",
"invariants": [
"cipherstash:install-eql-v3-bundle-v1",
"cipherstash:upgrade-eql-v3-bundle-3.0.2-v1"
"cipherstash:upgrade-eql-v3-bundle-3.0.2-v1",
"cipherstash:upgrade-eql-v3-bundle-3.0.4-v1"
]
}
2 changes: 1 addition & 1 deletion packages/prisma-next/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@
"clean": "rm -rf dist coverage .tmp-output"
},
"dependencies": {
"@cipherstash/eql": "3.0.2",
"@cipherstash/eql": "3.0.4",
"@cipherstash/stack": "workspace:*",
"@prisma-next/contract": "0.16.0",
"@prisma-next/family-sql": "0.16.0",
Expand Down
15 changes: 15 additions & 0 deletions packages/prisma-next/src/exports/control.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,17 @@ import v3Upgrade302Metadata from '../../migrations/20260720T0000_upgrade_eql_v3_
import v3Upgrade302Ops from '../../migrations/20260720T0000_upgrade_eql_v3_3_0_2/ops.json' with {
type: 'json',
}
import v3Upgrade304Metadata from '../../migrations/20260728T0000_upgrade_eql_v3_3_0_4/migration.json' with {
type: 'json',
}
import v3Upgrade304Ops from '../../migrations/20260728T0000_upgrade_eql_v3_3_0_4/ops.json' with {
type: 'json',
}
import headRef from '../../migrations/refs/head.json' with { type: 'json' }
import contractJson from '../contract.json' with { type: 'json' }
import {
CIPHERSTASH_V3_302_UPGRADE_MIGRATION_NAME,
CIPHERSTASH_V3_304_UPGRADE_MIGRATION_NAME,
CIPHERSTASH_V3_BASELINE_MIGRATION_NAME,
} from '../extension-metadata/constants-v3'
import { cipherstashPackMeta } from '../extension-metadata/descriptor-meta'
Expand Down Expand Up @@ -87,6 +94,14 @@ const cipherstashContractSpace = contractSpaceFromJson<Contract<SqlStorage>>({
metadata: v3Upgrade302Metadata,
ops: v3Upgrade302Ops,
},
// 3.0.4 upgrade edge — for databases installed at 3.0.0/3.0.2. Fresh
// databases never walk it: the re-emitted baseline bakes the 3.0.4
// bundle and carries this invariant itself.
{
dirName: CIPHERSTASH_V3_304_UPGRADE_MIGRATION_NAME,
metadata: v3Upgrade304Metadata,
ops: v3Upgrade304Ops,
},
],
headRef,
})
Expand Down
4 changes: 4 additions & 0 deletions packages/prisma-next/src/extension-metadata/constants-v3.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,9 +142,13 @@ export const CIPHERSTASH_V3_BASELINE_MIGRATION_NAME =
export const CIPHERSTASH_V3_302_UPGRADE_MIGRATION_NAME =
'20260720T0000_upgrade_eql_v3_3_0_2'

export const CIPHERSTASH_V3_304_UPGRADE_MIGRATION_NAME =
'20260728T0000_upgrade_eql_v3_3_0_4'

export const CIPHERSTASH_V3_INVARIANTS = {
installBundle: 'cipherstash:install-eql-v3-bundle-v1',
upgradeBundle302: 'cipherstash:upgrade-eql-v3-bundle-3.0.2-v1',
upgradeBundle304: 'cipherstash:upgrade-eql-v3-bundle-3.0.4-v1',
} as const

// v3's OWN extension identity (decision 1b) — DISTINCT from the v2
Expand Down
26 changes: 22 additions & 4 deletions packages/prisma-next/test/descriptor.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import cipherstashExtensionDescriptor from '../src/exports/control'
import { CIPHERSTASH_SPACE_ID } from '../src/extension-metadata/constants'
import {
CIPHERSTASH_V3_302_UPGRADE_MIGRATION_NAME,
CIPHERSTASH_V3_304_UPGRADE_MIGRATION_NAME,
CIPHERSTASH_V3_BASELINE_MIGRATION_NAME,
CIPHERSTASH_V3_INVARIANTS,
} from '../src/extension-metadata/constants-v3'
Expand Down Expand Up @@ -70,7 +71,7 @@ describe('cipherstash extension descriptor (contract-space package layout)', ()

it('publishes the v3 baseline and versioned EQL upgrade edges', () => {
const space = cipherstashExtensionDescriptor.contractSpace!
expect(space.migrations).toHaveLength(2)
expect(space.migrations).toHaveLength(3)
const v3Baseline = space.migrations[0]!
expect(v3Baseline.dirName).toBe(CIPHERSTASH_V3_BASELINE_MIGRATION_NAME)
// Genesis edge (`from: null`): the bundle declares no contract-space
Expand All @@ -82,13 +83,23 @@ describe('cipherstash extension descriptor (contract-space package layout)', ()
expect(v3Upgrade.dirName).toBe(CIPHERSTASH_V3_302_UPGRADE_MIGRATION_NAME)
expect(v3Upgrade.metadata.from).toBe(v3Baseline.metadata.to)
expect(v3Upgrade.metadata.to).toBe(v3Baseline.metadata.to)
const v3Upgrade304 = space.migrations[2]!
expect(v3Upgrade304.dirName).toBe(CIPHERSTASH_V3_304_UPGRADE_MIGRATION_NAME)
expect(v3Upgrade304.metadata.from).toBe(v3Baseline.metadata.to)
expect(v3Upgrade304.metadata.to).toBe(v3Baseline.metadata.to)
})

it('v3 baseline ops carry the installEqlV3Bundle op only', () => {
it('v3 baseline ops carry the install op plus the upgrade-invariant carriers', () => {
const space = cipherstashExtensionDescriptor.contractSpace!
const v3Baseline = space.migrations[0]!
const opIds = v3Baseline.ops.map((op) => op.invariantId).filter(Boolean)
expect(opIds).toEqual([CIPHERSTASH_V3_INVARIANTS.installBundle])
// The carriers let a fresh database satisfy every head-ref invariant
// from this single all-additive edge (see the migration file).
expect(opIds).toEqual([
CIPHERSTASH_V3_INVARIANTS.installBundle,
CIPHERSTASH_V3_INVARIANTS.upgradeBundle302,
CIPHERSTASH_V3_INVARIANTS.upgradeBundle304,
])
})

it('v3 3.0.2 upgrade ops carry a distinct versioned invariant', () => {
Expand Down Expand Up @@ -133,8 +144,15 @@ describe('cipherstash extension descriptor (contract-space package layout)', ()
const space = cipherstashExtensionDescriptor.contractSpace!
const latest = space.migrations[space.migrations.length - 1]!
expect(space.headRef.hash).toBe(latest.metadata.to)
// Deduped: the baseline carries the upgrade invariants too (so fresh
// databases can satisfy the head ref from one edge), so the same
// invariant id legitimately appears on more than one migration.
expect([...space.headRef.invariants].sort()).toEqual(
space.migrations.flatMap((m) => m.metadata.providedInvariants).sort(),
[
...new Set(
space.migrations.flatMap((m) => m.metadata.providedInvariants),
),
].sort(),
)
})

Expand Down
Loading