Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions packages/language/res/stdlib.zmodel
Original file line number Diff line number Diff line change
Expand Up @@ -407,6 +407,13 @@ attribute @omit()
*/
attribute @fuzzy() @@@targetField([StringField]) @@@once

/**
* Marks a `String` field as full-text-searchable. Fields with this attribute can be used with the
* `fts` filter operator and the `_ftsRelevance` orderBy. Full-text search is currently
* supported only on the `postgresql` provider (uses `to_tsvector` / `to_tsquery` / `ts_rank`).
*/
attribute @fullText() @@@targetField([StringField]) @@@once

/**
* Automatically stores the time when a record was last updated.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,18 @@ export default class AttributeApplicationValidator implements AstValidator<Attri
}
}

@check('@fullText')
private _checkFullText(attr: AttributeApplication, accept: ValidationAcceptor) {
const zmodel = AstUtils.getContainerOfType(attr, isModel);
if (!zmodel) {
return;
}
const provider = getDataSourceProvider(zmodel);
if (provider && provider !== 'postgresql') {
accept('error', `\`@fullText\` is only supported for the \`postgresql\` provider`, { node: attr });
}
}

@check('@@schema')
private _checkSchema(attr: AttributeApplication, accept: ValidationAcceptor) {
const schemaName = getStringLiteral(attr.args[0]?.value);
Expand Down
3 changes: 3 additions & 0 deletions packages/orm/src/client/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,9 @@ export const FILTER_PROPERTY_TO_KIND = {
// Fuzzy search operators
fuzzy: 'Fuzzy',

// Full-text search operators
fts: 'FullText',

// List operators
has: 'List',
hasEvery: 'List',
Expand Down
183 changes: 143 additions & 40 deletions packages/orm/src/client/crud-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -377,53 +377,94 @@ type FieldFilter<
AllowedKinds
>
: // primitive
AddFuzzyFilterIfSupported<
Schema,
Model,
Field,
AllowedKinds,
PrimitiveFilter<
GetModelFieldType<Schema, Model, Field>,
ModelFieldIsOptional<Schema, Model, Field>,
WithAggregations,
AllowedKinds
>
>;
GetModelFieldType<Schema, Model, Field> extends 'String'
? // string — additionally consider fuzzy / full-text augmentations
AddFullTextFilterIfSupported<
Schema,
Model,
Field,
AllowedKinds,
AddFuzzyFilterIfSupported<
Schema,
Model,
Field,
AllowedKinds,
PrimitiveFilter<
GetModelFieldType<Schema, Model, Field>,
ModelFieldIsOptional<Schema, Model, Field>,
WithAggregations,
AllowedKinds
>
>
>
: PrimitiveFilter<
GetModelFieldType<Schema, Model, Field>,
ModelFieldIsOptional<Schema, Model, Field>,
WithAggregations,
AllowedKinds
>;

/**
* Conditionally augments a primitive filter with the `fuzzy` operator when:
* 1. The field's type is `String`, AND
* 2. The `Fuzzy` filter kind is allowed for this field, AND
* 3. The schema's provider supports fuzzy search (postgres only), AND
* 4. The field is annotated with `@fuzzy` in the ZModel schema.
* Conditionally augments a string-field filter with the `fuzzy` operator when:
* 1. The `Fuzzy` filter kind is allowed for this field, AND
* 2. The schema's provider supports fuzzy search (postgres only), AND
* 3. The field is annotated with `@fuzzy` in the ZModel schema.
*
* Returns `Base` unchanged when any condition fails — never `Base & {}`,
* since intersecting with `{}` would strip `null`/`undefined` from `Base`.
* Caller is responsible for only invoking this on String-typed fields
* (the gate lives in `FieldFilter`).
*/
type AddFuzzyFilterIfSupported<
Schema extends SchemaDef,
Model extends GetModels<Schema>,
Field extends GetModelFields<Schema, Model>,
AllowedKinds extends FilterKind,
Base,
> =
GetModelFieldType<Schema, Model, Field> extends 'String'
? 'Fuzzy' extends AllowedKinds
? ProviderSupportsFuzzy<Schema> extends true
? GetModelField<Schema, Model, Field>['fuzzy'] extends true
? Base & {
/**
* Performs a fuzzy search on the string field. Only available when
* the schema's provider is `postgresql` (requires `pg_trgm` extension)
* and the field is annotated with `@fuzzy` in the ZModel schema.
* See {@link FuzzyFilterPayload} for the full options reference.
*/
fuzzy?: FuzzyFilterPayload;
}
: Base
: Base
> = 'Fuzzy' extends AllowedKinds
? ProviderSupportsFuzzy<Schema> extends true
? GetModelField<Schema, Model, Field>['fuzzy'] extends true
? Base & {
/**
* Performs a fuzzy search on the string field. Only available when
* the schema's provider is `postgresql` (requires `pg_trgm` extension)
* and the field is annotated with `@fuzzy` in the ZModel schema.
* See {@link FuzzyFilterPayload} for the full options reference.
*/
fuzzy?: FuzzyFilterPayload;
}
: Base
: Base
: Base;

