uql-orm 0.71.0 → 0.72.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/browser/uql-browser.min.js +2 -2
- package/dist/browser/uql-browser.min.js.map +3 -3
- package/dist/dialect/abstractSqlDialect.d.ts +15 -2
- package/dist/dialect/abstractSqlDialect.js +60 -11
- package/dist/dialect/aliases.d.ts +2 -0
- package/dist/dialect/aliases.js +2 -0
- package/dist/dialect/mysqlLikeSqlDialect.d.ts +5 -0
- package/dist/dialect/mysqlLikeSqlDialect.js +9 -1
- package/dist/dialect/pgLikeSqlDialect.d.ts +7 -0
- package/dist/dialect/pgLikeSqlDialect.js +24 -5
- package/dist/dialect/queryJoins.d.ts +0 -1
- package/dist/entity/metadata/definition.js +4 -2
- package/dist/maria/mariaDialect.js +1 -2
- package/dist/migrate/cli.js +2 -1
- package/dist/migrate/ddl/indexDdl.d.ts +2 -0
- package/dist/migrate/ddl/indexDdl.js +4 -0
- package/dist/migrate/ddl/mysqlIndexDdl.d.ts +5 -0
- package/dist/migrate/ddl/mysqlIndexDdl.js +7 -0
- package/dist/migrate/generator/mongoCommand.d.ts +2 -0
- package/dist/migrate/generator/mongoSchemaGenerator.js +3 -0
- package/dist/migrate/introspection/mongoIntrospector.d.ts +1 -1
- package/dist/migrate/introspection/mongoIntrospector.js +12 -6
- package/dist/migrate/schemaGenerator.d.ts +4 -1
- package/dist/migrate/schemaGenerator.js +14 -7
- package/dist/mongo/mongoDialect.d.ts +3 -3
- package/dist/mongo/mongoDialect.js +32 -13
- package/dist/mongo/mongodbQuerier.js +2 -3
- package/dist/mssql/mssqlDialect.js +1 -0
- package/dist/schema/indexDifferences.d.ts +2 -1
- package/dist/schema/indexDifferences.js +8 -1
- package/dist/schema/schemaASTBuilder.d.ts +2 -0
- package/dist/schema/schemaASTBuilder.js +23 -0
- package/dist/sqlite/sqliteDialect.d.ts +2 -0
- package/dist/sqlite/sqliteDialect.js +5 -0
- package/dist/type/dialect.d.ts +5 -0
- package/dist/type/entity.d.ts +18 -10
- package/dist/type/query.d.ts +11 -4
- package/dist/type/queryAggregate.d.ts +1 -8
- package/dist/type/utility.d.ts +13 -0
- package/dist/util/dialect.util.d.ts +29 -1
- package/dist/util/dialect.util.js +50 -1
- package/package.json +1 -2
- package/skills/uql-orm/SKILL.md +6 -2
|
@@ -50,6 +50,10 @@ export class IndexDdl {
|
|
|
50
50
|
`ON ${this.dialect.escapeId(tableName)}${this.indexAccessMethod(index)} (${columns})` +
|
|
51
51
|
`${this.indexInclude(index)}${this.indexTuning(index)}${this.indexPredicate(index)};`);
|
|
52
52
|
}
|
|
53
|
+
/** What an index added to a table that has rows needs run after it to serve queries; nothing, mostly. */
|
|
54
|
+
settleStatements(_tableName, _index) {
|
|
55
|
+
return [];
|
|
56
|
+
}
|
|
53
57
|
/**
|
|
54
58
|
* Index features this dialect can express. Everything here is supported by at least one engine and
|
|
55
59
|
* refused by at least one other, so an index asking for a missing one is rejected rather than
|
|
@@ -6,6 +6,11 @@ export declare class MysqlLikeIndexDdl extends IndexDdl {
|
|
|
6
6
|
protected readonly indexFeatures: Set<"expression" | "include" | "jsonArray" | "jsonPath" | "nullsOrder" | "opsClass" | "partial" | "prefixLength">;
|
|
7
7
|
protected readonly indexTypes: ReadonlySet<IndexType>;
|
|
8
8
|
protected readonly indexTypeKeywords: ReadonlyMap<IndexType, string>;
|
|
9
|
+
/**
|
|
10
|
+
* InnoDB fills a fulltext index added beside another on a loaded table only once the table is optimized:
|
|
11
|
+
* until then MariaDB scores it 0 and MySQL can fail a `MATCH` over it (MySQL 26.7, MariaDB 12.3).
|
|
12
|
+
*/
|
|
13
|
+
settleStatements(tableName: string, index: IndexSchema): string[];
|
|
9
14
|
/** ` USING btree|hash` trails the columns: between the table and them, it is a syntax error here. */
|
|
10
15
|
protected indexTuning(index: IndexSchema): string;
|
|
11
16
|
}
|
|
@@ -11,6 +11,13 @@ export class MysqlLikeIndexDdl extends IndexDdl {
|
|
|
11
11
|
indexFeatures = new Set(['expression', 'prefixLength']);
|
|
12
12
|
indexTypes = new Set(['btree', 'hash', 'fulltext']);
|
|
13
13
|
indexTypeKeywords = MYSQL_LIKE_INDEX_KEYWORDS;
|
|
14
|
+
/**
|
|
15
|
+
* InnoDB fills a fulltext index added beside another on a loaded table only once the table is optimized:
|
|
16
|
+
* until then MariaDB scores it 0 and MySQL can fail a `MATCH` over it (MySQL 26.7, MariaDB 12.3).
|
|
17
|
+
*/
|
|
18
|
+
settleStatements(tableName, index) {
|
|
19
|
+
return index.type === 'fulltext' ? [`OPTIMIZE TABLE ${this.dialect.escapeId(tableName)};`] : [];
|
|
20
|
+
}
|
|
14
21
|
/** ` USING btree|hash` trails the columns: between the table and them, it is a syntax error here. */
|
|
15
22
|
indexTuning(index) {
|
|
16
23
|
return index.type && !this.indexTypeKeywords.has(index.type) ? ` USING ${index.type}` : '';
|
|
@@ -5,6 +5,8 @@ export type MongoIndexOptions = {
|
|
|
5
5
|
readonly unique: boolean;
|
|
6
6
|
readonly name: string;
|
|
7
7
|
readonly partialFilterExpression?: Readonly<Record<string, unknown>>;
|
|
8
|
+
/** A text index's weight per field, which `textScore` multiplies a match in it by. */
|
|
9
|
+
readonly weights?: Readonly<Record<string, number>>;
|
|
8
10
|
};
|
|
9
11
|
/** A field of an Atlas vector search index: the vector itself, or one its `filter` pre-filters on. */
|
|
10
12
|
export type MongoVectorSearchField = {
|
|
@@ -3,6 +3,7 @@ import { MongoDialect } from '../../mongo/mongoDialect.js';
|
|
|
3
3
|
import { QueryRaw, } from '../../type/index.js';
|
|
4
4
|
import { indexDistance, unsupportedVectorMetric } from '../../type/vector.js';
|
|
5
5
|
import { declaredIndexes, declaredIndexName, renderIndexColumn } from '../../util/ddlExpression.util.js';
|
|
6
|
+
import { fulltextWeights } from '../../util/dialect.util.js';
|
|
6
7
|
import { assertIndexFeatures, assertIndexType } from '../ddl/indexDdl.js';
|
|
7
8
|
import { assertIndexPredicate, refusedIndexPredicate } from '../indexPredicate.js';
|
|
8
9
|
import { renderIndexDefinition } from './definitionToNode.js';
|
|
@@ -121,6 +122,7 @@ export class MongoSchemaGenerator extends MongoDialect {
|
|
|
121
122
|
for (const entry of index.entries) {
|
|
122
123
|
key[entry.column] = index.type === 'fulltext' ? 'text' : entry.order === 'desc' ? -1 : 1;
|
|
123
124
|
}
|
|
125
|
+
const weights = fulltextWeights(index);
|
|
124
126
|
return serializeMongoCommand({
|
|
125
127
|
action: 'createIndex',
|
|
126
128
|
collection: tableName,
|
|
@@ -130,6 +132,7 @@ export class MongoSchemaGenerator extends MongoDialect {
|
|
|
130
132
|
unique: index.unique,
|
|
131
133
|
name: index.name,
|
|
132
134
|
partialFilterExpression: index.where && JSON.parse(index.where),
|
|
135
|
+
weights: weights && Object.fromEntries(index.entries.map((entry, at) => [entry.column, weights[at]])),
|
|
133
136
|
},
|
|
134
137
|
});
|
|
135
138
|
}
|
|
@@ -7,7 +7,7 @@ import { type QuerierPool, type SchemaIntrospector, type TableSchema } from '../
|
|
|
7
7
|
*/
|
|
8
8
|
export declare class MongoSchemaIntrospector implements SchemaIntrospector {
|
|
9
9
|
private readonly pool;
|
|
10
|
-
/** `listIndexes` reports keys and
|
|
10
|
+
/** `listIndexes` reports keys, uniqueness and text weights; a `partialFilterExpression` is no SQL predicate. */
|
|
11
11
|
readonly indexFacets: ReadonlySet<IndexFacet>;
|
|
12
12
|
constructor(pool: QuerierPool);
|
|
13
13
|
introspect(tables?: readonly string[]): Promise<SchemaAST>;
|
|
@@ -8,8 +8,8 @@ const SEARCH_NOT_ENABLED = 31082;
|
|
|
8
8
|
*/
|
|
9
9
|
export class MongoSchemaIntrospector {
|
|
10
10
|
pool;
|
|
11
|
-
/** `listIndexes` reports keys and
|
|
12
|
-
indexFacets = new Set();
|
|
11
|
+
/** `listIndexes` reports keys, uniqueness and text weights; a `partialFilterExpression` is no SQL predicate. */
|
|
12
|
+
indexFacets = new Set(['textWeights']);
|
|
13
13
|
constructor(pool) {
|
|
14
14
|
this.pool = pool;
|
|
15
15
|
}
|
|
@@ -38,10 +38,12 @@ export class MongoSchemaIntrospector {
|
|
|
38
38
|
name: tableName,
|
|
39
39
|
columns: [],
|
|
40
40
|
indexes: [
|
|
41
|
-
...indexes.map((
|
|
42
|
-
name:
|
|
43
|
-
|
|
44
|
-
|
|
41
|
+
...indexes.map(({ name, key, unique, weights }) => ({
|
|
42
|
+
name: name ?? Object.keys(key).join('_'),
|
|
43
|
+
unique: !!unique,
|
|
44
|
+
...(weights
|
|
45
|
+
? { entries: Object.entries(weights).map(textIndexEntry), type: 'fulltext' }
|
|
46
|
+
: { entries: Object.keys(key).map((column) => ({ column })) }),
|
|
45
47
|
})),
|
|
46
48
|
...searchIndexes
|
|
47
49
|
.filter((idx) => idx.type === 'vectorSearch')
|
|
@@ -75,6 +77,10 @@ export class MongoSchemaIntrospector {
|
|
|
75
77
|
});
|
|
76
78
|
}
|
|
77
79
|
}
|
|
80
|
+
/** A text index field as an entity declares it: its weight stated only where it is not the default 1. */
|
|
81
|
+
function textIndexEntry([column, weight]) {
|
|
82
|
+
return weight === 1 ? { column } : { column, weight };
|
|
83
|
+
}
|
|
78
84
|
/** A collection's Atlas search indexes, none where the server has no Atlas Search. */
|
|
79
85
|
async function listSearchIndexes(collection) {
|
|
80
86
|
try {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { AbstractSqlDialect } from '../dialect/index.js';
|
|
2
2
|
import type { SchemaAST } from '../schema/schemaAST.js';
|
|
3
|
+
import { type BuildSchemaASTOptions } from '../schema/schemaASTBuilder.js';
|
|
3
4
|
import { type DiffOptions } from '../schema/schemaASTDiffer.js';
|
|
4
5
|
import type { CanonicalType, ColumnNode, ForeignKeyAction, IndexNode, TableNode } from '../schema/types.js';
|
|
5
6
|
import type { ColumnSchema, CreateSchemaOptions, DialectFeatures, DropSchemaOptions, EntityMeta, EntityWhereMeta, FieldMeta, FieldOptions, ForeignKeySchema, IndexSchema, NamingStrategy, SchemaDiff, SchemaGenerator, Type } from '../type/index.js';
|
|
@@ -65,6 +66,8 @@ export declare class SqlSchemaGenerator implements SchemaGenerator {
|
|
|
65
66
|
generateCreateIndex(tableName: string, index: IndexSchema, options?: {
|
|
66
67
|
ifNotExists?: boolean;
|
|
67
68
|
}): string;
|
|
69
|
+
/** An index added to a table that may already have rows: its `CREATE`, then what the engine needs after. */
|
|
70
|
+
private addIndexStatements;
|
|
68
71
|
/**
|
|
69
72
|
* `schema` is the table's, because that is where its indexes live. MySQL takes it from the table
|
|
70
73
|
* operand instead, which is already qualified.
|
|
@@ -175,4 +178,4 @@ export declare class SqlSchemaGenerator implements SchemaGenerator {
|
|
|
175
178
|
* The entities as an AST, named by `generator`'s resolvers rather than a naming strategy, which would
|
|
176
179
|
* also rename an explicit `@Entity({ name })` and so compare each table under another name.
|
|
177
180
|
*/
|
|
178
|
-
export declare function buildEntityAST(generator: Pick<SchemaGenerator, 'resolveTableAlias' | 'resolveSchema' | 'resolveColumnName' | 'compileDdl' | 'compileIndexPredicate'>, entities: readonly Type<object>[],
|
|
181
|
+
export declare function buildEntityAST(generator: Pick<SchemaGenerator, 'resolveTableAlias' | 'resolveSchema' | 'resolveColumnName' | 'compileDdl' | 'compileIndexPredicate'>, entities: readonly Type<object>[], options?: Pick<BuildSchemaASTOptions, 'defaultForeignKeyAction' | 'textScoreIndexes'>): SchemaAST;
|
|
@@ -74,7 +74,10 @@ export class SqlSchemaGenerator {
|
|
|
74
74
|
}
|
|
75
75
|
/** The entity side as an AST, carrying this generator's default referential action. */
|
|
76
76
|
buildAST(entities) {
|
|
77
|
-
return buildEntityAST(this, entities,
|
|
77
|
+
return buildEntityAST(this, entities, {
|
|
78
|
+
defaultForeignKeyAction: this.defaultForeignKeyAction,
|
|
79
|
+
textScoreIndexes: this.dialect.features.textScoreIndexes,
|
|
80
|
+
});
|
|
78
81
|
}
|
|
79
82
|
/**
|
|
80
83
|
* Every `CREATE TABLE` for `entities`, then their foreign keys, since a relation graph is routinely
|
|
@@ -115,7 +118,7 @@ export class SqlSchemaGenerator {
|
|
|
115
118
|
* resolves instead of being silently dropped.
|
|
116
119
|
*/
|
|
117
120
|
orderedTables(entities, direction, only) {
|
|
118
|
-
const ast =
|
|
121
|
+
const ast = this.buildAST(entities);
|
|
119
122
|
const tables = direction === 'create' ? ast.getCreateOrder() : ast.getDropOrder();
|
|
120
123
|
if (!only) {
|
|
121
124
|
return tables;
|
|
@@ -166,7 +169,7 @@ export class SqlSchemaGenerator {
|
|
|
166
169
|
// Add indexes
|
|
167
170
|
if (diff.indexesToAdd?.length) {
|
|
168
171
|
for (const index of diff.indexesToAdd) {
|
|
169
|
-
statements.push(this.
|
|
172
|
+
statements.push(...this.addIndexStatements(diff.tableName, index));
|
|
170
173
|
}
|
|
171
174
|
}
|
|
172
175
|
// Drop indexes
|
|
@@ -243,6 +246,10 @@ export class SqlSchemaGenerator {
|
|
|
243
246
|
generateCreateIndex(tableName, index, options = {}) {
|
|
244
247
|
return this.indexDdl.getCreateIndexStatement(tableName, index, options);
|
|
245
248
|
}
|
|
249
|
+
/** An index added to a table that may already have rows: its `CREATE`, then what the engine needs after. */
|
|
250
|
+
addIndexStatements(tableName, index) {
|
|
251
|
+
return [this.generateCreateIndex(tableName, index), ...this.indexDdl.settleStatements(tableName, index)];
|
|
252
|
+
}
|
|
246
253
|
/**
|
|
247
254
|
* `schema` is the table's, because that is where its indexes live. MySQL takes it from the table
|
|
248
255
|
* operand instead, which is already qualified.
|
|
@@ -338,7 +345,7 @@ export class SqlSchemaGenerator {
|
|
|
338
345
|
}
|
|
339
346
|
// Keyed by the qualified name this generator resolves, which is the key the AST stores the table
|
|
340
347
|
// under.
|
|
341
|
-
const desired = (desiredAst ??
|
|
348
|
+
const desired = (desiredAst ?? this.buildAST([entity])).getTable(tableName);
|
|
342
349
|
if (!desired) {
|
|
343
350
|
return undefined;
|
|
344
351
|
}
|
|
@@ -556,7 +563,7 @@ export class SqlSchemaGenerator {
|
|
|
556
563
|
case 'alterColumn':
|
|
557
564
|
return this.generateAlterColumnSql(operation.tableName, operation.columnName, operation.changes);
|
|
558
565
|
case 'createIndex':
|
|
559
|
-
return
|
|
566
|
+
return this.addIndexStatements(operation.tableName, renderIndexDefinition(operation.index, (sql) => this.dialect.compileDdl(sql)));
|
|
560
567
|
case 'dropIndex':
|
|
561
568
|
return [this.generateDropIndex(operation.tableName, operation.indexName)];
|
|
562
569
|
case 'addForeignKey':
|
|
@@ -693,7 +700,7 @@ function foreignKeyOf(relation) {
|
|
|
693
700
|
* The entities as an AST, named by `generator`'s resolvers rather than a naming strategy, which would
|
|
694
701
|
* also rename an explicit `@Entity({ name })` and so compare each table under another name.
|
|
695
702
|
*/
|
|
696
|
-
export function buildEntityAST(generator, entities,
|
|
703
|
+
export function buildEntityAST(generator, entities, options = {}) {
|
|
697
704
|
return buildSchemaAST(entities, {
|
|
698
705
|
// The alias, not `resolveTableName`: a node holds its schema separately, so that a name derived
|
|
699
706
|
// from it stays a single identifier.
|
|
@@ -702,6 +709,6 @@ export function buildEntityAST(generator, entities, defaultForeignKeyAction) {
|
|
|
702
709
|
resolveColumnName: (key, field) => generator.resolveColumnName(key, field),
|
|
703
710
|
compileDdl: (sql, entity) => generator.compileDdl(sql, entity),
|
|
704
711
|
compileIndexPredicate: (where, entity, indexName) => generator.compileIndexPredicate(where, entity, indexName),
|
|
705
|
-
|
|
712
|
+
...options,
|
|
706
713
|
});
|
|
707
714
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type Document, type Filter, type Sort, type UpdateFilter } from 'mongodb';
|
|
2
2
|
import { AbstractDialect } from '../dialect/abstractDialect.js';
|
|
3
|
-
import type { DialectFeatures, EntityData, EntityMeta, Query, QueryAggMap, QueryAggregate, QueryExclude, QueryGroupMap, QueryOptions, QueryPager,
|
|
3
|
+
import type { DialectFeatures, EntityData, EntityMeta, Query, QueryAggMap, QueryAggregate, QueryExclude, QueryGroupMap, QueryOptions, QueryPager, QuerySelectValue, QuerySortMap, QueryVectorSearch, QueryWhere, Type } from '../type/index.js';
|
|
4
4
|
import { type CallbackKey } from '../util/index.js';
|
|
5
5
|
/** What a read pipeline contributes to {@link MongoDialect.readStages} beyond the query itself. */
|
|
6
6
|
type MongoReadStages = {
|
|
@@ -122,7 +122,7 @@ export declare class MongoDialect extends AbstractDialect {
|
|
|
122
122
|
* means a *populated* one, at every level of the path: a lookup adds a field to the result, so one
|
|
123
123
|
* added for the sort alone would change what the caller gets back.
|
|
124
124
|
*/
|
|
125
|
-
sort<E extends Document>(entity: Type<E>, sort
|
|
125
|
+
sort<E extends Document>(entity: Type<E>, { $sort: sort, $populate: populate, $where: where }: Query<E>): Sort;
|
|
126
126
|
/** Walks `$sort` against the metadata of the entity each level addresses, as the SQL dialects do. */
|
|
127
127
|
private collectSort;
|
|
128
128
|
/**
|
|
@@ -237,7 +237,7 @@ export declare class MongoDialect extends AbstractDialect {
|
|
|
237
237
|
*/
|
|
238
238
|
getUpdateFilter<E extends Document>(persistable: Partial<E>): UpdateFilter<E> | Document[];
|
|
239
239
|
/**
|
|
240
|
-
*
|
|
240
|
+
* An update as one pipeline, since MongoDB refuses two operators on one path: each path composed as
|
|
241
241
|
* `$pull`, `$set`, `$push`, then `$unset`, as SQL does, with values as `$literal` so `$x` stays data.
|
|
242
242
|
*/
|
|
243
243
|
private getUpdatePipeline;
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { ObjectId } from 'mongodb';
|
|
2
2
|
import { AbstractDialect } from '../dialect/abstractDialect.js';
|
|
3
|
-
import { AGGREGATE_VALUE_ALIAS, REL_NESTED_KEY, REL_TEMP_PREFIX, SUM_COUNT_ALIAS, sortCountField, } from '../dialect/aliases.js';
|
|
3
|
+
import { AGGREGATE_VALUE_ALIAS, REL_NESTED_KEY, REL_TEMP_PREFIX, SUM_COUNT_ALIAS, sortCountField, TEXT_SCORE_ALIAS, } from '../dialect/aliases.js';
|
|
4
4
|
import { groupPathField, resolveGroupJoins, resolveQueryJoins, resolveSortableJoin, } from '../dialect/queryJoins.js';
|
|
5
5
|
import { assertSoleId, fieldOf, getMeta, relationOf, soleIdOf } from '../entity/index.js';
|
|
6
6
|
import { COUNT_RESULT_KEY } from '../type/query.js';
|
|
7
7
|
import { QueryRaw } from '../type/queryRaw.js';
|
|
8
|
-
import { aggregateOf, asSelectMap, assertAggregateColumns, assertNonNegativeInteger, columnFamily, countedRelations, entityName, fillOnFields, filterFieldKeys, findVectorIndex, findVectorSort, getKeys, getRelationRequestSummary, hasKeys, isJsonObject, isJsonUpdateOp, isOperatorMap, isOperatorObject, isRecord, isVectorSearch, normalizeScalarFieldSelection, parentJoins, parseGroupMap, parseRelationAtKey, parseRelationSize, parseSortByCount, someKey, targetKeyColumns, } from '../util/index.js';
|
|
8
|
+
import { aggregateOf, asSelectMap, assertAggregateColumns, assertNonNegativeInteger, columnFamily, countedRelations, entityName, fieldUpdateOf, fillOnFields, filterFieldKeys, findVectorIndex, findVectorSort, getKeys, getRelationRequestSummary, hasKeys, isFieldUpdateOp, isJsonObject, isJsonUpdateOp, isOperatorMap, isOperatorObject, isRecord, isVectorSearch, normalizeScalarFieldSelection, parentJoins, parseGroupMap, parseRelationAtKey, parseRelationSize, parseSortByCount, rankedTextSearch, someKey, targetKeyColumns, } from '../util/index.js';
|
|
9
|
+
/** A scalar field's operator as the aggregation operator computing it. */
|
|
10
|
+
const MONGO_ARITHMETIC = { $inc: '$add', $mul: '$multiply' };
|
|
9
11
|
/** Default {@link DialectFeatures} for MongoDB. */
|
|
10
12
|
export const mongoDialectFeatures = {
|
|
11
13
|
ifNotExists: false,
|
|
@@ -454,9 +456,13 @@ export class MongoDialect extends AbstractDialect {
|
|
|
454
456
|
* means a *populated* one, at every level of the path: a lookup adds a field to the result, so one
|
|
455
457
|
* added for the sort alone would change what the caller gets back.
|
|
456
458
|
*/
|
|
457
|
-
sort(entity, sort, populate) {
|
|
459
|
+
sort(entity, { $sort: sort, $populate: populate, $where: where }) {
|
|
458
460
|
const meta = getMeta(entity);
|
|
459
461
|
const normalized = {};
|
|
462
|
+
// Refused as the SQL dialects refuse it, before MongoDB answers a missing score with its own error.
|
|
463
|
+
if (sort?.$text) {
|
|
464
|
+
rankedTextSearch(where);
|
|
465
|
+
}
|
|
460
466
|
// The same join set the lookups are built from, so what an ordering may address and what the
|
|
461
467
|
// pipeline actually produces cannot drift apart - `$sort` contributes its own to-one joins here
|
|
462
468
|
// exactly as it does on the SQL dialects.
|
|
@@ -467,6 +473,13 @@ export class MongoDialect extends AbstractDialect {
|
|
|
467
473
|
collectSort(meta, sort, joins, path, out) {
|
|
468
474
|
for (const [key, value] of Object.entries(sort ?? {})) {
|
|
469
475
|
const relation = meta.relations[key];
|
|
476
|
+
if (key === '$text') {
|
|
477
|
+
if (path) {
|
|
478
|
+
throw new TypeError(`$sort by $text is only supported on the queried entity, not on relation '${path.slice(0, -1)}'`);
|
|
479
|
+
}
|
|
480
|
+
out[TEXT_SCORE_ALIAS] = { $meta: 'textScore' };
|
|
481
|
+
continue;
|
|
482
|
+
}
|
|
470
483
|
if (!relation) {
|
|
471
484
|
// The queried entity's own vector search is lifted out before this walk, so one reaching it
|
|
472
485
|
// sits under a relation, which a `$lookup` brings in one row at a time - there is nothing to
|
|
@@ -553,7 +566,7 @@ export class MongoDialect extends AbstractDialect {
|
|
|
553
566
|
const relOpts = relationOf(meta, spec.relation);
|
|
554
567
|
const query = spec.query ?? {};
|
|
555
568
|
const tail = [
|
|
556
|
-
...(query.$sort ? [{ $sort: this.sort(relOpts.entity(), query
|
|
569
|
+
...(query.$sort ? [{ $sort: this.sort(relOpts.entity(), query) }] : []),
|
|
557
570
|
...this.pagerStages(query),
|
|
558
571
|
spec.field
|
|
559
572
|
? {
|
|
@@ -669,7 +682,7 @@ export class MongoDialect extends AbstractDialect {
|
|
|
669
682
|
return [
|
|
670
683
|
...this.matchStages(entity, q.$where, opts, this.aggregateKeys(entity, q)),
|
|
671
684
|
...this.readStages(entity, q, {
|
|
672
|
-
sort: this.sort(entity, q
|
|
685
|
+
sort: this.sort(entity, q),
|
|
673
686
|
pager: this.pagerStages(q),
|
|
674
687
|
}),
|
|
675
688
|
];
|
|
@@ -912,8 +925,14 @@ export class MongoDialect extends AbstractDialect {
|
|
|
912
925
|
const set = {};
|
|
913
926
|
const push = {};
|
|
914
927
|
const pull = {};
|
|
928
|
+
const arithmetic = {};
|
|
915
929
|
const unset = new Set();
|
|
916
930
|
for (const [key, value] of Object.entries(persistable)) {
|
|
931
|
+
if (isFieldUpdateOp(value)) {
|
|
932
|
+
const [op, operand] = fieldUpdateOf(value);
|
|
933
|
+
arithmetic[key] = { [MONGO_ARITHMETIC[op]]: [{ $ifNull: [`$${key}`, 0] }, { $literal: operand }] };
|
|
934
|
+
continue;
|
|
935
|
+
}
|
|
917
936
|
if (!isJsonUpdateOp(value)) {
|
|
918
937
|
set[key] = value;
|
|
919
938
|
continue;
|
|
@@ -931,7 +950,7 @@ export class MongoDialect extends AbstractDialect {
|
|
|
931
950
|
pull[`${key}.${path}`] = v;
|
|
932
951
|
}
|
|
933
952
|
}
|
|
934
|
-
return { set, push, pull, unset };
|
|
953
|
+
return { set, push, pull, arithmetic, unset };
|
|
935
954
|
}
|
|
936
955
|
/**
|
|
937
956
|
* Turn a persistable payload into a MongoDB update, mapping UQL's JSON operators onto their native
|
|
@@ -940,12 +959,12 @@ export class MongoDialect extends AbstractDialect {
|
|
|
940
959
|
*/
|
|
941
960
|
getUpdateFilter(persistable) {
|
|
942
961
|
const groups = this.groupUpdateOperators(persistable);
|
|
943
|
-
const { set, push, pull, unset } = groups;
|
|
962
|
+
const { set, push, pull, arithmetic, unset } = groups;
|
|
944
963
|
const exprKeys = [...Object.keys(pull), ...Object.keys(set), ...Object.keys(push)];
|
|
945
|
-
// Native `$pull` fails on a value that is no array,
|
|
946
|
-
// path in a single update document:
|
|
964
|
+
// Native `$pull` fails on a value that is no array, native `$inc`/`$mul` on a `null`, and MongoDB
|
|
965
|
+
// rejects two operators targeting one path in a single update document: each forces the pipeline form.
|
|
947
966
|
const allPaths = [...exprKeys, ...unset];
|
|
948
|
-
if (hasKeys(pull) || new Set(allPaths).size < allPaths.length) {
|
|
967
|
+
if (hasKeys(pull) || hasKeys(arithmetic) || new Set(allPaths).size < allPaths.length) {
|
|
949
968
|
return this.getUpdatePipeline(groups, new Set(exprKeys));
|
|
950
969
|
}
|
|
951
970
|
return {
|
|
@@ -955,11 +974,11 @@ export class MongoDialect extends AbstractDialect {
|
|
|
955
974
|
};
|
|
956
975
|
}
|
|
957
976
|
/**
|
|
958
|
-
*
|
|
977
|
+
* An update as one pipeline, since MongoDB refuses two operators on one path: each path composed as
|
|
959
978
|
* `$pull`, `$set`, `$push`, then `$unset`, as SQL does, with values as `$literal` so `$x` stays data.
|
|
960
979
|
*/
|
|
961
|
-
getUpdatePipeline({ set, push, pull, unset }, exprPaths) {
|
|
962
|
-
const assignments = {};
|
|
980
|
+
getUpdatePipeline({ set, push, pull, arithmetic, unset }, exprPaths) {
|
|
981
|
+
const assignments = { ...arithmetic };
|
|
963
982
|
for (const path of exprPaths) {
|
|
964
983
|
let expr = { $ifNull: [`$${path}`, []] };
|
|
965
984
|
if (path in pull) {
|
|
@@ -93,7 +93,7 @@ export class MongodbQuerier extends AbstractQuerier {
|
|
|
93
93
|
if (hasKeys(select)) {
|
|
94
94
|
cursor.project(select);
|
|
95
95
|
}
|
|
96
|
-
const sort = this.dialect.sort(entity, q
|
|
96
|
+
const sort = this.dialect.sort(entity, q);
|
|
97
97
|
if (hasKeys(sort)) {
|
|
98
98
|
cursor.sort(sort);
|
|
99
99
|
}
|
|
@@ -119,7 +119,7 @@ export class MongodbQuerier extends AbstractQuerier {
|
|
|
119
119
|
...(scoreAlias ? [{ $addFields: { [scoreAlias]: { $meta: 'vectorSearchScore' } } }] : []),
|
|
120
120
|
// `$vectorSearch` has already applied `$limit`, so the pager is its own.
|
|
121
121
|
...this.dialect.readStages(entity, q, {
|
|
122
|
-
sort: this.dialect.sort(entity, vectorSort.regularSort
|
|
122
|
+
sort: this.dialect.sort(entity, { ...q, $sort: vectorSort.regularSort }),
|
|
123
123
|
project: scoreAlias ? { [scoreAlias]: 1 } : undefined,
|
|
124
124
|
}),
|
|
125
125
|
];
|
|
@@ -189,7 +189,6 @@ export class MongodbQuerier extends AbstractQuerier {
|
|
|
189
189
|
return this.timed('internalUpdateMany', undefined, async () => {
|
|
190
190
|
const persistable = this.dialect.getPersistable(getMeta(entity), payload, 'onUpdate');
|
|
191
191
|
const filter = this.dialect.where(entity, qm.$where, opts);
|
|
192
|
-
// Maps JSON operators ($set/$unset/$push/$pull) onto their native MongoDB equivalents.
|
|
193
192
|
const update = this.dialect.getUpdateFilter(persistable);
|
|
194
193
|
const { matchedCount } = await this.execute((session) => this.collection(entity).updateMany(filter, update, { session }));
|
|
195
194
|
return matchedCount;
|
|
@@ -2,8 +2,9 @@ import type { IndexNode } from './types.js';
|
|
|
2
2
|
/**
|
|
3
3
|
* What an introspector reports about an index, and so all a diff may compare; apart from `IndexFeature`, what an engine emits.
|
|
4
4
|
* `vector` is whether it is a vector index at all, for an engine with one vector index whatever type declared it.
|
|
5
|
+
* `textWeights` is a text index's weights, kept by an engine that lists its fields in no declared order.
|
|
5
6
|
*/
|
|
6
|
-
export type IndexFacet = 'order' | 'nulls' | 'opsClass' | 'accessMethod' | 'include' | 'vector';
|
|
7
|
+
export type IndexFacet = 'order' | 'nulls' | 'opsClass' | 'accessMethod' | 'include' | 'vector' | 'textWeights';
|
|
7
8
|
/**
|
|
8
9
|
* Whether the table has this index already, by shape rather than name, uniqueness included. An index
|
|
9
10
|
* over an expression, whose text the engine reprints, falls back to its name.
|
|
@@ -35,7 +35,7 @@ export function describeIndexDifferences(source, target, facets) {
|
|
|
35
35
|
const differences = [];
|
|
36
36
|
const comparableEntries = ![...source.entries, ...target.entries].some((entry) => entry.expression || entry.jsonPath || entry.jsonArray);
|
|
37
37
|
if (comparableEntries) {
|
|
38
|
-
const [sourceColumns, targetColumns] = [source
|
|
38
|
+
const [sourceColumns, targetColumns] = [source, target].map((index) => textFieldOrder(index, facets, index.entries.map((entry) => entrySignature(entry, facets))).join(', '));
|
|
39
39
|
if (sourceColumns !== targetColumns) {
|
|
40
40
|
differences.push(`columns: (${targetColumns}) -> (${sourceColumns})`);
|
|
41
41
|
}
|
|
@@ -59,6 +59,10 @@ export function describeIndexDifferences(source, target, facets) {
|
|
|
59
59
|
}
|
|
60
60
|
return differences;
|
|
61
61
|
}
|
|
62
|
+
/** A text index's fields as a set where the engine keeps its weights: MongoDB lists them alphabetically. */
|
|
63
|
+
function textFieldOrder(index, facets, entries) {
|
|
64
|
+
return facets.has('textWeights') && index.type === 'fulltext' ? entries.toSorted() : entries;
|
|
65
|
+
}
|
|
62
66
|
function entrySignature(entry, facets) {
|
|
63
67
|
const parts = [entry.column];
|
|
64
68
|
if (facets.has('order')) {
|
|
@@ -72,5 +76,8 @@ function entrySignature(entry, facets) {
|
|
|
72
76
|
if (facets.has('opsClass') && entry.opsClass) {
|
|
73
77
|
parts.push(entry.opsClass);
|
|
74
78
|
}
|
|
79
|
+
if (facets.has('textWeights') && (entry.weight ?? 1) !== 1) {
|
|
80
|
+
parts.push(`weight ${entry.weight}`);
|
|
81
|
+
}
|
|
75
82
|
return parts.join(' ');
|
|
76
83
|
}
|
|
@@ -24,6 +24,8 @@ export interface BuildSchemaASTOptions {
|
|
|
24
24
|
compileDdl?: (sql: EntityWhereMeta<object>, entity: Type<object>) => string;
|
|
25
25
|
/** A partial index's predicate as the engine writes it, `compileDdl` where none is given. `buildEntityAST` supplies it. */
|
|
26
26
|
compileIndexPredicate?: (where: EntityWhereMeta<object>, entity: Type<object>, indexName: string) => string;
|
|
27
|
+
/** Whether a weighted fulltext index declares one of its own for each heavier column, as MySQL scores through one. */
|
|
28
|
+
textScoreIndexes?: boolean;
|
|
27
29
|
}
|
|
28
30
|
/**
|
|
29
31
|
* Build a SchemaAST from entity classes (decorated with `@Entity`, `@Field`, etc.).
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { fieldOf, foreignKeysOf, getMeta, soleIdOf } from '../entity/metadata/definition.js';
|
|
2
2
|
import { declaredIndexes, declaredIndexName, renderIndexColumn } from '../util/ddlExpression.util.js';
|
|
3
|
+
import { fulltextWeights, textWeightSteps } from '../util/dialect.util.js';
|
|
3
4
|
import { isInlinedExpression } from '../util/field.util.js';
|
|
4
5
|
import { isSoleIdField } from '../util/field.util.js';
|
|
5
6
|
import { isAutoIncrement } from '../util/field.util.js';
|
|
@@ -26,6 +27,7 @@ export function buildSchemaAST(entities, options = {}) {
|
|
|
26
27
|
defaultForeignKeyAction: options.defaultForeignKeyAction ?? DEFAULT_FOREIGN_KEY_ACTION,
|
|
27
28
|
compileDdl,
|
|
28
29
|
compileIndexPredicate: options.compileIndexPredicate ?? compileDdl,
|
|
30
|
+
textScoreIndexes: options.textScoreIndexes ?? false,
|
|
29
31
|
};
|
|
30
32
|
for (const pass of [addTableFromEntity, addRelationshipsFromEntity, addIndexesFromEntity]) {
|
|
31
33
|
for (const entity of entities) {
|
|
@@ -213,4 +215,25 @@ function addCompositeIndex(ctx, table, meta, idxMeta) {
|
|
|
213
215
|
lists: idxMeta.lists,
|
|
214
216
|
config: idxMeta.config,
|
|
215
217
|
});
|
|
218
|
+
if (ctx.textScoreIndexes) {
|
|
219
|
+
addTextScoreIndexes(ctx, table, idxMeta.type, resolved);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
/** A fulltext index of its own for each column heavier than the index's lightest, which its score reads. */
|
|
223
|
+
function addTextScoreIndexes(ctx, table, type, entries) {
|
|
224
|
+
const weights = fulltextWeights({ type, entries });
|
|
225
|
+
if (!weights)
|
|
226
|
+
return;
|
|
227
|
+
const { extra } = textWeightSteps(weights);
|
|
228
|
+
entries.forEach((entry, at) => {
|
|
229
|
+
if (extra[at] && typeof entry.column === 'string') {
|
|
230
|
+
ctx.ast.addIndex({
|
|
231
|
+
name: derivedIndexName(table.name, [entry.column, 'score']),
|
|
232
|
+
table,
|
|
233
|
+
entries: [{ column: entry.column }],
|
|
234
|
+
unique: false,
|
|
235
|
+
type: 'fulltext',
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
});
|
|
216
239
|
}
|
|
@@ -67,6 +67,8 @@ export declare class SqliteDialect extends AbstractSqlDialect {
|
|
|
67
67
|
* FTS5 virtual table (UQL does not create those; declare it outside your entities).
|
|
68
68
|
*/
|
|
69
69
|
protected appendTextSearch<E>(ctx: QueryContext, entity: Type<E>, meta: EntityMeta<E>, search: QueryTextSearchOptions<E>): void;
|
|
70
|
+
/** FTS5's `BM25` of the match, lower for a better one, so negated to rank as every other engine does. */
|
|
71
|
+
protected appendTextScore<E>(ctx: QueryContext, meta: EntityMeta<E>): void;
|
|
70
72
|
protected jsonLength(slot: JsonSlot): string;
|
|
71
73
|
/** `JSON_EACH` walks the array at the path itself, each element's `fullkey` naming it from the column. */
|
|
72
74
|
protected jsonElemFrom(slot: JsonSlot, alias: string): string;
|
|
@@ -27,6 +27,7 @@ export const SQLITE_FEATURES = {
|
|
|
27
27
|
rowLocks: false,
|
|
28
28
|
rowLockWithWindow: true,
|
|
29
29
|
rowLockOf: true,
|
|
30
|
+
textScoreIndexes: false,
|
|
30
31
|
orderedUpsertReturning: true,
|
|
31
32
|
orderedJsonAggregates: true,
|
|
32
33
|
narrowVectorTypes: false,
|
|
@@ -163,6 +164,10 @@ export class SqliteDialect extends AbstractSqlDialect {
|
|
|
163
164
|
ctx.append(`${this.escapedTableName(meta)} MATCH {${columns.join(' ')}} : `);
|
|
164
165
|
ctx.addValue(search.$value);
|
|
165
166
|
}
|
|
167
|
+
/** FTS5's `BM25` of the match, lower for a better one, so negated to rank as every other engine does. */
|
|
168
|
+
appendTextScore(ctx, meta) {
|
|
169
|
+
ctx.append(`-BM25(${this.escapedTableName(meta)})`);
|
|
170
|
+
}
|
|
166
171
|
jsonLength(slot) {
|
|
167
172
|
return `JSON_ARRAY_LENGTH(${jsonArraySlotArgs(slot, this.jsonIsArray(slot))})`;
|
|
168
173
|
}
|
package/dist/type/dialect.d.ts
CHANGED
|
@@ -142,6 +142,11 @@ export interface SqlDialectFeatures extends DialectFeatures {
|
|
|
142
142
|
readonly rowLockWithWindow: boolean;
|
|
143
143
|
/** Whether a lock can be narrowed to one table of a join, `FOR UPDATE OF`, which MariaDB lacks. */
|
|
144
144
|
readonly rowLockOf: boolean;
|
|
145
|
+
/**
|
|
146
|
+
* Whether a fulltext index's heavier column needs a fulltext index of its own to be scored by, as
|
|
147
|
+
* MySQL's `MATCH` does, which reads only an index over exactly its columns.
|
|
148
|
+
*/
|
|
149
|
+
readonly textScoreIndexes: boolean;
|
|
145
150
|
/** Whether a multi-row upsert's `RETURNING` lists its rows in payload order; where not, the ids are read back. */
|
|
146
151
|
readonly orderedUpsertReturning: boolean;
|
|
147
152
|
/** Whether a JSON aggregate takes an `ORDER BY` of its own; where not, a relation's rows keep their derived table's order. */
|
package/dist/type/entity.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type { EnumValues, ForeignKeyAction, IndexType } from '../schema/types.js
|
|
|
2
2
|
import type { FilterOptions, RelationQuery } from './query.js';
|
|
3
3
|
import type { ColumnRef, QueryRaw, RelationAggregate } from './queryRaw.js';
|
|
4
4
|
import type { QueryWhere } from './queryWhere.js';
|
|
5
|
-
import type { Except, IsMany, Json, Scalar, Type, Unpacked, Writable } from './utility.js';
|
|
5
|
+
import type { Except, ExactlyOne, IsEqual, IsMany, Json, Scalar, Type, Unpacked, Writable } from './utility.js';
|
|
6
6
|
import type { VectorDistance, VectorIndexOptions, VectorIndexType } from './vector.js';
|
|
7
7
|
/** Brands the property an entity is identified by, where it is not `id`, `_id` or `uuid`. */
|
|
8
8
|
export declare const idKey: unique symbol;
|
|
@@ -20,19 +20,13 @@ export type Key<E> = keyof E & string;
|
|
|
20
20
|
export type FieldKey<E> = {
|
|
21
21
|
readonly [K in keyof E]-?: [NonNullable<E[K]>] extends [Scalar | readonly Scalar[] | Json | readonly Json[]] ? K : never;
|
|
22
22
|
}[Key<E>];
|
|
23
|
-
/**
|
|
24
|
-
* Whether `A` and `B` are the same type, `readonly` included - which no conditional sees, since
|
|
25
|
-
* assignability ignores the modifier. Two identical generic signatures compare equal only when their
|
|
26
|
-
* deferred bodies do.
|
|
27
|
-
*/
|
|
28
|
-
type IfEquals<A, B, Yes, No> = (<T>() => T extends A ? 1 : 2) extends <T>() => (T extends B ? 1 : 2) ? Yes : No;
|
|
29
23
|
/**
|
|
30
24
|
* The fields a caller writes: every one the class does not declare `readonly`. A field the database
|
|
31
25
|
* writes - a relation aggregate, a stored generated column, a trigger-kept stamp - is `readonly`, and
|
|
32
26
|
* its value never reaches the database, so a write payload leaves it out rather than dropping it.
|
|
33
27
|
*/
|
|
34
28
|
export type WritableKey<E> = {
|
|
35
|
-
readonly [K in FieldKey<E>]-?:
|
|
29
|
+
readonly [K in FieldKey<E>]-?: IsEqual<Pick<E, K>, Writable<Pick<E, K>>> extends true ? K : never;
|
|
36
30
|
}[FieldKey<E>];
|
|
37
31
|
/** A whole-record write as a caller supplies one: {@link EntityData} without the fields it cannot write. */
|
|
38
32
|
export type EntityWrite<E> = EntityData<E, WritableKey<E>>;
|
|
@@ -110,8 +104,17 @@ export type JsonUpdateOp<T = unknown> = {
|
|
|
110
104
|
* operators would address keys it does not have (engines disagree on what that does).
|
|
111
105
|
*/
|
|
112
106
|
type JsonUpdateOpFor<V, T = UnwrapJson<NonNullable<V>>> = [T] extends [never] ? never : IsMany<T> extends true ? never : JsonUpdateOp<T>;
|
|
113
|
-
/**
|
|
114
|
-
|
|
107
|
+
/**
|
|
108
|
+
* A scalar field's update operator, as {@link JsonUpdateOp} is a JSON field's, computed in the statement:
|
|
109
|
+
* `$inc` adds, `$mul` multiplies, a NULL counting as 0 on every engine. One per field, since their order
|
|
110
|
+
* would change the result. A `bigint` steps by a `bigint`, exactly.
|
|
111
|
+
* @example `{ stock: { $inc: -1 } }`
|
|
112
|
+
*/
|
|
113
|
+
export type FieldUpdateOp<T extends number | bigint = number | bigint> = ExactlyOne<Record<'$inc' | '$mul', T>>;
|
|
114
|
+
/** The {@link FieldUpdateOp} a field takes: `never` on one it has no operator for, which is any but a number. */
|
|
115
|
+
type FieldUpdateOpFor<V> = [NonNullable<V>] extends [number] ? FieldUpdateOp<number> : [NonNullable<V>] extends [bigint] ? FieldUpdateOp<bigint> : never;
|
|
116
|
+
/** What an update takes beyond the value: `null` to clear an optional member, `raw` SQL, and update operators. */
|
|
117
|
+
type UpdateExtra<V, Raw> = (undefined extends V ? null : never) | Raw | JsonUpdateOpFor<V> | FieldUpdateOpFor<V>;
|
|
115
118
|
/**
|
|
116
119
|
* What a whole-record write persists: the fields and relations with their declared optionality, a
|
|
117
120
|
* related row's alike, and no methods. Two mapped types, since asking each key costs a conditional.
|
|
@@ -560,6 +563,11 @@ export type IndexColumnModifiers = {
|
|
|
560
563
|
readonly nulls?: 'first' | 'last';
|
|
561
564
|
/** Operator class, e.g. `jsonb_path_ops` for a smaller GIN index. Postgres only. */
|
|
562
565
|
readonly opsClass?: string;
|
|
566
|
+
/**
|
|
567
|
+
* How many times a match in this column of a fulltext index counts in `$sort: { $text }`, 1 by default:
|
|
568
|
+
* a whole number from 1 to 99999, the range MongoDB weighs by.
|
|
569
|
+
*/
|
|
570
|
+
readonly weight?: number;
|
|
563
571
|
/** Index a path inside a JSON column. See {@link IndexJsonPath}. */
|
|
564
572
|
readonly jsonPath?: IndexJsonPath;
|
|
565
573
|
/** Index every element of a JSON array. See {@link IndexJsonArray}. */
|
package/dist/type/query.d.ts
CHANGED
|
@@ -130,15 +130,22 @@ export type QuerySortByCount = {
|
|
|
130
130
|
$count: QuerySortDirection;
|
|
131
131
|
};
|
|
132
132
|
/**
|
|
133
|
-
*
|
|
134
|
-
*
|
|
135
|
-
|
|
133
|
+
* Ordering by relevance to the `$text` at the root of `$where`: most relevant first, the one order every
|
|
134
|
+
* engine ranks by (MongoDB's `textScore` sorts no other way).
|
|
135
|
+
*/
|
|
136
|
+
export type QuerySortByText = {
|
|
137
|
+
$text?: -1 | 'desc';
|
|
138
|
+
};
|
|
139
|
+
/**
|
|
140
|
+
* A sort by fields, JSON paths, a to-one relation's fields, a to-many's `$count`, or a vector distance or
|
|
141
|
+
* `$text` relevance, which `Vector` confines to the queried entity. One mapped type over the key sets: an
|
|
142
|
+
* intersection is checked once per member, which made this the costliest type to check.
|
|
136
143
|
*/
|
|
137
144
|
export type QuerySortMap<E, Vector extends boolean = true, K extends keyof E = FieldKey<E> | RelationKey<E>> = {
|
|
138
145
|
[P in K]?: P extends RelationKey<E> ? IsMany<E[P]> extends true ? QuerySortByCount : QuerySortMap<RelationTarget<E[P]>, false> : Vector extends true ? NonNullable<E[P]> extends readonly number[] ? QuerySortValue : QuerySortDirection : QuerySortDirection;
|
|
139
146
|
} & ([JsonFieldPaths<E>] extends [never] ? unknown : {
|
|
140
147
|
[P in JsonFieldPaths<E>]?: QuerySortDirection;
|
|
141
|
-
});
|
|
148
|
+
}) & (Vector extends true ? QuerySortByText : unknown);
|
|
142
149
|
/**
|
|
143
150
|
* pager options.
|
|
144
151
|
*/
|