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.
@@ -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: Pick<IndexNode, 'name' | 'entries' | 'unique'>): string;
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((entry) => entry.expression || entry.jsonPath || entry.jsonArray);
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((entry) => entry.expression || entry.jsonPath || entry.jsonArray);
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) {
@@ -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
@@ -203,7 +203,8 @@ export interface SchemaDiff {
203
203
  }[];
204
204
  readonly columnsToDrop?: string[];
205
205
  readonly indexesToAdd?: IndexSchema[];
206
- readonly indexesToDrop?: string[];
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[];
@@ -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`, or - where `Root` says it
149
- * sorts the queried entity itself, not a relation's rows - a vector distance or a `$text` relevance. One
150
- * mapped type over the key sets: an intersection is checked once per member, which made this the costliest.
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> : Root extends true ? NonNullable<E[P]> extends readonly number[] ? QuerySortValue : QuerySortDirection : QuerySortDirection;
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.73.1",
6
+ "version": "0.74.1",
7
7
  "type": "module",
8
8
  "engines": {
9
9
  "node": ">=24"