uql-orm 0.37.1 → 0.39.0
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 +5 -5
- package/dist/cockroachdb/cockroachDialect.d.ts +5 -24
- package/dist/cockroachdb/cockroachDialect.js +3 -29
- package/dist/dialect/abstractSqlDialect.d.ts +42 -12
- package/dist/dialect/abstractSqlDialect.js +117 -48
- package/dist/dialect/aliases.d.ts +6 -0
- package/dist/dialect/aliases.js +6 -0
- package/dist/dialect/index.d.ts +0 -5
- package/dist/dialect/index.js +2 -5
- package/dist/dialect/jsonSql.d.ts +17 -2
- package/dist/dialect/jsonSql.js +15 -0
- package/dist/dialect/mysqlLikeSqlDialect.d.ts +13 -14
- package/dist/dialect/mysqlLikeSqlDialect.js +16 -20
- package/dist/dialect/pgLikeSqlDialect.d.ts +17 -20
- package/dist/dialect/pgLikeSqlDialect.js +30 -62
- package/dist/dialect/vectorCast.d.ts +0 -9
- package/dist/dialect/vectorCast.js +0 -8
- package/dist/dialect/vectorSqlDialect.d.ts +47 -17
- package/dist/dialect/vectorSqlDialect.js +78 -30
- package/dist/entity/decorator/entity.d.ts +1 -1
- package/dist/entity/metadata/definition.d.ts +1 -1
- package/dist/entity/metadata/definition.js +4 -3
- package/dist/http/query.js +3 -2
- package/dist/libsql/libsqlDialect.d.ts +2 -2
- package/dist/libsql/libsqlDialect.js +3 -3
- package/dist/maria/mariaDialect.d.ts +12 -17
- package/dist/maria/mariaDialect.js +21 -36
- package/dist/maria/mariaVectorMetrics.d.ts +8 -0
- package/dist/maria/mariaVectorMetrics.js +10 -0
- package/dist/maria/mariadbQuerier.d.ts +5 -0
- package/dist/maria/mariadbQuerier.js +5 -0
- package/dist/migrate/builder/migrationBuilder.d.ts +8 -17
- package/dist/migrate/builder/migrationBuilder.js +48 -136
- package/dist/migrate/builder/types.d.ts +0 -2
- package/dist/migrate/ddl/index.d.ts +11 -0
- package/dist/migrate/ddl/index.js +34 -0
- package/dist/{dialect/indexSqlDialect.d.ts → migrate/ddl/indexDdl.d.ts} +23 -19
- package/dist/migrate/ddl/indexDdl.js +126 -0
- package/dist/migrate/ddl/mysqlIndexDdl.d.ts +52 -0
- package/dist/migrate/ddl/mysqlIndexDdl.js +125 -0
- package/dist/migrate/ddl/pgIndexDdl.d.ts +36 -0
- package/dist/migrate/ddl/pgIndexDdl.js +87 -0
- package/dist/migrate/drift/driftDetector.js +6 -1
- package/dist/migrate/generator/mongoSchemaGenerator.js +1 -1
- package/dist/migrate/index.d.ts +1 -0
- package/dist/migrate/index.js +2 -0
- package/dist/migrate/introspection/mysqlIntrospector.d.ts +9 -3
- package/dist/migrate/introspection/mysqlIntrospector.js +27 -5
- package/dist/migrate/migrator.js +3 -2
- package/dist/migrate/schemaGenerator.d.ts +6 -6
- package/dist/migrate/schemaGenerator.js +13 -22
- package/dist/mongo/mongoDialect.d.ts +1 -2
- package/dist/mongo/mongoDialect.js +29 -22
- package/dist/mongo/mongodbQuerier.js +1 -1
- package/dist/mysql/mysqlDialect.d.ts +3 -6
- package/dist/mysql/mysqlDialect.js +4 -11
- package/dist/postgres/postgresDialect.d.ts +2 -0
- package/dist/postgres/postgresDialect.js +2 -0
- package/dist/querier/abstractSqlQuerier.d.ts +11 -0
- package/dist/querier/abstractSqlQuerier.js +35 -0
- package/dist/schema/canonicalType.js +35 -36
- package/dist/schema/indexDifferences.d.ts +2 -1
- package/dist/schema/indexDifferences.js +3 -2
- package/dist/sqlite/sqliteDialect.d.ts +2 -2
- package/dist/sqlite/sqliteDialect.js +5 -5
- package/dist/turso/tursoDialect.d.ts +2 -2
- package/dist/turso/tursoDialect.js +4 -4
- package/dist/type/dialect.d.ts +21 -7
- package/dist/type/dialect.js +17 -0
- package/dist/type/entity.d.ts +117 -8
- package/dist/type/entity.js +7 -0
- package/dist/type/query.d.ts +18 -0
- package/dist/type/query.js +6 -0
- package/dist/type/queryWhere.d.ts +46 -2
- package/dist/type/vector.d.ts +45 -7
- package/dist/type/vector.js +16 -1
- package/dist/util/dialect.util.d.ts +20 -1
- package/dist/util/dialect.util.js +49 -4
- package/dist/util/object.util.d.ts +7 -1
- package/dist/util/object.util.js +8 -0
- package/dist/util/relationQuery.util.d.ts +3 -1
- package/dist/util/relationQuery.util.js +8 -3
- package/package.json +1 -1
- package/dist/dialect/indexSqlDialect.js +0 -103
package/dist/type/dialect.js
CHANGED
|
@@ -1,3 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* An index capability that some engines have and others reject outright. Verified live: expression
|
|
3
|
+
* indexes exist everywhere but MariaDB 12.3 (which needs a generated column); prefix lengths are
|
|
4
|
+
* MySQL-family only and *required* there to index `TEXT`; `NULLS FIRST/LAST` and operator classes are
|
|
5
|
+
* Postgres-only (CockroachDB 26.2 answers "unimplemented"); `INCLUDE` is Postgres-wire only; the
|
|
6
|
+
* MySQL family is alone in having no partial indexes. The two JSON ones split the other way: the
|
|
7
|
+
* engines that index a path inside a document are the ones whose planner matches the query's own
|
|
8
|
+
* extraction back to it (Postgres, CockroachDB, SQLite - MySQL matches neither
|
|
9
|
+
* `CAST(col->>'$.x' AS CHAR(n))` nor `JSON_VALUE(... RETURNING ...)`, verified on 26.7, and needs a
|
|
10
|
+
* generated column instead), while MySQL alone has the multi-valued index a JSON array needs.
|
|
11
|
+
*
|
|
12
|
+
* Introspectors reuse the vocabulary for what they can read *back*, which is what diffing may
|
|
13
|
+
* compare. The two sets are deliberately not the same object and must not be unified: Postgres can
|
|
14
|
+
* emit an expression index and read one back, but MySQL emits one it cannot describe afterwards.
|
|
15
|
+
*/
|
|
1
16
|
export const INDEX_FEATURE_LABELS = {
|
|
2
17
|
expression: 'expression indexes',
|
|
3
18
|
partial: 'partial indexes',
|
|
@@ -5,4 +20,6 @@ export const INDEX_FEATURE_LABELS = {
|
|
|
5
20
|
nullsOrder: 'NULLS FIRST/LAST in an index',
|
|
6
21
|
opsClass: 'index operator classes',
|
|
7
22
|
include: 'covering indexes (INCLUDE)',
|
|
23
|
+
jsonPath: 'indexes over a path inside a JSON column',
|
|
24
|
+
jsonArray: 'multi-valued indexes over a JSON array',
|
|
8
25
|
};
|
package/dist/type/entity.d.ts
CHANGED
|
@@ -7,6 +7,13 @@ import type { VectorDistance, VectorIndexOptions, VectorIndexType } from './vect
|
|
|
7
7
|
* Allow to customize the name of the property that identifies an entity
|
|
8
8
|
*/
|
|
9
9
|
export declare const idKey: unique symbol;
|
|
10
|
+
/**
|
|
11
|
+
* The one filter name uql registers itself, from `@Field({ softDelete })`. Four ends have to agree on
|
|
12
|
+
* it and none would fail if they drifted: the field that registers it, the decorator that reserves
|
|
13
|
+
* the name against a user's own filter, the hard delete that switches it off, and the bypass check
|
|
14
|
+
* that lets it through on an entity which never declared one.
|
|
15
|
+
*/
|
|
16
|
+
export declare const SOFT_DELETE_FILTER = "softDelete";
|
|
10
17
|
/**
|
|
11
18
|
* Infers the key names of an entity
|
|
12
19
|
*/
|
|
@@ -521,10 +528,59 @@ export type IndexTypeOptions = {
|
|
|
521
528
|
* ```
|
|
522
529
|
*
|
|
523
530
|
* `C` is the entity's `FieldKey` on the `@Index`/`defineEntity` paths, where the decorated class says
|
|
524
|
-
* which columns exist
|
|
525
|
-
*
|
|
531
|
+
* which columns exist, and `E` the entity itself, which is what checks a JSON entry's path. Both
|
|
532
|
+
* default to the unchecked form for the migration builder's `table.index(...)`, which names raw
|
|
533
|
+
* table columns with no entity in scope.
|
|
526
534
|
*/
|
|
527
|
-
export type IndexColumnInput<C extends string = string> = C | QueryRaw | IndexColumnOptions<C>;
|
|
535
|
+
export type IndexColumnInput<C extends string = string, E = unknown> = C | QueryRaw | IndexColumnOptions<C> | IndexJsonColumnOptions<C, E>;
|
|
536
|
+
/**
|
|
537
|
+
* The JSON entries, whose `path` is checked against the payload of the column the same entry names -
|
|
538
|
+
* a mapped union, one arm per JSON field, so `{ column: 'kind', jsonPath: { path: 'thema.color' } }`
|
|
539
|
+
* cannot compile. It matters more here than anywhere else in the index API: a path that is merely
|
|
540
|
+
* *misspelled* still builds a perfectly valid index, one no query will ever match, and nothing at
|
|
541
|
+
* runtime can tell that from the index you meant.
|
|
542
|
+
*
|
|
543
|
+
* `jsonArray`'s path is the array's own, so on a column that *is* the array (`Json<string[]>`) it
|
|
544
|
+
* resolves to `never` and the property can only be omitted, which is exactly the truth.
|
|
545
|
+
*
|
|
546
|
+
* Falls back to the unchecked shape only where there is no entity to check against - the migration
|
|
547
|
+
* builder. An entity with no JSON field at all offers no arm, which is also the truth.
|
|
548
|
+
*/
|
|
549
|
+
type IndexJsonColumnOptions<C extends string, E> = unknown extends E ? IndexColumnModifiers & {
|
|
550
|
+
readonly column: C | QueryRaw;
|
|
551
|
+
} : {
|
|
552
|
+
[K in JsonColumnKey<E>]: IndexColumnPlainModifiers & {
|
|
553
|
+
readonly column: K;
|
|
554
|
+
} & ({
|
|
555
|
+
readonly jsonPath: WithCheckedPath<IndexJsonPath, E, K>;
|
|
556
|
+
readonly jsonArray?: never;
|
|
557
|
+
} | {
|
|
558
|
+
readonly jsonArray: WithCheckedPath<IndexJsonArray, E, K>;
|
|
559
|
+
readonly jsonPath?: never;
|
|
560
|
+
});
|
|
561
|
+
}[JsonColumnKey<E>];
|
|
562
|
+
/**
|
|
563
|
+
* The JSON columns an index can address, which is a wider set than {@link JsonFieldKey}: that one
|
|
564
|
+
* unwraps arrays to find the brand, so a column that *is* an array (`Json<string[]>`) reads as a
|
|
565
|
+
* plain one - right for `$where`, which has no path into it, and wrong for `jsonArray`, whose whole
|
|
566
|
+
* subject is that column.
|
|
567
|
+
*/
|
|
568
|
+
type JsonColumnKey<E> = {
|
|
569
|
+
readonly [K in keyof E]-?: IsJson<NonNullable<E[K]>> extends true ? K : IsJson<JsonElement<E[K]>> extends true ? K : never;
|
|
570
|
+
}[Key<E>];
|
|
571
|
+
/** The payload a path is checked against: the column's own brand, or that of the documents it holds. */
|
|
572
|
+
type JsonColumnPayload<V> = IsJson<NonNullable<V>> extends true ? UnwrapJson<NonNullable<V>> : JsonPayload<V>;
|
|
573
|
+
/**
|
|
574
|
+
* A JSON modifier with its `path` narrowed to the ones that column's payload actually has. Everything
|
|
575
|
+
* else - and `path`'s own optionality, which `jsonArray` needs and `jsonPath` does not - is taken
|
|
576
|
+
* from the declared type rather than restated, so a property added to either cannot miss the checked
|
|
577
|
+
* form.
|
|
578
|
+
*/
|
|
579
|
+
type WithCheckedPath<T extends {
|
|
580
|
+
path?: string;
|
|
581
|
+
}, E, K extends Key<E>> = Except<T, 'path' & keyof T> & {
|
|
582
|
+
[P in keyof Pick<T, Extract<keyof T, 'path'>>]: DeepJsonKeys<JsonColumnPayload<E[K]>>;
|
|
583
|
+
};
|
|
528
584
|
/**
|
|
529
585
|
* What an index entry can carry besides the thing being indexed. Shared with the normalized
|
|
530
586
|
* `IndexColumnSchema`, so the authored and internal shapes cannot drift apart.
|
|
@@ -541,8 +597,56 @@ export type IndexColumnModifiers = {
|
|
|
541
597
|
readonly nulls?: 'first' | 'last';
|
|
542
598
|
/** Operator class, e.g. `jsonb_path_ops` for a smaller GIN index. Postgres only. */
|
|
543
599
|
readonly opsClass?: string;
|
|
600
|
+
/** Index a path inside a JSON column. See {@link IndexJsonPath}. */
|
|
601
|
+
readonly jsonPath?: IndexJsonPath;
|
|
602
|
+
/** Index every element of a JSON array. See {@link IndexJsonArray}. */
|
|
603
|
+
readonly jsonArray?: IndexJsonArray;
|
|
544
604
|
};
|
|
545
|
-
|
|
605
|
+
/**
|
|
606
|
+
* An index over one path inside a JSON column, compiled by the same code a `$where` on that path is:
|
|
607
|
+
* an expression index is matched by its own text, so an index spelled even slightly differently is
|
|
608
|
+
* one the planner never reaches for.
|
|
609
|
+
*
|
|
610
|
+
* `type` picks the reading the way an operand's own type does (`jsonCompareMode`): compared as a
|
|
611
|
+
* number, indexed as a number. Which engines have it is `IndexFeature`'s `jsonPath`.
|
|
612
|
+
*
|
|
613
|
+
* @example
|
|
614
|
+
* ```ts
|
|
615
|
+
* @Index([{ column: 'kind', jsonPath: { path: 'theme.color', type: String } }]) // 'kind.theme.color': 'red'
|
|
616
|
+
* @Index([{ column: 'kind', jsonPath: { path: 'rating', type: Number } }]) // 'kind.rating': { $gte: 4 }
|
|
617
|
+
* ```
|
|
618
|
+
*/
|
|
619
|
+
export type IndexJsonPath = {
|
|
620
|
+
/** The path inside the column, spelled as a `$where` key spells it: `'theme.color'`. */
|
|
621
|
+
readonly path: string;
|
|
622
|
+
/** How the value is read, matching what the queries over it compare against. */
|
|
623
|
+
readonly type: FieldType;
|
|
624
|
+
};
|
|
625
|
+
/**
|
|
626
|
+
* MySQL's multi-valued index: one key per *element* of the JSON array at `path` (the column itself
|
|
627
|
+
* when there is none), which is the only index `$all`/`$elemMatch` containment can use. `type` is
|
|
628
|
+
* the element's, and a string or binary one needs a `length`, since the cast is what sizes the key.
|
|
629
|
+
*
|
|
630
|
+
* MySQL is alone in having it - `IndexFeature`'s `jsonArray` - and an index asking for it elsewhere
|
|
631
|
+
* is refused rather than silently built.
|
|
632
|
+
*
|
|
633
|
+
* @example
|
|
634
|
+
* ```ts
|
|
635
|
+
* @Index([{ column: 'tags', jsonArray: { type: String, length: 64 } }]) // tags: { $all: [...] }
|
|
636
|
+
* @Index([{ column: 'kind', jsonArray: { path: 'ids', type: Number } }]) // 'kind.ids': { $all: [...] }
|
|
637
|
+
* ```
|
|
638
|
+
*/
|
|
639
|
+
export type IndexJsonArray = {
|
|
640
|
+
/** The array's path inside the column, spelled as a `$where` key spells it; omit for the column. */
|
|
641
|
+
readonly path?: string;
|
|
642
|
+
/** The element type, matching what the queries over the array compare against. */
|
|
643
|
+
readonly type: FieldType;
|
|
644
|
+
/** Length of a string or binary element, which MySQL's `CHAR(n) ARRAY` cast requires. */
|
|
645
|
+
readonly length?: number;
|
|
646
|
+
};
|
|
647
|
+
/** The modifiers that do not name a JSON path, and so need no entity to be checked against. */
|
|
648
|
+
type IndexColumnPlainModifiers = Except<IndexColumnModifiers, 'jsonPath' | 'jsonArray'>;
|
|
649
|
+
export type IndexColumnOptions<C extends string = string> = IndexColumnPlainModifiers & {
|
|
546
650
|
/** The column to index, or `raw(...)` for an expression. */
|
|
547
651
|
readonly column: C | QueryRaw;
|
|
548
652
|
};
|
|
@@ -621,7 +725,7 @@ export type EntityOptions<E = unknown> = {
|
|
|
621
725
|
readonly relations?: {
|
|
622
726
|
readonly [K in RelationKey<E>]?: RelationOptionsFor<E[K]>;
|
|
623
727
|
};
|
|
624
|
-
readonly indexes?: readonly EntityIndexInput<FieldKey<E
|
|
728
|
+
readonly indexes?: readonly EntityIndexInput<FieldKey<E>, E>[];
|
|
625
729
|
/** Map hook events to method names on the entity class. */
|
|
626
730
|
readonly hooks?: Partial<Record<HookEvent, readonly MethodKey<E>[]>>;
|
|
627
731
|
};
|
|
@@ -630,11 +734,16 @@ export type EntityOptions<E = unknown> = {
|
|
|
630
734
|
* migration builder's `table.index(...)`. `Except` (not plain `Omit`) keeps `type`/`distance` a
|
|
631
735
|
* discriminated pair: omitting `distance` on a vector index type is a compile error.
|
|
632
736
|
*/
|
|
633
|
-
export type IndexOptions = Except<EntityIndexMeta, 'columns'
|
|
737
|
+
export type IndexOptions<E = unknown> = Except<EntityIndexMeta, 'columns' | 'include'> & {
|
|
738
|
+
/** Non-key columns stored in the index; a typo builds nothing, the server refusing the statement. */
|
|
739
|
+
readonly include?: readonly IndexFieldKey<E>[];
|
|
740
|
+
};
|
|
741
|
+
/** A field of `E`, or any name where there is no entity to check it against - the migration builder. */
|
|
742
|
+
type IndexFieldKey<E> = unknown extends E ? string : FieldKey<E>;
|
|
634
743
|
/**
|
|
635
744
|
* An index as authored, before `defineIndex` normalizes its columns.
|
|
636
745
|
*/
|
|
637
|
-
export type EntityIndexInput<C extends string = string> = IndexOptions & {
|
|
638
|
-
readonly columns: readonly IndexColumnInput<C>[];
|
|
746
|
+
export type EntityIndexInput<C extends string = string, E = unknown> = IndexOptions<E> & {
|
|
747
|
+
readonly columns: readonly IndexColumnInput<C, E>[];
|
|
639
748
|
};
|
|
640
749
|
export {};
|
package/dist/type/entity.js
CHANGED
|
@@ -2,3 +2,10 @@
|
|
|
2
2
|
* Allow to customize the name of the property that identifies an entity
|
|
3
3
|
*/
|
|
4
4
|
export const idKey = Symbol('idKey');
|
|
5
|
+
/**
|
|
6
|
+
* The one filter name uql registers itself, from `@Field({ softDelete })`. Four ends have to agree on
|
|
7
|
+
* it and none would fail if they drifted: the field that registers it, the decorator that reserves
|
|
8
|
+
* the name against a user's own filter, the hard delete that switches it off, and the bypass check
|
|
9
|
+
* that lets it through on an entity which never declared one.
|
|
10
|
+
*/
|
|
11
|
+
export const SOFT_DELETE_FILTER = 'softDelete';
|
package/dist/type/query.d.ts
CHANGED
|
@@ -245,6 +245,18 @@ export type Query<E> = {
|
|
|
245
245
|
* is what keeps the clause off those statements at the type level.
|
|
246
246
|
*/
|
|
247
247
|
$lock?: QueryLock;
|
|
248
|
+
/**
|
|
249
|
+
* how many candidates an approximate-nearest-neighbour index explores before ranking, for a vector
|
|
250
|
+
* search. Higher trades speed for recall; the default is whatever the engine's own is, which is
|
|
251
|
+
* tuned for speed. Ignored where the search is exact (SQLite, libSQL and Turso scan every row) and
|
|
252
|
+
* where the field carries no ANN index, since there is nothing to widen.
|
|
253
|
+
*
|
|
254
|
+
* The units are the index's, not UQL's, so the number is not comparable across index types: it
|
|
255
|
+
* becomes `hnsw.ef_search` or `ivfflat.probes` on Postgres, `mhnsw_ef_search` on MariaDB, and
|
|
256
|
+
* `numCandidates` on MongoDB Atlas. On Postgres it needs an open transaction, since a `SET LOCAL`
|
|
257
|
+
* outside one applies to nothing.
|
|
258
|
+
*/
|
|
259
|
+
$candidates?: number;
|
|
248
260
|
/**
|
|
249
261
|
* filtering options.
|
|
250
262
|
*/
|
|
@@ -275,6 +287,12 @@ export declare const QUERY_OBJECT_CLAUSES: readonly ["$select", "$populate", "$e
|
|
|
275
287
|
*/
|
|
276
288
|
export declare const QUERY_ROOT_OBJECT_CLAUSES: readonly ["$count"];
|
|
277
289
|
export declare const QUERY_NUMBER_CLAUSES: readonly ["$skip", "$limit"];
|
|
290
|
+
/**
|
|
291
|
+
* Number clauses only the statement itself takes - the numeric mirror of {@link QUERY_ROOT_OBJECT_CLAUSES}.
|
|
292
|
+
* `$candidates` tunes the index behind a vector search, and a vector search only ever ranks the rows
|
|
293
|
+
* the statement returns, so a relation's own query has nothing to tune.
|
|
294
|
+
*/
|
|
295
|
+
export declare const QUERY_ROOT_NUMBER_CLAUSES: readonly ["$candidates"];
|
|
278
296
|
export declare const QUERY_BOOLEAN_CLAUSES: readonly ["$distinct"];
|
|
279
297
|
/**
|
|
280
298
|
* options to get a single record.
|
package/dist/type/query.js
CHANGED
|
@@ -32,4 +32,10 @@ export const QUERY_OBJECT_CLAUSES = [
|
|
|
32
32
|
*/
|
|
33
33
|
export const QUERY_ROOT_OBJECT_CLAUSES = ['$count'];
|
|
34
34
|
export const QUERY_NUMBER_CLAUSES = ['$skip', '$limit'];
|
|
35
|
+
/**
|
|
36
|
+
* Number clauses only the statement itself takes - the numeric mirror of {@link QUERY_ROOT_OBJECT_CLAUSES}.
|
|
37
|
+
* `$candidates` tunes the index behind a vector search, and a vector search only ever ranks the rows
|
|
38
|
+
* the statement returns, so a relation's own query has nothing to tune.
|
|
39
|
+
*/
|
|
40
|
+
export const QUERY_ROOT_NUMBER_CLAUSES = ['$candidates'];
|
|
35
41
|
export const QUERY_BOOLEAN_CLAUSES = ['$distinct'];
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { FieldKey, IdValue, JsonFieldPaths, JsonFieldPathValue, RelationKey, RelationTarget } from './entity.js';
|
|
2
2
|
import type { QueryRaw } from './queryRaw.js';
|
|
3
3
|
import type { ExpandScalar, IsMany, QueryComparableScalar, Scalar } from './utility.js';
|
|
4
|
+
import type { QueryVectorQuery } from './vector.js';
|
|
4
5
|
/**
|
|
5
6
|
* options for full-text-search operator.
|
|
6
7
|
*/
|
|
@@ -91,6 +92,34 @@ export type QueryNegateOp = keyof Pick<QueryWhereRootOperator<unknown>, '$not' |
|
|
|
91
92
|
export type QuerySizeComparisonOps = {
|
|
92
93
|
[K in QueryHavingOp | '$between']?: NonNullable<QueryWhereFieldOperatorMap<number>[K]>;
|
|
93
94
|
};
|
|
95
|
+
/**
|
|
96
|
+
* Filter by distance to a query vector: `$where`'s counterpart to `$sort`'s ranking, so "the closest
|
|
97
|
+
* ten" and "everything closer than 0.35" stay separate asks.
|
|
98
|
+
*
|
|
99
|
+
* Bounded by {@link QueryOrderedOp} - what {@link QuerySizeComparisonOps} ranges over, minus
|
|
100
|
+
* `$eq`/`$ne`. A distance is a float, so exact equality against one is a bug every time, where
|
|
101
|
+
* `$size` compares an integer `COUNT`. No `$project` either: naming the distance is `$sort`'s job,
|
|
102
|
+
* since the `SELECT` list is built from `$sort` alone and a `$near` nested inside an `$or` has no
|
|
103
|
+
* business projecting a column.
|
|
104
|
+
*
|
|
105
|
+
* `$distance` is here for the same reason `$sort` has it: each clause states its own search
|
|
106
|
+
* completely, so neither depends on the other. Omitted, it falls back to the field's declared metric,
|
|
107
|
+
* which is where the metric belongs - beside the index it has to match. Naming a different one per
|
|
108
|
+
* query mostly buys a full scan, since an ANN index is built for exactly one operator class.
|
|
109
|
+
*
|
|
110
|
+
* `$vector` is required, and repeating it beside a `$sort` that ranks by the same field is the point:
|
|
111
|
+
* every other `$where` operator means the same thing wherever it appears, and inheriting one from a
|
|
112
|
+
* sibling clause would make this the first whose validity depends on what else the query contains -
|
|
113
|
+
* unfixable in the type, and carried into merged entity filters and `/http` payloads alike. Naming
|
|
114
|
+
* the vector in a `const` is what removes the repetition, at the call site where it belongs.
|
|
115
|
+
*
|
|
116
|
+
* A `$near` carrying no bound is a `WHERE` that is always true. The dialect rejects that rather than
|
|
117
|
+
* the type: `/http` casts client JSON straight to `Query`, so the check has to exist there anyway,
|
|
118
|
+
* and an "at least one of these five" union would cost every caller worse errors for a second copy.
|
|
119
|
+
*/
|
|
120
|
+
export type QueryVectorNear = QueryVectorQuery & {
|
|
121
|
+
[K in QueryOrderedOp]?: NonNullable<QueryWhereFieldOperatorMap<number>[K]>;
|
|
122
|
+
};
|
|
94
123
|
export type QueryWhereFieldOperatorMap<T> = {
|
|
95
124
|
/**
|
|
96
125
|
* whether a value is equal to the given value.
|
|
@@ -200,6 +229,12 @@ export type QueryWhereFieldOperatorMap<T> = {
|
|
|
200
229
|
* @example { addresses: { $elemMatch: { city: { $like: 'New%' } } } }
|
|
201
230
|
*/
|
|
202
231
|
$elemMatch?: unknown extends T ? QueryWhereElemMatch<unknown> : NonNullable<T> extends readonly (infer U)[] ? QueryWhereElemMatch<U> : never;
|
|
232
|
+
/**
|
|
233
|
+
* whether a vector is within a given distance of the query vector. `$sort` ranks by distance;
|
|
234
|
+
* this filters by it, so "the closest ten" and "everything closer than 0.35" are separate asks.
|
|
235
|
+
* @example { embedding: { $near: { $vector: queryVec, $lt: 0.35 } } }
|
|
236
|
+
*/
|
|
237
|
+
$near?: QueryVectorNear;
|
|
203
238
|
};
|
|
204
239
|
/**
|
|
205
240
|
* Element-level conditions for `$elemMatch`. Scalar elements take an operator map for the element
|
|
@@ -241,15 +276,24 @@ type QueryArrayOp = keyof Pick<QueryWhereFieldOperatorMap<unknown>, '$all' | '$s
|
|
|
241
276
|
* Ordering operators: {@link QueryCompareOp} plus `$between`.
|
|
242
277
|
*/
|
|
243
278
|
type QueryOrderedOp = QueryCompareOp | keyof Pick<QueryWhereFieldOperatorMap<unknown>, '$between'>;
|
|
279
|
+
/**
|
|
280
|
+
* Vector-only operators. `Pick`'s constraint ties this back to {@link QueryWhereFieldOperatorMap}
|
|
281
|
+
* so a rename there breaks this union at compile time.
|
|
282
|
+
*/
|
|
283
|
+
type QueryVectorOp = keyof Pick<QueryWhereFieldOperatorMap<unknown>, '$near'>;
|
|
244
284
|
/**
|
|
245
285
|
* Operators applicable to every field type: equality, membership, negation, and null checks.
|
|
286
|
+
*
|
|
287
|
+
* @remarks This is a subtraction, not a list, so an operator added to
|
|
288
|
+
* {@link QueryWhereFieldOperatorMap} without also being classified above lands here and is offered
|
|
289
|
+
* on every field - `$near` on a `boolean`, say. Classify first, then add.
|
|
246
290
|
*/
|
|
247
|
-
type QueryCommonOp = Exclude<keyof QueryWhereFieldOperatorMap<unknown>, QueryStringOp | QueryArrayOp | QueryOrderedOp>;
|
|
291
|
+
type QueryCommonOp = Exclude<keyof QueryWhereFieldOperatorMap<unknown>, QueryStringOp | QueryArrayOp | QueryOrderedOp | QueryVectorOp>;
|
|
248
292
|
/**
|
|
249
293
|
* Operator keys applicable to a field of type `T`. Brackets prevent union distribution so an
|
|
250
294
|
* optional field (`string | undefined`) or a literal union (`'a' | 'b'`) gates as one type.
|
|
251
295
|
*/
|
|
252
|
-
type QueryAllowedOp<T> = QueryCommonOp | ([NonNullable<T>] extends [QueryComparableScalar] ? QueryOrderedOp : never) | ([NonNullable<T>] extends [string] ? QueryStringOp : never) | (IsMany<T> extends true ? QueryArrayOp : never);
|
|
296
|
+
type QueryAllowedOp<T> = QueryCommonOp | ([NonNullable<T>] extends [QueryComparableScalar] ? QueryOrderedOp : never) | ([NonNullable<T>] extends [string] ? QueryStringOp : never) | ([NonNullable<T>] extends [readonly number[] | Uint8Array] ? QueryVectorOp : never) | (IsMany<T> extends true ? QueryArrayOp : never);
|
|
253
297
|
/**
|
|
254
298
|
* Operators applicable to a field of type `T`: string operators require string fields, ordering
|
|
255
299
|
* operators comparable fields, array operators array fields. `unknown` stays fully permissive
|
package/dist/type/vector.d.ts
CHANGED
|
@@ -11,6 +11,19 @@ import type { IndexType } from '../schema/types.js';
|
|
|
11
11
|
* vector, which no field type maps to. It would be a value that compiles and always throws.
|
|
12
12
|
*/
|
|
13
13
|
export type VectorDistance = 'cosine' | 'l2' | 'inner' | 'l1';
|
|
14
|
+
/**
|
|
15
|
+
* The vector and the metric: the half of a similarity search that names *what* distance to compute,
|
|
16
|
+
* shared by `$sort`'s ranking ({@link QueryVectorSearch}) and `$where`'s threshold
|
|
17
|
+
* ({@link QueryVectorNear}) so the two cannot describe the same distance differently.
|
|
18
|
+
*/
|
|
19
|
+
export interface QueryVectorQuery {
|
|
20
|
+
/** The query vector to compare against. */
|
|
21
|
+
readonly $vector: readonly number[];
|
|
22
|
+
/** Distance metric. Overrides entity-level default. Falls back to `'cosine'`. */
|
|
23
|
+
readonly $distance?: VectorDistance;
|
|
24
|
+
}
|
|
25
|
+
/** The keys that describe the search rather than bound it, so `$near`'s bounds are what is left. */
|
|
26
|
+
export declare const VECTOR_QUERY_KEYS: readonly ["$vector", "$distance"];
|
|
14
27
|
/**
|
|
15
28
|
* Vector similarity search options - used inside `$sort` on vector fields.
|
|
16
29
|
*
|
|
@@ -22,11 +35,7 @@ export type VectorDistance = 'cosine' | 'l2' | 'inner' | 'l1';
|
|
|
22
35
|
* });
|
|
23
36
|
* ```
|
|
24
37
|
*/
|
|
25
|
-
export interface QueryVectorSearch {
|
|
26
|
-
/** The query vector to compare against. */
|
|
27
|
-
readonly $vector: readonly number[];
|
|
28
|
-
/** Distance metric. Overrides entity-level default. Falls back to `'cosine'`. */
|
|
29
|
-
readonly $distance?: VectorDistance;
|
|
38
|
+
export interface QueryVectorSearch extends QueryVectorQuery {
|
|
30
39
|
/** Project the computed distance as a named field in the result. */
|
|
31
40
|
readonly $project?: string;
|
|
32
41
|
}
|
|
@@ -40,6 +49,29 @@ export interface QueryVectorSearch {
|
|
|
40
49
|
* ```
|
|
41
50
|
*/
|
|
42
51
|
export type WithDistance<E, K extends string = '_distance'> = E & Record<K, number>;
|
|
52
|
+
/**
|
|
53
|
+
* How one dialect spells one distance metric. Two shapes exist across engines - an infix operator
|
|
54
|
+
* (`"col" <=> $1`, pgvector) or a function call (`VEC_DISTANCE_COSINE(col, ?)`, MariaDB and the
|
|
55
|
+
* SQLite family) - so they are one discriminated map rather than two parallel ones. That is what
|
|
56
|
+
* lets a single `appendVectorSort` serve every engine, and makes the map's key set the one answer
|
|
57
|
+
* to "does this dialect have this metric".
|
|
58
|
+
*
|
|
59
|
+
* `opsSuffix` rides along on the operator form because pgvector's index operator class is named from
|
|
60
|
+
* the same metric (`vector_cosine_ops`): keeping them together is what stops a dialect from having
|
|
61
|
+
* the operator but not the class it indexes with.
|
|
62
|
+
*/
|
|
63
|
+
export type VectorMetric = {
|
|
64
|
+
readonly op: string;
|
|
65
|
+
readonly opsSuffix: string;
|
|
66
|
+
} | {
|
|
67
|
+
readonly fn: string;
|
|
68
|
+
};
|
|
69
|
+
/** The operator form, for the pgvector-family dialects whose index DDL also needs `opsSuffix`. */
|
|
70
|
+
export type VectorOperatorMetric = Extract<VectorMetric, {
|
|
71
|
+
op: string;
|
|
72
|
+
}>;
|
|
73
|
+
/** Every dialect words this the same, and one of them used to throw a bare `Error` for it. */
|
|
74
|
+
export declare function unsupportedVectorMetric(dialectName: string, distance: VectorDistance, indexName?: string): TypeError;
|
|
43
75
|
/**
|
|
44
76
|
* Vector-specific tuning options shared by `@Index` decorator, entity metadata, and migration schema.
|
|
45
77
|
*/
|
|
@@ -53,5 +85,11 @@ export type VectorIndexOptions = {
|
|
|
53
85
|
/** IVFFlat: number of inverted lists. */
|
|
54
86
|
lists?: number;
|
|
55
87
|
};
|
|
56
|
-
/**
|
|
57
|
-
|
|
88
|
+
/**
|
|
89
|
+
* Index types whose emitted DDL depends on the distance metric. The runtime list is the source, so
|
|
90
|
+
* the type and every dialect's "do I have this one?" answer cannot drift from each other.
|
|
91
|
+
*/
|
|
92
|
+
export declare const VECTOR_INDEX_TYPES: readonly ["hnsw", "ivfflat", "vector"];
|
|
93
|
+
export type VectorIndexType = (typeof VECTOR_INDEX_TYPES)[number];
|
|
94
|
+
/** Whether an index type is one of {@link VECTOR_INDEX_TYPES}; narrows an optional `IndexSchema.type`. */
|
|
95
|
+
export declare function isVectorIndexType(type: IndexType | undefined): type is VectorIndexType;
|
package/dist/type/vector.js
CHANGED
|
@@ -1 +1,16 @@
|
|
|
1
|
-
|
|
1
|
+
/** The keys that describe the search rather than bound it, so `$near`'s bounds are what is left. */
|
|
2
|
+
export const VECTOR_QUERY_KEYS = ['$vector', '$distance'];
|
|
3
|
+
/** Every dialect words this the same, and one of them used to throw a bare `Error` for it. */
|
|
4
|
+
export function unsupportedVectorMetric(dialectName, distance, indexName) {
|
|
5
|
+
const where = indexName === undefined ? '' : ` (index "${indexName}")`;
|
|
6
|
+
return new TypeError(`${dialectName} does not support vector distance metric: ${distance}${where}`);
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Index types whose emitted DDL depends on the distance metric. The runtime list is the source, so
|
|
10
|
+
* the type and every dialect's "do I have this one?" answer cannot drift from each other.
|
|
11
|
+
*/
|
|
12
|
+
export const VECTOR_INDEX_TYPES = ['hnsw', 'ivfflat', 'vector'];
|
|
13
|
+
/** Whether an index type is one of {@link VECTOR_INDEX_TYPES}; narrows an optional `IndexSchema.type`. */
|
|
14
|
+
export function isVectorIndexType(type) {
|
|
15
|
+
return type !== undefined && VECTOR_INDEX_TYPES.includes(type);
|
|
16
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type CascadeType, type EntityData, type EntityMeta, type FieldKey, type FieldOptions, 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 QueryVectorSearch, type QueryWhere, type QueryWhereMap, type RelationKey } from '../type/index.js';
|
|
1
|
+
import { type CascadeType, type EntityData, type EntityIndexMeta, type EntityMeta, type FieldKey, type FieldOptions, 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 QueryVectorSearch, type QueryWhere, type QueryWhereMap, type RelationKey } from '../type/index.js';
|
|
2
2
|
export type CallbackKey = keyof Pick<FieldOptions, 'onInsert' | 'onUpdate'>;
|
|
3
3
|
export declare function filterFieldKeys<E>(meta: EntityMeta<E>, payload: EntityData<E>, callbackKey: CallbackKey): FieldKey<E>[];
|
|
4
4
|
/**
|
|
@@ -62,6 +62,25 @@ export declare function asSelectMap<E>(select: QuerySelectValue<E> | undefined):
|
|
|
62
62
|
export declare function normalizeScalarFieldSelection<E>(meta: EntityMeta<E>, select?: QuerySelect<E>, exclude?: QueryExclude<E>): FieldKey<E>[];
|
|
63
63
|
/** Type guard: checks whether a sort value is a vector similarity search. */
|
|
64
64
|
export declare function isVectorSearch(value: unknown): value is QueryVectorSearch;
|
|
65
|
+
/**
|
|
66
|
+
* The vector search a `$sort` carries, if it ranks by one. First entry wins when two fields are
|
|
67
|
+
* ranked at once - one scan for every dialect, so the SQL side and MongoDB cannot disagree about
|
|
68
|
+
* which, as they did when one took the first and the other the last.
|
|
69
|
+
*/
|
|
70
|
+
export declare function findVectorSort<E>(sort: QuerySortMap<E> | undefined): {
|
|
71
|
+
key: string;
|
|
72
|
+
search: QueryVectorSearch;
|
|
73
|
+
} | undefined;
|
|
74
|
+
/**
|
|
75
|
+
* The vector index declared on `key`, if any. Answers both "is there an ANN index to tune here" and
|
|
76
|
+
* "which kind", which decide the name Atlas is queried by and the setting Postgres is tuned with.
|
|
77
|
+
*/
|
|
78
|
+
export declare function findVectorIndex<E>(meta: EntityMeta<E>, key: string): EntityIndexMeta | undefined;
|
|
79
|
+
/**
|
|
80
|
+
* Whether a `$where` filters by vector distance anywhere in its tree, `$and`/`$or`/`$not` included.
|
|
81
|
+
* What tells Postgres that an HNSW scan needs to iterate rather than return one candidate list.
|
|
82
|
+
*/
|
|
83
|
+
export declare function hasVectorNear(where: unknown): boolean;
|
|
65
84
|
/** Type guard: checks whether an update payload value is a JSON operator object. */
|
|
66
85
|
export declare function isJsonUpdateOp(value: unknown): value is JsonUpdateOp;
|
|
67
86
|
export declare function augmentWhere<E>(meta: EntityMeta<E>, target?: QueryWhere<E>, source?: QueryWhere<E>): QueryWhere<E>;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { getContext, UqlSecurityError } from '../context/context.js';
|
|
2
|
-
import { QueryRaw, resolveAggregateOp, } from '../type/index.js';
|
|
3
|
-
import {
|
|
2
|
+
import { QueryRaw, resolveAggregateOp, SOFT_DELETE_FILTER, } from '../type/index.js';
|
|
3
|
+
import { VECTOR_INDEX_TYPES } from '../type/vector.js';
|
|
4
|
+
import { entityName, getFieldKeys, getKeys, hasKeys, someKey } from './object.util.js';
|
|
4
5
|
export function filterFieldKeys(meta, payload, callbackKey) {
|
|
5
6
|
return getKeys(payload).filter((key) => {
|
|
6
7
|
const fieldOpts = meta.fields[key];
|
|
@@ -162,6 +163,50 @@ export function normalizeScalarFieldSelection(meta, select, exclude) {
|
|
|
162
163
|
export function isVectorSearch(value) {
|
|
163
164
|
return value !== null && typeof value === 'object' && '$vector' in value;
|
|
164
165
|
}
|
|
166
|
+
/**
|
|
167
|
+
* The vector search a `$sort` carries, if it ranks by one. First entry wins when two fields are
|
|
168
|
+
* ranked at once - one scan for every dialect, so the SQL side and MongoDB cannot disagree about
|
|
169
|
+
* which, as they did when one took the first and the other the last.
|
|
170
|
+
*/
|
|
171
|
+
export function findVectorSort(sort) {
|
|
172
|
+
for (const key of getKeys(sort ?? {})) {
|
|
173
|
+
const search = sort?.[key];
|
|
174
|
+
// The guard narrows here, where a `.find()` over entries would hand back an untyped tuple.
|
|
175
|
+
if (isVectorSearch(search)) {
|
|
176
|
+
return { key, search };
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return undefined;
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Every index type that means "vector" to some engine: pgvector's two, the generic one MariaDB and
|
|
183
|
+
* CockroachDB share, and Atlas's. Wider than {@link VECTOR_INDEX_TYPES}, which is the set whose DDL
|
|
184
|
+
* depends on a distance metric - Atlas takes its metric from the index definition instead.
|
|
185
|
+
*/
|
|
186
|
+
const VECTOR_INDEX_MATCH = new Set([...VECTOR_INDEX_TYPES, 'vectorSearch']);
|
|
187
|
+
/**
|
|
188
|
+
* The vector index declared on `key`, if any. Answers both "is there an ANN index to tune here" and
|
|
189
|
+
* "which kind", which decide the name Atlas is queried by and the setting Postgres is tuned with.
|
|
190
|
+
*/
|
|
191
|
+
export function findVectorIndex(meta, key) {
|
|
192
|
+
return meta.indexes?.find((index) => index.type !== undefined && VECTOR_INDEX_MATCH.has(index.type) && indexCoversColumn(index, key));
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Whether a `$where` filters by vector distance anywhere in its tree, `$and`/`$or`/`$not` included.
|
|
196
|
+
* What tells Postgres that an HNSW scan needs to iterate rather than return one candidate list.
|
|
197
|
+
*/
|
|
198
|
+
export function hasVectorNear(where) {
|
|
199
|
+
if (where === null || typeof where !== 'object') {
|
|
200
|
+
return false;
|
|
201
|
+
}
|
|
202
|
+
if (Array.isArray(where)) {
|
|
203
|
+
return where.some(hasVectorNear);
|
|
204
|
+
}
|
|
205
|
+
return Object.entries(where).some(([key, value]) => key === '$near' || hasVectorNear(value));
|
|
206
|
+
}
|
|
207
|
+
function indexCoversColumn(index, key) {
|
|
208
|
+
return index.columns.some((entry) => !(entry instanceof QueryRaw) && entry.column === key);
|
|
209
|
+
}
|
|
165
210
|
/** `satisfies` ties this to {@link JsonUpdateOp}, so renaming an operator breaks it at compile time. */
|
|
166
211
|
const JSON_UPDATE_OPS = [
|
|
167
212
|
'$set',
|
|
@@ -199,7 +244,7 @@ export function buildQueryWhereAsMap(meta, filter = {}) {
|
|
|
199
244
|
}
|
|
200
245
|
/** Returns a `QueryOptions.filters` value with the built-in soft-delete filter disabled (used by hard delete). */
|
|
201
246
|
export function withoutSoftDeleteFilter(filters) {
|
|
202
|
-
return filters === false ? false : { ...filters,
|
|
247
|
+
return filters === false ? false : { ...filters, [SOFT_DELETE_FILTER]: false };
|
|
203
248
|
}
|
|
204
249
|
/**
|
|
205
250
|
* Returns a new `$where` map with every active entity filter's condition merged in, resolving
|
|
@@ -241,7 +286,7 @@ export function applyFilters(meta, whereMap, opts) {
|
|
|
241
286
|
if (condition === undefined) {
|
|
242
287
|
const onMissing = filter.onMissing ?? (filter.security ? 'throw' : 'skip');
|
|
243
288
|
if (onMissing === 'throw') {
|
|
244
|
-
throw new UqlSecurityError(`filter '${name}' on '${meta
|
|
289
|
+
throw new UqlSecurityError(`filter '${name}' on '${entityName(meta)}' could not resolve (missing context)`);
|
|
245
290
|
}
|
|
246
291
|
continue;
|
|
247
292
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { FieldKey, FieldOptions } from '../type/index.js';
|
|
1
|
+
import type { EntityMeta, FieldKey, FieldOptions } from '../type/index.js';
|
|
2
2
|
export declare function throwPendingTransaction(): never;
|
|
3
3
|
export declare function throwNoPendingTransaction(): never;
|
|
4
4
|
export declare function clone<T>(value: T): T;
|
|
@@ -20,6 +20,12 @@ export declare function isOperatorObject(value: unknown): value is Record<string
|
|
|
20
20
|
/** Whether every key of the non-empty object `value` is an operator (no plain field names mixed in). */
|
|
21
21
|
export declare function isOperatorOnlyObject(value: unknown): value is Record<string, unknown>;
|
|
22
22
|
export declare function getKeys<T extends object>(obj: T): (keyof T & string)[];
|
|
23
|
+
/**
|
|
24
|
+
* The entity's own name for a message to carry, declared or its class's. `defineEntity` always sets
|
|
25
|
+
* one, so the fallback is for a meta a decorator is still building - which is why the sites spelling
|
|
26
|
+
* this out reached for three different fallbacks, `?? ''` among them, and named nothing at all.
|
|
27
|
+
*/
|
|
28
|
+
export declare function entityName<E>(meta: EntityMeta<E>): string;
|
|
23
29
|
export declare function getFieldKeys<E>(fields: {
|
|
24
30
|
[K in FieldKey<E>]?: FieldOptions;
|
|
25
31
|
}): FieldKey<E>[];
|
package/dist/util/object.util.js
CHANGED
|
@@ -52,6 +52,14 @@ export function isOperatorOnlyObject(value) {
|
|
|
52
52
|
export function getKeys(obj) {
|
|
53
53
|
return obj ? Object.keys(obj) : [];
|
|
54
54
|
}
|
|
55
|
+
/**
|
|
56
|
+
* The entity's own name for a message to carry, declared or its class's. `defineEntity` always sets
|
|
57
|
+
* one, so the fallback is for a meta a decorator is still building - which is why the sites spelling
|
|
58
|
+
* this out reached for three different fallbacks, `?? ''` among them, and named nothing at all.
|
|
59
|
+
*/
|
|
60
|
+
export function entityName(meta) {
|
|
61
|
+
return meta.name ?? meta.entity.name;
|
|
62
|
+
}
|
|
55
63
|
export function getFieldKeys(fields) {
|
|
56
64
|
return getKeys(fields).filter((field) => fields[field].eager ?? true);
|
|
57
65
|
}
|
|
@@ -30,9 +30,11 @@ export type JoinedRelationRejectedKey = (typeof JOINED_RELATION_REJECTIONS)[numb
|
|
|
30
30
|
export declare function getRelationRequestSummary<E>(meta: EntityMeta<E>, populate?: QueryPopulate<E>): RelationRequestSummary<E>;
|
|
31
31
|
/** True when `$populate` includes at least one relation key. */
|
|
32
32
|
export declare function populatesRelations<E>(meta: EntityMeta<E>, populate?: QueryPopulate<E>): boolean;
|
|
33
|
-
export type RelationQuery<E extends object = object> = Except<Query<E>,
|
|
33
|
+
export type RelationQuery<E extends object = object> = Except<Query<E>, StatementOnlyClause> & {
|
|
34
34
|
$required?: boolean;
|
|
35
35
|
};
|
|
36
|
+
/** The clauses that describe the statement rather than what a query selects. */
|
|
37
|
+
type StatementOnlyClause = '$lock' | '$candidates';
|
|
36
38
|
export type ParsedRelationQuery<E extends object = object> = {
|
|
37
39
|
query: RelationQuery<E>;
|
|
38
40
|
required: boolean;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { QUERY_BOOLEAN_CLAUSES, QUERY_NUMBER_CLAUSES, QUERY_OBJECT_CLAUSES } from '../type/query.js';
|
|
1
|
+
import { QUERY_BOOLEAN_CLAUSES, QUERY_NUMBER_CLAUSES, QUERY_OBJECT_CLAUSES, QUERY_ROOT_NUMBER_CLAUSES, } from '../type/query.js';
|
|
2
2
|
import { getKeys, someKey } from './object.util.js';
|
|
3
3
|
/**
|
|
4
4
|
* Whether a relation holds many rows per parent, so it cannot be joined into the parent's row. Takes
|
|
@@ -74,6 +74,8 @@ export function populatesRelations(meta, populate) {
|
|
|
74
74
|
return false;
|
|
75
75
|
return someKey(populate, (key) => !!populate[key] && key in meta.relations);
|
|
76
76
|
}
|
|
77
|
+
/** Their runtime half, so the check below cannot drift from the type above. */
|
|
78
|
+
const STATEMENT_ONLY_CLAUSES = ['$lock', ...QUERY_ROOT_NUMBER_CLAUSES];
|
|
77
79
|
// Taken from the clause groups declared beside `Query` itself, so a renamed clause fails to compile
|
|
78
80
|
// here instead of quietly narrowing what a relation query accepts. `$required` is the one key that
|
|
79
81
|
// is not a `Query` clause at all - it says how the relation joins, not what it selects.
|
|
@@ -91,8 +93,11 @@ function isRelationQueryObject(value) {
|
|
|
91
93
|
export function parseRelationQueryValue(value) {
|
|
92
94
|
// Caught before the shape check so the message names the key, rather than reporting the whole
|
|
93
95
|
// object as an unrecognized relation query value.
|
|
94
|
-
if (isRecord(value)
|
|
95
|
-
|
|
96
|
+
if (isRecord(value)) {
|
|
97
|
+
const statementOnly = STATEMENT_ONLY_CLAUSES.find((clause) => clause in value);
|
|
98
|
+
if (statementOnly) {
|
|
99
|
+
throw new TypeError(`'${statementOnly}' applies to the whole statement, not to a populated relation. Move it to the top level of the query.`);
|
|
100
|
+
}
|
|
96
101
|
}
|
|
97
102
|
if (isRelationQueryObject(value)) {
|
|
98
103
|
return { query: value, required: value.$required === true, nested: true };
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"homepage": "https://uql-orm.dev",
|
|
4
4
|
"description": "JSON-native ORM for Node.js, Bun and Deno. Supports PostgreSQL, PGlite, MySQL, MariaDB, SQLite, CockroachDB, Turso, Neon, Cloudflare D1 and MongoDB. Queries are plain JSON, typed to the leaf.",
|
|
5
5
|
"license": "MIT",
|
|
6
|
-
"version": "0.
|
|
6
|
+
"version": "0.39.0",
|
|
7
7
|
"type": "module",
|
|
8
8
|
"engines": {
|
|
9
9
|
"node": ">=24"
|