Skip to content

Expression, partial, and unique indexes are authorable in PSL and TypeScript#1048

Merged
wmadden-electric merged 16 commits into
mainfrom
slice/expression-index-authoring
Jul 27, 2026
Merged

Expression, partial, and unique indexes are authorable in PSL and TypeScript#1048
wmadden-electric merged 16 commits into
mainfrom
slice/expression-index-authoring

Conversation

@wmadden-electric

@wmadden-electric wmadden-electric commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

At a glance

The index the Cipherstash team asked for is now a line of PSL:

model User {
  id    Int    @id
  email String
  @@index(expression: "eql_v3.eq_term(email)", name: "users_email_eq", type: "btree")
}

or TypeScript: constraints.index({ expression: 'eql_v3.eq_term(email)', name: 'users_email_eq', type: 'btree' }). It emits, plans byte-exact DDL (eql_v3.eq_term(email) rendered verbatim), applies, and verifies — proven by an e2e journey in both authoring variants, including the follow-on flows: renaming the prefix plans exactly one ALTER INDEX … RENAME; editing the expression plans the rebuild.

Decision

Slice 2 of the functional-indexes project (spec projects/functional-indexes/specs/expression-index-authoring.spec.md on the base branch; substrate landed in slice 1, #1047). Both authoring surfaces gain the full D3 parameter matrix — fields xor expression, where, unique, name xor map, type/options — lowering through the one shared path, so PSL/TS parity holds by construction (parity tests pin identical IR, wire names included).

Invalid combinations are rejected where the user is: three span-anchored PSL diagnostics (PSL_INDEX_FIELDS_XOR_EXPRESSION, PSL_INDEX_EXPRESSION_REQUIRES_NAME, PSL_INDEX_NAME_XOR_MAP) point at the @@index attribute; the TS paths hit the same shared lowering guards (CONTRACT.ARGUMENT_INVALID). Two mechanism gaps the grounding surfaced are resolved without leaking family vocabulary into the framework:

  • The PSL diagnostic-code union is framework-closed, so the framework gains a seam, not vocabulary: PslDiagnostic.code admits package-contributed PSL_* codes (a pattern type), and the three index codes live in contract-psl. Refines also gain access to the attribute node — the minimal enabler for attribute-anchored spans (previously refines could only anchor at the model).
  • PSL has no non-blocking diagnostic concept, so the D9 warning (map: combined with a SQL body — reliable only for infer-captured text) emits through the existing process.emitWarning channel from the shared lowering, threshold-batched: each offending index named individually up to five, one summary with a name list beyond — sized for slice 4, where adopted contracts will carry many map: bodies.

SQLite rejects expression/where at its namespace construction (Postgres-only enablement, per the project non-goal). @@unique/constraints.unique are byte-frozen — the constraint surface stays out of the expression business.

Also fixed: authored type: "btree" no longer drifts forever

Writing the e2e with the DoD's literal spelling exposed a slice-1 gap: introspection normalized btree → undefined but contract derivation didn't, so a btree-typed index verified as perpetual drift (and the isEqualTo doc comment falsely claimed construction-side normalization). The normalization now lives in the SqlIndexIR constructor — single-homed, symmetric for both sides by construction; the adapter's pre-normalization is deleted. Contract JSON and wire hashes keep the authored spelling (zero fixture movement); the two spellings of a default-method index have distinct wire names but are content-equal, so converging between them is a phase-2 rename, not a rebuild — pinned by a planner test.

Scenario coverage (project spec rows)

  • B greenfield managed authoring — the Cipherstash e2e's base flow.
  • D prefix rename — byte-asserted single-rename plan in the e2e.
  • E body edit — byte-asserted create+drop under destructive; create-only under additive-only (planner suite).
  • G hand-authored body under map: — D9 warning asserted; the false-drift consequence pinned as a named documented-degradation test (authored text vs Postgres reprint).
  • H out-of-band ALTER INDEX … SET (fillfactor=70) — verify reports drift (integration).

Verification

Full standing gate green at every dispatch (build 68/68 · typecheck 143/143 · lint 82/82 · lint:deps · lint:casts/lint:throws delta 0 · lint:framework-vocabulary count unchanged · lint:skills · check:upgrade-coverage --mode pr · fixtures:check clean with zero fixture movement across the whole slice · test:packages · test:integration 247/247 · test:e2e 20/20 · test:examples). Docs updated (both authoring READMEs enumerate the matrix; error reference covers the new cases; the 0.16-to-0.17 upgrade notes gained the "newly available" passage and lost a now-false claim).

Base

Stacked on slice/indexes-are-name-identified (#1047), itself on project/functional-indexes-shaping (#1046). Merge order: #1046#1047 → this.

Skill update

No new upgrade-coverage entries required (no examples//extension substrate diff); the existing 0.16-to-0.17 entries were corrected as described above.

Checklist

  • All commits are signed off (git commit -s) per the DCO.
  • I read CONTRIBUTING.md and the change is scoped to one logical concern.
  • Tests are updated.
  • The Skill update section is filled in.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Added support for expression-based and partial indexes (including where, unique, access type/options, and name vs map) across PSL and TypeScript.
    • Expanded index validation with new contributed diagnostic codes and a fuller argument matrix.
    • Added batched warnings for exact-name (map:) cases where SQL bodies may drift.
  • Bug Fixes
    • Improved index naming/hash behavior and threading of where/unique, plus safer SQLite rejection for unsupported index features.
    • Normalized default (btree) index type at construction and improved introspection to reflect btree.
  • Documentation
    • Updated index authoring docs, upgrade guides, and error reference entries.
  • Tests
    • Added/expanded unit and integration coverage for diagnostics, naming, warnings, and index drift.

@wmadden-electric
wmadden-electric requested a review from a team as a code owner July 24, 2026 04:28
@coderabbitai

coderabbitai Bot commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@wmadden-electric, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 14 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro Plus

Run ID: 3948a135-a813-4410-8fb1-7d34430e9768

📥 Commits

Reviewing files that changed from the base of the PR and between 07b3a48 and 71b6066.

📒 Files selected for processing (2)
  • packages/2-sql/2-authoring/contract-ts/src/contract-dsl.ts
  • test/integration/test/cli-journeys/expression-index-migration.e2e.test.ts
📝 Walkthrough

Walkthrough

Expression and partial index authoring is added across PSL and TypeScript contracts. Validation diagnostics, managed/exact naming, warning collection, btree normalization, SQLite rejection, PostgreSQL introspection, migration planning, schema verification, documentation, and integration fixtures are updated.

Changes

Index authoring and lowering

Layer / File(s) Summary
Diagnostic callback and code contracts
packages/1-framework/...
PSL refinement callbacks receive attribute AST nodes, and diagnostics accept contributed PSL_ codes.
PSL index matrix and diagnostics
packages/2-sql/2-authoring/contract-psl/...
@@index supports fields or expressions with where, unique, naming, type, and options validation.
Shared index lowering and IR normalization
packages/2-sql/1-core/...
Index lowering enforces valid shapes, preserves index bodies and options, batches exact-name warnings, and normalizes default btree types.
TypeScript index DSL integration
packages/2-sql/2-authoring/contract-ts/...
constraints.index supports field and expression forms and flushes collected warnings during contract construction.
Target validation and migration journeys
packages/3-targets/..., test/integration/..., skills/..., docs/...
SQLite rejects expression and partial indexes; PostgreSQL, migration, drift, documentation, and fixture coverage are updated.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant ContractAuthor
  participant PSLOrTSAuthoring
  participant lowerAuthoredIndex
  participant SqlIndexIR
  participant Target
  ContractAuthor->>PSLOrTSAuthoring: define fields or expression index
  PSLOrTSAuthoring->>lowerAuthoredIndex: validate and lower index
  lowerAuthoredIndex->>SqlIndexIR: produce normalized index representation
  SqlIndexIR->>Target: provide contract index metadata
  Target-->>ContractAuthor: accept, reject, migrate, or report drift
Loading

Possibly related PRs

Suggested reviewers: wmadden

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 21.74% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: authoring expression, partial, and unique indexes in both PSL and TypeScript.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch slice/expression-index-authoring

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@pkg-pr-new

pkg-pr-new Bot commented Jul 24, 2026

Copy link
Copy Markdown

Open in StackBlitz

@prisma-next/extension-author-tools

npm i https://pkg.pr.new/@prisma-next/extension-author-tools@1048

@prisma-next/mongo-runtime

npm i https://pkg.pr.new/@prisma-next/mongo-runtime@1048

@prisma-next/family-mongo

npm i https://pkg.pr.new/@prisma-next/family-mongo@1048

@prisma-next/sql-runtime

npm i https://pkg.pr.new/@prisma-next/sql-runtime@1048

@prisma-next/family-sql

npm i https://pkg.pr.new/@prisma-next/family-sql@1048

@prisma-next/extension-arktype-json

npm i https://pkg.pr.new/@prisma-next/extension-arktype-json@1048

@prisma-next/middleware-cache

npm i https://pkg.pr.new/@prisma-next/middleware-cache@1048

@prisma-next/mongo

npm i https://pkg.pr.new/@prisma-next/mongo@1048

@prisma-next/extension-paradedb

npm i https://pkg.pr.new/@prisma-next/extension-paradedb@1048

@prisma-next/extension-pgvector

npm i https://pkg.pr.new/@prisma-next/extension-pgvector@1048

@prisma-next/extension-postgis

npm i https://pkg.pr.new/@prisma-next/extension-postgis@1048

@prisma-next/postgres

npm i https://pkg.pr.new/@prisma-next/postgres@1048

@prisma-next/sql-orm-client

npm i https://pkg.pr.new/@prisma-next/sql-orm-client@1048

@prisma-next/sqlite

npm i https://pkg.pr.new/@prisma-next/sqlite@1048

@prisma-next/extension-supabase

npm i https://pkg.pr.new/@prisma-next/extension-supabase@1048

@prisma-next/target-mongo

npm i https://pkg.pr.new/@prisma-next/target-mongo@1048

@prisma-next/adapter-mongo

npm i https://pkg.pr.new/@prisma-next/adapter-mongo@1048

@prisma-next/driver-mongo

npm i https://pkg.pr.new/@prisma-next/driver-mongo@1048

@prisma-next/contract

npm i https://pkg.pr.new/@prisma-next/contract@1048

@prisma-next/utils

npm i https://pkg.pr.new/@prisma-next/utils@1048

@prisma-next/config

npm i https://pkg.pr.new/@prisma-next/config@1048

@prisma-next/errors

npm i https://pkg.pr.new/@prisma-next/errors@1048

@prisma-next/framework-components

npm i https://pkg.pr.new/@prisma-next/framework-components@1048

@prisma-next/operations

npm i https://pkg.pr.new/@prisma-next/operations@1048

@prisma-next/ts-render

npm i https://pkg.pr.new/@prisma-next/ts-render@1048

@prisma-next/contract-authoring

npm i https://pkg.pr.new/@prisma-next/contract-authoring@1048

@prisma-next/ids

npm i https://pkg.pr.new/@prisma-next/ids@1048

@prisma-next/psl-parser

npm i https://pkg.pr.new/@prisma-next/psl-parser@1048

@prisma-next/psl-printer

npm i https://pkg.pr.new/@prisma-next/psl-printer@1048

@prisma-next/cli

npm i https://pkg.pr.new/@prisma-next/cli@1048

@prisma-next/cli-telemetry

npm i https://pkg.pr.new/@prisma-next/cli-telemetry@1048

@prisma-next/config-loader

npm i https://pkg.pr.new/@prisma-next/config-loader@1048

@prisma-next/emitter

npm i https://pkg.pr.new/@prisma-next/emitter@1048

@prisma-next/language-server

npm i https://pkg.pr.new/@prisma-next/language-server@1048

@prisma-next/migration-tools

npm i https://pkg.pr.new/@prisma-next/migration-tools@1048

prisma-next

npm i https://pkg.pr.new/prisma-next@1048

@prisma-next/vite-plugin-contract-emit

npm i https://pkg.pr.new/@prisma-next/vite-plugin-contract-emit@1048

@prisma-next/mongo-codec

npm i https://pkg.pr.new/@prisma-next/mongo-codec@1048

@prisma-next/mongo-contract

npm i https://pkg.pr.new/@prisma-next/mongo-contract@1048

@prisma-next/mongo-value

npm i https://pkg.pr.new/@prisma-next/mongo-value@1048

@prisma-next/mongo-contract-psl

npm i https://pkg.pr.new/@prisma-next/mongo-contract-psl@1048

@prisma-next/mongo-contract-ts

npm i https://pkg.pr.new/@prisma-next/mongo-contract-ts@1048

@prisma-next/mongo-emitter

npm i https://pkg.pr.new/@prisma-next/mongo-emitter@1048

@prisma-next/mongo-schema-ir

npm i https://pkg.pr.new/@prisma-next/mongo-schema-ir@1048

@prisma-next/mongo-query-ast

npm i https://pkg.pr.new/@prisma-next/mongo-query-ast@1048

@prisma-next/mongo-orm

npm i https://pkg.pr.new/@prisma-next/mongo-orm@1048

@prisma-next/mongo-query-builder

npm i https://pkg.pr.new/@prisma-next/mongo-query-builder@1048

@prisma-next/mongo-lowering

npm i https://pkg.pr.new/@prisma-next/mongo-lowering@1048

@prisma-next/mongo-wire

npm i https://pkg.pr.new/@prisma-next/mongo-wire@1048

@prisma-next/sql-contract

npm i https://pkg.pr.new/@prisma-next/sql-contract@1048

@prisma-next/sql-errors

npm i https://pkg.pr.new/@prisma-next/sql-errors@1048

@prisma-next/sql-operations

npm i https://pkg.pr.new/@prisma-next/sql-operations@1048

@prisma-next/sql-schema-ir

npm i https://pkg.pr.new/@prisma-next/sql-schema-ir@1048

@prisma-next/sql-contract-psl

npm i https://pkg.pr.new/@prisma-next/sql-contract-psl@1048

@prisma-next/sql-contract-ts

npm i https://pkg.pr.new/@prisma-next/sql-contract-ts@1048

@prisma-next/sql-contract-emitter

npm i https://pkg.pr.new/@prisma-next/sql-contract-emitter@1048

@prisma-next/sql-lane-query-builder

npm i https://pkg.pr.new/@prisma-next/sql-lane-query-builder@1048

@prisma-next/sql-relational-core

npm i https://pkg.pr.new/@prisma-next/sql-relational-core@1048

@prisma-next/sql-builder

npm i https://pkg.pr.new/@prisma-next/sql-builder@1048

@prisma-next/target-postgres

npm i https://pkg.pr.new/@prisma-next/target-postgres@1048

@prisma-next/target-sqlite

npm i https://pkg.pr.new/@prisma-next/target-sqlite@1048

@prisma-next/adapter-postgres

npm i https://pkg.pr.new/@prisma-next/adapter-postgres@1048

@prisma-next/adapter-sqlite

npm i https://pkg.pr.new/@prisma-next/adapter-sqlite@1048

@prisma-next/driver-postgres

npm i https://pkg.pr.new/@prisma-next/driver-postgres@1048

@prisma-next/driver-sqlite

npm i https://pkg.pr.new/@prisma-next/driver-sqlite@1048

commit: 71b6066

@github-actions

github-actions Bot commented Jul 24, 2026

Copy link
Copy Markdown

size-limit report 📦

Path Size
postgres / no-emit 166.66 KB (+0.47% 🔺)
postgres / emit 147.4 KB (-0.02% 🔽)
mongo / no-emit 100.26 KB (0%)
mongo / emit 90 KB (0%)
cf-worker / no-emit 192.36 KB (+0.42% 🔺)
cf-worker / emit 171.12 KB (-0.02% 🔽)


export interface PslDiagnostic {
readonly code: PslDiagnosticCode;
readonly code: PslDiagnosticCode | ContributedPslDiagnosticCode;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is this? Why do we have a union here at all?

);

const tablesByNamespace: Record<string, Record<string, StorageTableInput>> = {};
// D9 warnings collect across the whole build and flush once (threshold-

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NO TRANSIENT PROJECT IDS

@wmadden-electric
wmadden-electric force-pushed the slice/expression-index-authoring branch from 982f728 to c02144c Compare July 24, 2026 11:56
Base automatically changed from slice/indexes-are-name-identified to main July 24, 2026 12:28
…and unique

lowerAuthoredIndex accepts the full authoring matrix: expression, where,
and unique thread into both the carried entity and the content-hash
tuple (their tuple slots already existed, so fields-only wire names are
byte-unchanged — pinned by a regression test against the slice-1
literals). The cross-field guards are user-facing
CONTRACT.ARGUMENT_INVALID contract errors — fields xor expression,
expression requires name: or map:, and map xor name (no longer an
InternalError: it becomes reachable from TS authoring once map lands) —
the shared enforcement backstop both surfaces lower through.

The D9 warning ships from the same shared path: authoring a SQL body
(expression or where) under map: emits process.emitWarning with code
PN_EXACT_NAME_BODY_COMPARISON and the decision-pinned wording;
fields-only map: stays silent, managed name: bodies stay silent.

The semantic IndexNode carries the new fields and build-contract
threads them, but no authoring surface exposes them yet — everything
reachable today lowers identically (zero fixture movement).

Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
lowerAuthoredIndex pushes exact-name body hits into a caller-supplied
collector (falling back to an immediate single-warning flush for direct
callers) and buildSqlContractFromDefinition collects across the whole
build and flushes once through the contract-warnings threshold pattern:
per-item warnings naming each object up to five hits, one summary with
the name list above — an adopted contract re-emit must not wall-of-text
once infer emits map: plus body for every adopted object. The
ARGUMENT_INVALID error-reference meta line softens to "varies per site"
per the errorRuntime precedent.

Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
PslDiagnostic.code widens to the framework union or a
ContributedPslDiagnosticCode — a PSL_-patterned string type owned by the
contributing package, so family authoring layers can mint their own
diagnostic codes without any family vocabulary entering the framework
union. leafDiagnostic gains an optional code parameter defaulting to
the existing PSL_INVALID_ATTRIBUTE_SYNTAX, and attribute-spec refines
receive the attribute AST node as a third argument so cross-argument
diagnostics can span-anchor at the attribute itself instead of the
enclosing model.

Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
…an-anchored diagnostics

The fields positional becomes optional and @@index gains expression,
where, unique, and name beside the existing map/type/options. Three
cross-argument refines emit contributed-code diagnostics anchored at
the attribute: PSL_INDEX_FIELDS_XOR_EXPRESSION (exactly one of a
fields list or expression), PSL_INDEX_EXPRESSION_REQUIRES_NAME (an
expression index needs name: or map:), and PSL_INDEX_NAME_XOR_MAP (at
most one of name and map) — codes minted in this package through the
ContributedPslDiagnosticCode seam. The options-requires-type refine
stays and now also anchors at the attribute. The interpreter threads
every matrix field onto the semantic IndexNode; the shared lowering
computes wire names, hashes the bodies, and draws the D9 warning for
map: + body — pinned through interpretation tests including the ciphers
spelling. @@unique is untouched. (The attribute-wrapper configs in
psl-parser pick up the refine attributeNode parameter added with the
seam.)

Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
IndexConstraint gains expression/where/unique/map (fields becomes
optional, xor expression), and constraints.index gains the expression
overload — index({ expression, name?/map?, where?, unique?, type?,
options? }) — while the fields overload options add map, where, and
unique; both overloads keep the pack-typed type/options arms and the
SqlContext constraints surface carries them. The lowering threads every
field onto the semantic IndexNode; the cross-field rules the types
cannot express land on dispatch-1 shared guards, pinned from this
surface (expression-requires-name, map-xor-name) along with the D9
warning for map: + body. constraints.unique is untouched.

Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
…ce construction

The family-shared authoring surfaces parse expression:/where: for every
SQL target, but SQLite neither renders nor introspects them (project
non-goal). buildSqliteNamespace — the target-owned factory both
defineContract and the PSL path construct namespaces through — rejects
any index entry carrying an expression or where predicate with a
structured CONTRACT.ARGUMENT_INVALID error naming the index and table
and pointing at fields-only indexes or a Postgres target. No family
code branches on the target.

Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
Four parity rows pin byte-identical IR (wire names included) for
identical inputs on both surfaces: expression+name (managed,
users_email_eq_17273133), the full matrix expression+where+unique+type
under name: through a registered index-type pack
(users_email_eq_afd32a8f), fields+map (exact, no hash), and fields+name
(managed, user_email_lookup_46df9cad).

Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
…real surfaces

Rewrite the expression-index migration e2e to author through PSL and the
TS builder instead of the slice-1 factory-assembled fixture (deleted).
The PSL journey plans byte-exact DDL for the ciphers expression index,
a partial index, a unique expression index, and a hash-typed index,
applies, verifies clean, and fails verify on an out-of-band drop naming
the index; it then covers scenario D (name: change plans exactly one
ALTER INDEX RENAME) and scenario E (body edit under the same name plans
create + drop, both byte-asserted). A TS twin fixture runs the same base
flow through constraints.index.

Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
Scenario E's additive-only half: a body edit under the same name plans
only the create when destructive ops are disallowed (planner unit test).
Scenario G: an exact-named (map:) expression index created from exactly
the authored text still reports drift, because verification byte-compares
the authored text against Postgres's reprint — the documented degradation
behind the PN_EXACT_NAME_BODY_COMPARISON warning. Scenario H: an
out-of-band ALTER INDEX SET (fillfactor = 70) on a managed index is real
drift and verify reports the index not-equal.

Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
Enumerate the full @@index / constraints.index parameter surface in the
PSL and TS authoring READMEs and the contract skill: fields xor
expression, where, unique, type/options, and name xor map, with the
managed-vs-exact naming modes and the PN_EXACT_NAME_BODY_COMPARISON
warning documented where map: is explained. The error reference's
CONTRACT.ARGUMENT_INVALID entry now names the SQLite
expression/partial-index rejection. The 0.16-to-0.17 upgrade
instructions drop the stale 'authoring surfaces arrive in a later
release' claim and note the newly available matrix in both skills.

Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
…ction

An authored type: "btree" drifted on verify: introspection dropped the
default access method before node construction while contract derivation
passed it through, so isEqualTo's strict type compare failed on the exact
spelling the ciphers scenario uses. The btree-to-undefined normalization
now lives in the SqlIndexIR constructor — every derivation path (contract
tree, introspection, flat family tree) constructs through it, making both
sides symmetric by definition; the adapter's pre-construction filter is
removed so the rule has one home. The contract JSON and the wire-name
content hash keep the authored spelling, so a btree-typed index and an
untyped index remain distinct wire names, and the planner's DDL renders
no USING clause for the default method. Phase-2 content pairing inherits
the symmetry (pinned by a planner test), verify is pinned clean at
integration level, and the ciphers e2e now authors the literal
type: "btree" spelling alongside the hash-typed registry proof.

Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
…+drop

The DEFAULT_INDEX_TYPE comment claimed a btree-vs-untyped spelling change
plans create + drop; normalization makes the two spellings content-equal,
so phase-2 content pairing converges them with a rename (phase-1 never
pairs them — the wire hashes differ). Also note the deliberate PSL/TS
options asymmetry in both authoring READMEs: PSL accepts type: without
options: (absent validates as {}), the TS pack-typed arm requires the
options key at compile time, and both lower to the same IR.

Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
Grounded on the slice-2 branch. Notable resolutions: policy introspection
drops the ?? policyname prefix fallback (it conflated managed with
adopt-as-exact; the head-derivation role returns in slice-4 infer), the
"not-equal unreachable" planner assumption ends (exact policies make it
reachable; maps to drop+create per scenario F), and the D9 policy warning
reaches the shared batch through a generic warning sink on
AuthoringEntityContext (the pack lowering cannot reach the build-contract
collector).

Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
…ment union; planning IDs leave names and comments

IndexNode and IndexConstraint express columns/fields xor expression as an
input-type union (no stored discriminant) and IndexNode's remaining
fields become required-but-undefined; construction sites pick the arm
from the data or defer the xor decision to lowerAuthoredIndex's runtime
guard via blindCast. Warning docs and test names describe the exact-name
body warning by behavior, and the index-drift suite names its cases by
what they exercise.

Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
@wmadden-electric
wmadden-electric force-pushed the slice/expression-index-authoring branch from c02144c to 800c336 Compare July 24, 2026 12:41
…documented; boundary pins for spans and the warning threshold

The PslDiagnostic.code field documents why the union has a closed
framework set beside the contributed pattern arm (IDE completion and
self-documentation vs the contribution seam; extensionally equal, never
switched exhaustively), and ContributedPslDiagnosticCode states the
collision convention (domain-scoped prefix segments, never a framework
code name). Two new pins: a model carrying two offending @@index
attributes gets each diagnostic anchored at its own attribute's span,
and exactly five exact-name-body warning hits still emit per-item
warnings (the batch summary starts at six).

Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (2)
packages/3-targets/3-targets/sqlite/test/sqlite-unbound-database.test.ts (1)

225-228: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Assert error fields separately.

Separate code and message expectations so failures identify the mismatched field directly. Based on learnings, tests should prefer separate expect() assertions over toMatchObject() for multiple fields.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/3-targets/3-targets/sqlite/test/sqlite-unbound-database.test.ts`
around lines 225 - 228, Update the assertion around the caught error to verify
its code and message in separate expect() assertions instead of combining them
in toMatchObject(). Preserve the existing expected CONTRACT.ARGUMENT_INVALID
value and SQLite message substring.

Source: Learnings

packages/2-sql/2-authoring/contract-psl/src/interpreter.ts (1)

1003-1017: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use ifDefined for the remaining optional IndexNode fields, consistent with the rest of this function.

where, unique, name, map, type, options are all optional per indexModelSpec yet are assigned directly instead of via ifDefined, unlike uniqueConstraints/primaryKey construction just above in this same function. This leaves literal undefined-valued keys on the pushed IndexNode rather than omitting them, which is inconsistent with this repo's established pattern for conditional spreads and is worth tightening given this PR's explicit goal of preserving stable authored-contract JSON/wire hashes across PSL and TS authoring surfaces.

♻️ Proposed fix
       indexNodes.push(
         // The interpreter's own diagnostics pre-empt the neither/both
         // element cases; the cast defers final enforcement to
         // lowerAuthoredIndex's runtime guard.
         blindCast<IndexNode, 'columns-xor-expression enforced by lowerAuthoredIndex'>({
           ...ifDefined('columns', columnNames),
           ...ifDefined('expression', parsed.expression),
-          where: parsed.where,
-          unique: parsed.unique,
-          name: parsed.name,
-          map: parsed.map,
-          type: parsed.type,
-          options: parsed.options,
+          ...ifDefined('where', parsed.where),
+          ...ifDefined('unique', parsed.unique),
+          ...ifDefined('name', parsed.name),
+          ...ifDefined('map', parsed.map),
+          ...ifDefined('type', parsed.type),
+          ...ifDefined('options', parsed.options),
         }),
       );

Based on learnings, "prefer ifDefined from prisma-next/utils/defined for conditional object spreads instead of inline ternary-based spreads" — the same principle applies to plain conditionally-undefined assignments.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/2-sql/2-authoring/contract-psl/src/interpreter.ts` around lines 1003
- 1017, Update the IndexNode object construction in the parsed-index path to
wrap the optional fields where, unique, name, map, type, and options with
ifDefined, matching the uniqueConstraints and primaryKey construction above.
Preserve their existing values while omitting keys whose values are undefined,
and keep the columns/expression handling unchanged.

Source: Learnings

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/2-sql/2-authoring/contract-ts/src/contract-dsl.ts`:
- Line 1053: Replace the bare `as Record<string, unknown>` cast in the options
construction with the project-approved `castAs` or `blindCast` helper from
`@prisma-next/utils/casts`, preserving the existing conditional spread and
resulting type.

In `@test/integration/test/cli-journeys/expression-index-migration.e2e.test.ts`:
- Around line 89-92: Update the assertion in the expression-index migration
journey test to inspect only stripAnsi(verifyFail.stdout), removing the
concatenated stderr value. Preserve the existing expectation that stdout
contains the dropped index name users_email_eq_adef23ad.

---

Nitpick comments:
In `@packages/2-sql/2-authoring/contract-psl/src/interpreter.ts`:
- Around line 1003-1017: Update the IndexNode object construction in the
parsed-index path to wrap the optional fields where, unique, name, map, type,
and options with ifDefined, matching the uniqueConstraints and primaryKey
construction above. Preserve their existing values while omitting keys whose
values are undefined, and keep the columns/expression handling unchanged.

In `@packages/3-targets/3-targets/sqlite/test/sqlite-unbound-database.test.ts`:
- Around line 225-228: Update the assertion around the caught error to verify
its code and message in separate expect() assertions instead of combining them
in toMatchObject(). Preserve the existing expected CONTRACT.ARGUMENT_INVALID
value and SQLite message substring.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro Plus

Run ID: 76eb036d-d586-43aa-8019-ae67fbfde4d5

📥 Commits

Reviewing files that changed from the base of the PR and between cf0ab1c and 800c336.

⛔ Files ignored due to path filters (2)
  • projects/functional-indexes/plans/rls-exact-names.plan.md is excluded by !projects/**
  • projects/functional-indexes/specs/rls-exact-names.spec.md is excluded by !projects/**
📒 Files selected for processing (44)
  • docs/reference/error-reference.md
  • packages/1-framework/1-core/framework-components/src/control/psl-ast.ts
  • packages/1-framework/1-core/framework-components/src/shared/psl-extension-block.ts
  • packages/1-framework/2-authoring/psl-parser/src/attribute-spec/combinators/diagnostic.ts
  • packages/1-framework/2-authoring/psl-parser/src/attribute-spec/field-attribute.ts
  • packages/1-framework/2-authoring/psl-parser/src/attribute-spec/interpret.ts
  • packages/1-framework/2-authoring/psl-parser/src/attribute-spec/model-attribute.ts
  • packages/1-framework/2-authoring/psl-parser/src/attribute-spec/types.ts
  • packages/2-sql/1-core/contract/src/contract-errors.ts
  • packages/2-sql/1-core/contract/src/exports/index-naming.ts
  • packages/2-sql/1-core/contract/src/foreign-key-materialization.ts
  • packages/2-sql/1-core/contract/src/index-naming.ts
  • packages/2-sql/1-core/contract/test/index-naming.test.ts
  • packages/2-sql/1-core/schema-ir/src/ir/sql-index-ir.ts
  • packages/2-sql/1-core/schema-ir/test/sql-index-ir.test.ts
  • packages/2-sql/2-authoring/contract-psl/README.md
  • packages/2-sql/2-authoring/contract-psl/src/interpreter.ts
  • packages/2-sql/2-authoring/contract-psl/src/sql-attribute-specs.ts
  • packages/2-sql/2-authoring/contract-psl/test/interpreter.diagnostics.test.ts
  • packages/2-sql/2-authoring/contract-psl/test/interpreter.index-naming.test.ts
  • packages/2-sql/2-authoring/contract-psl/test/ts-psl-parity.test.ts
  • packages/2-sql/2-authoring/contract-ts/README.md
  • packages/2-sql/2-authoring/contract-ts/src/build-contract.ts
  • packages/2-sql/2-authoring/contract-ts/src/contract-definition.ts
  • packages/2-sql/2-authoring/contract-ts/src/contract-dsl.ts
  • packages/2-sql/2-authoring/contract-ts/src/contract-lowering.ts
  • packages/2-sql/2-authoring/contract-ts/test/contract-builder.index-naming.test.ts
  • packages/3-targets/3-targets/postgres/src/core/psl-infer/infer-psl-contract.ts
  • packages/3-targets/3-targets/postgres/test/migrations/index-rename-planner.test.ts
  • packages/3-targets/3-targets/sqlite/src/core/errors.ts
  • packages/3-targets/3-targets/sqlite/src/core/sqlite-unbound-database.ts
  • packages/3-targets/3-targets/sqlite/test/sqlite-unbound-database.test.ts
  • packages/3-targets/6-adapters/postgres/src/core/control-adapter.ts
  • skills/extension-author/prisma-next-extension-upgrade/upgrades/0.16-to-0.17/instructions.md
  • skills/prisma-next-contract/SKILL.md
  • skills/upgrade/prisma-next-upgrade/upgrades/0.16-to-0.17/instructions.md
  • test/integration/test/cli-journeys/expression-index-migration.e2e.test.ts
  • test/integration/test/family.schema-verify.index-drift.integration.test.ts
  • test/integration/test/fixtures/cli/cli-e2e-test-app/fixtures/cli-journeys/contract-expression-authored-editedbody.prisma
  • test/integration/test/fixtures/cli/cli-e2e-test-app/fixtures/cli-journeys/contract-expression-authored-renamed.prisma
  • test/integration/test/fixtures/cli/cli-e2e-test-app/fixtures/cli-journeys/contract-expression-authored.prisma
  • test/integration/test/fixtures/cli/cli-e2e-test-app/fixtures/cli-journeys/contract-expression-authored.ts
  • test/integration/test/fixtures/cli/cli-e2e-test-app/fixtures/cli-journeys/contract-expression-indexes.ts
  • test/integration/test/utils/journey-test-helpers.ts
💤 Files with no reviewable changes (1)
  • test/integration/test/fixtures/cli/cli-e2e-test-app/fixtures/cli-journeys/contract-expression-indexes.ts

Comment thread packages/2-sql/2-authoring/contract-ts/src/contract-dsl.ts Outdated
Comment thread test/integration/test/cli-journeys/expression-index-migration.e2e.test.ts Outdated
…he drift assertion reads stderr

The index builder's implementation signature erases the pack-typed
options object to unknown, so narrowing it back to a record is a genuine
type-system bypass — it now goes through blindCast with the reason
stated instead of a bare as. The expression-index journey's
verify-failure assertion reads stderr alone: the drift listing is a
diagnostic (ui.log), and the terminal-ui contract reserves stdout for
data output — the previous stderr+stdout concatenation only passed
because the test harness duplicates stderr into both captures.

Signed-off-by: willbot <w.a.madden+machine@gmail.com>
Signed-off-by: Will Madden <madden@prisma.io>
@wmadden-electric
wmadden-electric added this pull request to the merge queue Jul 27, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Jul 27, 2026
@wmadden
wmadden added this pull request to the merge queue Jul 27, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Jul 27, 2026
@wmadden-electric
wmadden-electric added this pull request to the merge queue Jul 27, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Jul 27, 2026
@wmadden-electric
wmadden-electric added this pull request to the merge queue Jul 27, 2026
Merged via the queue into main with commit 7a39ef4 Jul 27, 2026
31 of 32 checks passed
@wmadden-electric
wmadden-electric deleted the slice/expression-index-authoring branch July 27, 2026 11:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants