Skip to content

Commit e30d07d

Browse files
committed
docs(skills): teach the PSL expression-index form Prisma Next 0.17 added
Since 0.17, `@@index` takes an `expression` argument, so the eql_v3.* functional indexes belong in schema.prisma rather than hand-written rawSql operations — the claim that PSL cannot express them predates the upgrade merged in #749. rawSql keeps the ANALYZE (no PSL form exists) and stays documented as the fallback for DDL PSL cannot carry, e.g. CONCURRENTLY. Closes #895
1 parent 989ba10 commit e30d07d

3 files changed

Lines changed: 52 additions & 41 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'stash': patch
3+
---
4+
5+
Correct the bundled `stash-prisma` and `stash-indexing` skills for Prisma Next 0.17's functional-index support: `@@index` now takes an `expression` argument, so the `eql_v3.*` functional indexes are declared directly in `schema.prisma` (expression indexes require a `name` or `map`; `options` requires `type`) instead of hand-written raw-SQL migration operations. `rawSql` remains the home of the post-build `ANALYZE` and the fallback for DDL that PSL cannot carry.

‎skills/stash-indexing/SKILL.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -255,7 +255,7 @@ Index not being used:
255255
**The integrations emit the query operators for you — none applies index DDL on its own. Making sure these indexes exist is always your job.** This skill is the general model — recipes, engagement rules, verification. How to apply it in a specific integration lives in that integration's skill:
256256

257257
- **Drizzle** — `encryptedIndexes(t)` from `@cipherstash/stack-drizzle` derives the recommended indexes for every encrypted column in the table, or declare individual expression indexes in the schema DSL. See `stash-drizzle` § Indexing Encrypted Columns.
258-
- **Prisma Next** — Prisma's schema language cannot express functional indexes; the DDL goes in a migration in the adapter's flow. See `stash-prisma`.
258+
- **Prisma Next** — since Prisma Next 0.17, `@@index(expression: "eql_v3.eq_term(email)", name: "users_email_eq", type: "btree")` declares a functional index directly in `schema.prisma`; the accompanying `ANALYZE` rides a raw-SQL migration operation. See `stash-prisma` § Indexing encrypted columns.
259259
- **Supabase** — a `supabase/migrations/` file; no superuser needed (see above). See `stash-supabase`.
260260
- **Raw SQL / plain PostgreSQL** — the recipes in this skill, in whatever migration tool owns the schema. Never ad-hoc in production. The predicates those indexes serve are in `stash-postgres`.
261261

‎skills/stash-prisma/SKILL.md‎

Lines changed: 46 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -191,64 +191,70 @@ Two things are Prisma-Next-specific:
191191

192192
The adapter emits the encrypted query operators, but **no index DDL** — without
193193
functional indexes over the `eql_v3.*` extractors, every encrypted predicate
194-
sequential-scans. Two facts shape where the DDL goes:
195-
196-
- **`schema.prisma` cannot express functional indexes** (`@@index` takes
197-
fields, not expressions), so the schema file is not an option.
198-
- Prisma Next migrations execute **raw SQL operations**, so an index migration
199-
is just an operation whose statements are the `CREATE INDEX` recipes —
200-
authored in the same migration history that installs the EQL bundle, applied
201-
by the same `prisma-next migrate`. Never run index DDL out-of-band.
194+
sequential-scans. Since Prisma Next 0.17, `@@index` takes an `expression`
195+
argument, so the indexes are declared in `schema.prisma` next to the columns
196+
they serve and ride the same `prisma-next migration plan` / `prisma-next
197+
migrate` flow as everything else. Never run index DDL out-of-band.
202198

203199
One index per capability the column's domain carries:
204200

