Skip to content

Commit 365daa2

Browse files
project(functional-indexes): slice-4 spec + plan (infer-round-trip)
Grounded on the slice-3 branch. Resolutions of note: D8 assumed "permissive from the row" but RESTRICTIVE has no authoring surface — the five policy blocks gain an optional permissive property as the minimal enabler (the hash tuple already carries the slot); unique non-constraint indexes are currently dropped by infer entirely and start emitting; authored type:"btree" re-detects as exact rather than managed (benign, documented); release-notes authoring stays at release-cut per repo convention, satisfied here by complete upgrade entries. Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io>
1 parent e8a3af1 commit 365daa2

2 files changed

Lines changed: 117 additions & 0 deletions

File tree

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Plan — Slice 4: `infer-round-trip`
2+
3+
**Spec:** [infer-round-trip.spec.md](../specs/infer-round-trip.spec.md). Branch `slice/infer-round-trip`, stacked on `slice/rls-exact-names`. Same loop conventions and full standing gate per dispatch. This slice completes the project DoD; close-out is a separate final PR.
4+
5+
## Dispatch sequence
6+
7+
### 1 — Indexes round-trip at full fidelity
8+
9+
**Outcome:** infer emits every non-constraint index (skip guard and unique-gate deleted) with managed re-detection (`name:` when the wire hash recomputes, `map:` otherwise), full-matrix args via the restructured builder, the btree edge documented; the duplicate guard keys exact entries by name (twin databases signable). Policies untouched. Fields-only re-detection pinned byte-identically; expression/partial/unique-index emission pinned against real introspection.
10+
11+
**Builds on:** slices 1–3. **Hands to:** index half of scenario A provable.
12+
13+
### 2 — Policies and `@@rls` round-trip
14+
15+
**Outcome:** policy blocks emit per spec § 3 (sanitized disambiguated heads, `@@map` always, verbatim bodies, roles as-is, skip-with-note for unauthorable); `permissive` becomes an authorable block property (spec § 4, three hardcoded sites threaded, managed hashes byte-unchanged, stop-condition on spread); `@@rls` emits natively; the Supabase harness's `applyRlsEnablement` is deleted and the contract regenerated (transitional partial-index omission ends; movement explained against § 5's expectations; `CONTRACT-FIDELITY.md` updated).
16+
17+
**Builds on:** dispatch 1. **Hands to:** everything scenario A needs emitted.
18+
19+
### 3 — The project DoD
20+
21+
**Outcome:** DoD-2 sign-the-database e2e (incl. one RESTRICTIVE policy), DoD-3 transition e2e (exactly two renames), DoD-4 referenced, DoD-5 scenario A–J walk in the report; re-infer stability + fidelity suites extended; docs + upgrade entries; full standing gate.
22+
23+
**Builds on:** dispatches 1–2. **Hands to:** slice-DoD and project-DoD items 1–6 met; PR-open (stacked on slice 3). Close-out PR follows separately.
24+
25+
## Sizing notes
26+
27+
Three dispatches: the index half (mechanical once the builder branches), the policy half (the judgment core — sanitization, permissive, harness regen), acceptance. Sequential-green holds: dispatch 1 changes only index emission (existing suites pin fields-only behavior), dispatch 2 adds emissions nothing consumes until the journeys, dispatch 3 proves the whole.
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
# Spec — Slice 4: `infer-round-trip`
2+
3+
**Parent:** [project spec](../spec.md) § D8 · [plan](../plan.md) slice 4 (including its two carry-over bullets) · builds on slices 1–3. This slice completes scenario A and the project DoD.
4+
5+
## At a glance
6+
7+
Sign a database this toolchain has never seen:
8+
9+
```
10+
contract infer → emit → db verify: zero issues → db update --dry-run: zero operations
11+
```
12+
13+
— on a database containing an expression index, a partial index, a unique expression index, and RLS policies created by another tool. `contract infer` emits every index at full fidelity (managed re-detection where the wire name recomputes, `map:` otherwise) and every policy as a `policy_*` block with `@@map` and verbatim reprinted bodies, plus `@@rls`. The emitted contract *is* the database's signature.
14+
15+
## Chosen design
16+
17+
### 1. Index emission — full matrix with managed re-detection
18+
19+
`infer-psl-contract.ts`: the single slice-1 skip guard (line ~745, `columns === undefined || where !== undefined`) is deleted; the `!index.unique` gate (line ~746) is deleted (unique non-constraint indexes stop being silently dropped). Every non-constraint index emits:
20+
21+
- **Managed re-detection** (D8): `parseWireName(index.name)` and recompute `computeIndexContentHash` from the introspected node's own fields (columns/expression/where/unique/type/options — the tuple inputs are exactly recoverable; options `String()`-coercion agrees on both sides). Parsed hash === recomputed hash → emit `name: "<prefix>"` (managed). Otherwise → `map: "<live name>"` (exact) with the content verbatim.
22+
- Expression indexes always take the `map:` branch (the reprint never re-hashes to the authored suffix — confirmed, no counterexample path). **Known benign edge**: an index authored `type: "btree"` hashed `'btree'` into its suffix but introspects type-normalized, so recompute mismatches and it re-infers as `map:` — still a clean round-trip, just exact rather than managed; documented in a code comment, not special-cased.
23+
- Emission surface: `buildModelConstraintAttribute` grows a branch (or sibling builder) — expression indexes have no positional field list and emit `expression:` as a named string arg; `where:`/`unique: true`/`type:`/`options:` named args as introspected; the managed branch switches today's hardwired `map:` to `name:`. All strings through `escapePslString`.
24+
- Constraint-backed uniques keep flowing to `@@unique` exactly as today (tuple-identified).
25+
26+
### 2. The duplicate-guard carry-over
27+
28+
`validateStorageSemantics` (`validators.ts:532–556`): the duplicate-index rejection keys by content signature, which would reject a legally-twinned database (two content-identical indexes under different names — the Supabase reference carries one). For **exact-mode** entries (no `prefix`) the guard keys by `name`; managed entries keep the content key (two managed twins would collide on wire name anyway — same hash — so the content key is already equivalent there; state that in a comment). A signed twin-carrying database validates.
29+
30+
### 3. Policy block emission (D8 policy half)
31+
32+
Per introspected policy node, in its namespace block:
33+
34+
- **Head identifier**: `parseWireName(policyname)?.prefix ?? policyname`, sanitized to the PSL identifier grammar (tokenizer rules: leading `\p{L}`/`_`, then letters/digits/`_`/`-`; replace invalid runs with `_`, prepend `_` if the first char is invalid); within-namespace collisions disambiguated with a numeric suffix (`_2`, `_3`, … — deterministic by sorted physical name). The head is source-only.
35+
- **`@@map("<policyname>")` always** — a body reprint never re-hashes to the live suffix, so managed re-detection is impossible for policies; every adopted policy is exact. (Emitting these blocks means re-emitting an adopted contract fires the D9 warning batch — by design, the wording covers the infer-captured case.)
36+
- Properties: `target` (the model), `roles` (introspected list as-is), `using`/`withCheck` verbatim from `qual`/`with_check` reprints, `permissive` per § 4.
37+
- **Unauthorable policies skip with a PSL comment note** (`// prisma-next: skipped policy "<name>": <reason>`) rather than failing the whole infer: a role name outside the PSL identifier grammar (role refs have no `@@map` escape). The scenario-A fixture contains none; the skip path is unit-tested. A skipped policy is a live extra — strict verify will name it, which is the honest outcome.
38+
39+
### 4. `permissive` becomes authorable (falsified-assumption resolution)
40+
41+
D8 commits to "`permissive` from the row", but RESTRICTIVE has no authoring surface — the five block descriptors expose no parameter and both lowerings hardcode `true`. Completing D8 needs the minimal enabler: the five `policy_*` blocks gain an optional `permissive` boolean property (default `true`), threaded through `lowerRlsPolicyFromBlock`/`buildRlsPolicyEntity` (three hardcoded sites) into the existing hash-tuple slot and entity field. Managed `permissive: true` hashes are byte-unchanged (the tuple already contained `permissive`); a RESTRICTIVE live policy now infers and round-trips like any other. The TS entity-handle path stays as-is (managed, defaulted `true`) unless trivially symmetric. If threading this touches more than the descriptors + the two lowering functions + tests, stop and surface.
42+
43+
### 5. `@@rls` emission
44+
45+
`buildModel` pushes the existing `@@rls` model attribute (`{ target:'model', name:'rls', args:[] }`) when the table node carries `rlsEnabled` — the shape the Supabase harness currently bolts on out-of-band. The harness's `applyRlsEnablement` is then **deleted** and the Supabase contract regenerated through `contract:generate`: `@@rls` now native, partial indexes re-adopt (the slice-1 transitional omission ends), and policy blocks appear if the reference schema carries policies. `CONTRACT-FIDELITY.md` updated to match (the partial-index omission passage comes out).
46+
47+
### 6. Acceptance — the project DoD lands here
48+
49+
- **DoD-2, sign-the-database e2e**: raw-SQL database with an expression index, a partial index, a unique expression index, and two RLS policies (permissive + one RESTRICTIVE to prove § 4) created "by another tool" → infer → emit → verify **zero issues**`db update --dry-run` **zero operations**. Extends `index-name-convergence.e2e.test.ts`'s scenario-A journey or a new sibling.
50+
- **DoD-3, transition e2e**: from the signed contract, replace `map:` with `name:` on one index AND one policy (bodies verbatim) → the widening plan contains **exactly the two RENAMEs** → apply → verify clean.
51+
- **DoD-4, upgrade e2e**: the scenario-I journey already exists (slice 1); confirm it still holds on the full stack and reference it — no duplicate.
52+
- **DoD-5, scenario sweep**: a checklist walk in the report mapping every scenario row A–J to its named test across slices 1–4 (A completes here; B/D/E/G/H slice 2; C/F slice 3 + this slice's transition; I/J slice 1).
53+
- Re-infer stability (`contract-infer-workflow` journey) and the fidelity instrument (`infer-roundtrip-fidelity`) extended with the new emissions.
54+
55+
### 7. Release notes and close-out prep
56+
57+
The 0.16-to-0.17 upgrade entries gain this slice's additions (infer emissions, `permissive`, the guard change). The committed `docs/releases/v0.17.0.md` is **deliberately not authored here**: repo convention authors it at release-cut time via the `draft-release-notes` skill (which enumerates all merged PRs; the gate only fires on version bumps) — the plan's "release-notes draft" line is satisfied by the upgrade entries being complete and is noted as such in plan.md at close-out. Close-out itself (ADR promotion, reference stripping, `projects/functional-indexes/` deletion) is the project's final PR, after this slice merges — not this slice.
58+
59+
## Coherence rationale
60+
61+
One capability — "the emitted contract is the database's signature" — landed as one PR: emission, re-detection, the two carry-overs that block signing real databases, and the DoD journeys that prove it. The `permissive` addition is the one authoring change, justified as D8's minimal enabler.
62+
63+
## Scope
64+
65+
**Deliberately out:** TS policy authoring; RESTRICTIVE beyond the block parameter; role declarations/`@@map` on role refs (undeclared roles load by design; ungrammatical role names skip-with-note); the close-out PR; `docs/releases/v0.17.0.md`.
66+
67+
## Pre-investigated edge cases
68+
69+
| Case | Obligation |
70+
| --- | --- |
71+
| `type: "btree"` re-detection miss | Benign exact fallback; comment + test, no special case. |
72+
| RESTRICTIVE policies | Authorable via § 4; the e2e proves one. |
73+
| Ungrammatical role names | Policy skips with note; unit test; strict verify honestly names the leftover. |
74+
| Twin indexes | Guard keyed by name for exact entries (§ 2); the Supabase regen is the live proof. |
75+
| Head collisions after sanitization | Deterministic numeric suffix; unit test with two colliding physical names. |
76+
| Supabase harness | `applyRlsEnablement` deleted; regen through the checked-in generator only; expect partial indexes + `@@rls` (+ policies if seeded) — any other movement must be explained. |
77+
78+
## Slice-specific done conditions
79+
80+
1. DoD-2 and DoD-3 e2e green, byte-asserted where stated; DoD-4 referenced; DoD-5 walk complete in the report.
81+
2. Supabase contract regenerated with the transitional omission ended and the harness helper deleted.
82+
3. Full standing gate green.
83+
84+
## Open questions
85+
86+
None.
87+
88+
## References
89+
90+
Grounding: `infer-psl-contract.ts:306,318,336–429,700–755,1013–1068`, `naming.ts:34,57,85–97`, `index-naming.ts:116–175`, `sql-index-ir.ts:21,95–130,178–195`, `validators.ts:459–572`, `control-adapter.ts:1247–1330`, `authoring.ts:146–252,462–545`, `tokenizer.ts:249–255`, `generate-contract.ts:181–235,451–455`, `index-name-convergence.e2e.test.ts`, `infer-roundtrip-fidelity.e2e.test.ts`, `contract-infer-workflow.e2e.test.ts`, `docs/releases/README.md`, `scripts/check-release-notes.mjs`.

0 commit comments

Comments
 (0)