Repository navigation
stack-prisma: auto-derive EQL v3 functional indexes via onFieldEvent codec hook #896
Description
Activity
Two hazards missing from the list above, surfaced while reviewing the interim skill fix (#895). Both share one root:
onFieldEventfires only on field add/drop/alter, so any index state that changes out-of-band is never reconciled.6. EQL reinstall/upgrade cascade-drop. The EQL install SQL begins with
DROP SCHEMA IF EXISTS eql_v3 CASCADE, sostash eql upgrade(oreql install --force) drops every derived expression index. After that: no field event fires, the migration that created the ops is already applied, and lenient verification never notices — the indexes are silently gone forever and queries fall back to sequential scans without erroring. Thestash-indexingskill's current recovery recipe ("add a new migration re-issuing the CREATE INDEX statements") doesn't map onto this flow either.7. Brownfield/pre-existing columns. Encrypted columns that exist before this feature lands produce no
'added'event, so they get no index ops at all — the manual path (and the fact thatCREATE INDEX CONCURRENTLYcan't run through the runner's transaction) persists for exactly the deployments most likely to need indexes on populated tables.Both point at the same fix shape: a reconciliation pass, not (only) an event hook — e.g. a verify step in
migration plandiffing the expected derived index set againstpg_indexes, or astash-side check that emits the repair ops.Two design facts verified against the 0.17 dist that make the transition story easier than the sketch assumes:
- The op-level
createIndexfactory passesindexNamethrough literally (CreateIndexCall.toOp) — none of theformatWireName<name>_<8hex>hashing the PSL@@indexpath applies. Derived ops can use the exact<table>_<col>_eq/_ord/_match/_jsonnames the old skill recipe taught. - The runner skips any op whose postcheck is already satisfied (
postcheck_pre_satisfied, control apply loop). Combined with the point above: databases carrying old-recipe indexes under those names get adopted for free — the derivedcreateIndexop plans, sees the index, and skips. Keeping the sketch's proposed names identical to the old recipe's is therefore load-bearing, not cosmetic.
- The op-level
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).