uql-orm 0.72.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/dist/dialect/abstractSqlDialect.d.ts +9 -3
- package/dist/dialect/abstractSqlDialect.js +28 -4
- package/dist/dialect/mysqlLikeSqlDialect.d.ts +5 -2
- package/dist/dialect/mysqlLikeSqlDialect.js +8 -4
- package/dist/dialect/pgLikeSqlDialect.d.ts +6 -3
- package/dist/dialect/pgLikeSqlDialect.js +12 -8
- package/dist/entity/metadata/definition.js +4 -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/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 +1 -1
- package/dist/sqlite/sqliteDialect.js +2 -1
- package/dist/type/dialect.d.ts +5 -0
- package/dist/type/entity.d.ts +5 -0
- package/dist/util/dialect.util.d.ts +19 -0
- package/dist/util/dialect.util.js +29 -0
- package/package.json +1 -1
|
@@ -183,10 +183,16 @@ export declare abstract class AbstractSqlDialect extends VectorSqlDialect implem
|
|
|
183
183
|
*/
|
|
184
184
|
protected appendTextSearch<E>(_ctx: QueryContext, _entity: Type<E>, _meta: EntityMeta<E>, _search: QueryTextSearchOptions<E>): void;
|
|
185
185
|
/**
|
|
186
|
-
* A row's relevance to a `$text` search,
|
|
187
|
-
*
|
|
186
|
+
* A row's relevance to a `$text` search, what `$sort: { $text }` orders by. Where the fulltext index
|
|
187
|
+
* weighs its columns, a match counts its column's weight, as MongoDB's `textScore` counts it: the score
|
|
188
|
+
* over every column times the lightest weight, plus each heavier column's own times what it weighs more.
|
|
188
189
|
*/
|
|
189
|
-
|
|
190
|
+
private appendTextRank;
|
|
191
|
+
/**
|
|
192
|
+
* How relevant the `keys` of a row are to a `$text` search, higher for a better match. Each engine that
|
|
193
|
+
* searches scores too, so a dialect overrides this beside {@link appendTextSearch}.
|
|
194
|
+
*/
|
|
195
|
+
protected appendTextScore<E>(_ctx: QueryContext, _meta: EntityMeta<E>, _search: QueryTextSearchOptions<E>, _keys: readonly string[]): void;
|
|
190
196
|
/** Ranks by the root `$text` of `where`, which is looked up only once a `$sort` asks for it. */
|
|
191
197
|
private textRanker;
|
|
192
198
|
select<E>(ctx: QueryContext, entity: Type<E>, q: Query<E>, opts?: ReadOptions, joins?: QueryJoins, totalAlias?: string): ReadProjection;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { fieldOf, getMeta, relationOf, soleIdOf } from '../entity/index.js';
|
|
2
2
|
import { COUNT_RESULT_KEY, parseQueryLock, QueryRaw, RAW_ALIAS, VECTOR_QUERY_KEYS, } from '../type/index.js';
|
|
3
3
|
import { isInlinedExpression } from '../util/field.util.js';
|
|
4
|
-
import { asSelectMap, assertNonNegativeInteger, escapeSqlId, fillOnFields, filterFieldKeys, getInsertFieldKeys, getKeys, getRelationRequestSummary, getSoftDeleteValue, hasKeys, idOnlyQuery, columnFamily, countedRelations, fieldUpdateOf, isFieldUpdateOp, isJsonObject, isJsonUpdateOp, isOperatorMap, isOperatorKey, isVectorSearch, normalizeScalarFieldSelection, parentJoins, rankedTextSearch, targetKeyColumns, parseGroupMap, parseRelationAtKey, parseRelationSize, parseSortByCount, populatesRelations, aggregateOf, raw, refs, throwUnknownAggregateColumn, withoutSoftDeleteFilter, } from '../util/index.js';
|
|
4
|
+
import { asSelectMap, assertNonNegativeInteger, escapeSqlId, fillOnFields, filterFieldKeys, getInsertFieldKeys, getKeys, getRelationRequestSummary, getSoftDeleteValue, hasKeys, idOnlyQuery, columnFamily, countedRelations, fieldUpdateOf, fulltextIndexOver, fulltextWeights, isFieldUpdateOp, isJsonObject, isJsonUpdateOp, isOperatorMap, isOperatorKey, isVectorSearch, normalizeScalarFieldSelection, parentJoins, rankedTextSearch, targetKeyColumns, textSearchFields, textWeightSteps, parseGroupMap, parseRelationAtKey, parseRelationSize, parseSortByCount, populatesRelations, aggregateOf, raw, refs, throwUnknownAggregateColumn, withoutSoftDeleteFilter, } from '../util/index.js';
|
|
5
5
|
import { escapeAnsiSqlLiteral } from '../util/sqlLiteral.js';
|
|
6
6
|
import { AGGREGATE_PAGE_ALIAS, AGGREGATE_VALUE_ALIAS, ROWS_ALIAS, JSON_ELEM_ALIAS, JSON_PULL_ALIAS, relationSortColumn, } from './aliases.js';
|
|
7
7
|
import { holdsOperator, isJsonScalar, jsonCompareMode, jsonElemExists, jsonPath, } from './jsonSql.js';
|
|
@@ -225,10 +225,34 @@ export class AbstractSqlDialect extends VectorSqlDialect {
|
|
|
225
225
|
throw new TypeError(`${this.dialectName} does not support $text full-text search`);
|
|
226
226
|
}
|
|
227
227
|
/**
|
|
228
|
-
* A row's relevance to a `$text` search,
|
|
229
|
-
*
|
|
228
|
+
* A row's relevance to a `$text` search, what `$sort: { $text }` orders by. Where the fulltext index
|
|
229
|
+
* weighs its columns, a match counts its column's weight, as MongoDB's `textScore` counts it: the score
|
|
230
|
+
* over every column times the lightest weight, plus each heavier column's own times what it weighs more.
|
|
230
231
|
*/
|
|
231
|
-
appendTextRank(
|
|
232
|
+
appendTextRank(ctx, meta, search) {
|
|
233
|
+
const keys = textSearchFields(meta, search);
|
|
234
|
+
const index = fulltextIndexOver(meta, keys);
|
|
235
|
+
const weights = index && fulltextWeights({ type: index.type, entries: index.columns });
|
|
236
|
+
if (!weights) {
|
|
237
|
+
this.appendTextScore(ctx, meta, search, keys);
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
const { lightest, extra } = textWeightSteps(weights);
|
|
241
|
+
ctx.append(`(${lightest} * `);
|
|
242
|
+
this.appendTextScore(ctx, meta, search, keys);
|
|
243
|
+
keys.forEach((key, at) => {
|
|
244
|
+
if (extra[at]) {
|
|
245
|
+
ctx.append(` + ${extra[at]} * `);
|
|
246
|
+
this.appendTextScore(ctx, meta, search, [key]);
|
|
247
|
+
}
|
|
248
|
+
});
|
|
249
|
+
ctx.append(')');
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* How relevant the `keys` of a row are to a `$text` search, higher for a better match. Each engine that
|
|
253
|
+
* searches scores too, so a dialect overrides this beside {@link appendTextSearch}.
|
|
254
|
+
*/
|
|
255
|
+
appendTextScore(_ctx, _meta, _search, _keys) {
|
|
232
256
|
throw new TypeError(`${this.dialectName} does not support $text full-text search`);
|
|
233
257
|
}
|
|
234
258
|
/** Ranks by the root `$text` of `where`, which is looked up only once a `$sort` asks for it. */
|
|
@@ -80,8 +80,11 @@ export declare abstract class MysqlLikeSqlDialect extends AbstractSqlDialect {
|
|
|
80
80
|
* `@Index((post) => [...], { type: 'fulltext' })`.
|
|
81
81
|
*/
|
|
82
82
|
protected appendTextSearch<E>(ctx: QueryContext, _entity: Type<E>, meta: EntityMeta<E>, search: QueryTextSearchOptions<E>): void;
|
|
83
|
-
/**
|
|
84
|
-
|
|
83
|
+
/**
|
|
84
|
+
* `MATCH ... AGAINST` is the relevance itself, a match being any row it scores above zero. Over `keys`
|
|
85
|
+
* alone, it needs a `FULLTEXT` index of exactly those: a weighted index declares one per heavier column.
|
|
86
|
+
*/
|
|
87
|
+
protected appendTextScore<E>(ctx: QueryContext, meta: EntityMeta<E>, search: QueryTextSearchOptions<E>, keys: readonly string[]): void;
|
|
85
88
|
/** `DOUBLE`, never a bare `DECIMAL`, which is `DECIMAL(10,0)` and rounds `1.4` to `1`. */
|
|
86
89
|
protected numericCast(expr: string): string;
|
|
87
90
|
protected neExpr(field: string, ph: string): string;
|
|
@@ -32,6 +32,7 @@ export const MYSQL_FEATURES = {
|
|
|
32
32
|
rowLocks: true,
|
|
33
33
|
rowLockWithWindow: true,
|
|
34
34
|
rowLockOf: true,
|
|
35
|
+
textScoreIndexes: true,
|
|
35
36
|
orderedUpsertReturning: true,
|
|
36
37
|
orderedJsonAggregates: true,
|
|
37
38
|
narrowVectorTypes: false,
|
|
@@ -178,11 +179,14 @@ export class MysqlLikeSqlDialect extends AbstractSqlDialect {
|
|
|
178
179
|
* `@Index((post) => [...], { type: 'fulltext' })`.
|
|
179
180
|
*/
|
|
180
181
|
appendTextSearch(ctx, _entity, meta, search) {
|
|
181
|
-
this.
|
|
182
|
+
this.appendTextScore(ctx, meta, search, textSearchFields(meta, search));
|
|
182
183
|
}
|
|
183
|
-
/**
|
|
184
|
-
|
|
185
|
-
|
|
184
|
+
/**
|
|
185
|
+
* `MATCH ... AGAINST` is the relevance itself, a match being any row it scores above zero. Over `keys`
|
|
186
|
+
* alone, it needs a `FULLTEXT` index of exactly those: a weighted index declares one per heavier column.
|
|
187
|
+
*/
|
|
188
|
+
appendTextScore(ctx, meta, search, keys) {
|
|
189
|
+
const columns = keys.map((key) => this.escapeId(this.resolveColumnName(key, meta.fields[key])));
|
|
186
190
|
ctx.append(`MATCH(${this.textSearchTarget(columns)}) AGAINST(`);
|
|
187
191
|
ctx.addValue(search.$value);
|
|
188
192
|
ctx.append(')');
|
|
@@ -64,9 +64,12 @@ export declare abstract class PgLikeSqlDialect extends AbstractSqlDialect {
|
|
|
64
64
|
* input (quoted phrases, `or`, `-negation`) and never raises a syntax error, unlike `TO_TSQUERY`.
|
|
65
65
|
*/
|
|
66
66
|
protected appendTextSearch<E>(ctx: QueryContext, _entity: Type<E>, meta: EntityMeta<E>, search: QueryTextSearchOptions<E>): void;
|
|
67
|
-
/** `TS_RANK` of the document
|
|
68
|
-
protected
|
|
69
|
-
/**
|
|
67
|
+
/** `TS_RANK` of the document over `keys` against the same search the match reads. */
|
|
68
|
+
protected appendTextScore<E>(ctx: QueryContext, meta: EntityMeta<E>, search: QueryTextSearchOptions<E>, keys: readonly string[]): void;
|
|
69
|
+
/**
|
|
70
|
+
* The document over `keys` and the search itself, open for its value, under the `$config` asked for,
|
|
71
|
+
* else that of the fulltext index over every field searched, which is what serves the match.
|
|
72
|
+
*/
|
|
70
73
|
private textSearchParts;
|
|
71
74
|
/**
|
|
72
75
|
* The document, `TO_TSVECTOR('english'::regconfig, COALESCE("a", '') || ' ' || COALESCE("b", ''))`, a
|
|
@@ -36,6 +36,7 @@ export const PG_FEATURES = {
|
|
|
36
36
|
rowLocks: true,
|
|
37
37
|
rowLockWithWindow: false,
|
|
38
38
|
rowLockOf: true,
|
|
39
|
+
textScoreIndexes: false,
|
|
39
40
|
orderedUpsertReturning: true,
|
|
40
41
|
orderedJsonAggregates: true,
|
|
41
42
|
narrowVectorTypes: false,
|
|
@@ -134,19 +135,22 @@ export class PgLikeSqlDialect extends AbstractSqlDialect {
|
|
|
134
135
|
ctx.addValue(search.$value);
|
|
135
136
|
ctx.append(')');
|
|
136
137
|
}
|
|
137
|
-
/** `TS_RANK` of the document
|
|
138
|
-
|
|
139
|
-
const { document, query } = this.textSearchParts(meta, search);
|
|
138
|
+
/** `TS_RANK` of the document over `keys` against the same search the match reads. */
|
|
139
|
+
appendTextScore(ctx, meta, search, keys) {
|
|
140
|
+
const { document, query } = this.textSearchParts(meta, search, keys);
|
|
140
141
|
ctx.append(`TS_RANK(${document}, ${query}`);
|
|
141
142
|
ctx.addValue(search.$value);
|
|
142
143
|
ctx.append('))');
|
|
143
144
|
}
|
|
144
|
-
/**
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
145
|
+
/**
|
|
146
|
+
* The document over `keys` and the search itself, open for its value, under the `$config` asked for,
|
|
147
|
+
* else that of the fulltext index over every field searched, which is what serves the match.
|
|
148
|
+
*/
|
|
149
|
+
textSearchParts(meta, search, keys) {
|
|
150
|
+
const fields = textSearchFields(meta, search);
|
|
151
|
+
const index = fulltextIndexOver(meta, fields);
|
|
148
152
|
const config = search.$config ?? (index && fulltextConfig(index));
|
|
149
|
-
const columns = keys.map((key) => this.escapeId(this.resolveColumnName(key, meta.fields[key])));
|
|
153
|
+
const columns = (keys ?? fields).map((key) => this.escapeId(this.resolveColumnName(key, meta.fields[key])));
|
|
150
154
|
return {
|
|
151
155
|
document: this.textSearchTarget(columns, config),
|
|
152
156
|
query: `${this.textQueryFn}(${this.textConfigArg(config)}`,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { RelationAggregate, SOFT_DELETE_FILTER } from '../../type/index.js';
|
|
2
2
|
import { isInlinedExpression } from '../../util/field.util.js';
|
|
3
|
-
import { entitySql, entityWhere, fieldOptionConflict, getKeys, hasKeys, isToManyRelation, memberRefs, normalizeIndexColumn, definedEntries, } from '../../util/index.js';
|
|
3
|
+
import { entitySql, entityWhere, fieldOptionConflict, getKeys, hasKeys, isToManyRelation, memberRefs, fulltextWeights, normalizeIndexColumn, definedEntries, } from '../../util/index.js';
|
|
4
4
|
import { ownRegistrations } from '../decorator/bag.js';
|
|
5
5
|
/**
|
|
6
6
|
* A map held on `globalThis` through the global symbol registry, so a single one survives multiple
|
|
@@ -101,11 +101,13 @@ export function defineHook(entity, methodName, event) {
|
|
|
101
101
|
export function defineIndex(entity, index) {
|
|
102
102
|
const meta = ensureWritableMeta(entity);
|
|
103
103
|
const refs = memberRefs();
|
|
104
|
+
const columns = index.columns(refs).map(normalizeIndexColumn);
|
|
105
|
+
fulltextWeights({ type: index.type, entries: columns });
|
|
104
106
|
(meta.indexes ??= []).push({
|
|
105
107
|
...index,
|
|
106
108
|
unique: index.unique ?? false,
|
|
107
109
|
where: index.where && entityWhere(index.where),
|
|
108
|
-
columns
|
|
110
|
+
columns,
|
|
109
111
|
include: index.include?.(refs).map((ref) => ref.key),
|
|
110
112
|
});
|
|
111
113
|
return meta;
|
package/dist/migrate/cli.js
CHANGED
|
@@ -239,7 +239,8 @@ export async function runDriftCheck(migrator, config) {
|
|
|
239
239
|
}
|
|
240
240
|
else {
|
|
241
241
|
console.log('\nChecking for schema drift...');
|
|
242
|
-
const
|
|
242
|
+
const generator = await migrator.getSchemaGenerator();
|
|
243
|
+
const expectedAST = generator.buildAST?.(config.entities) ?? buildEntityAST(generator, config.entities);
|
|
243
244
|
// Build actual schema from database
|
|
244
245
|
const actualAST = await migrator.schemaIntrospector.introspect();
|
|
245
246
|
// Detect drift. The dialect renders canonical types as SQL - without it every type formats as
|
|
@@ -15,6 +15,8 @@ export declare class IndexDdl<D extends AbstractSqlDialect = AbstractSqlDialect>
|
|
|
15
15
|
getCreateIndexStatement(tableName: string, index: IndexSchema, opts?: {
|
|
16
16
|
ifNotExists?: boolean;
|
|
17
17
|
}): string;
|
|
18
|
+
/** What an index added to a table that has rows needs run after it to serve queries; nothing, mostly. */
|
|
19
|
+
settleStatements(_tableName: string, _index: IndexSchema): string[];
|
|
18
20
|
/**
|
|
19
21
|
* Index features this dialect can express. Everything here is supported by at least one engine and
|
|
20
22
|
* refused by at least one other, so an index asking for a missing one is rejected rather than
|
|
@@ -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
|
}
|
|
@@ -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
|
}
|
|
@@ -68,7 +68,7 @@ export declare class SqliteDialect extends AbstractSqlDialect {
|
|
|
68
68
|
*/
|
|
69
69
|
protected appendTextSearch<E>(ctx: QueryContext, entity: Type<E>, meta: EntityMeta<E>, search: QueryTextSearchOptions<E>): void;
|
|
70
70
|
/** FTS5's `BM25` of the match, lower for a better one, so negated to rank as every other engine does. */
|
|
71
|
-
protected
|
|
71
|
+
protected appendTextScore<E>(ctx: QueryContext, meta: EntityMeta<E>): void;
|
|
72
72
|
protected jsonLength(slot: JsonSlot): string;
|
|
73
73
|
/** `JSON_EACH` walks the array at the path itself, each element's `fullkey` naming it from the column. */
|
|
74
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,
|
|
@@ -164,7 +165,7 @@ export class SqliteDialect extends AbstractSqlDialect {
|
|
|
164
165
|
ctx.addValue(search.$value);
|
|
165
166
|
}
|
|
166
167
|
/** FTS5's `BM25` of the match, lower for a better one, so negated to rank as every other engine does. */
|
|
167
|
-
|
|
168
|
+
appendTextScore(ctx, meta) {
|
|
168
169
|
ctx.append(`-BM25(${this.escapedTableName(meta)})`);
|
|
169
170
|
}
|
|
170
171
|
jsonLength(slot) {
|
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
|
@@ -563,6 +563,11 @@ export type IndexColumnModifiers = {
|
|
|
563
563
|
readonly nulls?: 'first' | 'last';
|
|
564
564
|
/** Operator class, e.g. `jsonb_path_ops` for a smaller GIN index. Postgres only. */
|
|
565
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;
|
|
566
571
|
/** Index a path inside a JSON column. See {@link IndexJsonPath}. */
|
|
567
572
|
readonly jsonPath?: IndexJsonPath;
|
|
568
573
|
/** Index every element of a JSON array. See {@link IndexJsonArray}. */
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { IndexType } from '../schema/types.js';
|
|
1
2
|
import { type CascadeType, type EntityData, type EntityId, type EntityIndexMeta, type EntityMeta, type FieldKey, type FieldOptions, type FieldUpdateOp, type JsonUpdateOp, type OnFieldCallback, type Query, type QueryAggMap, type QueryAggregateOp, type QueryExclude, type QueryGroupMap, type QueryOptions, QueryRaw, type QuerySearch, type QuerySelect, type QuerySelectValue, type QuerySizeComparisonOps, type QuerySortMap, type QueryTextSearchOptions, type QueryVectorSearch, type QueryWhere, type RelationKey, type UpdatePayload } from '../type/index.js';
|
|
2
3
|
export type CallbackKey = keyof Pick<FieldOptions, 'onInsert' | 'onUpdate'>;
|
|
3
4
|
/** The keys of `payload` a write persists as columns. */
|
|
@@ -171,6 +172,24 @@ export declare function assertAggregateColumns(clauseMap: object, emitted: Reado
|
|
|
171
172
|
export declare function fulltextConfig(index: {
|
|
172
173
|
readonly config?: string;
|
|
173
174
|
}): string;
|
|
175
|
+
/**
|
|
176
|
+
* Each column's weight in a fulltext index, 1 where it states none, or none at all where they are alike.
|
|
177
|
+
* Checked wherever it is read, so the migration, the search and its rank refuse the same declaration.
|
|
178
|
+
*/
|
|
179
|
+
export declare function fulltextWeights(index: {
|
|
180
|
+
readonly type?: IndexType;
|
|
181
|
+
readonly entries: readonly {
|
|
182
|
+
readonly weight?: number;
|
|
183
|
+
}[];
|
|
184
|
+
}): readonly number[] | undefined;
|
|
185
|
+
/**
|
|
186
|
+
* A weighted fulltext index's lightest weight, and what each column weighs beyond it: the score over every
|
|
187
|
+
* column counts the lightest, and a column weighing more adds its own score times the rest.
|
|
188
|
+
*/
|
|
189
|
+
export declare function textWeightSteps(weights: readonly number[]): {
|
|
190
|
+
lightest: number;
|
|
191
|
+
extra: number[];
|
|
192
|
+
};
|
|
174
193
|
/** The fulltext index over exactly `fields`, in order, which a search of them is served by. */
|
|
175
194
|
export declare function fulltextIndexOver<E>(meta: EntityMeta<E>, fields: readonly string[]): EntityIndexMeta<E> | undefined;
|
|
176
195
|
/**
|
|
@@ -456,6 +456,35 @@ const DEFAULT_TEXT_CONFIG = 'simple';
|
|
|
456
456
|
export function fulltextConfig(index) {
|
|
457
457
|
return index.config ?? DEFAULT_TEXT_CONFIG;
|
|
458
458
|
}
|
|
459
|
+
/** The largest weight MongoDB's text index takes, which truncates a fraction to the whole number below. */
|
|
460
|
+
const MAX_TEXT_WEIGHT = 99_999;
|
|
461
|
+
/**
|
|
462
|
+
* Each column's weight in a fulltext index, 1 where it states none, or none at all where they are alike.
|
|
463
|
+
* Checked wherever it is read, so the migration, the search and its rank refuse the same declaration.
|
|
464
|
+
*/
|
|
465
|
+
export function fulltextWeights(index) {
|
|
466
|
+
if (!index.entries.some((entry) => entry.weight !== undefined)) {
|
|
467
|
+
return undefined;
|
|
468
|
+
}
|
|
469
|
+
if (index.type !== 'fulltext') {
|
|
470
|
+
throw new TypeError(`a column weight ranks a fulltext index, and this one is ${index.type ?? 'btree'}`);
|
|
471
|
+
}
|
|
472
|
+
const weights = index.entries.map(({ weight = 1 }) => {
|
|
473
|
+
if (!Number.isInteger(weight) || weight < 1 || weight > MAX_TEXT_WEIGHT) {
|
|
474
|
+
throw new TypeError(`a column weight is a whole number from 1 to ${MAX_TEXT_WEIGHT}, not ${weight}`);
|
|
475
|
+
}
|
|
476
|
+
return weight;
|
|
477
|
+
});
|
|
478
|
+
return new Set(weights).size > 1 ? weights : undefined;
|
|
479
|
+
}
|
|
480
|
+
/**
|
|
481
|
+
* A weighted fulltext index's lightest weight, and what each column weighs beyond it: the score over every
|
|
482
|
+
* column counts the lightest, and a column weighing more adds its own score times the rest.
|
|
483
|
+
*/
|
|
484
|
+
export function textWeightSteps(weights) {
|
|
485
|
+
const lightest = Math.min(...weights);
|
|
486
|
+
return { lightest, extra: weights.map((weight) => weight - lightest) };
|
|
487
|
+
}
|
|
459
488
|
/** The fulltext index over exactly `fields`, in order, which a search of them is served by. */
|
|
460
489
|
export function fulltextIndexOver(meta, fields) {
|
|
461
490
|
return meta.indexes?.find((index) => index.type === 'fulltext' &&
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"homepage": "https://uql-orm.dev",
|
|
4
4
|
"description": "The JSON-native TypeScript ORM for Bun, Browsers, Edge, Deno, Node, Workers. Supports PostgreSQL, PGlite, MySQL, MariaDB, SQLite, CockroachDB, SQL Server, Turso, Neon, Cloudflare D1 and MongoDB. Queries are plain JSON, typed to the leaf.",
|
|
5
5
|
"license": "MIT",
|
|
6
|
-
"version": "0.72.
|
|
6
|
+
"version": "0.72.1",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"engines": {
|
|
9
9
|
"node": ">=24"
|