uql-orm 0.73.1 → 0.74.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/browser/uql-browser.min.js +2 -2
- package/dist/browser/uql-browser.min.js.map +3 -3
- package/dist/dialect/abstractSqlDialect.d.ts +3 -2
- package/dist/dialect/abstractSqlDialect.js +46 -38
- package/dist/dialect/aliases.d.ts +5 -4
- package/dist/dialect/aliases.js +5 -5
- package/dist/dialect/queryJoins.d.ts +14 -1
- package/dist/dialect/queryJoins.js +32 -3
- package/dist/dialect/vectorSqlDialect.d.ts +6 -8
- package/dist/dialect/vectorSqlDialect.js +10 -13
- package/dist/migrate/drift/driftDetector.js +2 -2
- package/dist/migrate/generator/mongoSchemaGenerator.d.ts +1 -0
- package/dist/migrate/generator/mongoSchemaGenerator.js +20 -8
- package/dist/migrate/migrator.js +11 -4
- package/dist/migrate/schemaGenerator.d.ts +6 -9
- package/dist/migrate/schemaGenerator.js +46 -74
- package/dist/mongo/mongoDialect.d.ts +4 -4
- package/dist/mongo/mongoDialect.js +52 -41
- package/dist/mongo/vectorDistance.d.ts +8 -0
- package/dist/mongo/vectorDistance.js +26 -0
- package/dist/schema/indexDifferences.d.ts +11 -1
- package/dist/schema/indexDifferences.js +34 -2
- package/dist/type/dialect.d.ts +4 -1
- package/dist/type/migration.d.ts +2 -1
- package/dist/type/query.d.ts +18 -5
- package/dist/util/dialect.util.d.ts +6 -1
- package/dist/util/dialect.util.js +11 -1
- package/package.json +1 -1
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A document's distance from `vector` as an aggregation expression, computed exactly as the SQL engines
|
|
3
|
+
* compute theirs: Atlas ranks only through its own index, which no related document reaches. `null` where
|
|
4
|
+
* the field holds no vector, or a cosine has a zero-length side, and `$min` skips a `null`.
|
|
5
|
+
*/
|
|
6
|
+
export function vectorDistanceExpr(column, vector, metric) {
|
|
7
|
+
const field = `$${column}`;
|
|
8
|
+
const sum = (term) => ({
|
|
9
|
+
$sum: { $map: { input: { $zip: { inputs: [field, { $literal: vector }] } }, as: 'pair', in: term } },
|
|
10
|
+
});
|
|
11
|
+
const own = { $arrayElemAt: ['$$pair', 0] };
|
|
12
|
+
const other = { $arrayElemAt: ['$$pair', 1] };
|
|
13
|
+
const dot = sum({ $multiply: [own, other] });
|
|
14
|
+
const distance = {
|
|
15
|
+
cosine: {
|
|
16
|
+
$let: {
|
|
17
|
+
vars: { norms: { $multiply: [{ $sqrt: sum({ $multiply: [own, own] }) }, Math.hypot(...vector)] } },
|
|
18
|
+
in: { $cond: [{ $eq: ['$$norms', 0] }, null, { $subtract: [1, { $divide: [dot, '$$norms'] }] }] },
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
l2: { $sqrt: sum({ $pow: [{ $subtract: [own, other] }, 2] }) },
|
|
22
|
+
inner: { $multiply: [-1, dot] },
|
|
23
|
+
l1: sum({ $abs: { $subtract: [own, other] } }),
|
|
24
|
+
};
|
|
25
|
+
return { $cond: [{ $isArray: field }, distance[metric], null] };
|
|
26
|
+
}
|
|
@@ -5,18 +5,28 @@ import type { IndexNode } from './types.js';
|
|
|
5
5
|
* `textIndex` is a text index's weights and language, kept by an engine that lists its fields in no declared order.
|
|
6
6
|
*/
|
|
7
7
|
export type IndexFacet = 'order' | 'nulls' | 'opsClass' | 'accessMethod' | 'include' | 'vector' | 'textIndex';
|
|
8
|
+
type ComparableIndex = Pick<IndexNode, 'name' | 'entries' | 'unique'>;
|
|
8
9
|
/**
|
|
9
10
|
* Whether the table has this index already, by shape rather than name, uniqueness included. An index
|
|
10
11
|
* over an expression, whose text the engine reprints, falls back to its name.
|
|
11
12
|
*/
|
|
12
|
-
export declare function indexSignature(index:
|
|
13
|
+
export declare function indexSignature(index: ComparableIndex): string;
|
|
13
14
|
/**
|
|
14
15
|
* A constraint name without its kind marker, pairing an index with its older spelling
|
|
15
16
|
* (`idx_User_email` with `User__email_idx`). Only one marker, the trailing one first.
|
|
16
17
|
*/
|
|
17
18
|
export declare function indexNameStem(name: string): string;
|
|
19
|
+
/**
|
|
20
|
+
* The indexes a table lacks and the ones it no longer needs, matched by `keyOf`. Only an index uql
|
|
21
|
+
* named, or whose name the entity claims, is dropped: any other may have been made outside the ORM.
|
|
22
|
+
*/
|
|
23
|
+
export declare function indexChanges<I extends ComparableIndex>(table: string, declared: readonly I[], current: readonly IndexNode[], keyOf?: (index: ComparableIndex) => string): {
|
|
24
|
+
toAdd: I[];
|
|
25
|
+
toDrop: IndexNode[];
|
|
26
|
+
};
|
|
18
27
|
/**
|
|
19
28
|
* What two indexes differ by, comparing only what both sides state structurally: an expression, a JSON
|
|
20
29
|
* path or a predicate is reprinted by the engine, so never compared.
|
|
21
30
|
*/
|
|
22
31
|
export declare function describeIndexDifferences(source: IndexNode, target: IndexNode, facets: ReadonlySet<IndexFacet>): string[];
|
|
32
|
+
export {};
|
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
import { isVectorIndexType } from '../type/vector.js';
|
|
2
2
|
import { fulltextConfig } from '../util/dialect.util.js';
|
|
3
|
+
import { derivedIndexName } from '../util/sql.util.js';
|
|
4
|
+
/** An entry the engine reprints in its own words, so never compared as written. */
|
|
5
|
+
function isReprinted(entry) {
|
|
6
|
+
return Boolean(entry.expression || entry.jsonPath || entry.jsonArray);
|
|
7
|
+
}
|
|
3
8
|
/**
|
|
4
9
|
* Whether the table has this index already, by shape rather than name, uniqueness included. An index
|
|
5
10
|
* over an expression, whose text the engine reprints, falls back to its name.
|
|
6
11
|
*/
|
|
7
12
|
export function indexSignature(index) {
|
|
8
|
-
const comparable = !index.entries.some(
|
|
13
|
+
const comparable = !index.entries.some(isReprinted);
|
|
9
14
|
const identity = comparable
|
|
10
15
|
? index.entries.map((entry) => entry.column).join(',')
|
|
11
16
|
: `name:${indexNameStem(index.name)}`;
|
|
@@ -20,6 +25,33 @@ export function indexNameStem(name) {
|
|
|
20
25
|
const bare = withoutSuffix === name ? name.replace(KIND_PREFIX, '') : withoutSuffix;
|
|
21
26
|
return bare.replace(/__/g, '_');
|
|
22
27
|
}
|
|
28
|
+
/**
|
|
29
|
+
* The indexes a table lacks and the ones it no longer needs, matched by `keyOf`. Only an index uql
|
|
30
|
+
* named, or whose name the entity claims, is dropped: any other may have been made outside the ORM.
|
|
31
|
+
*/
|
|
32
|
+
export function indexChanges(table, declared, current, keyOf = indexSignature) {
|
|
33
|
+
const present = new Set(current.map(keyOf));
|
|
34
|
+
const wanted = new Set(declared.map(keyOf));
|
|
35
|
+
const claimed = new Set(declared.map((index) => index.name));
|
|
36
|
+
const owned = (index) => claimed.has(index.name) || hasDerivedName(table, index);
|
|
37
|
+
return {
|
|
38
|
+
toAdd: declared.filter((index) => !present.has(keyOf(index))),
|
|
39
|
+
toDrop: current.filter((index) => !wanted.has(keyOf(index)) && owned(index)),
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Whether uql named the index itself, from its own columns: `Order__total_idx`, its unique `_uk`, or
|
|
44
|
+
* the `idx_Order_total` it wrote until 0.42.1.
|
|
45
|
+
*/
|
|
46
|
+
function hasDerivedName(table, index) {
|
|
47
|
+
const parts = index.entries.map((entry, at) => (isReprinted(entry) ? `expr${at}` : entry.column));
|
|
48
|
+
const derived = [
|
|
49
|
+
derivedIndexName(table, parts),
|
|
50
|
+
derivedIndexName(table, parts, true),
|
|
51
|
+
`idx_${table}_${parts.join('_')}`,
|
|
52
|
+
];
|
|
53
|
+
return derived.includes(index.name);
|
|
54
|
+
}
|
|
23
55
|
/** What this version emits. */
|
|
24
56
|
const KIND_SUFFIX = /_(?:idx|fk|ck|pk|uk|uq)$/i;
|
|
25
57
|
/**
|
|
@@ -34,7 +66,7 @@ const KIND_PREFIX = /^(?:idx|fk|ck|pk|uk|uq)_/i;
|
|
|
34
66
|
*/
|
|
35
67
|
export function describeIndexDifferences(source, target, facets) {
|
|
36
68
|
const differences = [];
|
|
37
|
-
const comparableEntries = ![...source.entries, ...target.entries].some(
|
|
69
|
+
const comparableEntries = ![...source.entries, ...target.entries].some(isReprinted);
|
|
38
70
|
if (comparableEntries) {
|
|
39
71
|
const [sourceColumns, targetColumns] = [source, target].map((index) => textFieldOrder(index, facets, index.entries.map((entry) => entrySignature(entry, facets))).join(', '));
|
|
40
72
|
if (sourceColumns !== targetColumns) {
|
package/dist/type/dialect.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ import type { Query, QueryConflictPaths, QueryOptions, QueryPage, QuerySearch, R
|
|
|
3
3
|
import type { QueryAggMap, QueryAggregate, QueryAggregateOp, QueryGroupMap } from './queryAggregate.js';
|
|
4
4
|
import type { QueryWhere } from './queryWhere.js';
|
|
5
5
|
import type { Type } from './utility.js';
|
|
6
|
+
import type { QueryVectorQuery } from './vector.js';
|
|
6
7
|
/**
|
|
7
8
|
* comparison options.
|
|
8
9
|
*/
|
|
@@ -247,13 +248,15 @@ export type AggregateCall<E = object> = {
|
|
|
247
248
|
readonly field?: string;
|
|
248
249
|
readonly where?: QueryWhere<E>;
|
|
249
250
|
};
|
|
250
|
-
/** What a relation aggregate reads: how many rows, or one of the target's columns
|
|
251
|
+
/** What a relation aggregate reads: how many rows, or one of the target's columns, or its distance to `search`. */
|
|
251
252
|
export type RelationAggregateProjection = {
|
|
252
253
|
readonly op: '$count';
|
|
253
254
|
readonly field?: never;
|
|
255
|
+
readonly search?: never;
|
|
254
256
|
} | {
|
|
255
257
|
readonly op: Exclude<RelationAggregateOp, '$count'>;
|
|
256
258
|
readonly field: string;
|
|
259
|
+
readonly search?: QueryVectorQuery;
|
|
257
260
|
};
|
|
258
261
|
/**
|
|
259
262
|
* A relation aggregate as a `computed` field holds it: an {@link AggregateCall} over the rows of the
|
package/dist/type/migration.d.ts
CHANGED
|
@@ -203,7 +203,8 @@ export interface SchemaDiff {
|
|
|
203
203
|
}[];
|
|
204
204
|
readonly columnsToDrop?: string[];
|
|
205
205
|
readonly indexesToAdd?: IndexSchema[];
|
|
206
|
-
|
|
206
|
+
/** Whole rather than by name, so the rollback can create each again. */
|
|
207
|
+
readonly indexesToDrop?: IndexSchema[];
|
|
207
208
|
readonly foreignKeysToAdd?: ForeignKeySchema[];
|
|
208
209
|
/** Dropped under the name the *database* reported, which is the only name a `DROP` can use. */
|
|
209
210
|
readonly foreignKeysToDrop?: string[];
|
package/dist/type/query.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ import type { QueryLock } from './queryLock.js';
|
|
|
3
3
|
import type { QueryRaw } from './queryRaw.js';
|
|
4
4
|
import type { QueryWhere } from './queryWhere.js';
|
|
5
5
|
import type { BooleanLike, Except, IsMany, PrimaryKey } from './utility.js';
|
|
6
|
-
import type { QueryVectorSearch } from './vector.js';
|
|
6
|
+
import type { QueryVectorQuery, QueryVectorSearch } from './vector.js';
|
|
7
7
|
export type QueryOptions = {
|
|
8
8
|
/**
|
|
9
9
|
* Toggle named entity filters for this query. `false` disables all filters;
|
|
@@ -129,6 +129,18 @@ export type QuerySortValue = QuerySortDirection | QueryVectorSearch;
|
|
|
129
129
|
export type QuerySortByCount = {
|
|
130
130
|
$count: QuerySortDirection;
|
|
131
131
|
};
|
|
132
|
+
/** The fields of `E` a vector search can rank by. */
|
|
133
|
+
type VectorFieldKey<E> = {
|
|
134
|
+
[P in FieldKey<E>]: NonNullable<E[P]> extends readonly number[] ? P : never;
|
|
135
|
+
}[FieldKey<E>];
|
|
136
|
+
/**
|
|
137
|
+
* Ordering parents by the row of a to-many nearest a vector, per vector field: its distance is the
|
|
138
|
+
* smallest of theirs. Nothing to `$project`, since no one row of the parent's answers under it. Never
|
|
139
|
+
* where the target has no vector, since an empty map would admit any value at all.
|
|
140
|
+
*/
|
|
141
|
+
export type QuerySortByNearest<E> = [VectorFieldKey<E>] extends [never] ? never : {
|
|
142
|
+
[P in VectorFieldKey<E>]?: QueryVectorQuery;
|
|
143
|
+
};
|
|
132
144
|
/**
|
|
133
145
|
* Ordering by relevance to the `$text` at the root of `$where`, in either direction as any key sorts. The
|
|
134
146
|
* object form also answers it under the name `$project` gives it, most relevant first unless `$order` says.
|
|
@@ -145,12 +157,13 @@ export type QuerySortByText = {
|
|
|
145
157
|
*/
|
|
146
158
|
export type WithProjection<E, K extends string> = E & Record<K, number>;
|
|
147
159
|
/**
|
|
148
|
-
* A sort by fields, JSON paths, a to-one relation's fields, a to-many's `$count
|
|
149
|
-
* sorts the queried entity itself, not a relation's rows - a
|
|
150
|
-
* mapped type over the key sets: an intersection is checked once
|
|
160
|
+
* A sort by fields, JSON paths, a to-one relation's fields, a to-many's `$count` or nearest row, a vector
|
|
161
|
+
* distance, or - where `Root` says it sorts the queried entity itself, not a relation's rows - a `$text`
|
|
162
|
+
* relevance or a distance it projects. One mapped type over the key sets: an intersection is checked once
|
|
163
|
+
* per member, which made this the costliest.
|
|
151
164
|
*/
|
|
152
165
|
export type QuerySortMap<E, Root extends boolean = true, K extends keyof E = FieldKey<E> | RelationKey<E>> = {
|
|
153
|
-
[P in K]?: P extends RelationKey<E> ? IsMany<E[P]> extends true ? QuerySortByCount : QuerySortMap<RelationTarget<E[P]>, false> :
|
|
166
|
+
[P in K]?: P extends RelationKey<E> ? IsMany<E[P]> extends true ? QuerySortByCount | QuerySortByNearest<RelationTarget<E[P]>> : QuerySortMap<RelationTarget<E[P]>, false> : NonNullable<E[P]> extends readonly number[] ? Root extends true ? QuerySortValue : QuerySortDirection | QueryVectorQuery : QuerySortDirection;
|
|
154
167
|
} & ([JsonFieldPaths<E>] extends [never] ? unknown : {
|
|
155
168
|
[P in JsonFieldPaths<E>]?: QuerySortDirection;
|
|
156
169
|
}) & (Root extends true ? QuerySortByText : unknown);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { IndexType } from '../schema/types.js';
|
|
2
|
-
import { type AggregateCall, 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 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
|
+
import { type AggregateCall, 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 QueryExclude, type QueryGroupMap, type QueryOptions, QueryRaw, type QuerySearch, type QuerySelect, type QuerySelectValue, type QuerySortDirection, type QuerySizeComparisonOps, type QuerySortMap, type QueryTextSearchOptions, type QueryVectorQuery, type QueryVectorSearch, type QueryWhere, type RelationKey, type UpdatePayload, type VectorDistance } from '../type/index.js';
|
|
3
3
|
export type CallbackKey = keyof Pick<FieldOptions, 'onInsert' | 'onUpdate'>;
|
|
4
4
|
/** The keys of `payload` a write persists as columns. */
|
|
5
5
|
export declare function filterFieldKeys<E>(meta: EntityMeta<E>, payload: EntityData<E> | UpdatePayload<E>, callbackKey: CallbackKey): FieldKey<E>[];
|
|
@@ -81,6 +81,11 @@ export declare function vectorCandidates(q: {
|
|
|
81
81
|
* "which kind", which decide the name Atlas is queried by and the setting Postgres is tuned with.
|
|
82
82
|
*/
|
|
83
83
|
export declare function findVectorIndex<E>(meta: EntityMeta<E>, key: string): EntityIndexMeta<E> | undefined;
|
|
84
|
+
/**
|
|
85
|
+
* The metric a distance to `key` measures by: the search's own, else the field's, else its index's, which
|
|
86
|
+
* serves no other, else cosine. The one fallback every engine resolves, so none can rank by another.
|
|
87
|
+
*/
|
|
88
|
+
export declare function vectorDistanceOf<E>(meta: EntityMeta<E>, key: string, search: QueryVectorQuery): VectorDistance;
|
|
84
89
|
/**
|
|
85
90
|
* Whether a `$where` filters by vector distance anywhere in its tree, `$and`/`$or`/`$not` included.
|
|
86
91
|
* What tells Postgres that an HNSW scan needs to iterate rather than return one candidate list.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { getContext, UqlSecurityError } from '../context/context.js';
|
|
2
2
|
import { soleIdOf } from '../entity/metadata/definition.js';
|
|
3
3
|
import { QueryRaw, resolveAggregateOp, SOFT_DELETE_FILTER, } from '../type/index.js';
|
|
4
|
-
import { VECTOR_INDEX_TYPES } from '../type/vector.js';
|
|
4
|
+
import { DEFAULT_VECTOR_DISTANCE, VECTOR_INDEX_TYPES } from '../type/vector.js';
|
|
5
5
|
import { getFieldKeys, isDatabaseWritten } from './field.util.js';
|
|
6
6
|
import { entityName, getKeys, hasKeys, isOperatorObject, isScalarId, isRecord, isWhereMap, someKey, } from './object.util.js';
|
|
7
7
|
/** The keys of `payload` a write persists as columns. */
|
|
@@ -209,6 +209,16 @@ const VECTOR_INDEX_MATCH = new Set([...VECTOR_INDEX_TYPES, 'vectorSearch']);
|
|
|
209
209
|
export function findVectorIndex(meta, key) {
|
|
210
210
|
return meta.indexes?.find((index) => index.type !== undefined && VECTOR_INDEX_MATCH.has(index.type) && indexCoversColumn(index, key));
|
|
211
211
|
}
|
|
212
|
+
/**
|
|
213
|
+
* The metric a distance to `key` measures by: the search's own, else the field's, else its index's, which
|
|
214
|
+
* serves no other, else cosine. The one fallback every engine resolves, so none can rank by another.
|
|
215
|
+
*/
|
|
216
|
+
export function vectorDistanceOf(meta, key, search) {
|
|
217
|
+
return (search.$distance ??
|
|
218
|
+
meta.fields[key]?.distance ??
|
|
219
|
+
findVectorIndex(meta, key)?.distance ??
|
|
220
|
+
DEFAULT_VECTOR_DISTANCE);
|
|
221
|
+
}
|
|
212
222
|
/**
|
|
213
223
|
* Whether a `$where` filters by vector distance anywhere in its tree, `$and`/`$or`/`$not` included.
|
|
214
224
|
* What tells Postgres that an HNSW scan needs to iterate rather than return one candidate list.
|
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.
|
|
6
|
+
"version": "0.74.1",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"engines": {
|
|
9
9
|
"node": ">=24"
|