feat(extension-paradedb): query ops and demo#433
Draft
SevInf wants to merge 27 commits intopsl-index-plusfrom
Draft
feat(extension-paradedb): query ops and demo#433SevInf wants to merge 27 commits intopsl-index-plusfrom
SevInf wants to merge 27 commits intopsl-index-plusfrom
Conversation
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.
|
Important Review skippedDraft detected. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Path: .coderabbit.yml Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ 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/mongo-runtime
@prisma-next/family-mongo
@prisma-next/sql-runtime
@prisma-next/family-sql
@prisma-next/extension-arktype-json
@prisma-next/middleware-telemetry
@prisma-next/mongo
@prisma-next/extension-paradedb
@prisma-next/extension-pgvector
@prisma-next/postgres
@prisma-next/sql-orm-client
@prisma-next/sqlite
@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/emitter
@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: |
Adds the paradedb extension query plane on top of upstream bm25 index-type
registration:
- 11 query operations via two shared helpers — matchOp for the five
match-mode operators (paradeDbMatch @@@, paradeDbMatchAny |||,
paradeDbMatchAll &&&, paradeDbTerm ===, paradeDbPhrase ###) and
typmodCastOp for the four typmod casts (paradeDbFuzzy, paradeDbBoost,
paradeDbConst, paradeDbSlop). Cast args wrap a JS-side integer in
LiteralExpr.of(n) because PG rejects parameterized typmods; per-op
validators enforce the documented ranges.
- paradeDbScore (pdb.score(key)) and ParadeDbProximityChain — an
immutable builder with .within(distance, term, { ordered? }) for
arbitrary-length chains mixing ## and ##>. Distances render as
LiteralExpr (chained ##/##> only accept literal slop). The chain
implements Expression<text> so it composes directly through
paradeDbMatch via the @@@ overload.
- pg_search install via SqlControlExtensionDescriptor.databaseDependencies.
examples/paradedb-demo runs every operation end-to-end against a live
paradedb container. Contract authored with the new shape:
constraints.index([cols...], { type: "bm25", options: { key_field: "id" },
name: "item_bm25_idx" }). docker-compose.yaml + init script create a
dedicated demo database to sidestep the paradedb image preloaded PostGIS
tables in the default database. CLI commands: match, top, fuzzy,
proximity, proximity-chain, chain-demo, mode-tour (curated tour of the
five match modes against contrastive seed data), cast-demo.
Also fixes a pre-existing introspection bug in adapter-postgres: the
indexes query ordered columns by pg_attribute.attnum (table declaration
order) rather than by position within pg_index.indkey (the index key
order). Multi-column indexes whose declared column order differs from
the table column declaration order would spuriously fail schema
verification — including any contract whose columns get alphabetized
during canonicalization while an index declares its own ordering.
6af7b34 to
e5643ee
Compare
…onTypes Drop matchOp/typmodCastOp helpers and inline all 11 entries in a record annotated with `DescriptorMap<CT>`, a mapped type derived from `QueryOperationTypes<CT>`. Renaming a method or changing a return codec in the type module now fails typecheck inside `descriptor-meta.ts` instead of silently drifting into a runtime mismatch.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Intent
Wire up the paradedb extension's query plane on top of the bm25 index-type registration that landed on
psl-index-plus. Authoring + emit + DDL already work via the upstream registry; this PR adds the runtime-side: a complete set of paradedb query operations, the proximity chain builder, and an end-to-end CLI demo that exercises everything against a live paradedb container.Change map
The extension itself (
packages/3-extensions/paradedb) gains:src/core/descriptor-meta.ts:matchOpcovers the five match-mode operators (paradeDbMatch/@@@,paradeDbMatchAny/|||,paradeDbMatchAll/&&&,paradeDbTerm/===,paradeDbPhrase/###).typmodCastOpcovers the four typmod casts (paradeDbFuzzy,paradeDbBoost,paradeDbConst,paradeDbSlop). Each wraps its integer argument inLiteralExpr.of(n)because Postgres rejects parameterized typmods, with per-op range validators.paradeDbScoreforpdb.score(<key>).ParadeDbProximityChaininsrc/core/proximity-chain.ts— an immutable builder with.within(distance, term, { ordered? })for multi-step chains mixing##and##>. Distances render asLiteralExpr(chained operators only accept literal slop). The chain implementsExpression<text>so it composes throughparadeDbMatchvia the@@@overload.src/exports/runtime.tsandpg_searchinstall viaSqlControlExtensionDescriptor.databaseDependencies.examples/paradedb-demois a new CLI example.prisma/contract.tsauthors a singleItemmodel with a bm25 index using the upstreamconstraints.index([...], { type: 'bm25', options: { key_field: 'id' } })shape;pnpm db:initproduces the actualCREATE INDEX … USING bm25 …DDL via the registry path. Adocker-compose.yamlplusinit/01-create-demo-db.sqlcreate a dedicateddemodatabase to sidestep the paradedb image's preloaded PostGIS tables.CLI commands:
match,top,fuzzy,proximity,proximity-chain,chain-demo(hardcoded multi-step + mixed direction),mode-tour(curated comparison of the five match modes against contrastive seed data), andcast-demo(boost/const/slop).Also includes a small adapter fix in
packages/3-targets/6-adapters/postgres/src/core/control-adapter.ts— index introspection now orders columns byarray_position(indkey, attnum)rather thanattnum, so multi-column indexes whose declared column order differs from the table's column declaration order satisfy schema verification.Behavior evidence
pnpm start -- mode-tourreturns distinct, contrastive results across the five match modes against the seed data, demonstrating each operator's character (any vs all vs term vs phrase).pnpm start -- cast-demoshows boost/const/slop modifying the BM25 score as expected (pdb.boost(5)raises score ~2 → ~10;pdb.const(1)flattens to 1;pdb.slop(1)matches non-adjacent phrase tokens).Follow-ups / open questions
paradeDbFuzzy/Boost/Const/Slopare typed as returningpg/text@1; actual SQL types are paradedb-specific (pdb.fuzzy,pdb.boost, etc.). The TS fudge works because match operators are overloaded to accept these types alongsidetext, but a more honest typing would introduce a paradedb codec.text[]query input (term-set withARRAY[...], pretokenized phrase) is deferred pending scalar-list support.'q'::pdb.whitespace), highlight (pdb.snippet*), aggregations, and search-time joins are not yet exposed.Non-goals
type/options, and PSL@@indexparsing all live inpsl-index-plus, not here.bm25Index({...})andbm25.text/numeric/...authoring helpers the original branch carried were intentionally dropped in favor of the genericconstraints.index({ type, options })registry path.