uql-orm 0.72.0 → 0.72.2
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/browser/uql-browser.min.js +2 -2
- package/dist/browser/uql-browser.min.js.map +3 -3
- package/dist/dialect/abstractDialect.d.ts +6 -3
- package/dist/dialect/abstractDialect.js +22 -1
- package/dist/dialect/abstractSqlDialect.d.ts +15 -8
- package/dist/dialect/abstractSqlDialect.js +61 -28
- package/dist/dialect/aliases.d.ts +1 -1
- package/dist/dialect/aliases.js +1 -1
- package/dist/dialect/mysqlLikeSqlDialect.d.ts +6 -3
- package/dist/dialect/mysqlLikeSqlDialect.js +9 -6
- package/dist/dialect/pgLikeSqlDialect.d.ts +8 -5
- package/dist/dialect/pgLikeSqlDialect.js +14 -11
- package/dist/dialect/queryJoins.d.ts +11 -1
- package/dist/dialect/queryJoins.js +14 -0
- package/dist/dialect/vectorSqlDialect.d.ts +0 -5
- package/dist/dialect/vectorSqlDialect.js +0 -15
- 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 +4 -0
- package/dist/migrate/generator/mongoSchemaGenerator.js +5 -1
- package/dist/migrate/introspection/mongoIntrospector.d.ts +1 -1
- package/dist/migrate/introspection/mongoIntrospector.js +17 -6
- package/dist/migrate/schemaGenerator.d.ts +4 -1
- package/dist/migrate/schemaGenerator.js +14 -7
- package/dist/mongo/mongoDialect.d.ts +15 -2
- package/dist/mongo/mongoDialect.js +49 -8
- package/dist/mongo/mongodbQuerier.js +6 -7
- package/dist/mongo/mongodbQuerierPool.js +4 -1
- package/dist/mssql/mssqlDialect.js +1 -0
- package/dist/schema/indexDifferences.d.ts +2 -1
- package/dist/schema/indexDifferences.js +15 -1
- package/dist/schema/schemaASTBuilder.d.ts +2 -0
- package/dist/schema/schemaASTBuilder.js +23 -0
- package/dist/sqlite/sqliteDialect.d.ts +5 -5
- package/dist/sqlite/sqliteDialect.js +17 -7
- package/dist/type/dialect.d.ts +5 -0
- package/dist/type/entity.d.ts +5 -0
- package/dist/type/query.d.ts +11 -3
- package/dist/type/queryWhere.d.ts +3 -2
- package/dist/util/dialect.util.d.ts +25 -1
- package/dist/util/dialect.util.js +37 -0
- package/dist/util/wideNumber.d.ts +5 -3
- package/dist/util/wideNumber.js +8 -4
- package/package.json +1 -1
|
@@ -2,7 +2,7 @@ import { AGGREGATE_VALUE_ALIAS } from '../dialect/aliases.js';
|
|
|
2
2
|
import { hasRequiredJoin } from '../dialect/queryJoins.js';
|
|
3
3
|
import { fieldOf, getMeta, namesKey, soleIdOf } from '../entity/index.js';
|
|
4
4
|
import { AbstractQuerier, enrichError } from '../querier/index.js';
|
|
5
|
-
import { clone, getKeys, getSoftDeleteValue, hasKeys, populatesRelations, throwNoPendingTransaction, throwPendingTransaction, vectorCandidates, withoutSoftDeleteFilter, } from '../util/index.js';
|
|
5
|
+
import { clone, getKeys, getSoftDeleteValue, hasKeys, populatesRelations, textSortOf, throwNoPendingTransaction, throwPendingTransaction, vectorCandidates, withoutSoftDeleteFilter, } from '../util/index.js';
|
|
6
6
|
/**
|
|
7
7
|
* `$limit: 0` asks for no rows, the way it does on every SQL dialect - but MongoDB reads `limit(0)`
|
|
8
8
|
* as *unlimited*, so a read that passed it straight to the driver came back with the whole
|
|
@@ -77,7 +77,8 @@ export class MongodbQuerier extends AbstractQuerier {
|
|
|
77
77
|
populatesRelations(getMeta(entity), q.$populate) ||
|
|
78
78
|
this.dialect.constrainsRelations(entity, q.$where) ||
|
|
79
79
|
this.dialect.sortsRelations(entity, q.$sort) ||
|
|
80
|
-
this.dialect.readsAggregates(entity, q)
|
|
80
|
+
this.dialect.readsAggregates(entity, q) ||
|
|
81
|
+
textSortOf(q.$sort) !== undefined);
|
|
81
82
|
}
|
|
82
83
|
buildScalarProjection(entity, q) {
|
|
83
84
|
return this.dialect.select(entity, q.$select, q.$exclude);
|
|
@@ -114,20 +115,18 @@ export class MongodbQuerier extends AbstractQuerier {
|
|
|
114
115
|
const scoreAlias = vectorSort.vectorSearch.$project;
|
|
115
116
|
return [
|
|
116
117
|
this.dialect.buildVectorSearchStage(entity, vectorSort.vectorKey, vectorSort.vectorSearch, q.$where, q.$limit ?? 10, opts, vectorCandidates(q)),
|
|
117
|
-
// The score becomes a real field before anything reads it, so the lookups and the projection
|
|
118
|
-
// that follow treat it like any other - and a query with no projection keeps its own columns.
|
|
119
|
-
...(scoreAlias ? [{ $addFields: { [scoreAlias]: { $meta: 'vectorSearchScore' } } }] : []),
|
|
120
118
|
// `$vectorSearch` has already applied `$limit`, so the pager is its own.
|
|
121
119
|
...this.dialect.readStages(entity, q, {
|
|
122
120
|
sort: this.dialect.sort(entity, { ...q, $sort: vectorSort.regularSort }),
|
|
123
|
-
|
|
121
|
+
score: scoreAlias ? { field: scoreAlias, meta: 'vectorSearchScore' } : undefined,
|
|
124
122
|
}),
|
|
125
123
|
];
|
|
126
124
|
}
|
|
127
125
|
async internalAggregate(entity, q, opts) {
|
|
128
126
|
return this.timed('internalAggregate', undefined, async () => {
|
|
129
127
|
const pipeline = this.dialect.buildAggregateStages(entity, q, opts);
|
|
130
|
-
|
|
128
|
+
const rows = await this.execute((session) => this.collection(entity).aggregate(pipeline, { session }).toArray());
|
|
129
|
+
return this.dialect.normalizeAggregateRows(entity, q, rows);
|
|
131
130
|
});
|
|
132
131
|
}
|
|
133
132
|
/**
|
|
@@ -7,7 +7,10 @@ export class MongodbQuerierPool extends AbstractQuerierPool {
|
|
|
7
7
|
client;
|
|
8
8
|
constructor(uri, opts, extra) {
|
|
9
9
|
super(new MongoDialect(dialectOptionsFrom(extra)), extra);
|
|
10
|
-
|
|
10
|
+
// A 64-bit integer read as the exact `bigint` it is, where the driver would round it past 2^53 or
|
|
11
|
+
// hand back its own `Long`; each read then decodes it the way every SQL driver does. First, as the
|
|
12
|
+
// MySQL pool's `supportBigNumbers` is, so an explicit choice of the caller's wins.
|
|
13
|
+
this.client = new MongoClient(uri, { useBigInt64: true, ...opts });
|
|
11
14
|
}
|
|
12
15
|
async getQuerier() {
|
|
13
16
|
const conn = await this.client.connect();
|
|
@@ -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
|
+
* `textIndex` is a text index's weights and language, 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' | 'textIndex';
|
|
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.
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { isVectorIndexType } from '../type/vector.js';
|
|
2
|
+
import { fulltextConfig } from '../util/dialect.util.js';
|
|
2
3
|
/**
|
|
3
4
|
* Whether the table has this index already, by shape rather than name, uniqueness included. An index
|
|
4
5
|
* over an expression, whose text the engine reprints, falls back to its name.
|
|
@@ -35,11 +36,17 @@ export function describeIndexDifferences(source, target, facets) {
|
|
|
35
36
|
const differences = [];
|
|
36
37
|
const comparableEntries = ![...source.entries, ...target.entries].some((entry) => entry.expression || entry.jsonPath || entry.jsonArray);
|
|
37
38
|
if (comparableEntries) {
|
|
38
|
-
const [sourceColumns, targetColumns] = [source
|
|
39
|
+
const [sourceColumns, targetColumns] = [source, target].map((index) => textFieldOrder(index, facets, index.entries.map((entry) => entrySignature(entry, facets))).join(', '));
|
|
39
40
|
if (sourceColumns !== targetColumns) {
|
|
40
41
|
differences.push(`columns: (${targetColumns}) -> (${sourceColumns})`);
|
|
41
42
|
}
|
|
42
43
|
}
|
|
44
|
+
if (facets.has('textIndex') && source.type === 'fulltext' && target.type === 'fulltext') {
|
|
45
|
+
const [expected, actual] = [fulltextConfig(source), fulltextConfig(target)];
|
|
46
|
+
if (expected !== actual) {
|
|
47
|
+
differences.push(`config: ${actual} -> ${expected}`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
43
50
|
if (source.unique !== target.unique) {
|
|
44
51
|
differences.push(`unique: ${target.unique} -> ${source.unique}`);
|
|
45
52
|
}
|
|
@@ -59,6 +66,10 @@ export function describeIndexDifferences(source, target, facets) {
|
|
|
59
66
|
}
|
|
60
67
|
return differences;
|
|
61
68
|
}
|
|
69
|
+
/** A text index's fields as a set where the engine keeps its weights: MongoDB lists them alphabetically. */
|
|
70
|
+
function textFieldOrder(index, facets, entries) {
|
|
71
|
+
return facets.has('textIndex') && index.type === 'fulltext' ? entries.toSorted() : entries;
|
|
72
|
+
}
|
|
62
73
|
function entrySignature(entry, facets) {
|
|
63
74
|
const parts = [entry.column];
|
|
64
75
|
if (facets.has('order')) {
|
|
@@ -72,5 +83,8 @@ function entrySignature(entry, facets) {
|
|
|
72
83
|
if (facets.has('opsClass') && entry.opsClass) {
|
|
73
84
|
parts.push(entry.opsClass);
|
|
74
85
|
}
|
|
86
|
+
if (facets.has('textIndex') && (entry.weight ?? 1) !== 1) {
|
|
87
|
+
parts.push(`weight ${entry.weight}`);
|
|
88
|
+
}
|
|
75
89
|
return parts.join(' ');
|
|
76
90
|
}
|
|
@@ -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
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { AbstractSqlDialect, type DerivedRelation, type HydrateKind, type RelationRows } from '../dialect/abstractSqlDialect.js';
|
|
2
2
|
import { type JsonAccessMode, type JsonSlot } from '../dialect/jsonSql.js';
|
|
3
|
-
import { type EntityMeta, type FieldOptions, type Query, type QueryContext, type QueryPager, type QueryTextSearchOptions, type QueryWhere, type SqlDialectFeatures, type
|
|
3
|
+
import { type EntityMeta, type FieldOptions, type Query, type QueryContext, type QueryPager, type QueryTextSearchOptions, type QueryWhere, type SqlDialectFeatures, type VectorDistance, type VectorMetric } from '../type/index.js';
|
|
4
4
|
/** What SQLite and the engines derived from it have. */
|
|
5
5
|
export declare const SQLITE_FEATURES: SqlDialectFeatures;
|
|
6
6
|
export declare class SqliteDialect extends AbstractSqlDialect {
|
|
@@ -63,12 +63,12 @@ export declare class SqliteDialect extends AbstractSqlDialect {
|
|
|
63
63
|
/** A date reads back as SQLite stored it, a number or text, which JSON carries unchanged. */
|
|
64
64
|
protected hydrateKind(field: FieldOptions | undefined): HydrateKind | undefined;
|
|
65
65
|
/**
|
|
66
|
-
* FTS5 matches the table itself
|
|
67
|
-
*
|
|
66
|
+
* FTS5 matches the table itself, so this works only where the table *is* an FTS5 virtual table (UQL does
|
|
67
|
+
* not create those; declare it outside your entities). The whole query is bound, column filter and all.
|
|
68
68
|
*/
|
|
69
|
-
protected appendTextSearch<E>(ctx: QueryContext,
|
|
69
|
+
protected appendTextSearch<E>(ctx: QueryContext, 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;
|
|
@@ -6,6 +6,15 @@ import { indexDistance, isVectorIndexType } from '../type/vector.js';
|
|
|
6
6
|
import { declaredIndexName } from '../util/ddlExpression.util.js';
|
|
7
7
|
import { findVectorIndex, findVectorSort, textSearchFields, vectorCandidates } from '../util/dialect.util.js';
|
|
8
8
|
import { columnFamily, isIntegerColumn } from '../util/field.util.js';
|
|
9
|
+
/**
|
|
10
|
+
* An FTS5 query over `columns` for what a person typed: each word a quoted string, which FTS5 reads as a
|
|
11
|
+
* term to match and never as syntax, and every one required, as the other engines read plain words.
|
|
12
|
+
*/
|
|
13
|
+
function ftsQuery(columns, value) {
|
|
14
|
+
const quote = (text) => `"${text.replaceAll('"', '""')}"`;
|
|
15
|
+
const words = value.split(/\s+/).filter(Boolean);
|
|
16
|
+
return `{${columns.map(quote).join(' ')}} : (${words.map(quote).join(' ') || '""'})`;
|
|
17
|
+
}
|
|
9
18
|
/** What SQLite and the engines derived from it have. */
|
|
10
19
|
export const SQLITE_FEATURES = {
|
|
11
20
|
ifNotExists: true,
|
|
@@ -27,6 +36,7 @@ export const SQLITE_FEATURES = {
|
|
|
27
36
|
rowLocks: false,
|
|
28
37
|
rowLockWithWindow: true,
|
|
29
38
|
rowLockOf: true,
|
|
39
|
+
textScoreIndexes: false,
|
|
30
40
|
orderedUpsertReturning: true,
|
|
31
41
|
orderedJsonAggregates: true,
|
|
32
42
|
narrowVectorTypes: false,
|
|
@@ -155,16 +165,16 @@ export class SqliteDialect extends AbstractSqlDialect {
|
|
|
155
165
|
return columnFamily(field?.type) === 'date' ? undefined : super.hydrateKind(field);
|
|
156
166
|
}
|
|
157
167
|
/**
|
|
158
|
-
* FTS5 matches the table itself
|
|
159
|
-
*
|
|
168
|
+
* FTS5 matches the table itself, so this works only where the table *is* an FTS5 virtual table (UQL does
|
|
169
|
+
* not create those; declare it outside your entities). The whole query is bound, column filter and all.
|
|
160
170
|
*/
|
|
161
|
-
appendTextSearch(ctx,
|
|
162
|
-
const columns = textSearchFields(meta, search).map((key) => this.
|
|
163
|
-
ctx.append(`${this.escapedTableName(meta)} MATCH
|
|
164
|
-
ctx.addValue(search.$value);
|
|
171
|
+
appendTextSearch(ctx, meta, search) {
|
|
172
|
+
const columns = textSearchFields(meta, search).map((key) => this.resolveColumnName(key, meta.fields[key]));
|
|
173
|
+
ctx.append(`${this.escapedTableName(meta)} MATCH `);
|
|
174
|
+
ctx.addValue(ftsQuery(columns, search.$value));
|
|
165
175
|
}
|
|
166
176
|
/** FTS5's `BM25` of the match, lower for a better one, so negated to rank as every other engine does. */
|
|
167
|
-
|
|
177
|
+
appendTextScore(ctx, meta) {
|
|
168
178
|
ctx.append(`-BM25(${this.escapedTableName(meta)})`);
|
|
169
179
|
}
|
|
170
180
|
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}. */
|
package/dist/type/query.d.ts
CHANGED
|
@@ -130,12 +130,20 @@ export type QuerySortByCount = {
|
|
|
130
130
|
$count: QuerySortDirection;
|
|
131
131
|
};
|
|
132
132
|
/**
|
|
133
|
-
* Ordering by relevance to the `$text` at the root of `$where
|
|
134
|
-
*
|
|
133
|
+
* Ordering by relevance to the `$text` at the root of `$where`, in either direction as any key sorts. The
|
|
134
|
+
* object form also answers it under the name `$project` gives it, most relevant first unless `$order` says.
|
|
135
135
|
*/
|
|
136
136
|
export type QuerySortByText = {
|
|
137
|
-
$text?:
|
|
137
|
+
$text?: QuerySortDirection | {
|
|
138
|
+
readonly $project: string;
|
|
139
|
+
readonly $order?: QuerySortDirection;
|
|
140
|
+
};
|
|
138
141
|
};
|
|
142
|
+
/**
|
|
143
|
+
* A row with the relevance a `$sort: { $text: { $project } }` names, which is not inferred:
|
|
144
|
+
* `(await querier.findMany(Post, q)) as WithScore<Post, 'score'>[]`.
|
|
145
|
+
*/
|
|
146
|
+
export type WithScore<E, K extends string> = E & Record<K, number>;
|
|
139
147
|
/**
|
|
140
148
|
* A sort by fields, JSON paths, a to-one relation's fields, a to-many's `$count`, or a vector distance or
|
|
141
149
|
* `$text` relevance, which `Vector` confines to the queried entity. One mapped type over the key sets: an
|
|
@@ -16,8 +16,9 @@ export type QueryTextSearchOptions<E> = {
|
|
|
16
16
|
*/
|
|
17
17
|
$fields?: QuerySelect<E>;
|
|
18
18
|
/**
|
|
19
|
-
*
|
|
20
|
-
*
|
|
19
|
+
* The language the search is parsed in (e.g. `'english'`, or `'simple'` for no stemming), else that of
|
|
20
|
+
* the fulltext index over its fields: the Postgres family's text-search config, MongoDB's `$language`.
|
|
21
|
+
* MySQL and SQLite parse by their index alone.
|
|
21
22
|
*/
|
|
22
23
|
$config?: string;
|
|
23
24
|
};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import
|
|
1
|
+
import type { IndexType } from '../schema/types.js';
|
|
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 QuerySortDirection, 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. */
|
|
4
5
|
export declare function filterFieldKeys<E>(meta: EntityMeta<E>, payload: EntityData<E> | UpdatePayload<E>, callbackKey: CallbackKey): FieldKey<E>[];
|
|
@@ -171,8 +172,31 @@ 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;
|
|
195
|
+
/** How a `$sort` orders by `$text`: its direction, and the name it answers the relevance under, if any. */
|
|
196
|
+
export declare function textSortOf<E>(sort: QuerySortMap<E> | undefined): {
|
|
197
|
+
readonly order: QuerySortDirection;
|
|
198
|
+
readonly project?: string;
|
|
199
|
+
} | undefined;
|
|
176
200
|
/**
|
|
177
201
|
* The search a `$sort` by `$text` ranks by: the one at the root of the same query's `$where`. A nested or
|
|
178
202
|
* negated one has no score to order by, and MongoDB scores only the one `$text` it allows.
|
|
@@ -456,12 +456,49 @@ 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' &&
|
|
462
491
|
index.columns.length === fields.length &&
|
|
463
492
|
index.columns.every((entry, at) => entry.column === fields[at]));
|
|
464
493
|
}
|
|
494
|
+
/** How a `$sort` orders by `$text`: its direction, and the name it answers the relevance under, if any. */
|
|
495
|
+
export function textSortOf(sort) {
|
|
496
|
+
const text = sort?.$text;
|
|
497
|
+
if (text === undefined) {
|
|
498
|
+
return undefined;
|
|
499
|
+
}
|
|
500
|
+
return isRecord(text) ? { order: text.$order ?? 'desc', project: text.$project } : { order: text };
|
|
501
|
+
}
|
|
465
502
|
/**
|
|
466
503
|
* The search a `$sort` by `$text` ranks by: the one at the root of the same query's `$where`. A nested or
|
|
467
504
|
* negated one has no score to order by, and MongoDB scores only the one `$text` it allows.
|
|
@@ -7,8 +7,10 @@ import type { RawRow } from '../type/index.js';
|
|
|
7
7
|
*/
|
|
8
8
|
export declare function decodeWideNumber(value: string | bigint): number | string;
|
|
9
9
|
/**
|
|
10
|
-
* {@link decodeWideNumber} over every `bigint` cell of a row, for the drivers that hand a BIGINT back
|
|
11
|
-
*
|
|
12
|
-
* object, and a copy per row cost more than the decode it carried.
|
|
10
|
+
* {@link decodeWideNumber} over every `bigint` cell of a row, for the drivers that hand a BIGINT back as
|
|
11
|
+
* one (`bun:sql`, `mariadb`, and every SQLite driver but D1). In place: the row is the driver's fresh
|
|
12
|
+
* object, and a copy per row cost more than the decode it carried. One argument, so it maps rows as is.
|
|
13
13
|
*/
|
|
14
14
|
export declare function decodeBigInts(row: RawRow): RawRow;
|
|
15
|
+
/** {@link decodeBigInts}, keeping the cells `exact` names: what MongoDB reads a `BigInt` field into. */
|
|
16
|
+
export declare function decodeBigIntsExcept(row: RawRow, exact: (key: string) => boolean): RawRow;
|
package/dist/util/wideNumber.js
CHANGED
|
@@ -9,14 +9,18 @@ export function decodeWideNumber(value) {
|
|
|
9
9
|
return Math.abs(decoded) <= Number.MAX_SAFE_INTEGER ? decoded : String(value);
|
|
10
10
|
}
|
|
11
11
|
/**
|
|
12
|
-
* {@link decodeWideNumber} over every `bigint` cell of a row, for the drivers that hand a BIGINT back
|
|
13
|
-
*
|
|
14
|
-
* object, and a copy per row cost more than the decode it carried.
|
|
12
|
+
* {@link decodeWideNumber} over every `bigint` cell of a row, for the drivers that hand a BIGINT back as
|
|
13
|
+
* one (`bun:sql`, `mariadb`, and every SQLite driver but D1). In place: the row is the driver's fresh
|
|
14
|
+
* object, and a copy per row cost more than the decode it carried. One argument, so it maps rows as is.
|
|
15
15
|
*/
|
|
16
16
|
export function decodeBigInts(row) {
|
|
17
|
+
return decodeBigIntsExcept(row, () => false);
|
|
18
|
+
}
|
|
19
|
+
/** {@link decodeBigInts}, keeping the cells `exact` names: what MongoDB reads a `BigInt` field into. */
|
|
20
|
+
export function decodeBigIntsExcept(row, exact) {
|
|
17
21
|
for (const key in row) {
|
|
18
22
|
const value = row[key];
|
|
19
|
-
if (typeof value === 'bigint') {
|
|
23
|
+
if (typeof value === 'bigint' && !exact(key)) {
|
|
20
24
|
row[key] = decodeWideNumber(value);
|
|
21
25
|
}
|
|
22
26
|
}
|
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.2",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"engines": {
|
|
9
9
|
"node": ">=24"
|