Goal
An encrypted column declared in the contract should get its eql_v3.* functional indexes created by prisma-next migration plan automatically — no hand-written rawSql recipes (today's story, see #895 for the interim doc fix).
Why it's now possible (Prisma Next 0.17)
CodecControlHooks.onFieldEvent(event, ctx) (family-sql) fires per added/dropped/altered field, dispatched by the field's codecId, and returns OpFactoryCall[] inlined into the app-space migration's ops.json (ADR 213).
- target-postgres exposes a
createIndex op factory whose elements are {columns} | {expression} — the expression form plus type/where/unique/options extras, rendered as CREATE INDEX ... USING <type> (<expression>).
- Everything needed to derive the right indexes already exists in
packages/stack-prisma/src/v3/catalog.ts: V3DomainMeta carries capabilities and indexes per domain.
Sketch
Implement onFieldEvent in packages/stack-prisma/src/migration/cipherstash-codec-v3.ts (its header currently documents the deliberate absence — that rationale was about v2-style search config, not index DDL):
'added': emit one createIndex per capability the domain carries — eql_v3.eq_term(col) btree (eq-capable text domains), eql_v3.ord_term(col) btree (ord), eql_v3.match_term(col) gin (match), (eql_v3.to_ste_vec_query(col)::jsonb) jsonb_path_ops gin (Json containment).
'dropped': matching dropIndex ops.
- Deterministic names (
<table>_<col>_eq / _ord / _match / _json) — expression indexes require explicit names, and stable names keep re-plans no-op.
Domain rules (from skills/stash-indexing): numeric/date _ord domains get no eq_term index (no overload exists; eq inlines to ord_term = ord_term); TextOrd/TextSearch need both; _ord_ore opclass is superuser-gated → must be opt-in or skipped (Supabase breaks otherwise).
Known hazards to resolve
- Introspection round-trip churn: Postgres normalizes stored index expressions (
pg_get_indexdef), and the IR treats expression as opaque/never-parsed. If schema-verify compares authored vs introspected strings, every re-plan diffs dirty. Needs a live round-trip test before anything ships.
- Codec-swap gap: per ADR 213, a change where only
codecId differs fires no 'altered' event — a domain swap (e.g. TextEq → TextSearch) won't re-derive indexes. Handle or document.
ANALYZE: part of the recipe (expression indexes have no statistics until it runs) — emit as a companion op.
- Existing pins:
test/v3/migration-v3.test.ts and the example e2e assert v3 columns contribute zero extra migration ops; they flip to asserting the derived index set.
- Update
skills/stash-prisma + skills/stash-indexing again once this lands (auto vs manual story), with a stash changeset; @cipherstash/stack-prisma gets a minor changeset.
Refs: #749 (the 0.17 upgrade), #895 (interim skill fix).
Goal
An encrypted column declared in the contract should get its
eql_v3.*functional indexes created byprisma-next migration planautomatically — no hand-writtenrawSqlrecipes (today's story, see #895 for the interim doc fix).Why it's now possible (Prisma Next 0.17)
CodecControlHooks.onFieldEvent(event, ctx)(family-sql) fires per added/dropped/altered field, dispatched by the field'scodecId, and returnsOpFactoryCall[]inlined into the app-space migration'sops.json(ADR 213).createIndexop factory whose elements are{columns} | {expression}— the expression form plustype/where/unique/optionsextras, rendered asCREATE INDEX ... USING <type> (<expression>).packages/stack-prisma/src/v3/catalog.ts:V3DomainMetacarriescapabilitiesandindexesper domain.Sketch
Implement
onFieldEventinpackages/stack-prisma/src/migration/cipherstash-codec-v3.ts(its header currently documents the deliberate absence — that rationale was about v2-style search config, not index DDL):'added': emit onecreateIndexper capability the domain carries —eql_v3.eq_term(col)btree (eq-capable text domains),eql_v3.ord_term(col)btree (ord),eql_v3.match_term(col)gin (match),(eql_v3.to_ste_vec_query(col)::jsonb) jsonb_path_opsgin (Json containment).'dropped': matchingdropIndexops.<table>_<col>_eq/_ord/_match/_json) — expression indexes require explicit names, and stable names keep re-plans no-op.Domain rules (from
skills/stash-indexing): numeric/date_orddomains get noeq_termindex (no overload exists;eqinlines toord_term = ord_term);TextOrd/TextSearchneed both;_ord_oreopclass is superuser-gated → must be opt-in or skipped (Supabase breaks otherwise).Known hazards to resolve
pg_get_indexdef), and the IR treatsexpressionas opaque/never-parsed. If schema-verify compares authored vs introspected strings, every re-plan diffs dirty. Needs a live round-trip test before anything ships.codecIddiffers fires no'altered'event — a domain swap (e.g. TextEq → TextSearch) won't re-derive indexes. Handle or document.ANALYZE: part of the recipe (expression indexes have no statistics until it runs) — emit as a companion op.test/v3/migration-v3.test.tsand the example e2e assert v3 columns contribute zero extra migration ops; they flip to asserting the derived index set.skills/stash-prisma+skills/stash-indexingagain once this lands (auto vs manual story), with astashchangeset;@cipherstash/stack-prismagets a minor changeset.Refs: #749 (the 0.17 upgrade), #895 (interim skill fix).