feat(orm): implement postgres full-text search#2653
Conversation
Introduces a Prisma-style full-text search capability gated by a new
field-level `@fullText` ZModel attribute. PostgreSQL only — MySQL/SQLite
throw NotSupported. Mirrors the existing `@fuzzy` design.
- Filter operator: `where: { title: { fts: { search, config? } } }`
emits `to_tsvector(field) @@ to_tsquery(query)` (or with a `::regconfig`
cast when `config` is provided; otherwise Postgres uses the database's
`default_text_search_config`).
- OrderBy operator: `_ftsRelevance: { fields, search, config?, sort }`
emits a single `ts_rank(...)`. Multi-field combines fields with
`concat_ws(' ', ...)` so AND queries match terms across fields
(matches Prisma's behavior).
- Type-level gating: the `fts` operator and `_ftsRelevance` orderBy
appear only on String fields annotated with `@fullText` and only when
the schema's provider is `postgresql`. Slicing's `'FullText'` filter
kind controls availability of the runtime operator.
- Cursor pagination is rejected when combined with `_ftsRelevance`
(parallel to `_fuzzyRelevance`).
Also refactors `buildOrderBy` to dispatch to small per-branch helpers
(`applyScalarOrderBy`, `applyAggregationOrderBy`, `applyRelationOrderBy`,
`applyFuzzyRelevanceOrderBy`, `applyFtsRelevanceOrderBy`).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (4)
✅ Files skipped from review due to trivial changes (1)
🚧 Files skipped from review as they are similar to previous changes (2)
📝 WalkthroughWalkthroughA new ChangesFull-Text Search Feature
Estimated Code Review Effort🎯 4 (Complex) | ⏱️ ~60 minutes Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Tip 💬 Introducing Slack Agent: The best way for teams to turn conversations into code.Slack Agent is built on CodeRabbit's deep understanding of your code, so your team can collaborate across the entire SDLC without losing context.
Built for teams:
One agent for your entire SDLC. Right inside Slack. 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 |
|
Claude finished @ymc9's task —— View job PR Review: Full-Text Search ImplementationI've conducted a comprehensive review of the full-text search implementation. Overall, this is a well-architected and secure implementation that follows good patterns. Review Tasks:
✅ StrengthsSecurity & SQL Injection Prevention
Type Safety
Architecture & Code Organization
Comprehensive Test CoverageThe test suite in
🔍 Areas for ConsiderationError Handling
Performance Considerations
Documentation
🏗️ Technical Implementation DetailsSQL Generation (
|
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/orm/src/client/crud-types.ts`:
- Around line 416-467: The current AddFuzzyFilterIfSupported and
AddFullTextFilterIfSupported intersect the extra operator bag directly into Base
causing union distribution that drops primitive shorthand (e.g., string); change
the augmentation to only apply to object branches by replacing "Base & { ... } :
Base" with a distributive conditional that checks whether Base extends object
(or Record<string, unknown>) and only then adds the fuzzy/fts operator (i.e.,
Base extends object ? Base & { fuzzy?: FuzzyFilterPayload } : Base for
AddFuzzyFilterIfSupported and similarly Base extends object ? Base & { fts?:
FullTextFilterPayload } : Base for AddFullTextFilterIfSupported), keeping the
existing guards that check ProviderSupportsFuzzy/ProviderSupportsFullText and
GetModelField[...]['fuzzy'|'fullText'].
In `@packages/orm/src/client/crud/dialects/postgresql.ts`:
- Around line 709-719: The single-field branch builds `document` from
`fieldRefs[0]` but doesn't coalesce nullable strings, so
`to_tsvector(${document})` can yield NULL and make `ts_rank` return NULL; update
the single-field branch that defines `document` (the `document` variable used
with `to_tsvector`/`ts_rank` and in `query.orderBy`) to coalesce the single
field to an empty string (same null-skipping behavior as the multi-field
`concat_ws` path) before passing it to `to_tsvector` and `ts_rank`.
🪄 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.yaml
Review profile: CHILL
Plan: Pro
Run ID: 9913c800-a26e-4f36-a060-f28be32e840f
📒 Files selected for processing (14)
packages/language/res/stdlib.zmodelpackages/language/src/validators/attribute-application-validator.tspackages/orm/src/client/constants.tspackages/orm/src/client/crud-types.tspackages/orm/src/client/crud/dialects/base-dialect.tspackages/orm/src/client/crud/dialects/mysql.tspackages/orm/src/client/crud/dialects/postgresql.tspackages/orm/src/client/crud/dialects/sqlite.tspackages/orm/src/client/zod/factory.tspackages/schema/src/schema.tspackages/sdk/src/ts-schema-generator.tstests/e2e/orm/client-api/full-text-search.test.tstests/e2e/orm/schemas/full-text-search/schema.tstests/e2e/orm/schemas/full-text-search/schema.zmodel
`to_tsvector(NULL)` returns NULL and `ts_rank(NULL, ...)` therefore
returns NULL — under Postgres's default `NULLS FIRST` for `ORDER BY DESC`
this would surface NULL-valued rows ahead of any matching ones, an
asymmetry with the multi-field path where `concat_ws(' ', ...)` already
skips NULLs and yields a 0.0 rank. Coalescing the field to `''` aligns
the two paths.
Adds `subtitle: String? @fullText` to the test fixture and a regression
test that orders a NULL-subtitle row against a matching one — without
the fix, the NULL row ranks first under DESC.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Claude finished @ymc9's task —— View job I'll review this full-text search implementation for code quality, security, performance, and test coverage. Review Tasks:
PR Review: Full-Text Search ImplementationI've conducted a comprehensive review of the full-text search implementation. Overall, this is a well-architected and secure implementation that follows good patterns. ✅ StrengthsSecurity & SQL Injection Prevention
Type Safety
Architecture & Code Organization
Comprehensive Test CoverageThe test suite in
🔍 Areas for ConsiderationError Handling
Performance Considerations
Documentation
🏗️ Technical Implementation DetailsSQL Generation (
|

Summary
@fullTextZModel attribute. PostgreSQL only — MySQL/SQLite throwNotSupported. Mirrors the existing@fuzzydesign.fts:where: { title: { fts: { search, config? } } }emitsto_tsvector(field) @@ to_tsquery(query)(with an inline::regconfigcast whenconfigis provided; otherwise Postgres falls back todefault_text_search_config)._ftsRelevance:{ fields, search, config?, sort }emits a singlets_rank(...). Multi-field combines fields withconcat_ws(' ', ...)so AND queries (e.g.cat & dog) match rows where the terms are split across fields — matches Prisma's behavior.ftsoperator and_ftsRelevanceorderBy appear only on String fields annotated with@fullText, only when the provider ispostgresql. The'FullText'filter kind plugs into existing slicing._ftsRelevance(parallel to_fuzzyRelevance).buildOrderByinto per-branch dispatchers:applyScalarOrderBy,applyAggregationOrderBy,applyRelationOrderBy,applyFuzzyRelevanceOrderBy,applyFtsRelevanceOrderBy.Surface
Test plan
pnpm build(full graph)pnpm lintpnpm test:typecheck)full-text-search.test.ts— 31 tests covering: basicfts,&/|/!/<->operators,config(english stems, simple does not, per-query isolation, DB-default fallback), composition with other filters,_ftsRelevancesingle + multi-field (incl. AND-across-fields), pagination + cursor guard, mutations (updateMany/deleteMany),@fullTextgating (Zod-precise error messages), malformed query passthrough, OrArray contract, and three filter-kind slicing testsfuzzy-search.test.ts(64 tests)find,filter,aggregate,group-by,relation)Notes
concat_ws(Prisma parity) rather than summing per-fieldts_rank— chosen because the SUM form silently scores0on AND queries when terms span fields.setweight(), schema-level default config (@fullText(config: "english")), GIN-index hint generation, and storedtsvectorcolumns. Wrapping underfts(vs. Prisma's flatsearch: string) leaves room for these.🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Tests