/**
* Conditionally augments a string-field filter with the `fts` operator when:
* 1. The `FullText` filter kind is allowed for this field, AND
* 2. The schema's provider supports full-text search (postgres only), AND
* 3. The field is annotated with `@fullText` in the ZModel schema.
*
* Caller is responsible for only invoking this on String-typed fields
* (the gate lives in `FieldFilter`).
*/
type AddFullTextFilterIfSupported<
Schema extends SchemaDef,
Model extends GetModels<Schema>,
Field extends GetModelFields<Schema, Model>,
AllowedKinds extends FilterKind,
Base,
> = 'FullText' extends AllowedKinds
? ProviderSupportsFullText<Schema> extends true
? GetModelField<Schema, Model, Field>['fullText'] extends true
? Base & {
/**
* Performs a full-text search on the string field. Only available when
* the schema's provider is `postgresql` and the field is annotated with
* `@fullText` in the ZModel schema.
* See {@link FullTextFilterPayload} for the full options reference.
*/
fts?: FullTextFilterPayload;
}
: Base
: Base;
: Base
: Base;
Comment thread
ymc9 marked this conversation as resolved.

type EnumFilter<
Schema extends SchemaDef,
Expand Down Expand Up @@ -994,9 +1035,6 @@ export type FuzzyRelevanceOrderBy<Schema extends SchemaDef, Model extends GetMod
* Sorts by fuzzy search relevance using PostgreSQL `pg_trgm` similarity functions.
* Not supported on MySQL or SQLite (throws `NotSupported` at runtime).
* Cannot be combined with cursor-based pagination.
*
* The `_fuzzyRelevance` name is intentionally distinct from `_searchRelevance`
* (reserved for future full-text-search relevance) so the two can coexist.
*/
_fuzzyRelevance?: {
/**
Expand Down Expand Up @@ -1025,6 +1063,66 @@ export type FuzzyRelevanceOrderBy<Schema extends SchemaDef, Model extends GetMod
};
};

/**
* String fields that have been annotated with `@fullText` and are therefore eligible
* for `_ftsRelevance` ordering.
*/
type FullTextFields<Schema extends SchemaDef, Model extends GetModels<Schema>> = {
[Key in StringFields<Schema, Model>]: GetModelField<Schema, Model, Key>['fullText'] extends true ? Key : never;
}[StringFields<Schema, Model>];

/**
* Payload for the `fts` string filter operator. Performs full-text search using
* PostgreSQL `to_tsvector` / `to_tsquery` (postgresql provider only).
*
* Query syntax follows `to_tsquery`: callers write raw `&` (AND), `|` (OR),
* `!` (NOT), `<->` (FOLLOWED BY). Malformed queries throw at SQL execution time.
*/
export type FullTextFilterPayload = {
/**
* Search query in `to_tsquery` syntax (must be a non-empty string).
*/
search: string;
/**
* Postgres text-search configuration (e.g. `'english'`, `'simple'`). When
* omitted, the database's `default_text_search_config` setting is used —
* the SQL is emitted as `to_tsvector(field) @@ to_tsquery(query)` without
* an explicit regconfig argument.
*/
config?: string;
};

export type FtsRelevanceOrderBy<Schema extends SchemaDef, Model extends GetModels<Schema>> = {
/**
* Sorts by full-text-search relevance using PostgreSQL `ts_rank`.
*/
_ftsRelevance?: {
/**
* String fields annotated with `@fullText` to compute relevance against (must be non-empty).
*
* When multiple fields are provided, the fields are concatenated with a
* space separator and a single `ts_rank` is computed over the combined
* document — i.e. `ts_rank(to_tsvector(concat_ws(' ', f1, f2, ...)), q)`.
* This means an AND query (e.g. `'cat & dog'`) matches rows where the
* terms appear across different fields, not just within the same field.
*/
fields: [FullTextFields<Schema, Model>, ...FullTextFields<Schema, Model>[]];
/**
* The search term to compute relevance for (in `to_tsquery` syntax).
*/
search: string;
/**
* Postgres text-search configuration. When omitted, the database's
* `default_text_search_config` setting is used.
*/
config?: string;
/**
* Sort direction.
*/
sort: SortOrder;
};
};

export type OrderBy<
Schema extends SchemaDef,
Model extends GetModels<Schema>,
Expand Down Expand Up @@ -1377,7 +1475,8 @@ type SortAndTakeArgs<
*/
orderBy?: OrArray<
OrderBy<Schema, Model, true, false> &
(ProviderSupportsFuzzy<Schema> extends true ? FuzzyRelevanceOrderBy<Schema, Model> : {})
(ProviderSupportsFuzzy<Schema> extends true ? FuzzyRelevanceOrderBy<Schema, Model> : {}) &
(ProviderSupportsFullText<Schema> extends true ? FtsRelevanceOrderBy<Schema, Model> : {})
>;

/**
Expand Down Expand Up @@ -2757,6 +2856,10 @@ type ProviderSupportsDistinct<Schema extends SchemaDef> = Schema['provider']['ty

type ProviderSupportsFuzzy<Schema extends SchemaDef> = Schema['provider']['type'] extends 'postgresql' ? true : false;

type ProviderSupportsFullText<Schema extends SchemaDef> = Schema['provider']['type'] extends 'postgresql'
? true
: false;

/**
* Extracts extended query args for a specific operation.
*/
Expand Down
Loading
Loading