205-
```sql
206-
-- cipherstash.TextEq / TextSearch: equality
207-
CREATE INDEX users_email_eq ON users USING btree (eql_v3.eq_term(email));
208-
-- cipherstash.*Ord / TextSearch: ordering + range (on numeric/date/timestamp
209-
-- _ord domains this one index serves = too; TextOrd needs the eq_term index
210-
-- above as well)
211-
CREATE INDEX users_created_at_ord ON users USING btree (eql_v3.ord_term(created_at));
212-
-- cipherstash.TextMatch / TextSearch: free-text match
213-
CREATE INDEX users_bio_match ON users USING gin (eql_v3.match_term(bio));
214-
-- cipherstash.Json: containment
215-
CREATE INDEX users_profile_json
216-
ON users USING gin ((eql_v3.to_ste_vec_query(profile)::jsonb) jsonb_path_ops);
217-
218-
ANALYZE users;
201+
```prisma
202+
model User {
203+
// ... fields, including the encrypted columns ...
204+
205+
// cipherstash.TextEq / TextSearch: equality
206+
@@index(expression: "eql_v3.eq_term(email)", name: "users_email_eq", type: "btree")
207+
// cipherstash.*Ord / TextSearch: ordering + range (on numeric/date/timestamp
208+
// _ord domains this one index serves = too; TextOrd needs the eq_term index
209+
// above as well)
210+
@@index(expression: "eql_v3.ord_term(created_at)", name: "users_created_at_ord", type: "btree")
211+
// cipherstash.TextMatch / TextSearch: free-text match
212+
@@index(expression: "eql_v3.match_term(bio)", name: "users_bio_match", type: "gin")
213+
// cipherstash.Json: containment
214+
@@index(expression: "(eql_v3.to_ste_vec_query(profile)::jsonb) jsonb_path_ops", name: "users_profile_json", type: "gin")
215+
}
219216
```
220217

221-
The `ANALYZE` is part of the recipe — an expression index has no statistics
222-
until it runs. Works as a non-superuser role (Supabase included); only the
223-
ORE-flavour (`_ord_ore`) ordering opclass is superuser-gated. For the full
224-
model — which domains take which index, engagement rules, `EXPLAIN`
225-
verification, rollout timing — see the `stash-indexing` skill. For encrypted
226-
predicates written as raw SQL rather than through the `cipherstash:*`
227-
operators — operand casts to `eql_v3.query_*`, per-driver parameter binding —
228-
see the `stash-postgres` skill.
218+
Three rules the interpreter enforces: an `@@index` takes exactly one of a
219+
fields list or an `expression`; an expression index **requires `name` or
220+
`map`** (no default name can be derived from an expression); and an `options`
221+
argument requires `type`. The expression string is the entire element list
222+
between the parens of `CREATE INDEX`, inserted verbatim — which is why the
223+
Json recipe carries its own parens and the `jsonb_path_ops` opclass. TS-authored
224+
contracts have the same surface: `index({ expression, name, type })` alongside
225+
the column factories.
229226

230-
In a migration, the recipes ride a raw-SQL operation (`rawSql` from
231-
`@prisma/orm-postgres/migration`) in the migration's `operations`:
227+
`ANALYZE` is still part of the recipe — an expression index has no statistics
228+
until it runs, and PSL cannot express it — so it rides a raw-SQL operation
229+
(`rawSql` from `@prisma/orm-postgres/migration`) in the migration that
230+
introduces the indexes:
232231

233232
```typescript
234233
rawSql({
235-
id: 'index.users.encrypted',
236-
label: 'Index encrypted columns on users',
234+
id: 'analyze.users',
235+
label: 'Refresh statistics for the new expression indexes',
237236
operationClass: 'additive',
238237
target: {
239238
id: 'postgres',
240-
details: { schema: 'public', objectType: 'index', name: 'users_email_eq', table: 'users' },
239+
details: { schema: 'public', objectType: 'table', name: 'users' },
241240
},
242241
precheck: [],
243-
execute: [
244-
{ description: 'equality index',
245-
sql: 'CREATE INDEX IF NOT EXISTS users_email_eq ON "public"."users" USING btree (eql_v3.eq_term(email))' },
246-
{ description: 'refresh statistics', sql: 'ANALYZE "public"."users"' },
247-
],
242+
execute: [{ description: 'refresh statistics', sql: 'ANALYZE "public"."users"' }],
248243
postcheck: [],
249244
})
250245
```
251246

247+
(`rawSql` also remains the fallback for index DDL itself if you need something
248+
PSL doesn't carry — `CREATE INDEX CONCURRENTLY`, for instance.)
249+
250+
Everything above works as a non-superuser role (Supabase included); only the
251+
ORE-flavour (`_ord_ore`) ordering opclass is superuser-gated. For the full
252+
model — which domains take which index, engagement rules, `EXPLAIN`
253+
verification, rollout timing — see the `stash-indexing` skill. For encrypted
254+
predicates written as raw SQL rather than through the `cipherstash:*`
255+
operators — operand casts to `eql_v3.query_*`, per-driver parameter binding —
256+
see the `stash-postgres` skill.
257+
252258
## Writing and reading encrypted values
253259

254260
At the value boundary you wrap plaintext in a **runtime envelope** (primitive-named,

0 commit comments

Comments
 (0)