Expression, partial, and unique indexes are authorable in PSL and TypeScript#1048
Conversation
|
Warning Review limit reached
Next review available in: 14 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the 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 configurationConfiguration used: Path: .coderabbit.yml Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (2)
📝 WalkthroughWalkthroughExpression 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. ChangesIndex authoring and lowering
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
Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
@prisma-next/extension-author-tools
@prisma-next/mongo-runtime
@prisma-next/family-mongo
@prisma-next/sql-runtime
@prisma-next/family-sql
@prisma-next/extension-arktype-json
@prisma-next/middleware-cache
@prisma-next/mongo
@prisma-next/extension-paradedb
@prisma-next/extension-pgvector
@prisma-next/extension-postgis
@prisma-next/postgres
@prisma-next/sql-orm-client
@prisma-next/sqlite
@prisma-next/extension-supabase
@prisma-next/target-mongo
@prisma-next/adapter-mongo
@prisma-next/driver-mongo
@prisma-next/contract
@prisma-next/utils
@prisma-next/config
@prisma-next/errors
@prisma-next/framework-components
@prisma-next/operations
@prisma-next/ts-render
@prisma-next/contract-authoring
@prisma-next/ids
@prisma-next/psl-parser
@prisma-next/psl-printer
@prisma-next/cli
@prisma-next/cli-telemetry
@prisma-next/config-loader
@prisma-next/emitter
@prisma-next/language-server
@prisma-next/migration-tools
prisma-next
@prisma-next/vite-plugin-contract-emit
@prisma-next/mongo-codec
@prisma-next/mongo-contract
@prisma-next/mongo-value
@prisma-next/mongo-contract-psl
@prisma-next/mongo-contract-ts
@prisma-next/mongo-emitter
@prisma-next/mongo-schema-ir
@prisma-next/mongo-query-ast
@prisma-next/mongo-orm
@prisma-next/mongo-query-builder
@prisma-next/mongo-lowering
@prisma-next/mongo-wire
@prisma-next/sql-contract
@prisma-next/sql-errors
@prisma-next/sql-operations
@prisma-next/sql-schema-ir
@prisma-next/sql-contract-psl
@prisma-next/sql-contract-ts
@prisma-next/sql-contract-emitter
@prisma-next/sql-lane-query-builder
@prisma-next/sql-relational-core
@prisma-next/sql-builder
@prisma-next/target-postgres
@prisma-next/target-sqlite
@prisma-next/adapter-postgres
@prisma-next/adapter-sqlite
@prisma-next/driver-postgres
@prisma-next/driver-sqlite
commit: |
size-limit report 📦
|
6765474 to
41418eb
Compare
f5c303a to
982f728
Compare
|
|
||
| export interface PslDiagnostic { | ||
| readonly code: PslDiagnosticCode; | ||
| readonly code: PslDiagnosticCode | ContributedPslDiagnosticCode; |
There was a problem hiding this comment.
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- |
982f728 to
c02144c
Compare
…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>
c02144c to
800c336
Compare
…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>
There was a problem hiding this comment.
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 winAssert error fields separately.
Separate
codeandmessageexpectations so failures identify the mismatched field directly. Based on learnings, tests should prefer separateexpect()assertions overtoMatchObject()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 winUse
ifDefinedfor the remaining optionalIndexNodefields, consistent with the rest of this function.
where,unique,name,map,type,optionsare all optional perindexModelSpecyet are assigned directly instead of viaifDefined, unlikeuniqueConstraints/primaryKeyconstruction just above in this same function. This leaves literalundefined-valued keys on the pushedIndexNoderather 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
ifDefinedfromprisma-next/utils/definedfor 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
⛔ Files ignored due to path filters (2)
projects/functional-indexes/plans/rls-exact-names.plan.mdis excluded by!projects/**projects/functional-indexes/specs/rls-exact-names.spec.mdis excluded by!projects/**
📒 Files selected for processing (44)
docs/reference/error-reference.mdpackages/1-framework/1-core/framework-components/src/control/psl-ast.tspackages/1-framework/1-core/framework-components/src/shared/psl-extension-block.tspackages/1-framework/2-authoring/psl-parser/src/attribute-spec/combinators/diagnostic.tspackages/1-framework/2-authoring/psl-parser/src/attribute-spec/field-attribute.tspackages/1-framework/2-authoring/psl-parser/src/attribute-spec/interpret.tspackages/1-framework/2-authoring/psl-parser/src/attribute-spec/model-attribute.tspackages/1-framework/2-authoring/psl-parser/src/attribute-spec/types.tspackages/2-sql/1-core/contract/src/contract-errors.tspackages/2-sql/1-core/contract/src/exports/index-naming.tspackages/2-sql/1-core/contract/src/foreign-key-materialization.tspackages/2-sql/1-core/contract/src/index-naming.tspackages/2-sql/1-core/contract/test/index-naming.test.tspackages/2-sql/1-core/schema-ir/src/ir/sql-index-ir.tspackages/2-sql/1-core/schema-ir/test/sql-index-ir.test.tspackages/2-sql/2-authoring/contract-psl/README.mdpackages/2-sql/2-authoring/contract-psl/src/interpreter.tspackages/2-sql/2-authoring/contract-psl/src/sql-attribute-specs.tspackages/2-sql/2-authoring/contract-psl/test/interpreter.diagnostics.test.tspackages/2-sql/2-authoring/contract-psl/test/interpreter.index-naming.test.tspackages/2-sql/2-authoring/contract-psl/test/ts-psl-parity.test.tspackages/2-sql/2-authoring/contract-ts/README.mdpackages/2-sql/2-authoring/contract-ts/src/build-contract.tspackages/2-sql/2-authoring/contract-ts/src/contract-definition.tspackages/2-sql/2-authoring/contract-ts/src/contract-dsl.tspackages/2-sql/2-authoring/contract-ts/src/contract-lowering.tspackages/2-sql/2-authoring/contract-ts/test/contract-builder.index-naming.test.tspackages/3-targets/3-targets/postgres/src/core/psl-infer/infer-psl-contract.tspackages/3-targets/3-targets/postgres/test/migrations/index-rename-planner.test.tspackages/3-targets/3-targets/sqlite/src/core/errors.tspackages/3-targets/3-targets/sqlite/src/core/sqlite-unbound-database.tspackages/3-targets/3-targets/sqlite/test/sqlite-unbound-database.test.tspackages/3-targets/6-adapters/postgres/src/core/control-adapter.tsskills/extension-author/prisma-next-extension-upgrade/upgrades/0.16-to-0.17/instructions.mdskills/prisma-next-contract/SKILL.mdskills/upgrade/prisma-next-upgrade/upgrades/0.16-to-0.17/instructions.mdtest/integration/test/cli-journeys/expression-index-migration.e2e.test.tstest/integration/test/family.schema-verify.index-drift.integration.test.tstest/integration/test/fixtures/cli/cli-e2e-test-app/fixtures/cli-journeys/contract-expression-authored-editedbody.prismatest/integration/test/fixtures/cli/cli-e2e-test-app/fixtures/cli-journeys/contract-expression-authored-renamed.prismatest/integration/test/fixtures/cli/cli-e2e-test-app/fixtures/cli-journeys/contract-expression-authored.prismatest/integration/test/fixtures/cli/cli-e2e-test-app/fixtures/cli-journeys/contract-expression-authored.tstest/integration/test/fixtures/cli/cli-e2e-test-app/fixtures/cli-journeys/contract-expression-indexes.tstest/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
…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>
At a glance
The index the Cipherstash team asked for is now a line of PSL:
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 oneALTER 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.mdon the base branch; substrate landed in slice 1, #1047). Both authoring surfaces gain the full D3 parameter matrix —fieldsxorexpression,where,unique,namexormap,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@@indexattribute; 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:PslDiagnostic.codeadmits package-contributedPSL_*codes (a pattern type), and the three index codes live incontract-psl. Refines also gain access to the attribute node — the minimal enabler for attribute-anchored spans (previously refines could only anchor at the model).map:combined with a SQL body — reliable only for infer-captured text) emits through the existingprocess.emitWarningchannel 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 manymap:bodies.SQLite rejects
expression/whereat its namespace construction (Postgres-only enablement, per the project non-goal).@@unique/constraints.uniqueare byte-frozen — the constraint surface stays out of the expression business.Also fixed: authored
type: "btree"no longer drifts foreverWriting the e2e with the DoD's literal spelling exposed a slice-1 gap: introspection normalized
btree → undefinedbut contract derivation didn't, so a btree-typed index verified as perpetual drift (and theisEqualTodoc comment falsely claimed construction-side normalization). The normalization now lives in theSqlIndexIRconstructor — 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)
map:— D9 warning asserted; the false-drift consequence pinned as a named documented-degradation test (authored text vs Postgres reprint).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:throwsdelta 0 ·lint:framework-vocabularycount unchanged ·lint:skills·check:upgrade-coverage --mode pr·fixtures:checkclean with zero fixture movement across the whole slice ·test:packages·test:integration247/247 ·test:e2e20/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 onproject/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
git commit -s) per the DCO.🤖 Generated with Claude Code
Summary by CodeRabbit
where,unique, accesstype/options, andnamevsmap) across PSL and TypeScript.map:) cases where SQL bodies may drift.where/unique, plus safer SQLite rejection for unsupported index features.btree) index type at construction and improved introspection to reflectbtree.