uql-orm 0.56.0 → 0.58.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/README.md +7 -9
- package/dist/browser/uql-browser.min.js +2 -2
- package/dist/browser/uql-browser.min.js.map +4 -4
- package/dist/cockroachdb/cockroachDialect.d.ts +5 -2
- package/dist/cockroachdb/cockroachDialect.js +2 -10
- package/dist/d1/d1SqliteDialect.d.ts +1 -0
- package/dist/d1/d1SqliteDialect.js +2 -0
- package/dist/dialect/abstractSqlDialect.d.ts +196 -33
- package/dist/dialect/abstractSqlDialect.js +410 -203
- package/dist/dialect/aliases.d.ts +10 -7
- package/dist/dialect/aliases.js +12 -7
- package/dist/dialect/hydrateColumn.d.ts +8 -2
- package/dist/dialect/hydrateColumn.js +33 -1
- package/dist/dialect/jsonSql.d.ts +13 -5
- package/dist/dialect/jsonSql.js +24 -7
- package/dist/dialect/mysqlLikeSqlDialect.d.ts +31 -3
- package/dist/dialect/mysqlLikeSqlDialect.js +57 -5
- package/dist/dialect/pgLikeSqlDialect.d.ts +20 -20
- package/dist/dialect/pgLikeSqlDialect.js +23 -48
- package/dist/dialect/pgVectorMetrics.d.ts +13 -0
- package/dist/dialect/pgVectorMetrics.js +17 -0
- package/dist/dialect/queryContext.d.ts +3 -7
- package/dist/dialect/queryContext.js +13 -8
- package/dist/dialect/queryJoins.d.ts +8 -4
- package/dist/dialect/queryJoins.js +26 -11
- package/dist/dialect/vectorSqlDialect.d.ts +2 -2
- package/dist/dialect/vectorSqlDialect.js +2 -3
- package/dist/entity/decorator/bag.d.ts +2 -2
- package/dist/entity/decorator/entity.d.ts +8 -9
- package/dist/entity/decorator/entity.js +6 -7
- package/dist/entity/decorator/members.d.ts +7 -6
- package/dist/entity/decorator/members.js +2 -1
- package/dist/entity/metadata/definition.d.ts +16 -11
- package/dist/entity/metadata/definition.js +54 -42
- package/dist/http/handler.d.ts +2 -2
- package/dist/http/handler.js +0 -1
- package/dist/maria/mariaDialect.d.ts +13 -6
- package/dist/maria/mariaDialect.js +29 -9
- package/dist/migrate/cli.d.ts +2 -3
- package/dist/migrate/cli.js +2 -2
- package/dist/migrate/codegen/entityCodeGenerator.js +6 -4
- package/dist/migrate/codegen/entityTypes.d.ts +1 -1
- package/dist/migrate/codegen/entityTypes.js +4 -3
- package/dist/migrate/codegen/indexDecoratorSource.d.ts +5 -4
- package/dist/migrate/codegen/indexDecoratorSource.js +17 -13
- package/dist/migrate/codegen/sourceLiteral.d.ts +2 -0
- package/dist/migrate/codegen/sourceLiteral.js +4 -0
- package/dist/migrate/ddl/index.d.ts +1 -5
- package/dist/migrate/ddl/index.js +14 -25
- package/dist/migrate/ddl/indexDdl.d.ts +11 -2
- package/dist/migrate/ddl/indexDdl.js +17 -1
- package/dist/migrate/ddl/mssqlIndexDdl.d.ts +10 -0
- package/dist/migrate/ddl/mssqlIndexDdl.js +10 -0
- package/dist/migrate/ddl/mysqlIndexDdl.d.ts +10 -17
- package/dist/migrate/ddl/mysqlIndexDdl.js +16 -27
- package/dist/migrate/ddl/pgIndexDdl.d.ts +18 -8
- package/dist/migrate/ddl/pgIndexDdl.js +29 -12
- package/dist/migrate/generator/mongoSchemaGenerator.d.ts +3 -3
- package/dist/migrate/migrator.d.ts +4 -4
- package/dist/migrate/schemaGenerator.d.ts +8 -8
- package/dist/migrate/schemaGenerator.js +5 -7
- package/dist/migrate/schemaGeneratorAsync.d.ts +2 -3
- package/dist/mongo/mongoDialect.d.ts +31 -18
- package/dist/mongo/mongoDialect.js +146 -104
- package/dist/mongo/mongodbQuerier.d.ts +10 -17
- package/dist/mongo/mongodbQuerier.js +31 -106
- package/dist/mssql/mssqlDialect.d.ts +16 -0
- package/dist/mssql/mssqlDialect.js +26 -4
- package/dist/mysql/mysqlDialect.d.ts +2 -0
- package/dist/mysql/mysqlDialect.js +4 -0
- package/dist/querier/abstractQuerier.d.ts +20 -36
- package/dist/querier/abstractQuerier.js +35 -129
- package/dist/querier/abstractQuerierPool.d.ts +2 -2
- package/dist/querier/abstractSqlQuerier.d.ts +4 -17
- package/dist/querier/abstractSqlQuerier.js +40 -50
- package/dist/schema/canonicalType.js +4 -4
- package/dist/schema/indexDifferences.js +4 -4
- package/dist/schema/schemaASTBuilder.d.ts +3 -3
- package/dist/schema/schemaASTBuilder.js +32 -3
- package/dist/schema/schemaASTDiffer.js +5 -5
- package/dist/sqlite/sqliteDialect.d.ts +20 -1
- package/dist/sqlite/sqliteDialect.js +38 -7
- package/dist/turso/tursoDialect.d.ts +2 -0
- package/dist/turso/tursoDialect.js +2 -0
- package/dist/type/config.d.ts +3 -3
- package/dist/type/dialect.d.ts +4 -5
- package/dist/type/entity.d.ts +110 -69
- package/dist/type/migration.d.ts +7 -7
- package/dist/type/migratorDialect.d.ts +4 -0
- package/dist/type/querier.d.ts +6 -6
- package/dist/type/querierPool.d.ts +2 -2
- package/dist/type/query.d.ts +41 -72
- package/dist/type/query.js +10 -5
- package/dist/type/queryAggregate.d.ts +43 -34
- package/dist/type/queryAggregate.js +1 -1
- package/dist/type/queryWhere.d.ts +12 -9
- package/dist/type/universalQuerier.d.ts +4 -4
- package/dist/util/dialect.util.d.ts +4 -4
- package/dist/util/dialect.util.js +24 -15
- package/dist/util/field.util.d.ts +5 -0
- package/dist/util/field.util.js +19 -0
- package/dist/util/object.util.d.ts +2 -0
- package/dist/util/object.util.js +4 -0
- package/dist/util/relationQuery.util.d.ts +12 -65
- package/dist/util/relationQuery.util.js +27 -81
- package/dist/util/rowKey.util.d.ts +1 -11
- package/dist/util/rowKey.util.js +1 -13
- package/package.json +1 -1
- package/dist/querier/relationCount.d.ts +0 -16
- package/dist/querier/relationCount.js +0 -121
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import type { EntityMeta, Query, QuerySortMap, RelationMeta, Type } from '../type/index.js';
|
|
2
|
-
import { type RelationQuery } from '../util/index.js';
|
|
1
|
+
import type { EntityMeta, Query, QuerySortMap, RelationMeta, RelationQuery, Type } from '../type/index.js';
|
|
3
2
|
/**
|
|
4
3
|
* One relation a statement joins, keyed by the alias its columns are addressed by (`tax`,
|
|
5
4
|
* `tax.category`). `projected` tells a `$populate` join, whose columns are selected, from one only
|
|
@@ -8,8 +7,10 @@ import { type RelationQuery } from '../util/index.js';
|
|
|
8
7
|
export type QueryJoin = {
|
|
9
8
|
/** The relation key on its parent, which is how MongoDB names the field a `$lookup` adds. */
|
|
10
9
|
readonly key: string;
|
|
11
|
-
/** Dotted path from the queried entity, which is
|
|
10
|
+
/** Dotted path from the queried entity, which is what a joined row's columns answer under. */
|
|
12
11
|
readonly path: string;
|
|
12
|
+
/** The alias the statement reads it through: its path, unless another table of the statement took it. */
|
|
13
|
+
readonly alias: string;
|
|
13
14
|
readonly entity: Type<object>;
|
|
14
15
|
readonly meta: EntityMeta<object>;
|
|
15
16
|
readonly relation: RelationMeta;
|
|
@@ -38,14 +39,17 @@ export type QuerySortOptions = {
|
|
|
38
39
|
* related column needs that relation joined just as much as selecting it does. The two sources meet
|
|
39
40
|
* here, so the columns, the `ORDER BY` and the row lock cannot disagree about what is in the
|
|
40
41
|
* statement. `$sort` contributes to-one relations only; the rest is rejected where it is rendered.
|
|
42
|
+
* `claimAlias` names each join's table, parents first.
|
|
41
43
|
*/
|
|
42
|
-
export declare function resolveQueryJoins<E>(meta: EntityMeta<E>, q: Query<E
|
|
44
|
+
export declare function resolveQueryJoins<E>(meta: EntityMeta<E>, q: Query<E>, claimAlias?: (path: string) => string): QueryJoins;
|
|
43
45
|
/**
|
|
44
46
|
* Whether a join drops parents that have no match, which is the one thing a join does to *how many*
|
|
45
47
|
* rows a read returns rather than how wide they are. A count that skips the joins has to be told, or
|
|
46
48
|
* it counts the parents the read will never hand back.
|
|
47
49
|
*/
|
|
48
50
|
export declare function hasRequiredJoin<E>(meta: EntityMeta<E>, q: Query<E>): boolean;
|
|
51
|
+
/** Whether a statement aggregates a relation's rows: a to-many off its own row, or off a row it joins. */
|
|
52
|
+
export declare function aggregatesRelations<E>(meta: EntityMeta<E>, q: Query<E>): boolean;
|
|
49
53
|
/**
|
|
50
54
|
* The join an ordering may address at `path`, with the relation's own sort map, or why it may not.
|
|
51
55
|
* Every backend answers this the same way - a to-many has no single value to order by, a relation
|
|
@@ -1,19 +1,20 @@
|
|
|
1
1
|
import { getMeta, relationOf } from '../entity/index.js';
|
|
2
|
-
import { getKeys, getRelationRequestSummary, isToManyRelation, parseRelationAtKey
|
|
2
|
+
import { getKeys, getRelationRequestSummary, isToManyRelation, parseRelationAtKey } from '../util/index.js';
|
|
3
3
|
export const NO_JOINS = new Map();
|
|
4
4
|
/**
|
|
5
5
|
* What the statement joins, from the whole query rather than from `$populate` alone: ordering by a
|
|
6
6
|
* related column needs that relation joined just as much as selecting it does. The two sources meet
|
|
7
7
|
* here, so the columns, the `ORDER BY` and the row lock cannot disagree about what is in the
|
|
8
8
|
* statement. `$sort` contributes to-one relations only; the rest is rejected where it is rendered.
|
|
9
|
+
* `claimAlias` names each join's table, parents first.
|
|
9
10
|
*/
|
|
10
|
-
export function resolveQueryJoins(meta, q) {
|
|
11
|
+
export function resolveQueryJoins(meta, q, claimAlias = (path) => path) {
|
|
11
12
|
if (!q.$populate && !q.$sort) {
|
|
12
13
|
return NO_JOINS;
|
|
13
14
|
}
|
|
14
15
|
const joins = new Map();
|
|
15
|
-
addPopulateJoins(joins, meta, q.$populate);
|
|
16
|
-
addSortJoins(joins, meta, q.$sort);
|
|
16
|
+
addPopulateJoins(joins, claimAlias, meta, q.$populate);
|
|
17
|
+
addSortJoins(joins, claimAlias, meta, q.$sort);
|
|
17
18
|
return joins;
|
|
18
19
|
}
|
|
19
20
|
/**
|
|
@@ -29,7 +30,19 @@ export function hasRequiredJoin(meta, q) {
|
|
|
29
30
|
}
|
|
30
31
|
return false;
|
|
31
32
|
}
|
|
32
|
-
|
|
33
|
+
/** Whether a statement aggregates a relation's rows: a to-many off its own row, or off a row it joins. */
|
|
34
|
+
export function aggregatesRelations(meta, q) {
|
|
35
|
+
if (getRelationRequestSummary(meta, q.$populate).toManyKeys.length) {
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
for (const join of resolveQueryJoins(meta, q).values()) {
|
|
39
|
+
if (getRelationRequestSummary(join.meta, join.query.$populate).toManyKeys.length) {
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
function addJoin(joins, claimAlias, parent, key, relation, query, required, projected) {
|
|
33
46
|
const path = parent ? `${parent.path}.${key}` : key;
|
|
34
47
|
const existing = joins.get(path);
|
|
35
48
|
// `$populate` runs first, so an already-joined relation keeps its columns and its `$required`
|
|
@@ -41,6 +54,7 @@ function addJoin(joins, parent, key, relation, query, required, projected) {
|
|
|
41
54
|
const join = {
|
|
42
55
|
key,
|
|
43
56
|
path,
|
|
57
|
+
alias: claimAlias(path),
|
|
44
58
|
entity,
|
|
45
59
|
meta: getMeta(entity),
|
|
46
60
|
relation,
|
|
@@ -52,15 +66,15 @@ function addJoin(joins, parent, key, relation, query, required, projected) {
|
|
|
52
66
|
joins.set(path, join);
|
|
53
67
|
return join;
|
|
54
68
|
}
|
|
55
|
-
function addPopulateJoins(joins, meta, populate, parent) {
|
|
69
|
+
function addPopulateJoins(joins, claimAlias, meta, populate, parent) {
|
|
56
70
|
for (const key of getRelationRequestSummary(meta, populate).joinableKeys) {
|
|
57
71
|
const relation = relationOf(meta, key);
|
|
58
72
|
const { query, required } = parseRelationAtKey(key, populate);
|
|
59
|
-
const join = addJoin(joins, parent, key, relation, query, required, true);
|
|
60
|
-
addPopulateJoins(joins, join.meta, query.$populate, join);
|
|
73
|
+
const join = addJoin(joins, claimAlias, parent, key, relation, query, required, true);
|
|
74
|
+
addPopulateJoins(joins, claimAlias, join.meta, query.$populate, join);
|
|
61
75
|
}
|
|
62
76
|
}
|
|
63
|
-
function addSortJoins(joins, meta, sort, parent) {
|
|
77
|
+
function addSortJoins(joins, claimAlias, meta, sort, parent) {
|
|
64
78
|
if (!sort) {
|
|
65
79
|
return;
|
|
66
80
|
}
|
|
@@ -72,8 +86,9 @@ function addSortJoins(joins, meta, sort, parent) {
|
|
|
72
86
|
if (!relation || isToManyRelation(relation) || !isSortMap(value)) {
|
|
73
87
|
continue;
|
|
74
88
|
}
|
|
75
|
-
const join = addJoin(joins, parent, key, relation, {}, false, false);
|
|
76
|
-
|
|
89
|
+
const join = addJoin(joins, claimAlias, parent, key, relation, {}, false, false);
|
|
90
|
+
// `E` stated: inferred from a `QuerySortMap<object>`, it lands on the nested relation's target.
|
|
91
|
+
addSortJoins(joins, claimAlias, join.meta, value, join);
|
|
77
92
|
}
|
|
78
93
|
}
|
|
79
94
|
/**
|
|
@@ -78,8 +78,8 @@ export declare abstract class VectorSqlDialect extends AbstractDialect {
|
|
|
78
78
|
*/
|
|
79
79
|
supportedVectorType(cast: VectorCast): VectorCast;
|
|
80
80
|
/**
|
|
81
|
-
*
|
|
82
|
-
*
|
|
81
|
+
* The distance a vector `$sort` projects, which the projection names after `$project`. Delegates to
|
|
82
|
+
* `appendVectorSort` so each dialect's distance syntax is written once.
|
|
83
83
|
*/
|
|
84
84
|
protected appendVectorProjection<E>(ctx: QueryContext, meta: EntityMeta<E>, key: string, search: QueryVectorSearch): void;
|
|
85
85
|
/**
|
|
@@ -96,8 +96,8 @@ export class VectorSqlDialect extends AbstractDialect {
|
|
|
96
96
|
return this.hasNarrowVectorTypes ? cast : 'vector';
|
|
97
97
|
}
|
|
98
98
|
/**
|
|
99
|
-
*
|
|
100
|
-
*
|
|
99
|
+
* The distance a vector `$sort` projects, which the projection names after `$project`. Delegates to
|
|
100
|
+
* `appendVectorSort` so each dialect's distance syntax is written once.
|
|
101
101
|
*/
|
|
102
102
|
appendVectorProjection(ctx, meta, key, search) {
|
|
103
103
|
const alias = search.$project;
|
|
@@ -108,7 +108,6 @@ export class VectorSqlDialect extends AbstractDialect {
|
|
|
108
108
|
throw new TypeError(`$project '${alias}' collides with a field of '${entityName(meta)}'`);
|
|
109
109
|
}
|
|
110
110
|
this.appendVectorSort(ctx, meta, key, search);
|
|
111
|
-
ctx.append(` AS ${this.escapeId(alias)}`);
|
|
112
111
|
}
|
|
113
112
|
/**
|
|
114
113
|
* The distance expression, in whichever of the two shapes this dialect spells it. One method for
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { FieldOptions, HookEvent,
|
|
1
|
+
import type { FieldOptions, HookEvent, RelationRegistration, Type } from '../../type/index.js';
|
|
2
2
|
/**
|
|
3
3
|
* What the member decorators record for one class, waiting for `@Entity()` or `defineEntity` to drain
|
|
4
4
|
* it into the metadata registry. Member decorators receive no class reference under the standard
|
|
@@ -9,7 +9,7 @@ import type { FieldOptions, HookEvent, RelationOptions, Type } from '../../type/
|
|
|
9
9
|
*/
|
|
10
10
|
export type MemberRegistrations = {
|
|
11
11
|
readonly fields: Record<string, FieldOptions>;
|
|
12
|
-
readonly relations: Record<string,
|
|
12
|
+
readonly relations: Record<string, RelationRegistration>;
|
|
13
13
|
readonly hooks: Partial<Record<HookEvent, string[]>>;
|
|
14
14
|
};
|
|
15
15
|
/**
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { EntityOptions, FieldKey, FilterOptions, IndexColumnInput,
|
|
1
|
+
import type { EntityIndexOptions, EntityOptions, FieldKey, FilterOptions, IndexColumnInput, KeyMap, Type } from '../../type/index.js';
|
|
2
2
|
/**
|
|
3
3
|
* Marks a class as an entity and finalizes its metadata.
|
|
4
4
|
*
|
|
@@ -16,13 +16,12 @@ export declare function Entity<E>(opts?: EntityOptions<E>): (entity: Type<E>, co
|
|
|
16
16
|
*/
|
|
17
17
|
export declare function Filter<E>(name: string, opts: FilterOptions<E>): (entity: Type<E>) => void;
|
|
18
18
|
/**
|
|
19
|
-
* Declares a composite index. Stacks, so several may sit above one
|
|
19
|
+
* Declares a composite index, its columns read off the key map. Stacks, so several may sit above one
|
|
20
|
+
* class. `E` is inferred from the class the returned decorator is applied to, which is what types the
|
|
21
|
+
* key map: `@Index((user) => [user.nope])` does not compile, and a rename reaches every column.
|
|
20
22
|
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
* @example `@Index(['lastName', 'firstName'], { name: 'users_fullname_idx' })`
|
|
25
|
-
* @example `@Index(['email'], { unique: true })`
|
|
26
|
-
* @example `@Index(['status'], { where: "status = 'active'" })`
|
|
23
|
+
* @example `@Index((user) => [user.lastName, user.firstName], { name: 'users_fullname_idx' })`
|
|
24
|
+
* @example `@Index((user) => [user.email], { unique: true })`
|
|
25
|
+
* @example `@Index((user) => [user.status], { where: "status = 'active'" })`
|
|
27
26
|
*/
|
|
28
|
-
export declare function Index<E>(columns: readonly IndexColumnInput<FieldKey<E
|
|
27
|
+
export declare function Index<E>(columns: (keys: KeyMap<E>) => readonly IndexColumnInput<FieldKey<NoInfer<E>>, NoInfer<E>>[], options?: EntityIndexOptions<NoInfer<E>>): (entity: Type<E>) => void;
|
|
@@ -28,14 +28,13 @@ export function Filter(name, opts) {
|
|
|
28
28
|
};
|
|
29
29
|
}
|
|
30
30
|
/**
|
|
31
|
-
* Declares a composite index. Stacks, so several may sit above one
|
|
31
|
+
* Declares a composite index, its columns read off the key map. Stacks, so several may sit above one
|
|
32
|
+
* class. `E` is inferred from the class the returned decorator is applied to, which is what types the
|
|
33
|
+
* key map: `@Index((user) => [user.nope])` does not compile, and a rename reaches every column.
|
|
32
34
|
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
* @example `@Index(['lastName', 'firstName'], { name: 'users_fullname_idx' })`
|
|
37
|
-
* @example `@Index(['email'], { unique: true })`
|
|
38
|
-
* @example `@Index(['status'], { where: "status = 'active'" })`
|
|
35
|
+
* @example `@Index((user) => [user.lastName, user.firstName], { name: 'users_fullname_idx' })`
|
|
36
|
+
* @example `@Index((user) => [user.email], { unique: true })`
|
|
37
|
+
* @example `@Index((user) => [user.status], { where: "status = 'active'" })`
|
|
39
38
|
*/
|
|
40
39
|
export function Index(columns, options = {}) {
|
|
41
40
|
return (entity) => {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { EntityGetter, FieldOptions, FieldType, IdValue, NamedIdKey, RelationManyToManyOptions, RelationManyToOneOptions, RelationOneToManyOptions, RelationOneToOneOptions, TsTypeOf } from '../../type/index.js';
|
|
2
2
|
import type { RejectIncompatible } from '../../util/index.js';
|
|
3
|
-
/** A member decorator that also constrains the property it may be applied to
|
|
4
|
-
type MemberDecorator<V> = (value: undefined, context: ClassFieldDecoratorContext<
|
|
3
|
+
/** A member decorator that also constrains the property it may be applied to, on a class `O`. */
|
|
4
|
+
type MemberDecorator<V, O = unknown> = (value: undefined, context: ClassFieldDecoratorContext<O, V>) => void;
|
|
5
5
|
/**
|
|
6
6
|
* Maps any option the type does not declare to `never`, turning a typo into a compile error.
|
|
7
7
|
*
|
|
@@ -73,14 +73,15 @@ export declare function Id<O extends FieldOptions<DeclaredValue<O>> & {
|
|
|
73
73
|
* `E` comes from the mandatory `entity` getter, so the context can insist the property really holds that
|
|
74
74
|
* entity: `@ManyToOne({ entity: () => Other })` on a `Company` field stops compiling, and a to-many
|
|
75
75
|
* cardinality on a non-array property does too. `entity` is required because nothing reflects it now.
|
|
76
|
+
* `O` is inferred from the class the decorator sits on, which types `references`' own side.
|
|
76
77
|
*/
|
|
77
78
|
type WithEntity<E, O> = O & {
|
|
78
79
|
readonly entity: EntityGetter<E>;
|
|
79
80
|
};
|
|
80
|
-
export declare function OneToOne<E>(opts: WithEntity<E, RelationOneToOneOptions<E>>): MemberDecorator<E | undefined>;
|
|
81
|
-
export declare function ManyToOne<E>(opts: WithEntity<E, RelationManyToOneOptions<E>>): MemberDecorator<E | undefined>;
|
|
82
|
-
export declare function OneToMany<E>(opts: WithEntity<E, RelationOneToManyOptions<E>>): MemberDecorator<readonly E[] | undefined>;
|
|
83
|
-
export declare function ManyToMany<E>(opts: WithEntity<E, RelationManyToManyOptions<E>>): MemberDecorator<readonly E[] | undefined>;
|
|
81
|
+
export declare function OneToOne<E extends object, O>(opts: WithEntity<E, RelationOneToOneOptions<E, O>>): MemberDecorator<E | undefined, O>;
|
|
82
|
+
export declare function ManyToOne<E extends object, O>(opts: WithEntity<E, RelationManyToOneOptions<E, O>>): MemberDecorator<E | undefined, O>;
|
|
83
|
+
export declare function OneToMany<E extends object, O>(opts: WithEntity<E, RelationOneToManyOptions<E, O>>): MemberDecorator<readonly E[] | undefined, O>;
|
|
84
|
+
export declare function ManyToMany<E extends object, O>(opts: WithEntity<E, RelationManyToManyOptions<E, O>>): MemberDecorator<readonly E[] | undefined, O>;
|
|
84
85
|
export declare const BeforeInsert: () => <This>(_value: unknown, context: ClassMethodDecoratorContext<This>) => void;
|
|
85
86
|
export declare const AfterInsert: () => <This>(_value: unknown, context: ClassMethodDecoratorContext<This>) => void;
|
|
86
87
|
export declare const BeforeUpdate: () => <This>(_value: unknown, context: ClassMethodDecoratorContext<This>) => void;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { relationRegistration } from '../metadata/definition.js';
|
|
1
2
|
import { memberRegistrations } from './bag.js';
|
|
2
3
|
/**
|
|
3
4
|
* Declares a persisted field.
|
|
@@ -28,7 +29,7 @@ export function Id(opts) {
|
|
|
28
29
|
}
|
|
29
30
|
function relation(opts) {
|
|
30
31
|
return (_value, context) => {
|
|
31
|
-
memberRegistrations(context.metadata).relations[String(context.name)] = opts;
|
|
32
|
+
memberRegistrations(context.metadata).relations[String(context.name)] = relationRegistration(opts);
|
|
32
33
|
};
|
|
33
34
|
}
|
|
34
35
|
export function OneToOne(opts) {
|
|
@@ -1,13 +1,20 @@
|
|
|
1
|
-
import type { EntityData, EntityIndexInput, EntityMembers, EntityMeta, EntityOptions,
|
|
1
|
+
import type { EntityData, EntityIndexInput, EntityMembers, EntityMeta, EntityOptions, FieldMeta, FieldOptions, FilterOptions, HookEvent, IdKey, RelationKey, RelationMeta, RelationOptions, RelationRegistration, Type, WrittenId } from '../../type/index.js';
|
|
2
2
|
export declare function defineField<E>(entity: Type<E>, key: string, opts?: FieldOptions): EntityMeta<E>;
|
|
3
3
|
export declare function defineId<E>(entity: Type<E>, key: string, opts: FieldOptions): EntityMeta<E>;
|
|
4
|
-
|
|
4
|
+
/** `T` is the relation's target, independent of the owner `E`. */
|
|
5
|
+
export declare function defineRelation<E, T extends object>(entity: Type<E>, key: string, opts: RelationOptions<T, E>): EntityMeta<E>;
|
|
6
|
+
/**
|
|
7
|
+
* `opts` as the registry takes them: `mappedBy` and `references` read off their key maps down to the
|
|
8
|
+
* names they give. The callbacks only read properties, so they run here, before any entity has to
|
|
9
|
+
* exist, and the registry holds data alone.
|
|
10
|
+
*/
|
|
11
|
+
export declare function relationRegistration<T extends object, O>({ mappedBy, references, ...opts }: RelationOptions<T, O>): RelationRegistration;
|
|
5
12
|
export declare function defineHook<E>(entity: Type<E>, methodName: string, event: HookEvent): EntityMeta<E>;
|
|
6
13
|
/**
|
|
7
|
-
* Declares a composite index. `unique` and the authored column sugar
|
|
8
|
-
* lets the dialects render one shape instead of re-parsing it.
|
|
14
|
+
* Declares a composite index, its columns read off the key map. `unique` and the authored column sugar
|
|
15
|
+
* are normalized here, which is what lets the dialects render one shape instead of re-parsing it.
|
|
9
16
|
*/
|
|
10
|
-
export declare function defineIndex<E>(entity: Type<E>, index: EntityIndexInput<
|
|
17
|
+
export declare function defineIndex<E>(entity: Type<E>, index: EntityIndexInput<E>): EntityMeta<E>;
|
|
11
18
|
export declare function defineFilter<E>(entity: Type<E>, name: string, opts: FilterOptions<E>): EntityMeta<E>;
|
|
12
19
|
/**
|
|
13
20
|
* Feeds fields, relations and hooks into the `define*` primitives, so the decorators and the imperative
|
|
@@ -15,10 +22,8 @@ export declare function defineFilter<E>(entity: Type<E>, name: string, opts: Fil
|
|
|
15
22
|
*/
|
|
16
23
|
export declare function applyMembers<E>(entity: Type<E>, specs: EntityMembers | undefined): void;
|
|
17
24
|
/**
|
|
18
|
-
* Registers an entity
|
|
19
|
-
*
|
|
20
|
-
* column, and one assembled at runtime is the column bag it is. Pass `Row` to name a shape the spec
|
|
21
|
-
* cannot describe, such as the interface `uql-migrate types` generated for it.
|
|
25
|
+
* Registers a class as an entity from `opts` alone, the decorator-free counterpart of `@Entity()` with
|
|
26
|
+
* `@Field`/`@ManyToOne`/...
|
|
22
27
|
*/
|
|
23
28
|
export declare function defineEntity<E>(entity: Type<E>, opts?: EntityOptions<E>): EntityMeta<E>;
|
|
24
29
|
/**
|
|
@@ -31,7 +36,7 @@ export declare function assertSoleId<E>(meta: EntityMeta<E>, what: string): void
|
|
|
31
36
|
/** The entity's one primary key, for a path that cannot express a composite. See {@link assertSoleId}. */
|
|
32
37
|
export declare function soleIdOf<E>(meta: EntityMeta<E>, what: string): IdKey<E>;
|
|
33
38
|
/** The field `key` names, for a caller that took `key` from the metadata itself. */
|
|
34
|
-
export declare function fieldOf<E>(meta: EntityMeta<E>, key:
|
|
39
|
+
export declare function fieldOf<E>(meta: EntityMeta<E>, key: string): FieldMeta;
|
|
35
40
|
/** The relation `key` names, for a caller that took `key` from the metadata itself. */
|
|
36
41
|
export declare function relationOf<E>(meta: EntityMeta<E>, key: RelationKey<E>): RelationMeta;
|
|
37
42
|
/**
|
|
@@ -61,5 +66,5 @@ export declare function idOf<E>(meta: EntityMeta<E>, row: EntityData<E>): Writte
|
|
|
61
66
|
* registration). See the Runtime Schemas guide.
|
|
62
67
|
*/
|
|
63
68
|
export declare function removeEntity<E>(entity: Type<E>): boolean;
|
|
64
|
-
export declare function getEntities(): Type<
|
|
69
|
+
export declare function getEntities(): Type<object>[];
|
|
65
70
|
export declare function getMeta<E>(entity: Type<E>): EntityMeta<E>;
|
|
@@ -38,18 +38,41 @@ export function defineField(entity, key, opts = {}) {
|
|
|
38
38
|
export function defineId(entity, key, opts) {
|
|
39
39
|
return defineField(entity, key, { ...opts, isId: true });
|
|
40
40
|
}
|
|
41
|
-
|
|
42
|
-
// is left at its default here rather than tied to the class being registered.
|
|
41
|
+
/** `T` is the relation's target, independent of the owner `E`. */
|
|
43
42
|
export function defineRelation(entity, key, opts) {
|
|
44
|
-
|
|
43
|
+
return addRelation(entity, key, relationRegistration(opts));
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* `opts` as the registry takes them: `mappedBy` and `references` read off their key maps down to the
|
|
47
|
+
* names they give. The callbacks only read properties, so they run here, before any entity has to
|
|
48
|
+
* exist, and the registry holds data alone.
|
|
49
|
+
*/
|
|
50
|
+
export function relationRegistration({ mappedBy, references, ...opts }) {
|
|
51
|
+
return {
|
|
52
|
+
...opts,
|
|
53
|
+
...(mappedBy ? { mappedBy: mappedBy(keyMap()) } : {}),
|
|
54
|
+
...(references ? { references: [...references(keyMap(), keyMap())] } : {}),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
/** Every entity's key map: a callback only reads one property off it, and that property is its own key. */
|
|
58
|
+
function keyMap() {
|
|
59
|
+
return KEY_MAP;
|
|
60
|
+
}
|
|
61
|
+
const KEY_MAP = new Proxy({}, { get: (_, key) => key });
|
|
62
|
+
function addRelation(entity, key, registration) {
|
|
63
|
+
if (!registration.entity) {
|
|
45
64
|
throw new TypeError(`'${entity.name}.${key}' needs an 'entity' getter, e.g. '@ManyToOne({ entity: () => Company })'.`);
|
|
46
65
|
}
|
|
66
|
+
if (registration.through && registration.references) {
|
|
67
|
+
throw new TypeError(`'${entity.name}.${key}' joins through a junction, whose columns follow the convention; 'references' ` +
|
|
68
|
+
"pairs the declaring entity's columns with the target's instead.");
|
|
69
|
+
}
|
|
47
70
|
const meta = ensureWritableMeta(entity);
|
|
48
|
-
// Registration writes
|
|
49
|
-
// `
|
|
50
|
-
//
|
|
71
|
+
// Registration writes into a map declared as resolved: `getMeta` runs `fillRelations`, which settles
|
|
72
|
+
// `references` or throws. Bridging the two shapes here is what lets every consumer read `RelationMeta`
|
|
73
|
+
// without asserting.
|
|
51
74
|
const relations = meta.relations;
|
|
52
|
-
relations[key] = { ...relations[key], ...
|
|
75
|
+
relations[key] = { ...relations[key], ...registration };
|
|
53
76
|
return meta;
|
|
54
77
|
}
|
|
55
78
|
export function defineHook(entity, methodName, event) {
|
|
@@ -62,18 +85,18 @@ export function defineHook(entity, methodName, event) {
|
|
|
62
85
|
return meta;
|
|
63
86
|
}
|
|
64
87
|
/**
|
|
65
|
-
* Declares a composite index. `unique` and the authored column sugar
|
|
66
|
-
* lets the dialects render one shape instead of re-parsing it.
|
|
88
|
+
* Declares a composite index, its columns read off the key map. `unique` and the authored column sugar
|
|
89
|
+
* are normalized here, which is what lets the dialects render one shape instead of re-parsing it.
|
|
67
90
|
*/
|
|
68
91
|
export function defineIndex(entity, index) {
|
|
69
92
|
const meta = ensureWritableMeta(entity);
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
meta.indexes.push({
|
|
93
|
+
const keys = keyMap();
|
|
94
|
+
(meta.indexes ??= []).push({
|
|
73
95
|
...index,
|
|
74
96
|
unique: index.unique ?? false,
|
|
75
97
|
where: ddlText(index.where, 'a partial-index predicate'),
|
|
76
|
-
columns: index.columns.map(normalizeIndexColumn),
|
|
98
|
+
columns: index.columns(keys).map(normalizeIndexColumn),
|
|
99
|
+
include: index.include?.(keys),
|
|
77
100
|
});
|
|
78
101
|
return meta;
|
|
79
102
|
}
|
|
@@ -104,7 +127,7 @@ export function applyMembers(entity, specs) {
|
|
|
104
127
|
}
|
|
105
128
|
}
|
|
106
129
|
for (const [key, spec] of definedEntries(specs?.relations ?? {})) {
|
|
107
|
-
|
|
130
|
+
addRelation(entity, key, spec);
|
|
108
131
|
}
|
|
109
132
|
for (const [event, methodNames] of definedEntries(specs?.hooks ?? {})) {
|
|
110
133
|
for (const methodName of methodNames) {
|
|
@@ -113,10 +136,8 @@ export function applyMembers(entity, specs) {
|
|
|
113
136
|
}
|
|
114
137
|
}
|
|
115
138
|
/**
|
|
116
|
-
* Registers an entity
|
|
117
|
-
*
|
|
118
|
-
* column, and one assembled at runtime is the column bag it is. Pass `Row` to name a shape the spec
|
|
119
|
-
* cannot describe, such as the interface `uql-migrate types` generated for it.
|
|
139
|
+
* Registers a class as an entity from `opts` alone, the decorator-free counterpart of `@Entity()` with
|
|
140
|
+
* `@Field`/`@ManyToOne`/...
|
|
120
141
|
*/
|
|
121
142
|
export function defineEntity(entity, opts = {}) {
|
|
122
143
|
// Ahead of any registration, so a rejected definition leaves nothing half-written in the registry.
|
|
@@ -132,7 +153,12 @@ export function defineEntity(entity, opts = {}) {
|
|
|
132
153
|
// drains `context.metadata` itself, because TypeScript only attaches `Symbol.metadata` to the class
|
|
133
154
|
// after class decorators return; draining empties the bag, so whichever runs second is a no-op.
|
|
134
155
|
applyMembers(entity, ownRegistrations(entity));
|
|
135
|
-
|
|
156
|
+
const keys = keyMap();
|
|
157
|
+
applyMembers(entity, {
|
|
158
|
+
fields: opts.fields,
|
|
159
|
+
relations: Object.fromEntries(definedEntries(opts.relations ?? {}).map(([key, spec]) => [key, relationRegistration(spec)])),
|
|
160
|
+
hooks: Object.fromEntries(definedEntries(opts.hooks ?? {}).map(([event, methods]) => [event, methods(keys)])),
|
|
161
|
+
});
|
|
136
162
|
// Unnamed checks are named by the generator, as unnamed indexes are.
|
|
137
163
|
for (const check of opts.checks ?? []) {
|
|
138
164
|
(meta.checks ??= []).push({ name: check.name, expression: ddlText(check.expression, 'a check constraint') });
|
|
@@ -267,12 +293,7 @@ export function removeEntity(entity) {
|
|
|
267
293
|
return metas.delete(entity);
|
|
268
294
|
}
|
|
269
295
|
export function getEntities() {
|
|
270
|
-
return metas.
|
|
271
|
-
if (val.ids.length) {
|
|
272
|
-
acc.push(key);
|
|
273
|
-
}
|
|
274
|
-
return acc;
|
|
275
|
-
}, []);
|
|
296
|
+
return [...metas.values()].filter((meta) => meta.ids.length).map((meta) => meta.entity);
|
|
276
297
|
}
|
|
277
298
|
/**
|
|
278
299
|
* The metadata of `entity`, marked as changed. Every `define*` goes through this, and nothing outside
|
|
@@ -308,11 +329,11 @@ export function getMeta(entity) {
|
|
|
308
329
|
}
|
|
309
330
|
function fillRelations(meta) {
|
|
310
331
|
for (const [relKey, relation] of definedEntries(meta.relations)) {
|
|
311
|
-
// The
|
|
332
|
+
// The registered view: `references` may be unset until this settles it.
|
|
312
333
|
const relOpts = relation;
|
|
313
334
|
const at = `'${meta.entity.name}.${relKey}'`;
|
|
314
335
|
if (relOpts.mappedBy) {
|
|
315
|
-
fillInverseSide(at, meta, relOpts);
|
|
336
|
+
fillInverseSide(at, meta, relOpts, relOpts.mappedBy);
|
|
316
337
|
}
|
|
317
338
|
else if (!relOpts.references) {
|
|
318
339
|
fillOwningSide(at, meta, relKey, relOpts);
|
|
@@ -326,8 +347,8 @@ function fillRelations(meta) {
|
|
|
326
347
|
for (const { local } of relOpts.references) {
|
|
327
348
|
if (junction.fields[local])
|
|
328
349
|
continue;
|
|
329
|
-
throw new TypeError(`${at} joins through '${junction.entity.name}', which has no '${local}' field
|
|
330
|
-
|
|
350
|
+
throw new TypeError(`${at} joins through '${junction.entity.name}', which has no '${local}' field: a junction's ` +
|
|
351
|
+
'columns are named after the entities it joins. Declare it.');
|
|
331
352
|
}
|
|
332
353
|
}
|
|
333
354
|
}
|
|
@@ -337,9 +358,9 @@ function fillRelations(meta) {
|
|
|
337
358
|
function fillOwningSide(at, meta, relKey, relOpts) {
|
|
338
359
|
const relMeta = ensureMeta(relOpts.entity());
|
|
339
360
|
if (relOpts.through) {
|
|
340
|
-
// Both columns live on the junction, whatever the cardinality: `
|
|
341
|
-
//
|
|
342
|
-
//
|
|
361
|
+
// Both columns live on the junction, whatever the cardinality: `deleteRelations` and every dialect
|
|
362
|
+
// read them as junction columns. A composite key contributes one pair per column of it, which is
|
|
363
|
+
// what makes the join address a whole key rather than part.
|
|
343
364
|
relOpts.references = [
|
|
344
365
|
...meta.ids.map((key) => ({ local: junctionColumn(meta, key), foreign: key })),
|
|
345
366
|
...relMeta.ids.map((key) => ({ local: junctionColumn(relMeta, key), foreign: key })),
|
|
@@ -372,11 +393,9 @@ function fillOwningSide(at, meta, relKey, relOpts) {
|
|
|
372
393
|
};
|
|
373
394
|
}
|
|
374
395
|
}
|
|
375
|
-
function fillInverseSide(at, meta, relOpts) {
|
|
396
|
+
function fillInverseSide(at, meta, relOpts, mappedBy) {
|
|
376
397
|
const relEntity = relOpts.entity();
|
|
377
398
|
const relMeta = getMeta(relEntity);
|
|
378
|
-
const mappedBy = getMappedByKey(relOpts);
|
|
379
|
-
relOpts.mappedBy = mappedBy;
|
|
380
399
|
if (relOpts.references)
|
|
381
400
|
return;
|
|
382
401
|
if (relMeta.fields[mappedBy]) {
|
|
@@ -448,13 +467,6 @@ function fillForeignKeyRelations(meta) {
|
|
|
448
467
|
function junctionColumn(meta, idKey) {
|
|
449
468
|
return lowerFirst(entityName(meta)) + upperFirst(fieldOf(meta, idKey).name ?? idKey);
|
|
450
469
|
}
|
|
451
|
-
/** A callback only reads one property off the key map, and that property is the key, so one serves every entity. */
|
|
452
|
-
const RELATION_KEY_MAP = new Proxy({}, { get: (_, key) => key });
|
|
453
|
-
function getMappedByKey(relOpts) {
|
|
454
|
-
return typeof relOpts.mappedBy === 'function'
|
|
455
|
-
? relOpts.mappedBy(RELATION_KEY_MAP)
|
|
456
|
-
: relOpts.mappedBy;
|
|
457
|
-
}
|
|
458
470
|
/** Every key the entity marks, in declaration order. More than one is a composite primary key. */
|
|
459
471
|
function getIdKeys(meta) {
|
|
460
472
|
return getKeys(meta.fields).filter((key) => meta.fields[key]?.isId);
|
package/dist/http/handler.d.ts
CHANGED
|
@@ -49,8 +49,8 @@ export type HookContext<E extends object, Ctx = unknown> = {
|
|
|
49
49
|
export type Hook<Ctx = unknown> = <E extends object>(ctx: HookContext<E, Ctx>) => void | Promise<void>;
|
|
50
50
|
export type ResponseHook<Ctx = unknown> = <E extends object>(ctx: HookContext<E, Ctx>, envelope: RequestSuccessResponse<unknown>) => void | Promise<void>;
|
|
51
51
|
export type RequestHandlerOptions<Ctx = unknown> = {
|
|
52
|
-
include?: Type<
|
|
53
|
-
exclude?: Type<
|
|
52
|
+
include?: Type<object>[];
|
|
53
|
+
exclude?: Type<object>[];
|
|
54
54
|
/**
|
|
55
55
|
* The URL segment an entity is addressed by, defaulting to its kebab-cased class name.
|
|
56
56
|
*
|
package/dist/http/handler.js
CHANGED
|
@@ -27,7 +27,6 @@ export function createRequestHandler(opts) {
|
|
|
27
27
|
"A route is the kebab-cased class name unless 'entityPath' says otherwise. Name them apart, " +
|
|
28
28
|
"pass an 'entityPath', or pass only one of them in 'include'.");
|
|
29
29
|
}
|
|
30
|
-
// oxlint-disable-next-line typescript/no-explicit-any -- heterogeneous entity map
|
|
31
30
|
const entityByPath = new Map([...byPath].map(([path, [entity]]) => [path, entity]));
|
|
32
31
|
return (req) => {
|
|
33
32
|
const entity = entityByPath.get(req.entityPath);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { type RelationRows } from '../dialect/abstractSqlDialect.js';
|
|
1
2
|
import { MysqlLikeSqlDialect } from '../dialect/mysqlLikeSqlDialect.js';
|
|
2
|
-
import type { DialectFeatures, EntityMeta, FieldOptions, Query, QueryContext,
|
|
3
|
+
import type { DialectFeatures, EntityMeta, FieldOptions, Query, QueryContext, Type, VectorDistance, VectorMetric } from '../type/index.js';
|
|
3
4
|
export declare class MariaDialect extends MysqlLikeSqlDialect {
|
|
4
5
|
readonly dialectName = "mariadb";
|
|
5
6
|
readonly insertIdSource = "returning";
|
|
@@ -10,6 +11,11 @@ export declare class MariaDialect extends MysqlLikeSqlDialect {
|
|
|
10
11
|
* and `CREATE INDEX` takes `IF NOT EXISTS` - which MySQL's grammar has no place for.
|
|
11
12
|
*/
|
|
12
13
|
protected readonly featureOverrides: Partial<DialectFeatures>;
|
|
14
|
+
/**
|
|
15
|
+
* A derived table here reads no column of the statement around it, so the aggregate reads the
|
|
16
|
+
* related table itself, and orders and pages inside `JSON_ARRAYAGG`, which takes both.
|
|
17
|
+
*/
|
|
18
|
+
protected appendRelationArray(ctx: QueryContext, { entity, query, alias, joins }: RelationRows): void;
|
|
13
19
|
protected upsertReturning<E>(meta: EntityMeta<E>): string;
|
|
14
20
|
/**
|
|
15
21
|
* MariaDB supports neither MySQL's `->`/`->>` shorthand nor the base's chained form. `JSON_VALUE`
|
|
@@ -36,12 +42,13 @@ export declare class MariaDialect extends MysqlLikeSqlDialect {
|
|
|
36
42
|
*/
|
|
37
43
|
protected appendVectorValue(ctx: QueryContext, value: readonly unknown[]): void;
|
|
38
44
|
/**
|
|
39
|
-
* `
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
* from `vectorTuningStatements`, which is Postgres's `SET LOCAL` shape.
|
|
45
|
+
* `mhnsw_ef_search` too, where a vector search is tuned. A setting scoped to one statement needs
|
|
46
|
+
* neither a transaction nor a restore, and cannot leak to the next query on this pooled connection,
|
|
47
|
+
* which is why the tuning is not `vectorTuningStatements`, Postgres's `SET LOCAL` shape.
|
|
43
48
|
*/
|
|
44
|
-
|
|
49
|
+
protected statementSettings<E>(entity: Type<E>, q: Query<E>): string[];
|
|
50
|
+
/** `SET STATEMENT ... FOR`, which scopes a variable to the statement it prefixes. */
|
|
51
|
+
protected applySettings(sql: string, settings: readonly string[]): string;
|
|
45
52
|
/** The reverse: selecting a `VECTOR` column raw yields that blob, so it is read back as text. */
|
|
46
53
|
protected selectFieldExpr(escapedColumn: string, field: FieldOptions): string;
|
|
47
54
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { relationTermKey } from '../dialect/abstractSqlDialect.js';
|
|
1
2
|
import { jsonPath } from '../dialect/jsonSql.js';
|
|
2
3
|
import { MysqlLikeSqlDialect } from '../dialect/mysqlLikeSqlDialect.js';
|
|
3
4
|
import { getMeta } from '../entity/index.js';
|
|
@@ -18,6 +19,24 @@ export class MariaDialect extends MysqlLikeSqlDialect {
|
|
|
18
19
|
vectorIndexRequiresNotNull: true,
|
|
19
20
|
indexIfNotExists: true,
|
|
20
21
|
};
|
|
22
|
+
/**
|
|
23
|
+
* A derived table here reads no column of the statement around it, so the aggregate reads the
|
|
24
|
+
* related table itself, and orders and pages inside `JSON_ARRAYAGG`, which takes both.
|
|
25
|
+
*/
|
|
26
|
+
appendRelationArray(ctx, { entity, query, alias, joins }) {
|
|
27
|
+
const meta = getMeta(entity);
|
|
28
|
+
const terms = this.projection(ctx, entity, query, { prefix: alias, json: true }, joins);
|
|
29
|
+
const sortOpts = { prefix: alias, joins, distinct: query.$distinct };
|
|
30
|
+
const order = this.buildFragment(ctx, (fragmentCtx) => this.sort(fragmentCtx, entity, query.$sort, sortOpts));
|
|
31
|
+
const page = this.buildFragment(ctx, (fragmentCtx) => this.pager(fragmentCtx, query));
|
|
32
|
+
const from = this.buildFragment(ctx, (fragmentCtx) => {
|
|
33
|
+
this.selectRelationJoins(fragmentCtx, meta, alias, joins);
|
|
34
|
+
this.where(fragmentCtx, entity, query.$where, { prefix: alias });
|
|
35
|
+
});
|
|
36
|
+
const object = this.jsonObject(terms.map((term) => [relationTermKey(term), term.sql]));
|
|
37
|
+
const rows = `${query.$distinct ? 'DISTINCT ' : ''}${object}${order}${page}`;
|
|
38
|
+
ctx.append(`COALESCE((SELECT JSON_ARRAYAGG(${rows}) FROM ${this.tableRef(meta, alias).ref}${from}), JSON_ARRAY())`);
|
|
39
|
+
}
|
|
21
40
|
upsertReturning(meta) {
|
|
22
41
|
const returning = this.returningId(meta);
|
|
23
42
|
return returning ? ` ${returning}` : '';
|
|
@@ -61,18 +80,19 @@ export class MariaDialect extends MysqlLikeSqlDialect {
|
|
|
61
80
|
ctx.append(')');
|
|
62
81
|
}
|
|
63
82
|
/**
|
|
64
|
-
* `
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
* from `vectorTuningStatements`, which is Postgres's `SET LOCAL` shape.
|
|
83
|
+
* `mhnsw_ef_search` too, where a vector search is tuned. A setting scoped to one statement needs
|
|
84
|
+
* neither a transaction nor a restore, and cannot leak to the next query on this pooled connection,
|
|
85
|
+
* which is why the tuning is not `vectorTuningStatements`, Postgres's `SET LOCAL` shape.
|
|
68
86
|
*/
|
|
69
|
-
|
|
87
|
+
statementSettings(entity, q) {
|
|
70
88
|
// `$candidates` first: `getMeta` would otherwise be resolved on every read, to discover that
|
|
71
89
|
// almost none of them tune anything.
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
90
|
+
const tuned = q.$candidates !== undefined && this.tunedVectorIndex(getMeta(entity), q);
|
|
91
|
+
return [...(tuned ? [`mhnsw_ef_search=${q.$candidates}`] : []), ...super.statementSettings(entity, q)];
|
|
92
|
+
}
|
|
93
|
+
/** `SET STATEMENT ... FOR`, which scopes a variable to the statement it prefixes. */
|
|
94
|
+
applySettings(sql, settings) {
|
|
95
|
+
return `SET STATEMENT ${settings.join(', ')} FOR ${sql}`;
|
|
76
96
|
}
|
|
77
97
|
/** The reverse: selecting a `VECTOR` column raw yields that blob, so it is read back as text. */
|
|
78
98
|
selectFieldExpr(escapedColumn, field) {
|