Skip to content

feat(sql): add index-type registry primitive#430

Open
SevInf wants to merge 29 commits intomainfrom
psl-index-plus
Open

feat(sql): add index-type registry primitive#430
SevInf wants to merge 29 commits intomainfrom
psl-index-plus

Conversation

@SevInf
Copy link
Copy Markdown
Contributor

@SevInf SevInf commented May 6, 2026

closes TML-2390

Intent

Make @@index(type:, options:) a real first-class authoring surface across PSL and TS, replacing the inert using/config placeholders that round-tripped silently. Authors can now register their own index types via an extension pack — the registered shape narrows the TS DSL at the call site, validates the contract IR at runtime, drives CREATE INDEX … USING <method> WITH (…) DDL, and survives a round-trip through Postgres introspection. ParadeDB's bespoke bm25Index() helper is replaced with a registry entry; the helper and its column-builder namespace (bm25.text, .numeric, …) are deleted.

Change map

The story

The contract IR already had inert using/config fields. The shape of the registry came first: rename to type/options (matching Prisma's @@index(type:)), put the registered shape in one place, and make every consumer derive from that one place.

The registry is per-contract, not module-global. A pack stores a single registration value (the output of defineIndexTypes()) on its metadata; contract assembly creates a fresh registry and registers each pack's entries into it. Two contracts in the same workspace with different pack lists see different valid type sets. The first attempt at this used a module-level Map; the maintainer flagged that as wrong (WHY IS REGISTRY A GLOBAL?) and the design moved to the instance-based shape it has now.

One field, both halves. The factory builder produces a value with two surfaces: a runtime entries array and a TypeScript-only IndexTypes phantom map. The pack stores that value verbatim in its indexTypes field; the builder type extends a read-only IndexTypeRegistration<TMap> interface so the pack can't be misused as a mutable registry from outside. Both halves stay in lockstep automatically — adding .add('hnsw', …) to the builder updates runtime registration and call-site narrowing in one step.

Type-level threading lands at the call site, not at defineContract. The registered shape flows through three legs: (1) indexTypes registration on the pack, (2) MergeAllPackIndexTypes over family + target + extension packs in ComposedAuthoringHelpers, (3) a sixth IndexTypes generic on ContractModelBuilder that threads through SqlContext<Fields, IndexTypes> to a discriminated-union IndexInput<Name, IndexTypes>. The result: inside defineContract({...}, ({ model }) => …), constraints.index(cols.body, { type: 'made-up', options: {} }) is a TS error on that line — not buried fifty levels deep in a defineContract type. The bare model() import (no packs available) defaults IndexTypes to Record<never, never>, so only the no-options default-index form typechecks; there is no WildcardIndexTypes escape hatch.

Migration diff falls out of identity. Schema-verify already grouped indexes by columns; once type and options are part of identity, a contract gin against a schema btree simply doesn't match — verifyIndexes emits index_mismatch for the new contract index and (in strict mode) extra_index for the stale schema index. The Postgres planner then looks up the contract index by columns from ctx.toContract and emits a fully-typed CreateIndexCall (DROP + CREATE). No ALTER INDEX path; Postgres has no clean ALTER for index method or WITH-key changes anyway.

Introspection had to come along. Without it, every plan against a live DB would force DROP+CREATE on any contract index whose type is set. The Postgres adapter now joins pg_am, reads pg_class.reloptions, and the family verifier's indexExtrasMatch does string-coerced option comparison so contract fillfactor: 70 matches an introspected fillfactor: '70' (Postgres returns reloption values as raw text regardless of the underlying scalar). btree introspects to undefined — it's the Postgres default — so a default-method contract index matches a default-method live index without ceremony.

ParadeDB shrinks dramatically. The old bm25Index() helper plus the entire bm25.{text, numeric, json, expression, …} namespace and the Bm25*FieldConfig / TokenizerId types are deleted (~470 lines). What's left is one defineIndexTypes().add('bm25', { options: type({ '+': 'reject', key_field: 'string' }) }) declaration plus a plain const paradedbPackMeta that stores it under indexTypes:. Per-field tokenizer / column configuration is deferred to a future expression-index surface.

Behavior changes & evidence

Authoring: constraints.index(cols.body, { type: 'bm25', options: { key_field: 'id' } }) now typechecks against the merged pack registry; type: 'made-up', missing required options keys, and unknown options keys are TS errors at the offending line. PSL's @@index([body], type: "bm25", options: { key_field: "id" }) lowers to the same IR shape. Contract validation rejects unregistered types and bad options at runtime via arktype (strict-key rejection is registrant-opt-in via '+': 'reject'). Postgres DDL emission produces CREATE INDEX … USING <method> WITH (…) on both new tables and existing tables (typed indexes added later). Schema verification treats type/options as part of identity. Postgres introspection populates the new fields; the verifier's loose option comparison handles the contract/reloptions typing gap.

Evidence:

Compatibility / migration / risk

  • No backward-compat shim for using/config. The fields were inert (no PSL or TS surface populated them), and the only in-repo writer was bm25Index() which is removed in the same PR. Out-of-repo consumers using the old field names will need to rename in lockstep.
  • No ALTER INDEX for type/options changes. Any change to columns, type, or options is DROP INDEX + CREATE INDEX. This is a property of how Postgres handles index method and WITH-key changes, not a regression introduced here. Lock and rebuild cost on large tables is non-trivial.
  • PSL boolean/number leaves are rejected in V1. options: { fastupdate: false } doesn't parse; only options: { key: "value" } (string leaves) works from PSL. TS authoring is unrestricted. The PSL grammar lift ships with the built-in-entries follow-up that actually needs non-string options.
  • Future SQL adapters would need their own rendering path. The IR vocabulary is dialect-neutral, but the only renderer in this repo is Postgres-shaped (USING/WITH).
  • constraints.index(...) is array-only. The single-field shortcut (constraints.index(cols.x, { type, options })) is removed; callers always pass the field list as an array (constraints.index([cols.x], { type, options })). Collapsing to one signature was what let TypeScript pin per-property errors at the offending key (e.g. an unknown options key, a wrong type literal, missing required option fields) instead of reporting a generic "no overload matches this call" at the call expression. In-repo callers were migrated in lockstep.

Follow-ups / open questions

  • Built-in registry entries for btree, hash, gin, gist, brin, spgist (tracked separately).
  • PSL boolean/number leaves in options — needed once built-in entries land (fastupdate: false, fillfactor: 70).
  • Per-column index options (e.g. gist's per-column operator classes) — out of scope; v1 carries options as a single object on the index, not per-column.
  • ParadeDB tokenizer / column configuration — deferred to expression-index support.
  • pdb.* query-builder operator surface (@@@, etc.) for ParadeDB BM25 search — separate workstream.

Non-goals / intentionally out of scope

  • Built-in entries seeded by the framework (extension-pack-provided is the v1 path).
  • Capability gating per index type (capabilities describe runtime environment; the registry is the design-time vocabulary).
  • ALTER INDEX rendering paths.
  • Rendering paths for any future SQL adapter beyond Postgres reading type/options.

Summary by CodeRabbit

  • New Features

    • Index-type registry with validated per-type options and runtime validation for contracts.
    • Indexes can declare dialect-neutral type and options; PSL and authoring DSL accept and validate them.
    • ParadeDB BM25 index type published with option validation; emitted DDL can include USING <type> WITH (...).
    • Postgres introspection and migrations now round-trip index type/options.
  • Refactor

    • Index builder API now accepts columns as an array: constraints.index([ ... ]).
  • Documentation

    • ParadeDB README updated to show the new index-type usage.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 6, 2026

Warning

Rate limit exceeded

@wmadden has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 19 minutes and 23 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: CHILL

Plan: Pro

Run ID: 8baeca8e-12f0-4750-8d3d-713c1711ca7c

📥 Commits

Reviewing files that changed from the base of the PR and between a0acadc and 8c6195f.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (70)
  • docs/architecture docs/adrs/ADR 210 - Index-type registry.md
  • packages/1-framework/2-authoring/contract/src/descriptors.ts
  • packages/1-framework/2-authoring/contract/test/descriptors.test.ts
  • packages/2-sql/1-core/contract/package.json
  • packages/2-sql/1-core/contract/src/exports/index-type-validation.ts
  • packages/2-sql/1-core/contract/src/exports/index-types.ts
  • packages/2-sql/1-core/contract/src/index-type-validation.ts
  • packages/2-sql/1-core/contract/src/index-types.ts
  • packages/2-sql/1-core/contract/src/index.ts
  • packages/2-sql/1-core/contract/src/types.ts
  • packages/2-sql/1-core/contract/src/validators.ts
  • packages/2-sql/1-core/contract/test/index-types.test.ts
  • packages/2-sql/1-core/contract/test/validate.test.ts
  • packages/2-sql/1-core/contract/test/validators.test.ts
  • packages/2-sql/1-core/contract/tsdown.config.ts
  • packages/2-sql/1-core/schema-ir/src/types.ts
  • packages/2-sql/2-authoring/contract-psl/package.json
  • packages/2-sql/2-authoring/contract-psl/src/interpreter.ts
  • packages/2-sql/2-authoring/contract-psl/src/psl-attribute-parsing.ts
  • packages/2-sql/2-authoring/contract-psl/test/interpreter.test.ts
  • packages/2-sql/2-authoring/contract-psl/test/ts-psl-parity.test.ts
  • packages/2-sql/2-authoring/contract-ts/src/build-contract.ts
  • packages/2-sql/2-authoring/contract-ts/src/composed-authoring-helpers.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/src/contract-types.ts
  • packages/2-sql/2-authoring/contract-ts/test/contract-builder.constraints.test.ts
  • packages/2-sql/2-authoring/contract-ts/test/contract-builder.dsl.test.ts
  • packages/2-sql/2-authoring/contract-ts/test/contract-builder.index-types.test.ts
  • packages/2-sql/2-authoring/contract-ts/test/contract-builder.normalization.test.ts
  • packages/2-sql/2-authoring/contract-ts/test/contract-dsl.runtime.test.ts
  • packages/2-sql/2-authoring/contract-ts/test/contract-lowering.runtime.test.ts
  • packages/2-sql/2-authoring/contract-ts/test/helpers/test-index-pack.ts
  • packages/2-sql/3-tooling/emitter/src/index.ts
  • packages/2-sql/3-tooling/emitter/test/emitter-hook.generation.basic.test.ts
  • packages/2-sql/3-tooling/emitter/test/emitter-hook.structure.test.ts
  • packages/2-sql/9-family/src/core/schema-verify/verify-helpers.ts
  • packages/2-sql/9-family/test/schema-verify.helpers.ts
  • packages/2-sql/9-family/test/schema-verify.semantic-satisfaction.test.ts
  • packages/3-extensions/paradedb/README.md
  • packages/3-extensions/paradedb/package.json
  • packages/3-extensions/paradedb/src/core/constants.ts
  • packages/3-extensions/paradedb/src/core/descriptor-meta.ts
  • packages/3-extensions/paradedb/src/exports/index-types.ts
  • packages/3-extensions/paradedb/src/types/index-types.ts
  • packages/3-extensions/paradedb/test/index-types.test.ts
  • packages/3-targets/3-targets/postgres/src/core/migrations/issue-planner.ts
  • packages/3-targets/3-targets/postgres/src/core/migrations/op-factory-call.ts
  • packages/3-targets/3-targets/postgres/src/core/migrations/operations/indexes.ts
  • packages/3-targets/3-targets/postgres/test/migrations/index-ddl.test.ts
  • packages/3-targets/3-targets/postgres/test/migrations/issue-planner.test.ts
  • packages/3-targets/6-adapters/postgres/src/core/control-adapter.ts
  • packages/3-targets/6-adapters/postgres/test/control-adapter.test.ts
  • packages/3-targets/6-adapters/postgres/test/migrations/index-introspection.integration.test.ts
  • packages/3-targets/6-adapters/postgres/test/migrations/op-factory-call.rendering.test.ts
  • test/e2e/framework/test/sqlite/migrations/additive.test.ts
  • test/e2e/framework/test/sqlite/migrations/destructive.test.ts
  • test/e2e/framework/test/sqlite/migrations/fk-preservation.test.ts
  • test/integration/package.json
  • test/integration/test/authoring/paradedb-bm25-narrowing.test.ts
  • test/integration/test/authoring/parity/core-surface/contract.ts
  • test/integration/test/authoring/parity/map-attributes/contract.ts
  • test/integration/test/authoring/psl-index-type-options.integration.test.ts
  • test/integration/test/family.schema-verify.basic.integration.test.ts
  • test/integration/test/family.schema-verify.basic.test.ts
  • test/integration/test/fixtures/cli/cli-e2e-test-app/fixtures/db-update-scenarios/contract-add-project-slug.ts
  • test/integration/test/fixtures/cli/cli-e2e-test-app/fixtures/db-update-scenarios/contract.ts
  • test/integration/test/fixtures/cli/cli-integration-test-app/fixtures/emit-command/contract.parity.ts
  • test/integration/test/referential-actions.integration.test.ts
📝 Walkthrough

Walkthrough

This PR implements an index-type registry, renames index metadata from using/config to type/options, threads pack-provided index-types through authoring and PSL, validates types/options at contract build, and updates Postgres DDL/introspection, emitter, ParadeDB extension, and tests.

Changes

Index-Type Registry Foundation

Layer / File(s) Summary
Core Registry Types
packages/2-sql/1-core/contract/src/index-types.ts
Introduces IndexTypeEntry, IndexTypeBuilder, defineIndexTypes(), and IndexTypeRegistry with runtime Map-backed implementation.
Registry Exports
packages/2-sql/1-core/contract/src/exports/index-types.ts, packages/2-sql/1-core/contract/src/index.ts, packages/2-sql/1-core/contract/package.json
Expose registry APIs and add package exports for index-type entry points.
Architecture Documentation
docs/architecture docs/adrs/ADR 210 - Index-type registry.md
New ADR documenting the registry model, validation boundary, and Postgres rendering approach.

Index IR Shape Transformation

Layer / File(s) Summary
Contract Types
packages/2-sql/1-core/contract/src/types.ts, packages/2-sql/1-core/schema-ir/src/types.ts
Add optional type?: string and options?: Record<string, unknown> to Index and SqlIndexIR.
Descriptor Shapes
packages/1-framework/2-authoring/contract/src/descriptors.ts, packages/2-sql/2-authoring/contract-ts/src/contract-definition.ts
Replace using/config with type/options in descriptor interfaces.
Descriptor Tests
packages/1-framework/2-authoring/contract/test/descriptors.test.ts
Update test fixtures and assertions to expect type/options fields.

Validation and Semantics

Layer / File(s) Summary
Index Validation Schema
packages/2-sql/1-core/contract/src/validators.ts
Update IndexSchema and duplicate detection to include type and a deterministic sorted options signature.
Index-Type Validation
packages/2-sql/1-core/contract/src/index-type-validation.ts
New validateIndexTypes(contract, registry) enforcing registered types and running per-type arktype validators; throws contextual ContractValidationError on failures.
Build Integration
packages/2-sql/2-authoring/contract-ts/src/build-contract.ts
assertStorageSemantics now assembles an IndexTypeRegistry from target and extension packs and calls validateIndexTypes; signature updated to accept ContractDefinition plus Contract.
Validation Tests
packages/2-sql/1-core/contract/test/index-types.test.ts, packages/2-sql/1-core/contract/test/validate.test.ts, packages/2-sql/1-core/contract/test/validators.test.ts
Add tests for builder/registry behavior, validateIndexTypes scenarios, and duplicate-index detection tolerant to option key ordering.

TypeScript Contract Authoring

Layer / File(s) Summary
Index-Type Type Utilities
packages/2-sql/2-authoring/contract-ts/src/contract-types.ts
Add ExtractIndexTypesFromPack, MergeExtensionIndexTypes, and IndexTypesFromDefinition to derive merged per-definition index types.
DSL Generic Threading
packages/2-sql/2-authoring/contract-ts/src/contract-dsl.ts
Make createConstraintsDsl generic over IndexTypes, add IndexTypeMap and IndexInput, and propagate IndexTypes through SqlContext and ContractModelBuilder.
Composed Helpers
packages/2-sql/2-authoring/contract-ts/src/composed-authoring-helpers.ts
Create PackAwareModel that merges pack index types for bound helpers.
Contract Lowering
packages/2-sql/2-authoring/contract-ts/src/contract-lowering.ts
Emit type and options into lowered IndexNode via ifDefined.
Test Helpers & Constraints Tests
packages/2-sql/2-authoring/contract-ts/test/helpers/test-index-pack.ts, packages/2-sql/2-authoring/contract-ts/test/contract-builder.constraints.test.ts
Add testIndexPack helper and tests for array-based index API and authoring-time type validation.
DSL & Normalization Tests
packages/2-sql/2-authoring/contract-ts/test/contract-builder.dsl.test.ts, packages/2-sql/2-authoring/contract-ts/test/contract-builder.normalization.test.ts
Update tests to new IndexInput shapes and verify propagation of type/options.
Type-Level & Lowering Tests
packages/2-sql/2-authoring/contract-ts/test/contract-builder.index-types.test.ts, packages/2-sql/2-authoring/contract-ts/test/contract-dsl.runtime.test.ts, packages/2-sql/2-authoring/contract-ts/test/contract-lowering.runtime.test.ts
Add type-level threading tests and update runtime expectations for lowered index metadata.

PSL Contract Authoring

Layer / File(s) Summary
Object-Literal Parsing
packages/2-sql/2-authoring/contract-psl/src/psl-attribute-parsing.ts
Add parseObjectLiteralStringMap with top-level splitting, quoted-string leaf enforcement, duplicate-key detection, and diagnostics.
PSL Interpreter
packages/2-sql/2-authoring/contract-psl/src/interpreter.ts, packages/2-sql/2-authoring/contract-psl/package.json
Update @@index parsing to accept named type and options, emit diagnostics for malformed args, and add arktype runtime dependency.
PSL Tests
packages/2-sql/2-authoring/contract-psl/test/interpreter.test.ts, packages/2-sql/2-authoring/contract-psl/test/ts-psl-parity.test.ts, test/integration/test/authoring/psl-index-type-options.integration.test.ts
Add positive and negative PSL lowering tests for type/options and parity updates for TS authoring output.

ParadeDB Extension

Layer / File(s) Summary
Index Types
packages/3-extensions/paradedb/src/types/index-types.ts
Refactor to paradedbIndexTypes using defineIndexTypes().add('bm25', ...) with arktype-validated key_field.
Package Metadata & Exports
packages/3-extensions/paradedb/src/core/descriptor-meta.ts, packages/3-extensions/paradedb/src/exports/index-types.ts, packages/3-extensions/paradedb/package.json, packages/3-extensions/paradedb/src/core/constants.ts
Attach indexTypes to pack meta, simplify exports to paradedbIndexTypes and option aliases, remove TokenizerId type, and add dependencies.
Documentation
packages/3-extensions/paradedb/README.md
Update README to document index-type registration and usage via constraints.index(...).
Tests
packages/3-extensions/paradedb/test/index-types.test.ts, test/integration/test/authoring/paradedb-bm25-narrowing.test.ts
Refocused tests to validate paradedbIndexTypes entries and TypeScript narrowing/authoring behavior.

Schema Verification and Migrations

Layer / File(s) Summary
Schema Verification
packages/2-sql/9-family/src/core/schema-verify/verify-helpers.ts, packages/2-sql/9-family/test/schema-verify.helpers.ts, packages/2-sql/9-family/test/schema-verify.semantic-satisfaction.test.ts
Add indexOptionsLooselyEqual and indexExtrasMatch; update verifyIndexes to require type/options alignment for matches and add strict-mode tests.
Postgres DDL
packages/3-targets/3-targets/postgres/src/core/migrations/operations/indexes.ts
Extend createIndex to accept extras {type, options} and render USING and WITH (...) with literal rendering and quoting.
Migration Planning
packages/3-targets/3-targets/postgres/src/core/migrations/issue-planner.ts, packages/3-targets/3-targets/postgres/src/core/migrations/op-factory-call.ts
Planner threads contract index type/options into CreateIndexCall; CreateIndexCall extended to carry and render extras.
Introspection
packages/3-targets/6-adapters/postgres/src/core/control-adapter.ts
Extend introspection to return amname and reloptions, derive SqlIndexIR.type (suppress btree) and options via parsePgReloptions; add parsePgReloptions helper and tests.
Migration Tests
packages/3-targets/3-targets/postgres/test/migrations/index-ddl.test.ts, packages/3-targets/3-targets/postgres/test/migrations/issue-planner.test.ts, packages/3-targets/6-adapters/postgres/test/migrations/index-introspection.integration.test.ts, packages/3-targets/6-adapters/postgres/test/migrations/op-factory-call.rendering.test.ts
Add DDL emission, planner, introspection, and rendering tests covering type/options behavior and validation.

Code Emission

Layer / File(s) Summary
SQL Type Emitter
packages/2-sql/3-tooling/emitter/src/index.ts
generateStorageType now emits type and options for indexes when present.
Emitter Tests
packages/2-sql/3-tooling/emitter/test/emitter-hook.generation.basic.test.ts, packages/2-sql/3-tooling/emitter/test/emitter-hook.structure.test.ts
Update expected emitted strings to reflect the new index metadata shape.

E2E and Integration Tests

Layer / File(s) Summary
SQLite E2E Migrations
test/e2e/framework/test/sqlite/migrations/*
Update tests to use array-based constraints.index([cols...]) calling convention.
Integration Tests & Fixtures
various files under test/integration and fixtures
Update fixtures and tests to the array-based index API, add paradedb workspace dependency, and add integration tests for PSL and authoring narrowing.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

A registry emerges where types can dwell,
Index options now validate so well,
From contract to schema, migrations take flight,
ParadeDB awakens with arktype's light,
Through authoring and introspection complete,
The index-type system makes contracts neat! 🐰✨

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch psl-index-plus

@SevInf SevInf force-pushed the psl-index-plus branch from 2ab731b to 9528df7 Compare May 6, 2026 17:08
@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented May 6, 2026

Open in StackBlitz

@prisma-next/mongo-runtime

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

@prisma-next/family-mongo

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

@prisma-next/sql-runtime

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

@prisma-next/family-sql

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

@prisma-next/extension-arktype-json

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

@prisma-next/middleware-telemetry

npm i https://pkg.pr.new/prisma/prisma-next/@prisma-next/middleware-telemetry@430

@prisma-next/mongo

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

@prisma-next/extension-paradedb

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

@prisma-next/extension-pgvector

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

@prisma-next/postgres

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

@prisma-next/sql-orm-client

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

@prisma-next/sqlite

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

@prisma-next/target-mongo

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

@prisma-next/adapter-mongo

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

@prisma-next/driver-mongo

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

@prisma-next/contract

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

@prisma-next/utils

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

@prisma-next/config

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

@prisma-next/errors

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

@prisma-next/framework-components

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

@prisma-next/operations

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

@prisma-next/ts-render

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

@prisma-next/contract-authoring

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

@prisma-next/ids

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

@prisma-next/psl-parser

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

@prisma-next/psl-printer

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

@prisma-next/cli

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

@prisma-next/emitter

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

@prisma-next/migration-tools

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

prisma-next

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

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

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

@prisma-next/mongo-codec

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

@prisma-next/mongo-contract

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

@prisma-next/mongo-value

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

@prisma-next/mongo-contract-psl

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

@prisma-next/mongo-contract-ts

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

@prisma-next/mongo-emitter

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

@prisma-next/mongo-schema-ir

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

@prisma-next/mongo-query-ast

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

@prisma-next/mongo-orm

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

@prisma-next/mongo-query-builder

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

@prisma-next/mongo-lowering

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

@prisma-next/mongo-wire

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

@prisma-next/sql-contract

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

@prisma-next/sql-errors

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

@prisma-next/sql-operations

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

@prisma-next/sql-schema-ir

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

@prisma-next/sql-contract-psl

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

@prisma-next/sql-contract-ts

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

@prisma-next/sql-contract-emitter

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

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

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

@prisma-next/sql-relational-core

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

@prisma-next/sql-builder

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

@prisma-next/target-postgres

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

@prisma-next/target-sqlite

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

@prisma-next/adapter-postgres

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

@prisma-next/adapter-sqlite

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

@prisma-next/driver-postgres

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

@prisma-next/driver-sqlite

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

commit: 8c6195f

@SevInf SevInf marked this pull request as ready for review May 6, 2026 17:47
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/2-sql/2-authoring/contract-ts/test/contract-builder.normalization.test.ts (1)

220-229: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Also assert plain indexes omit type/options.

After moving to type/options, this test still only checks legacy using/config. At Line [220], add assertions that plain indexes do not carry type or options, so default metadata leaks are caught.

Suggested patch
     const idx = contract.storage.tables.user.indexes[0]!;
     expect(idx.columns).toEqual(['email']);
     expect(idx).not.toHaveProperty('using');
     expect(idx).not.toHaveProperty('config');
+    expect(idx).not.toHaveProperty('type');
+    expect(idx).not.toHaveProperty('options');
🤖 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-ts/test/contract-builder.normalization.test.ts`
around lines 220 - 229, The test should also assert that plain indexes do not
include the new metadata keys; after retrieving idx from
contract.storage.tables.user.indexes[0] (the existing idx variable), add two
assertions that idx does not have properties 'type' and 'options' (e.g.,
expect(idx).not.toHaveProperty('type') and
expect(idx).not.toHaveProperty('options')) so plain indexes omit type/options
alongside the existing using/config checks.
🧹 Nitpick comments (6)
packages/2-sql/2-authoring/contract-ts/src/build-contract.ts (1)

81-90: ⚡ Quick win

Consider replacing the structural cast with a type predicate.

(pack as { readonly indexTypes?: IndexTypeRegistration<IndexTypeMap> }) is a widening cast to probe a property not in the formal pack type. A narrow type predicate avoids the cast and lets the compiler narrow cleanly:

♻️ Suggested refactor
+function hasIndexTypes(
+  pack: object,
+): pack is { readonly indexTypes: IndexTypeRegistration<IndexTypeMap> } {
+  return 'indexTypes' in pack;
+}

   const indexTypeRegistry = createIndexTypeRegistry();
   for (const pack of [definition.target, ...Object.values(definition.extensionPacks ?? {})]) {
-    const registration = (pack as { readonly indexTypes?: IndexTypeRegistration<IndexTypeMap> })
-      .indexTypes;
-    if (!registration) continue;
-    for (const entry of registration.entries) {
+    if (!hasIndexTypes(pack)) continue;
+    for (const entry of pack.indexTypes.entries) {
       indexTypeRegistry.register(entry);
     }
   }
🤖 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-ts/src/build-contract.ts` around lines 81
- 90, The code uses a structural cast on "pack" to access "indexTypes"; replace
it with a type predicate function (e.g., hasIndexTypes) that asserts "pack is {
readonly indexTypes?: IndexTypeRegistration<IndexTypeMap> }" so the compiler can
narrow the type without a widening cast; update the loop over
"definition.target" and "definition.extensionPacks" to call hasIndexTypes(pack)
before reading "pack.indexTypes", then assign to "registration" and call
"indexTypeRegistry.register(entry)" as before and keep the subsequent call to
"validateIndexTypes(contract, indexTypeRegistry)" unchanged.
packages/3-targets/6-adapters/postgres/src/core/control-adapter.ts (1)

509-515: ⚡ Quick win

Use ifDefined() instead of inline conditional spreads for type/options.

Line 513 and Line 514 currently use inline conditional spread patterns, which diverges from the project convention.

♻️ Suggested patch
 const indexes: readonly SqlIndexIR[] = Array.from(indexesMap.values()).map((idx) => ({
   columns: Object.freeze([...idx.columns]) as readonly string[],
   name: idx.name,
   unique: idx.unique,
-  ...(idx.type !== undefined && { type: idx.type }),
-  ...(idx.options !== undefined && { options: idx.options }),
+  ...ifDefined('type', idx.type),
+  ...ifDefined('options', idx.options),
 }));

As per coding guidelines: "Use ifDefined() from @prisma-next/utils/defined for conditional object spreads instead of inline conditional spread patterns".

🤖 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/6-adapters/postgres/src/core/control-adapter.ts` around
lines 509 - 515, The inline conditional spreads for type/options inside the
mapping that builds the readonly SqlIndexIR array (the indexes variable created
from indexesMap.values()) should be replaced with the project-convention helper
ifDefined from `@prisma-next/utils/defined`; update the mapping that produces each
SqlIndexIR (referencing SqlIndexIR, indexesMap, and the indexes constant) to
import and call ifDefined for idx.type and idx.options so the conditional
properties are spread using ifDefined instead of the current ...(idx.type !==
undefined && ...) and ...(idx.options !== undefined && ...) patterns.
packages/2-sql/1-core/contract/test/validate.test.ts (1)

899-985: ⚡ Quick win

describe('validateIndexTypes', ...) is nested inside describe('validateContract', ...) — wrong grouping

validateIndexTypes is a separate validator. Its test suite should be a top-level describe block (sibling to describe('validateContract', ...)), not a child of it. As-is, test runners report these as validateContract > validateIndexTypes > ..., which is misleading.

🔧 Proposed fix
-  describe('validateIndexTypes', () => {
+});
+
+describe('validateIndexTypes', () => {
     function makeContractWithIndex(index: Record<string, unknown>) {
       ...
     }
     ...
-  });
-});
+});
🤖 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/1-core/contract/test/validate.test.ts` around lines 899 - 985,
The tests for validateIndexTypes are incorrectly nested inside
describe('validateContract', ...) — move the entire
describe('validateIndexTypes', ...) block (the function makeContractWithIndex
and all its it(...) cases) out so it is a top-level describe sibling to
validateContract; ensure validateIndexTypes, createIndexTypeRegistry,
makeContract (and any helpers used inside makeContractWithIndex) remain in scope
or are imported/declared above the new top-level describe; verify braces/exports
so the test file still parses and run tests to confirm the suite now reports
"validateIndexTypes > ..." as a top-level group.
packages/2-sql/1-core/contract/src/index-types.ts (1)

39-43: 💤 Low value

Minor: needless widening cast.

entry.options as Type<unknown> widens from Type<TOpts> to Type<unknown>. Since the entries array is typed as ReadonlyArray<IndexTypeEntry> (whose default TOptions is unknown), assignment is structurally fine without the cast — Type<TOpts> flows into Type<unknown> via the variance arktype already provides on its inferred output. If TS still complains, prefer constructing the entry without an explicit annotation rather than masking with an as-cast.

🤖 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/1-core/contract/src/index-types.ts` around lines 39 - 43, The
cast entry.options as Type<unknown> is unnecessary and widens types; remove the
explicit cast and push the entry as { type: typeLiteral, options: entry.options
} so the structural assignment into the ReadonlyArray<IndexTypeEntry> works via
Type variance; update the return in IndexTypeBuilderImpl (the constructor call
creating new IndexTypeBuilderImpl<TMap & Record<TLit, { readonly options: TOpts
}>>([...this.entries, { type: typeLiteral, options: entry.options }])) to
eliminate the cast and let Type<TOpts> flow naturally into the entries array.
packages/2-sql/2-authoring/contract-ts/src/contract-dsl.ts (1)

712-721: 💤 Low value

Prefer ifDefined for conditional object spreads.

ifDefined is already imported at the top of the file (line 19). Per repo conventions, conditional object spreads should use it instead of the inline ... !== undefined ? { x } : {} pattern. The rest of the file pre-dates this guideline, but the new index impl is fresh code worth aligning.

♻️ Suggested refactor
   return {
     kind: 'index',
     fields: normalizeFieldRefInput(fields),
-    ...(options?.name !== undefined ? { name: options.name } : {}),
-    ...(options?.type !== undefined ? { type: options.type } : {}),
-    ...(options?.options !== undefined
-      ? { options: options.options as Record<string, unknown> }
-      : {}),
+    ...ifDefined('name', options?.name),
+    ...ifDefined('type', options?.type),
+    ...ifDefined('options', options?.options as Record<string, unknown> | undefined),
   };

As per coding guidelines: "Use ifDefined() from @prisma-next/utils/defined for conditional object spreads instead of inline conditional spread patterns".

🤖 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-ts/src/contract-dsl.ts` around lines 712
- 721, The returned index object uses inline conditional spreads for
options.name, options.type and options.options; replace these with
ifDefined(...) calls from `@prisma-next/utils/defined` (already imported) so the
return becomes a single object with fields: kind: 'index', fields:
normalizeFieldRefInput(fields), and conditional properties inserted via
ifDefined({ name: options.name }), ifDefined({ type: options.type }), and
ifDefined({ options: options.options as Record<string, unknown> }) (keep the
cast and the normalizeFieldRefInput call intact) so the code follows the repo
convention for conditional spreads.
packages/2-sql/2-authoring/contract-psl/src/psl-attribute-parsing.ts (1)

209-260: 💤 Low value

Naive backslash-escape handling in quote tracking.

In splitObjectLiteralEntries and findTopLevelColon, the closing-quote check body[index - 1] !== '\\' does not handle escaped backslashes: an even number of trailing backslashes means the quote is unescaped. For example, { key: "a\\", key2: "b" } would be parsed as one continuous quote because the " after \\ is treated as escaped.

For V1 (string-leaf only with simple values like "id"), this is unlikely to trigger in practice, but worth tightening to count consecutive preceding backslashes.

🤖 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/psl-attribute-parsing.ts` around
lines 209 - 260, The quote-closing logic in splitObjectLiteralEntries and
findTopLevelColon incorrectly treats a quote as escaped if the immediately
preceding char is a backslash; instead, count consecutive backslashes
immediately before the quote and treat the quote as escaped only if the count is
odd. Update the closing-quote checks in splitObjectLiteralEntries (the block
that currently does if (ch === quote && body[index - 1] !== '\\')) and in
findTopLevelColon (if (ch === quote && entry[index - 1] !== '\\')) to compute
the number of consecutive backslashes before index and only clear quote when
that count is even (i.e., the quote is not escaped).
🤖 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 `@docs/architecture` docs/adrs/ADR 210 - Index-type registry.md:
- Line 7: The ADR currently links a transient project file via the "Spec:" line
pointing to projects/index-type-registry/spec.md; update ADR 210 - Index-type
registry to avoid referencing ephemeral project artifacts by either removing the
"Spec:" link entirely, replacing it with a stable/versioned document URL (or a
tag/commit-based link) or by inlining the essential spec content into the ADR
body so the ADR stands alone (locate the "Spec:" line in the ADR and modify it
to one of these three options).

In `@packages/2-sql/2-authoring/contract-ts/src/composed-authoring-helpers.ts`:
- Around line 101-108: The MergeAllPackIndexTypes helper currently injects a
__family property causing asymmetry with the contract types; update
MergeAllPackIndexTypes so it no longer adds { readonly __family: Family } and
instead only carries { readonly __target: Target } plus ExtensionPacks before
passing into MergeExtensionIndexTypes, so its shape matches
AllPacks/IndexTypesFromDefinition (remove __family from the intersection and
keep __target and ExtensionPacks intact).

In `@packages/3-extensions/paradedb/README.md`:
- Around line 53-60: The README example calls constraints.index with a single
ColumnRef argument which no longer typechecks because constraints.index now
requires an array/tuple parameter (see the new overload in contract-dsl.ts that
expects { readonly [K in keyof FieldNames]: ColumnRef<...> }). Update the
example and any single-column usages to pass a one-element array/tuple (e.g.,
replace constraints.index(cols.body, {...}) with constraints.index([cols.body],
{...})) and audit other uses of constraints.index to ensure they supply arrays
per the new ColumnRef tuple type.

In `@packages/3-targets/3-targets/postgres/test/migrations/issue-planner.test.ts`:
- Around line 530-589: The tests that call planIssues (in the two index-mismatch
cases) only assert result.value.calls[0], which allows extra unexpected planner
operations; update both tests to also assert the planner produced exactly one
call by adding expect(result.value.calls).toHaveLength(1) after the result.ok
checks so any extra calls are caught (look for the planIssues invocations in the
two index-mismatch tests using makeContract and add the toHaveLength(1)
assertion before matching calls[0]).

---

Outside diff comments:
In
`@packages/2-sql/2-authoring/contract-ts/test/contract-builder.normalization.test.ts`:
- Around line 220-229: The test should also assert that plain indexes do not
include the new metadata keys; after retrieving idx from
contract.storage.tables.user.indexes[0] (the existing idx variable), add two
assertions that idx does not have properties 'type' and 'options' (e.g.,
expect(idx).not.toHaveProperty('type') and
expect(idx).not.toHaveProperty('options')) so plain indexes omit type/options
alongside the existing using/config checks.

---

Nitpick comments:
In `@packages/2-sql/1-core/contract/src/index-types.ts`:
- Around line 39-43: The cast entry.options as Type<unknown> is unnecessary and
widens types; remove the explicit cast and push the entry as { type:
typeLiteral, options: entry.options } so the structural assignment into the
ReadonlyArray<IndexTypeEntry> works via Type variance; update the return in
IndexTypeBuilderImpl (the constructor call creating new
IndexTypeBuilderImpl<TMap & Record<TLit, { readonly options: TOpts
}>>([...this.entries, { type: typeLiteral, options: entry.options }])) to
eliminate the cast and let Type<TOpts> flow naturally into the entries array.

In `@packages/2-sql/1-core/contract/test/validate.test.ts`:
- Around line 899-985: The tests for validateIndexTypes are incorrectly nested
inside describe('validateContract', ...) — move the entire
describe('validateIndexTypes', ...) block (the function makeContractWithIndex
and all its it(...) cases) out so it is a top-level describe sibling to
validateContract; ensure validateIndexTypes, createIndexTypeRegistry,
makeContract (and any helpers used inside makeContractWithIndex) remain in scope
or are imported/declared above the new top-level describe; verify braces/exports
so the test file still parses and run tests to confirm the suite now reports
"validateIndexTypes > ..." as a top-level group.

In `@packages/2-sql/2-authoring/contract-psl/src/psl-attribute-parsing.ts`:
- Around line 209-260: The quote-closing logic in splitObjectLiteralEntries and
findTopLevelColon incorrectly treats a quote as escaped if the immediately
preceding char is a backslash; instead, count consecutive backslashes
immediately before the quote and treat the quote as escaped only if the count is
odd. Update the closing-quote checks in splitObjectLiteralEntries (the block
that currently does if (ch === quote && body[index - 1] !== '\\')) and in
findTopLevelColon (if (ch === quote && entry[index - 1] !== '\\')) to compute
the number of consecutive backslashes before index and only clear quote when
that count is even (i.e., the quote is not escaped).

In `@packages/2-sql/2-authoring/contract-ts/src/build-contract.ts`:
- Around line 81-90: The code uses a structural cast on "pack" to access
"indexTypes"; replace it with a type predicate function (e.g., hasIndexTypes)
that asserts "pack is { readonly indexTypes?:
IndexTypeRegistration<IndexTypeMap> }" so the compiler can narrow the type
without a widening cast; update the loop over "definition.target" and
"definition.extensionPacks" to call hasIndexTypes(pack) before reading
"pack.indexTypes", then assign to "registration" and call
"indexTypeRegistry.register(entry)" as before and keep the subsequent call to
"validateIndexTypes(contract, indexTypeRegistry)" unchanged.

In `@packages/2-sql/2-authoring/contract-ts/src/contract-dsl.ts`:
- Around line 712-721: The returned index object uses inline conditional spreads
for options.name, options.type and options.options; replace these with
ifDefined(...) calls from `@prisma-next/utils/defined` (already imported) so the
return becomes a single object with fields: kind: 'index', fields:
normalizeFieldRefInput(fields), and conditional properties inserted via
ifDefined({ name: options.name }), ifDefined({ type: options.type }), and
ifDefined({ options: options.options as Record<string, unknown> }) (keep the
cast and the normalizeFieldRefInput call intact) so the code follows the repo
convention for conditional spreads.

In `@packages/3-targets/6-adapters/postgres/src/core/control-adapter.ts`:
- Around line 509-515: The inline conditional spreads for type/options inside
the mapping that builds the readonly SqlIndexIR array (the indexes variable
created from indexesMap.values()) should be replaced with the project-convention
helper ifDefined from `@prisma-next/utils/defined`; update the mapping that
produces each SqlIndexIR (referencing SqlIndexIR, indexesMap, and the indexes
constant) to import and call ifDefined for idx.type and idx.options so the
conditional properties are spread using ifDefined instead of the current
...(idx.type !== undefined && ...) and ...(idx.options !== undefined && ...)
patterns.
🪄 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

Run ID: 5907af77-0930-4006-84b5-1ef4d43910ae

📥 Commits

Reviewing files that changed from the base of the PR and between 286fdef and aaafe3d.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (66)
  • docs/architecture docs/adrs/ADR 210 - Index-type registry.md
  • packages/1-framework/2-authoring/contract/src/descriptors.ts
  • packages/1-framework/2-authoring/contract/test/descriptors.test.ts
  • packages/2-sql/1-core/contract/package.json
  • packages/2-sql/1-core/contract/src/exports/index-types.ts
  • packages/2-sql/1-core/contract/src/index-types.ts
  • packages/2-sql/1-core/contract/src/index.ts
  • packages/2-sql/1-core/contract/src/types.ts
  • packages/2-sql/1-core/contract/src/validators.ts
  • packages/2-sql/1-core/contract/test/index-types.test.ts
  • packages/2-sql/1-core/contract/test/validate.test.ts
  • packages/2-sql/1-core/contract/tsdown.config.ts
  • packages/2-sql/1-core/schema-ir/src/types.ts
  • packages/2-sql/2-authoring/contract-psl/package.json
  • packages/2-sql/2-authoring/contract-psl/src/interpreter.ts
  • packages/2-sql/2-authoring/contract-psl/src/psl-attribute-parsing.ts
  • packages/2-sql/2-authoring/contract-psl/test/interpreter.test.ts
  • packages/2-sql/2-authoring/contract-psl/test/ts-psl-parity.test.ts
  • packages/2-sql/2-authoring/contract-ts/src/build-contract.ts
  • packages/2-sql/2-authoring/contract-ts/src/composed-authoring-helpers.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/src/contract-types.ts
  • packages/2-sql/2-authoring/contract-ts/test/contract-builder.constraints.test.ts
  • packages/2-sql/2-authoring/contract-ts/test/contract-builder.dsl.test.ts
  • packages/2-sql/2-authoring/contract-ts/test/contract-builder.index-types.test.ts
  • packages/2-sql/2-authoring/contract-ts/test/contract-builder.normalization.test.ts
  • packages/2-sql/2-authoring/contract-ts/test/contract-dsl.runtime.test.ts
  • packages/2-sql/2-authoring/contract-ts/test/contract-lowering.runtime.test.ts
  • packages/2-sql/2-authoring/contract-ts/test/helpers/test-index-pack.ts
  • packages/2-sql/3-tooling/emitter/src/index.ts
  • packages/2-sql/3-tooling/emitter/test/emitter-hook.generation.basic.test.ts
  • packages/2-sql/3-tooling/emitter/test/emitter-hook.structure.test.ts
  • packages/2-sql/9-family/src/core/schema-verify/verify-helpers.ts
  • packages/2-sql/9-family/test/schema-verify.helpers.ts
  • packages/2-sql/9-family/test/schema-verify.semantic-satisfaction.test.ts
  • packages/3-extensions/paradedb/README.md
  • packages/3-extensions/paradedb/package.json
  • packages/3-extensions/paradedb/src/core/constants.ts
  • packages/3-extensions/paradedb/src/core/descriptor-meta.ts
  • packages/3-extensions/paradedb/src/exports/index-types.ts
  • packages/3-extensions/paradedb/src/types/index-types.ts
  • packages/3-extensions/paradedb/test/index-types.test.ts
  • packages/3-targets/3-targets/postgres/src/core/migrations/issue-planner.ts
  • packages/3-targets/3-targets/postgres/src/core/migrations/op-factory-call.ts
  • packages/3-targets/3-targets/postgres/src/core/migrations/operations/indexes.ts
  • packages/3-targets/3-targets/postgres/test/migrations/index-ddl.test.ts
  • packages/3-targets/3-targets/postgres/test/migrations/issue-planner.test.ts
  • packages/3-targets/6-adapters/postgres/src/core/control-adapter.ts
  • packages/3-targets/6-adapters/postgres/test/migrations/index-introspection.integration.test.ts
  • packages/3-targets/6-adapters/postgres/test/migrations/op-factory-call.rendering.test.ts
  • test/e2e/framework/test/sqlite/migrations/additive.test.ts
  • test/e2e/framework/test/sqlite/migrations/destructive.test.ts
  • test/e2e/framework/test/sqlite/migrations/fk-preservation.test.ts
  • test/integration/package.json
  • test/integration/test/authoring/paradedb-bm25-narrowing.test.ts
  • test/integration/test/authoring/parity/core-surface/contract.ts
  • test/integration/test/authoring/parity/map-attributes/contract.ts
  • test/integration/test/authoring/psl-index-type-options.integration.test.ts
  • test/integration/test/family.schema-verify.basic.integration.test.ts
  • test/integration/test/family.schema-verify.basic.test.ts
  • test/integration/test/fixtures/cli/cli-e2e-test-app/fixtures/db-update-scenarios/contract-add-project-slug.ts
  • test/integration/test/fixtures/cli/cli-e2e-test-app/fixtures/db-update-scenarios/contract.ts
  • test/integration/test/fixtures/cli/cli-integration-test-app/fixtures/emit-command/contract.parity.ts
  • test/integration/test/referential-actions.integration.test.ts
💤 Files with no reviewable changes (1)
  • packages/3-extensions/paradedb/src/core/constants.ts

Comment thread docs/architecture docs/adrs/ADR 210 - Index-type registry.md Outdated
Comment thread packages/3-extensions/paradedb/README.md
Comment on lines +530 to +589
const result = planIssues({
...defaultCtx,
issues,
toContract,
fromContract: null,
storageTypes: toContract.storage.types ?? {},
});

expect(result.ok).toBe(true);
if (!result.ok) throw new Error('expected ok');
expect(result.value.calls[0]).toMatchObject({
factoryName: 'createIndex',
tableName: 'doc',
indexName: 'doc_body_bm25_idx',
indexType: 'bm25',
options: { key_field: 'id' },
});
});

it('falls back to a default index name when the contract index has no name', () => {
const toContract = makeContract({
tables: {
doc: {
columns: {
id: { nativeType: 'uuid', codecId: 'pg/uuid@1', nullable: false },
body: { nativeType: 'text', codecId: 'pg/text@1', nullable: false },
},
primaryKey: { columns: ['id'] },
uniques: [],
indexes: [{ columns: ['body'] }],
foreignKeys: [],
},
},
});
const issues: SchemaIssue[] = [
{
kind: 'index_mismatch',
table: 'doc',
expected: 'body',
message: 'Table "doc" is missing index: body',
},
];

const result = planIssues({
...defaultCtx,
issues,
toContract,
fromContract: null,
storageTypes: toContract.storage.types ?? {},
});

expect(result.ok).toBe(true);
if (!result.ok) throw new Error('expected ok');
expect(result.value.calls[0]).toMatchObject({
factoryName: 'createIndex',
tableName: 'doc',
indexName: 'doc_body_idx',
indexType: undefined,
options: undefined,
});
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Assert single-call planning in the last two index-mismatch cases.

At Line [540] and Line [583], the tests only validate calls[0]. Add toHaveLength(1) so extra unexpected planner operations are caught.

Suggested patch
       expect(result.ok).toBe(true);
       if (!result.ok) throw new Error('expected ok');
+      expect(result.value.calls).toHaveLength(1);
       expect(result.value.calls[0]).toMatchObject({
         factoryName: 'createIndex',
         tableName: 'doc',
         indexName: 'doc_body_bm25_idx',
         indexType: 'bm25',
         options: { key_field: 'id' },
       });
@@
       expect(result.ok).toBe(true);
       if (!result.ok) throw new Error('expected ok');
+      expect(result.value.calls).toHaveLength(1);
       expect(result.value.calls[0]).toMatchObject({
         factoryName: 'createIndex',
         tableName: 'doc',
         indexName: 'doc_body_idx',
         indexType: undefined,
         options: undefined,
       });
🤖 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/postgres/test/migrations/issue-planner.test.ts`
around lines 530 - 589, The tests that call planIssues (in the two
index-mismatch cases) only assert result.value.calls[0], which allows extra
unexpected planner operations; update both tests to also assert the planner
produced exactly one call by adding expect(result.value.calls).toHaveLength(1)
after the result.ok checks so any extra calls are caught (look for the
planIssues invocations in the two index-mismatch tests using makeContract and
add the toHaveLength(1) assertion before matching calls[0]).

SevInf added 25 commits May 9, 2026 12:19
Establishes the index-type registry primitive: factory builder declaring
the type literal once, phantom-typed __indexTypes pack threading,
validation seam in validateSqlStorage, single Postgres-style renderer,
and the lockstep using/config → type/options field-name rename.
Adds defineIndexTypes() factory builder and createIndexTypeRegistry()
in @prisma-next/sql-contract/index-types. The builder declares each
type literal exactly once and exposes both a phantom IndexTypes map
(for type-level narrowing) and a runtime entry list. The registry is
instance-based — created per contract assembly, not module-global —
so two contracts with different pack lists see different valid type
sets. No consumers wired yet.

Also clarifies ADR 210: the per-contract registry is assembled from
each pack's entries during contract definition, not via global
register() calls at module load.
Lockstep rename across the IR, the SQL family validator, the schema-IR,
the TS authoring surface (IndexOptions, IndexConstraint, IndexNode,
contract-lowering, build-contract), and the contract emitter. The
schema-IR SqlIndexIR gains the new fields. The ParadeDB bm25Index()
helper writes the new field names but is otherwise untouched (full
removal happens when the registry is wired in M3).

The placeholder fields are inert in every active code path today, so
this rename is observationally silent. No backwards-compatibility
shim.
…ntract definition

ExtractIndexTypesFromPack pulls the phantom __indexTypes off any pack
that carries one. AllIndexTypeLiterals walks all literal keys across a
record of packs, and MergeExtensionIndexTypes builds the merged map by
looking each literal up in its contributing pack. IndexTypesFromDefinition
folds target + extension packs into a single record and feeds it through
MergeExtensionIndexTypes. SqlContractResult exposes the merged map as
a phantom __indexTypes? field.

The implementation deliberately avoids UnionToIntersection so the empty
case resolves cleanly to Record<never, never> rather than the
UnionToIntersection<never> = unknown footgun.

Type tests cover single-pack extraction, fall-through for packs without
__indexTypes, multi-pack merging, definition-level resolution, and the
end-to-end defineContract path.
validateContract grows an optional indexTypeRegistry option. When
supplied, validateSqlStorage looks up each index's type in the
registry, validates options against the registered arktype validator,
and rejects unknown types or options that fail validation. Even when no
registry is supplied, options-without-type is still rejected.

Test coverage: registered+valid passes, unregistered type rejects with
the type name, invalid option fails with arktype path, strict-mode extra
key rejects, options-without-type rejects, no type/options accepts, no
registry skips type lookup but still rejects options-without-type.
…ions are set

createIndex grows an optional extras argument carrying type and options.
When supplied, it emits CREATE INDEX <name> ON <table> USING <method>
(<cols>) WITH (<key> = <literal>, ...). The framework owns the renderer:
strings are escapeLiteral-quoted, finite numbers stringify, booleans
emit true/false, and any other leaf shape (null, NaN, objects) throws.

CreateIndexCall and renderTypeScript carry the new args verbatim; the
issue planner threads contract index.type / index.options through.
Existing callers without extras get the same plain CREATE INDEX as
before.

Tests: 8 DDL emission cases in index-ddl.test.ts (plain, USING-only,
USING+WITH, empty options, mixed scalars, single-quote escape, null
rejection, NaN rejection) plus a renderTypeScript case for the new
extras argument shape.
…ema indexes

verifyIndexes now treats type and options as part of an index's identity.
A contract index with type "gin" no longer matches a schema index that
has columns the same but type "btree" (or no type at all); instead the
verifier emits index_mismatch (the contract index needs creation) plus
extra_index in strict mode (the schema index needs to be dropped). The
issue planner naturally translates this pair into DROP + CREATE — the
spec'd behavior for any change to columns / type / options.

Unique constraints can only satisfy a contract index requirement when
that requirement does not specify a type or options, since a unique
constraint carries no index-method or storage-parameter information of
its own.

Tests: three new cases in schema-verify.semantic-satisfaction.test.ts —
type differs (mismatch + extra), options differ (mismatch + extra), and
type/options match exactly (no issues). The createContractTable /
createSchemaTable helpers grow optional type / options fields so the
index inputs accept the new shape.
…ry; drop helpers

Replace the bespoke bm25Index() helper, the bm25 namespace
(text/numeric/boolean/json/datetime/range/expression builders), the
Bm25*FieldConfig / *FieldOptions types, the TokenizerId catalog, and
the rich Bm25IndexConfig with a single defineIndexTypes().add("bm25",
{ options }) registration. The arktype validator narrows options to
{ key_field: string } in strict mode; per-field tokenizer / column
configuration is deferred to expression-index support.

paradedbPackMeta exposes the resulting __indexTypes phantom map so
SqlContractResult can narrow constraints.index({ type: "bm25" }) at
the call site, and exposes the registry entries on indexTypes so
contract assembly can register them at validation time.

Tests rewritten around the new shape: pack identity + capability
checks, single-entry verification, and the four validator paths
(valid options, missing key_field, extra key, wrong type).

Adds @prisma-next/sql-contract and arktype to paradedb deps.
…pe, options })

Threads the merged IndexTypes map all the way to the constraints DSL
inside .sql(({ constraints }) => ...) so wrong index type literals and
wrong option shapes surface as TS errors on the offending
constraints.index(...) line — not buried in a deep defineContract type.

Wiring:
- ContractModelBuilder gains a sixth generic IndexTypes (default wildcard)
- SqlContext<Fields, IndexTypes> picks PackAwareSqlConstraints<IndexTypes>
- A discriminated-union IndexInput<Name, IndexTypes> rejects unregistered
  types and bad option shapes; "options without type" is also rejected
  via the type?: never branch
- ComposedAuthoringHelpers.model becomes PackAwareModel<MergeAllPackIndexTypes>
  so the helpers form (defineContract({...}, ({ model }) => ...)) carries
  the merged map; the bare model() import keeps the wildcard default to
  preserve backward compat

The runtime is unchanged — narrowing is pure type machinery.

e2e: test/integration/test/authoring/paradedb-bm25-narrowing.test.ts
exercises the real paradedbPack and asserts six cases via vitest +
@ts-expect-error: well-formed bm25 typechecks; unknown options key
rejects; missing key_field rejects; unregistered type rejects; options
without type rejects; bare model() degraded path still accepts (no
narrowing).

Adds extension-paradedb to integration-tests deps.
Adds parseObjectLiteralStringMap to psl-attribute-parsing — parses
brace-balanced { key: "value", ... } argument values into
Record<string, string>. Bare-identifier keys, quoted-string-literal
values; boolean / number / nested-object leaves are rejected with a
clear diagnostic (V1 is string-leaves-only per the spec).

The PSL grammar itself needs no change: splitTopLevelSegments is
already brace-aware, so the existing parser preserves the raw
{...} value through to the interpreter.

The @@index interpreter now extracts the type and options named
arguments alongside the existing @Map handling. Validates that
options requires a surrounding type, that type is a quoted string
literal, and lowers everything into IndexNode.type / IndexNode.options.

Tests cover: documented example shape, multi-key options, boolean
leaf rejection, number leaf rejection, options-without-type, malformed
object literal, and the unchanged "no type or options" path.
…pack

Adds an integration test that exercises the full PSL -> IR -> registry
validation path with the real paradedbPack:

1. The documented spec example (@@index([body], type: "bm25",
   options: { key_field: "id" }, map: "doc_body_bm25_idx")) lowers
   to a Contract IR index node carrying type, options, and name.
2. The lowered contract validates clean against a registry built from
   paradedbIndexTypes.entries.
3. A PSL-authored bm25 index whose options miss the required key_field
   is rejected by the registry with a clear error.

This complements the unit tests in contract-psl by going through the
actual pack metadata and validateContract path.
…exIR

The Postgres introspection query joins pg_am and reads
pg_class.reloptions so introspected SqlIndexIR now carries:
- type: the index method (dropped to undefined when amname is "btree",
  since that is the Postgres default and a contract index without an
  explicit type should match a default-method introspected index)
- options: pg_class.reloptions parsed from {key=value, ...} into
  Record<string, string> (Postgres returns reloption values as raw
  text regardless of the underlying scalar type)

The family verifier's indexExtrasMatch now compares contract options
to introspected options via String() coercion so a contract
`fillfactor: 70` matches an introspected `fillfactor: "70"` without
forcing a spurious DROP+CREATE.

Without this wiring the migration planner would treat any contract
index with type set as different from any introspected index on the
same columns — forcing DROP+CREATE on every plan even when the live
index already matches the contract.

Tests: four PGlite integration cases covering the matrix — default
btree (type/options unset), non-default gin (type set, options unset),
default btree with WITH options (type unset, options set), and gin
with WITH options (both set).
…clause

Hoist extras?.options into a local so the WITH-clause check uses
optional chaining instead of !-assertions, which biome rejects under
lint/style/noNonNullAssertion.
…x_mismatch planner branch

The issue-planner index_mismatch branch reconstructed the contract index
from issue.expected (column names only), so adding a typed index to an
existing table emitted CREATE INDEX without USING/WITH and rebuilt as a
default btree. Look up the contract index by columns and pass type,
options, and an explicit name through to CreateIndexCall.
…uild registry from packs in family validateContract

The internal validateSqlStorage previously short-circuited the type lookup
when no registry was supplied, and the SQL family runtime never passed
one. Net result: the CLI validateContract codepath silently accepted any
index type literal, defeating the design-time gate the registry is meant
to be (per ADR 210).

- Internal validateSqlStorage now takes a non-undefined IndexTypeRegistry.
- Public validateContract defaults a missing options.indexTypeRegistry to
  an empty registry, so unregistered type literals fail validation
  regardless of caller setup.
- createSqlFamilyInstance walks [adapter, target, ...extensions] for
  indexTypes entries via buildIndexTypeRegistry(), caches the registry
  once per family instance, and passes it to all four sqlValidateContract
  call sites (validateContract, verify, schemaVerify, sign).
…rant-opt-in, not framework-enforced

The previous wording implied validateSqlStorage validates options "in
strict mode", which read as a framework guarantee. arktype is
loose-by-default and the framework merely invokes whatever validator the
registrant constructed; strictness is a property of that validator. Make
the recommendation explicit without claiming the framework imposes it.
Two gaps in the PSL integration coverage for the index-type registry:
unregistered type (TS DSL covered both compile-time and runtime, PSL
only covered compile-time via parser; runtime side untested) and an
empty options literal (parser produces {}; pipeline through to the
validator was unasserted). Both authored as @@index attributes, lowered
through the real paradedb pack, and asserted at the runtime registry
boundary.
…== undefined

The contract-dsl `index()` factory and the emitter index serialization
used truthy checks for `name`, `type`, and `options`, while the rest of
the framework (validate.ts, verify-helpers.ts, op-factory-call.ts,
issue-planner.ts) reads these fields with `=== undefined`. Align both
authoring sites on the framework convention.
…pes to Record<never, never>

The bare model() import previously defaulted IndexTypes to a wildcard
(Record<string, { options: Record<string, unknown> }>), so any string
literal typechecked at constraints.index({ type, options }) — even when
no packs were attached and no index types were registered. Drop the
wildcard. With no attached packs, only the default-index form
constraints.index(cols.x) (no type/options) typechecks; type literals
are restricted to whatever attached packs registered.

The IndexInput discriminated union already had the right structure for
the empty case (keyof IndexTypes extends never → ConstraintOptions<Name>);
the wildcard was the inconsistency.

Three lowering/runtime tests authored typed indexes via the bare model()
to exercise IR pass-through. They now use createComposedAuthoringHelpers
with a small test pack (test/helpers/test-index-pack.ts) that registers
bm25 and hash with permissive option shapes. The paradedb narrowing test
flips: bare model() now @ts-expect-errors on type: "made-up" and accepts
the no-options default-index form.

contract-builder.normalization.test.ts: simplified the "extension index
config with expression fields" test to a single pass-through case using
the real { key_field: "id" } shape; the old nested-array fields[]
payload was a leftover from the deleted bm25Index() helper.
…ten bare index overloads

Three small cleanups enabled by the wildcard removal:

- composed-authoring-helpers.ts dropped its local copies of
  ExtractIndexTypesFromPack, AllPackIndexTypeLiterals, and
  MergedPackIndexTypes. They were near-duplicates of the canonical
  versions in contract-types.ts. MergeAllPackIndexTypes now reuses
  MergeExtensionIndexTypes directly.

- contract-types.ts MergeExtensionIndexTypes now clamps the value side
  with Extract<..., { readonly options: unknown }>. The clamp tells the
  type-checker the resulting map structurally extends IndexTypeMap, so
  MergeAllPackIndexTypes no longer needs the extends infer M extends
  IndexTypeMap ? M : Record<never, never> coercion at use sites.

- contract-dsl.ts bare index() overloads use ConstraintOptions<Name>
  directly instead of IndexInput<Name, Record<never, never>>. With no
  packs attached IndexInput already evaluates to ConstraintOptions, so
  using it directly is self-documenting.
…es IndexTypeRegistration directly

The previous design carried two parallel artifacts on every pack: the
runtime indexTypes entries array and a __indexTypes phantom typed map.
Both had to be kept in sync by hand, with no compile-time link between
them — adding .add(...) to the builder would update one and silently
leave the other behind.

Introduce a read-only IndexTypeRegistration<TMap> interface that exposes
just entries (runtime) and IndexTypes (phantom). IndexTypeBuilder
extends it. The pack stores the builder verbatim in its indexTypes
field; both halves flow from a single source of truth, and the
read-only interface prevents the pack from being misused as a mutable
registry.

Consumer-side adjustments:
- ExtractIndexTypesFromPack<P> reads P[indexTypes] typed as
  IndexTypeRegistration<infer M>.
- buildIndexTypeRegistry iterates descriptor.indexTypes.entries.
- __indexTypes? dropped from SqlContractResult (only tests read it;
  narrowing is verified end-to-end via paradedb-bm25-narrowing).
- ParadeDB descriptor-meta.ts collapsed from base+intersection to a
  plain as const literal.
- contract-builder.index-types.test.ts updated; the two
  SqlContractResult.__indexTypes tests removed (redundant with upstream
  MergeExtensionIndexTypes tests).

ADR 210 §3 and the matching/lookup/dispatch prose updated to drop the
"phantom-typed __indexTypes field" framing in favor of "single
registration value carrying both halves".
…Types; remove buildSqlSpec cast

The cast at buildSqlSpec(createSqlConstraintsDsl() as unknown as
SqlContext<Fields, IndexTypes>[constraints]) was bridging from a bare
DSL value to one with pack-aware narrowing. Both are the same runtime
function; only the type-level signature differs.

Make createConstraintsDsl and createSqlConstraintsDsl generic over
IndexTypes, with the inner index overloads using IndexInput<Name,
IndexTypes> directly. buildSqlSpec passes its IndexTypes generic to
createSqlConstraintsDsl<IndexTypes>(); no cast needed.

The implementation signatures inner options field was loosened from
Record<string, unknown> to unknown, because per-pack option shapes
(like { key_field: string }) are closed object types that arent
strictly assignable to Record<string, unknown>. A single property-
scoped as Record<string, unknown> cast at the spread point stores the
value into the IR.

Net: one as unknown as cast removed, one minimal as cast introduced at
the storage boundary.
…p dead branches

F08: paradedb-bm25-narrowing.test.ts gains two positive expectTypeOf
assertions on Doc.__indexTypes — bare model() resolves to Record<never,
never>, helpers-bound model() (with paradedb attached) carries the bm25
shape. The boundary is now locked at the type level rather than relying
on absence of TS errors.

F10: psl-index-type-options.integration.test.ts "rejects bm25 with bad
options" case tightened from .toThrow(/key_field|bm25/) to instanceof
ContractValidationError plus message-contains-both bm25 AND key_field.
Either-word match was loose enough to pass on incidental wording.

F14: dropped the hasExtras ternary in CreateIndexCall.toOp and the
two issue-planner.ts branches (missing_table, index_mismatch). Calling
createIndex(..., extras) with an empty extras object behaves the same
as omitting the arg, so the conditional was dead. Postgres tests still
155/155.
…r-property error positions

Collapsing constraints.index to a single multi-field overload makes
TypeScript pin error positions to the offending property (an unknown
options key, a wrong type literal, missing required option fields)
instead of reporting a generic "no overload matches this call" at the
call expression. The trade-off: callers always pass the field list as
an array, even for single-column indexes:

  constraints.index(cols.body, ...)    // before
  constraints.index([cols.body], ...)  // after

Migrated all in-repo callers (tests, integration test fixtures, CLI
test fixtures, e2e migrations) in lockstep. The paradedb narrowing test
now lands @ts-expect-error directives on the offending property line
inside the options literal, matching the new TS error positions.

Also fixed obsolete MySQL/MariaDB/MSSQL adapter mentions in ADR 210
and the PR description; only postgres and sqlite SQL adapters exist.
…owering, not at runtime

Index-type validation now runs inside buildSqlContractFromDefinition,
the shared lowering both TS (defineContract) and PSL
(interpretPslDocumentToSqlContract) authoring funnel through. Bad
types or option shapes throw at authoring time.

- packages/2-sql/1-core/contract/src/validators.ts: validateIndexTypes
  lives here next to validateStorageSemantics. Both validate the
  storage IR; build-contract.ts assertStorageSemantics calls them in
  one step.
- packages/2-sql/1-core/contract/src/validate.ts: drop validateIndexTypes
  (moved to validators.ts) and the indexTypeRegistry option from the
  public validateContract API. Runtime keeps structural / referential /
  semantic checks; no registry needed.
- packages/2-sql/2-authoring/contract-ts/src/build-contract.ts: build a
  per-contract IndexTypeRegistry from definition.target +
  definition.extensionPacks inline in assertStorageSemantics.
- packages/2-sql/9-family/src/core/control-instance.ts: strip the family
  registry build, the DescriptorWithIndexTypes interface, and the
  { indexTypeRegistry } argument at all four sqlValidateContract call
  sites.

Tests:
- validate.test.ts cases call validateIndexTypes directly from
  validators.ts.
- contract-builder.constraints.test.ts asserts defineContract throws
  at authoring time on an unregistered type.
- contract-psl/test/interpreter.test.ts attaches a permissive bm25
  test pack so the existing PSL parser tests can lower successfully.
- paradedb-bm25-narrowing.test.ts wraps the @ts-expect-error cases in
  expect(...).toThrow(...) since defineContract now throws synchronously.
- psl-index-type-options.integration.test.ts: rewrite negative cases
  to assert interpret() throws directly (validation happens inside
  the interpreter, not in a separate validateContract call).

Docs:
- ADR 210 §4 retitled "Validation seam at the ContractIR -> Contract
  boundary" and rewritten around the new seam.
- PR description: "Authoring-time validator" bullet replaces the old
  "Validator hook" + "Family runtime wiring" pair.
wmadden added 3 commits May 9, 2026 12:21
…switch

Wraps the single-field column reference in an array to match the
post-rename API. The single-field overload of constraints.index(...)
was removed in lockstep with the array-only switch; in-repo callers
were migrated but the user-facing README example was missed and would
not compile against this branch.

Addresses F01.
…validators

Splits validateIndexTypes into a sibling module (index-type-validation.ts)
to reflect its real layering. The other validators in validators.ts only
check internal consistency of the loaded contract JSON; validateIndexTypes
checks the contract against an external IndexTypeRegistry assembled from
extension packs that are not part of the JSON, so it correctly belongs at
the ContractIR -> Contract lowering seam (where the packs are in scope)
rather than next to validators called from validateContract.

The file boundary surfaces this distinction in a way a docstring would
not: validators.ts becomes the natural home for things validateContract
would call, while index-type-validation.ts is clearly a registry-dependent
check called only from the authoring layer.

Updates the import in build-contract.ts and the test in validate.test.ts.
Adds a new ./index-type-validation export entry to the package and the
tsdown bundle.

Pure file move + import update; no behavior change.

Addresses F04.
…, and PSL diagnostics

Six focused tweaks following PR review:

- contract-lowering.ts: switch index name/type/options spreads from
  truthy checks to ifDefined() so the lowering matches build-contract.ts
  semantics. Empty-string type and empty-record options are not valid
  today, but the divergence between IR writers in the same authoring
  layer is the kind of thing that drifts and bites later. (F02)
- build-contract.ts: replace the inline structural cast on pack.indexTypes
  with a positive type guard that throws a contextual error naming the
  pack id when the value is not a real IndexTypeRegistration. A misconfigured
  third-party pack now produces a useful error instead of an opaque
  "entries is not iterable" stack trace. (F03)
- op-factory-call.ts: comment why the field is named indexType rather
  than typeName — typeName is read by the planner location helper and
  identifies a CREATE TYPE target on CreateEnumTypeCall. (F05)
- control-adapter.ts: parsePgReloptions now throws on a malformed entry
  with no "=" separator, naming both the entry and the index. The previous
  silent skip would surface downstream as a confusing index_mismatch with
  no breadcrumb back to the malformed catalog row. The function takes an
  indexName parameter threaded from the introspection call site and is
  exported so it can be tested directly. (F06)
- validators.ts: sort options keys before serializing the duplicate-index
  signature so two indexes whose options differ only in key order are
  detected as duplicates. JSON.stringify preserves insertion order, which
  was reachable through TS authoring across two source files. (F07)
- psl-attribute-parsing.ts: append a short hint to the
  "must be a quoted string literal" diagnostic explaining that V1 PSL
  @@index options support string leaves only and pointing at the TS
  authoring surface for non-string options. (F09)

Adds three unit tests, written before each fix:
- validators.test.ts: detects two indexes whose options differ only in
  key order as duplicates.
- contract-builder.constraints.test.ts: a pack with indexTypes set to a
  non-registration value triggers a contextual error naming the pack id.
- control-adapter.test.ts: parsePgReloptions throws on malformed entry,
  parses well-formed entries, returns undefined for null/empty.

Existing PSL interpreter tests use a regex substring match against
"must be a quoted string literal" and continue to pass with the
appended hint.

Addresses F02, F03, F05, F06, F07, F09.
…n-first, no project state

Restructure ADR 210 to follow the write-architecture-docs conventions
applied across the recent ADRs (e.g. ADR 209): one-sentence decision
callout above the fold, complete grounding example (ParadeDB bm25
declaration + TS/PSL authoring + the three failure modes), narrative
buildup of the registry primitive, per-contract composition, lowering
seam, strictness, rendering, identity, and authoring surfaces.

Drop project-state references unsuitable for a long-lived doc:
the Status / Date / Spec headers, the link to a transient
projects/ spec file, the using/config rename narrative, and the
V1/follow-up framing in non-goals. Reframe non-goals as architectural
boundaries (no built-in entries — the registry is open by design;
default B-tree is expressed by omitting type).

Add an explicit subsection on the load-bearing layering between
validateContract (JSON-internal-consistency only) and the
authoring-time validateIndexTypes (registry-dependent, lives at the
ContractIR -> Contract lowering seam). Clarify that PSL string-leaf-only
options is a grammar property, not a registry property.

No design changes; same decisions, same trade-offs.
Copy link
Copy Markdown
Contributor

@wmadden wmadden left a comment

Choose a reason for hiding this comment

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

I made a few small corrections, I think this is good to go 👍🏻

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