Skip to content

Commit 17c9fad

Browse files
authored
Merge pull request #961 from cipherstash/feat/eql-safe-reinstall
fix(cli): make EQL reinstall dependency-safe
2 parents be4d114 + 3867928 commit 17c9fad

38 files changed

Lines changed: 4002 additions & 604 deletions

‎.changeset/safe-eql-reinstall.md‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
---
2+
"stash": minor
3+
---
4+
5+
Preserve encrypted data and reconstruct functional indexes when reinstalling EQL v3, while refusing unsupported external dependencies before mutation.
6+
7+
`stash eql install` and `stash eql upgrade` now capture dependent functional
8+
indexes before replacing the disposable EQL schemas, then restore and verify
9+
their definitions, clustering, replica-identity role, comments, explicit
10+
statistics targets, and health in the same transaction. A
11+
reconstruction failure rolls the replacement back.
12+
PostgreSQL derives index ownership from the table owner, so reinstall verifies
13+
the resulting owner and rolls back on a mismatch rather than independently
14+
restoring ownership.
15+
Unsupported dependencies—including views, policies, constraints, and
16+
partitioned indexes—are named and refused before mutation.
17+
18+
Reinstall remains a maintenance-window operation: its advisory lock serializes
19+
`stash` lifecycle commands, not unrelated database DDL. Generated EQL migrations
20+
contain the raw bundle and do not include these reinstall protections.

‎.github/workflows/tests.yml‎

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -52,14 +52,23 @@ jobs:
5252
strategy:
5353
matrix:
5454
node-version: [22, 24]
55+
postgres-version: [16, 17]
56+
exclude:
57+
- node-version: 22
58+
postgres-version: 17
59+
- node-version: 24
60+
postgres-version: 16
5561

5662
# Postgres + EQL for the integration tests. Official EQL image —
57-
# PostgreSQL 17 with EQL pre-installed via /docker-entrypoint-initdb.d.
63+
# PostgreSQL 16 and 17 with EQL pre-installed via
64+
# /docker-entrypoint-initdb.d. Keeping one supported Node line on each
65+
# server version exercises the pre-PG17 attstattarget representation
66+
# without adding a third full test leg.
5867
# Pinned to eql-2.3.1 to match the EQL payload format the code emits
5968
# (protect-ffi 0.23.x); bump in lockstep with the protect-ffi upgrade.
6069
services:
6170
postgres:
62-
image: ghcr.io/cipherstash/postgres-eql:17-2.3.1
71+
image: ghcr.io/cipherstash/postgres-eql:${{ matrix.postgres-version }}-2.3.1
6372
env:
6473
POSTGRES_USER: cipherstash
6574
POSTGRES_PASSWORD: password
@@ -333,11 +342,13 @@ jobs:
333342
# (`installer/__tests__/verify.live.test.ts`) would run in no CI
334343
# workflow at all: a routine `@cipherstash/eql` bump could then make
335344
# every `stash eql install` fail with phantom damage, on green CI.
336-
# These suites need Postgres only, no CipherStash credentials; the
337-
# verify suite installs EQL v3 into its own schemas, which coexists
345+
# Most suites need Postgres only. The encrypted-index upgrade suite also
346+
# loads this job's CipherStash credentials from packages/stack/.env and
347+
# uses the binding built above to create genuine ciphertext. The verify
348+
# suite installs EQL v3 into its own schemas, which coexists
338349
# with the image's pre-installed EQL v2 that the stack tests use.
339350
# They share that one database, so the CLI vitest config runs them
340-
# serially (the `live` project sets `fileParallelism: false` —
351+
# serially (the `live` project uses a single fork —
341352
# verify.live's bundle install opens with DROP SCHEMA … CASCADE, which
342353
# races destructively under the other suites in parallel forks).
343354
# (`supabase-push.live.test.ts` gates on different env vars and still
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
---
2+
status: accepted
3+
---
4+
5+
# Keep encrypted data durable and EQL schemas disposable
6+
7+
EQL data-bearing domains live in `public` and must survive install, uninstall,
8+
and reinstall, while the `eql_v3` and `eql_v3_internal` schemas remain
9+
disposable and may be dropped with `CASCADE`. Search indexes are derived state:
10+
tooling must capture, rebuild, and verify them around reinstall. Tooling must
11+
refuse before mutation when it finds customer-owned dependencies such as
12+
policies, constraints, or views that it cannot reconstruct safely. This follows
13+
the EQL v2 persistence boundary and deliberately rejects brittle
14+
object-by-object in-place upgrades and permanently versioned implementation
15+
schemas.
16+
17+
## Consequences
18+
19+
- Losing an encrypted application column or stored encrypted value during any
20+
EQL lifecycle operation is a correctness failure.
21+
- Reinstall may incur an explicit, potentially expensive index rebuild.
22+
- Index restoration failures are loud and actionable; they never degrade
23+
silently to sequential scans.
24+
- Changes that make an existing index definition invalid require operator
25+
intervention rather than guessed migration semantics.

‎packages/cli/README.md‎

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -210,7 +210,16 @@ npx stash eql upgrade [options]
210210
| `--dry-run` | Show what would happen without making changes |
211211
| `--supabase` | Use Supabase-compatible upgrade |
212212

213-
The install SQL is idempotent and safe to re-run. If EQL is not installed, the command suggests running `npx stash eql install` instead.
213+
Encrypted columns and rows live outside the disposable EQL schemas. Before
214+
replacing those schemas, the CLI captures dependent functional indexes and then
215+
restores their definitions and supported catalog properties in the same
216+
transaction. It refuses unsupported dependencies before mutation and rolls back
217+
if restoration fails. If EQL is absent, the command suggests
218+
`npx stash eql install` instead.
219+
220+
Run upgrade in a schema-migration maintenance window. Its advisory lock prevents
221+
overlapping `stash` lifecycle commands, but unrelated sessions must not create,
222+
alter, or drop EQL-backed indexes while replacement is running.
214223

215224
---
216225

@@ -305,6 +314,10 @@ Reads `databaseUrl` from `stash.config.ts`.
305314

306315
Use `eql migration` to add the EQL v3 installation to your migration history instead of applying it directly. The install then ships to every environment through the same migrate step as the rest of your schema.
307316

317+
**Generated migrations contain the raw EQL bundle, not the CLI's reinstall
318+
protocol.** A first install is safe. To replace an existing installation, use
319+
`eql upgrade` or recreate every dependent object in the same migration.
320+
308321
### Drizzle
309322

310323
```bash

‎packages/cli/package.json‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,8 @@
7575
}
7676
},
7777
"devDependencies": {
78+
"@cipherstash/eql-upgrade-baseline": "npm:@cipherstash/eql@3.0.2",
79+
"@cipherstash/protect-ffi": "workspace:*",
7880
"@cipherstash/stack": "workspace:*",
7981
"@types/pg": "^8.23.1",
8082
"node-pty": "^1.1.0",

0 commit comments

Comments
 (0)