uql-orm 0.68.1 → 0.69.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/dialect/abstractSqlDialect.d.ts +31 -4
- package/dist/dialect/abstractSqlDialect.js +99 -21
- package/dist/dialect/aliases.d.ts +8 -0
- package/dist/dialect/aliases.js +8 -0
- package/dist/entity/decorator/members.d.ts +29 -1
- package/dist/entity/decorator/members.js +0 -5
- package/dist/entity/metadata/definition.js +10 -3
- package/dist/migrate/introspection/postgresIntrospector.d.ts +6 -0
- package/dist/migrate/introspection/postgresIntrospector.js +7 -1
- package/dist/mongo/mongoDialect.d.ts +29 -3
- package/dist/mongo/mongoDialect.js +111 -14
- package/dist/mongo/mongodbQuerier.d.ts +2 -2
- package/dist/mongo/mongodbQuerier.js +4 -3
- package/dist/type/dialect.d.ts +29 -2
- package/dist/type/entity.d.ts +90 -3
- package/dist/type/query.d.ts +1 -10
- package/dist/type/queryRaw.d.ts +19 -1
- package/dist/type/queryRaw.js +18 -0
- package/dist/util/dialect.util.js +2 -2
- package/dist/util/field.util.d.ts +15 -1
- package/dist/util/field.util.js +18 -1
- package/dist/util/object.util.d.ts +1 -4
- package/dist/util/object.util.js +0 -3
- package/dist/util/raw.d.ts +2 -2
- package/dist/util/raw.js +29 -2
- package/package.json +1 -1
|
@@ -1,10 +1,5 @@
|
|
|
1
1
|
import { relationRegistration } from '../metadata/definition.js';
|
|
2
2
|
import { memberRegistrations } from './bag.js';
|
|
3
|
-
/**
|
|
4
|
-
* Declares a persisted field, its `type` checked against the property's.
|
|
5
|
-
* @example `@Field({ type: String }) name?: string;`
|
|
6
|
-
* @example `@Field({ references: () => User }) userId?: string;`
|
|
7
|
-
*/
|
|
8
3
|
export function Field(opts) {
|
|
9
4
|
return (_value, context) => {
|
|
10
5
|
memberRegistrations(context.metadata).fields[String(context.name)] = opts;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { SOFT_DELETE_FILTER } from '../../type/index.js';
|
|
1
|
+
import { RelationAggregate, SOFT_DELETE_FILTER } from '../../type/index.js';
|
|
2
2
|
import { isInlinedExpression } from '../../util/field.util.js';
|
|
3
3
|
import { entitySql, entityWhere, fieldOptionConflict, getKeys, hasKeys, isToManyRelation, memberRefs, normalizeIndexColumn, definedEntries, } from '../../util/index.js';
|
|
4
4
|
import { ownRegistrations } from '../decorator/bag.js';
|
|
@@ -17,6 +17,14 @@ function globalMap(key) {
|
|
|
17
17
|
const metas = globalMap('uql-orm/entity/metadata/v1');
|
|
18
18
|
export function defineField(entity, key, opts = {}) {
|
|
19
19
|
const meta = ensureWritableMeta(entity);
|
|
20
|
+
const { computed, ...rest } = opts;
|
|
21
|
+
const sql = computed === undefined ? undefined : entitySql(computed);
|
|
22
|
+
// A relation aggregate reads as a correlated subquery, which no engine accepts in a generated column:
|
|
23
|
+
// keeping one on the row takes the triggers a write fires, which are not built yet.
|
|
24
|
+
if (opts.stored && sql instanceof RelationAggregate) {
|
|
25
|
+
throw new TypeError(`'${entity.name}.${key}' cannot be 'stored': a relation aggregate reads as a subquery, which no ` +
|
|
26
|
+
"engine keeps in a generated column. Drop 'stored' to have it read on each query.");
|
|
27
|
+
}
|
|
20
28
|
// A stored computed column is a real column and still needs a type; only an inlined one is exempt,
|
|
21
29
|
// its expression being spliced in rather than declared.
|
|
22
30
|
if (!opts.type && !opts.references && !isInlinedExpression(opts)) {
|
|
@@ -31,13 +39,12 @@ export function defineField(entity, key, opts = {}) {
|
|
|
31
39
|
// Flagged when the author gave `references` but no `type`, so schema generation knows to resolve the
|
|
32
40
|
// column from the referenced primary key (picking up its `columnType`, length and chained keys)
|
|
33
41
|
// instead of treating whatever ends up in `type` as deliberate.
|
|
34
|
-
const { computed, ...rest } = opts;
|
|
35
42
|
const resolved = rest.type ? rest : { ...rest, typeFromReference: true };
|
|
36
43
|
meta.fields[fieldKey] = {
|
|
37
44
|
...meta.fields[fieldKey],
|
|
38
45
|
name: key,
|
|
39
46
|
...resolved,
|
|
40
|
-
...(
|
|
47
|
+
...(sql && { computed: sql }),
|
|
41
48
|
};
|
|
42
49
|
return meta;
|
|
43
50
|
}
|
|
@@ -14,6 +14,12 @@ export declare class PostgresSchemaIntrospector extends AbstractSqlSchemaIntrosp
|
|
|
14
14
|
protected getTableNamesQuery(): string;
|
|
15
15
|
protected tableExistsQuery(): string;
|
|
16
16
|
protected parseTableExistsResult([row]: RawRow[]): boolean;
|
|
17
|
+
/**
|
|
18
|
+
* The comment reads through `to_regclass` rather than a `::regclass` cast: a name resolves against
|
|
19
|
+
* the live catalogue while `information_schema` answers from this statement's snapshot, so a table
|
|
20
|
+
* another connection has just dropped is still listed here and the cast would raise on it. Whole
|
|
21
|
+
* database scans meet that table every time something else is migrating.
|
|
22
|
+
*/
|
|
17
23
|
protected getColumnsQuery(_tableName: string): string;
|
|
18
24
|
/**
|
|
19
25
|
* `attname` where the entry is a column, `pg_get_indexdef` for that one position where it is an
|
|
@@ -37,6 +37,12 @@ export class PostgresSchemaIntrospector extends AbstractSqlSchemaIntrospector {
|
|
|
37
37
|
parseTableExistsResult([row]) {
|
|
38
38
|
return row['exists'] === true;
|
|
39
39
|
}
|
|
40
|
+
/**
|
|
41
|
+
* The comment reads through `to_regclass` rather than a `::regclass` cast: a name resolves against
|
|
42
|
+
* the live catalogue while `information_schema` answers from this statement's snapshot, so a table
|
|
43
|
+
* another connection has just dropped is still listed here and the cast would raise on it. Whole
|
|
44
|
+
* database scans meet that table every time something else is migrating.
|
|
45
|
+
*/
|
|
40
46
|
getColumnsQuery(_tableName) {
|
|
41
47
|
return /*sql*/ `
|
|
42
48
|
SELECT
|
|
@@ -68,7 +74,7 @@ export class PostgresSchemaIntrospector extends AbstractSqlSchemaIntrospector {
|
|
|
68
74
|
HAVING COUNT(*) = 1 AND MIN(kcu.column_name) = c.column_name
|
|
69
75
|
) AS is_unique,
|
|
70
76
|
pg_catalog.col_description(
|
|
71
|
-
(quote_ident(c.table_schema) || '.' || quote_ident(c.table_name))
|
|
77
|
+
to_regclass(quote_ident(c.table_schema) || '.' || quote_ident(c.table_name)),
|
|
72
78
|
c.ordinal_position
|
|
73
79
|
) AS column_comment
|
|
74
80
|
FROM information_schema.columns c
|
|
@@ -139,9 +139,35 @@ export declare class MongoDialect extends AbstractDialect {
|
|
|
139
139
|
readonly stages: MongoAggregationPipelineEntry<Document>[];
|
|
140
140
|
readonly fields: string[];
|
|
141
141
|
};
|
|
142
|
-
/**
|
|
143
|
-
|
|
144
|
-
/**
|
|
142
|
+
/** Whether a read answers with a relation aggregate, which only the pipeline can build. */
|
|
143
|
+
readsAggregates<E extends Document>(entity: Type<E>, q: Query<E>): boolean;
|
|
144
|
+
/**
|
|
145
|
+
* The relation aggregates one query reads: the ones its projection carries, plus any its `$where` or
|
|
146
|
+
* `$sort` names, which a read materializes whether or not it answers with them.
|
|
147
|
+
*/
|
|
148
|
+
private aggregateKeys;
|
|
149
|
+
/**
|
|
150
|
+
* The stages a relation aggregate a query names needs: the correlated lookup that reads the related
|
|
151
|
+
* rows - narrowed, ordered and capped as the field declared - ending in the tally or total it wants,
|
|
152
|
+
* and the `$addFields` that puts the value on the document under the field's own name.
|
|
153
|
+
*
|
|
154
|
+
* The same spec the SQL dialects render as a correlated subquery: a relation aggregate is data, so a
|
|
155
|
+
* document engine builds it out of stages rather than being refused a language it does not speak.
|
|
156
|
+
*/
|
|
157
|
+
private aggregateFieldStages;
|
|
158
|
+
/**
|
|
159
|
+
* One relation aggregate on the document under `field`: the correlated lookup that reads the related
|
|
160
|
+
* rows - narrowed, ordered and capped as the spec says - ending in the tally or total it wants, and
|
|
161
|
+
* the `$addFields` reading that back, `0` or `null` where the lookup matched nothing.
|
|
162
|
+
*
|
|
163
|
+
* Every aggregate MongoDB answers is built here: a `$count` a query asks for, an ordering by one, and
|
|
164
|
+
* a field a `computed` declares, which is the same spec the SQL dialects render as one subquery.
|
|
165
|
+
*/
|
|
166
|
+
private aggregateStages;
|
|
167
|
+
/**
|
|
168
|
+
* The value a lookup left in `temp`, which holds no row at all where nothing matched: `0` for the
|
|
169
|
+
* aggregates that count something, and `null` for the ones with no value to report.
|
|
170
|
+
*/
|
|
145
171
|
private tally;
|
|
146
172
|
/**
|
|
147
173
|
* The lookups reading each to-many a query populates, and the tally of each `$count`, onto the fields
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { ObjectId } from 'mongodb';
|
|
2
2
|
import { AbstractDialect } from '../dialect/abstractDialect.js';
|
|
3
|
-
import {
|
|
3
|
+
import { AGGREGATE_VALUE_ALIAS, REL_NESTED_KEY, REL_TEMP_PREFIX, sortCountField } from '../dialect/aliases.js';
|
|
4
4
|
import { resolveQueryJoins, resolveSortableJoin } from '../dialect/queryJoins.js';
|
|
5
5
|
import { assertSoleId, fieldOf, getMeta, relationOf, soleIdOf } from '../entity/index.js';
|
|
6
6
|
import { COUNT_RESULT_KEY } from '../type/query.js';
|
|
7
|
-
import { QueryRaw } from '../type/queryRaw.js';
|
|
7
|
+
import { QueryRaw, RelationAggregate } from '../type/queryRaw.js';
|
|
8
8
|
import { asSelectMap, assertAggregateColumns, assertNonNegativeInteger, columnFamily, countedRelations, entityName, fillOnFields, filterFieldKeys, findVectorIndex, findVectorSort, getKeys, getRelationRequestSummary, hasKeys, isJsonObject, isJsonUpdateOp, isOperatorMap, isOperatorObject, isRecord, isVectorSearch, normalizeScalarFieldSelection, parentJoins, parseGroupMap, parseRelationAtKey, parseRelationSize, parseSortByCount, someKey, targetKeyColumns, } from '../util/index.js';
|
|
9
9
|
/** Default {@link DialectFeatures} for MongoDB. */
|
|
10
10
|
export const mongoDialectFeatures = {
|
|
@@ -165,7 +165,7 @@ export class MongoDialect extends AbstractDialect {
|
|
|
165
165
|
appendRelationLookup(filter, meta, relKey, val, lookups) {
|
|
166
166
|
const temp = `${REL_TEMP_PREFIX}${lookups.temps.length}`;
|
|
167
167
|
const sizeVal = parseRelationSize(val);
|
|
168
|
-
const tail = sizeVal === undefined ? [{ $limit: 1 }] : [{ $count:
|
|
168
|
+
const tail = sizeVal === undefined ? [{ $limit: 1 }] : [{ $count: AGGREGATE_VALUE_ALIAS }];
|
|
169
169
|
const where = (sizeVal === undefined ? val : {});
|
|
170
170
|
lookups.temps.push(temp);
|
|
171
171
|
lookups.stages.push(this.relationLookup(meta, meta.relations[relKey], where, temp, tail));
|
|
@@ -423,6 +423,20 @@ export class MongoDialect extends AbstractDialect {
|
|
|
423
423
|
const selectMap = asSelectMap(select);
|
|
424
424
|
// Projected by column, not by field key; `normalizeId` maps them back on the way out.
|
|
425
425
|
const projection = normalizeScalarFieldSelection(meta, selectMap, exclude).reduce((acc, key) => {
|
|
426
|
+
// Swept in with the rest of the entity's fields, a computed one is skipped; asked for by name
|
|
427
|
+
// it is refused, since the document holds nothing to project under it.
|
|
428
|
+
const field = meta.fields[key];
|
|
429
|
+
if (field?.computed) {
|
|
430
|
+
// An aggregate is on the document by the time this projects, under the field's own name; SQL
|
|
431
|
+
// is refused where it was asked for by name, and skipped where it was swept in with the rest.
|
|
432
|
+
if (aggregateOf(field)) {
|
|
433
|
+
acc[key] = 1;
|
|
434
|
+
}
|
|
435
|
+
else if (selectMap && key in selectMap) {
|
|
436
|
+
assertReadable(meta, key);
|
|
437
|
+
}
|
|
438
|
+
return acc;
|
|
439
|
+
}
|
|
426
440
|
acc[this.columnOf(meta, key)] = 1;
|
|
427
441
|
return acc;
|
|
428
442
|
}, {});
|
|
@@ -498,18 +512,81 @@ export class MongoDialect extends AbstractDialect {
|
|
|
498
512
|
continue;
|
|
499
513
|
}
|
|
500
514
|
const temp = sortCountField(key);
|
|
501
|
-
stages.push(this.
|
|
515
|
+
stages.push(...this.aggregateStages(meta, { relation: key, op: '$count' }, `${REL_TEMP_PREFIX}${temp}`, temp));
|
|
502
516
|
fields.push(temp);
|
|
503
517
|
}
|
|
504
518
|
return { stages, fields };
|
|
505
519
|
}
|
|
506
|
-
/**
|
|
507
|
-
|
|
508
|
-
return this.
|
|
520
|
+
/** Whether a read answers with a relation aggregate, which only the pipeline can build. */
|
|
521
|
+
readsAggregates(entity, q) {
|
|
522
|
+
return this.aggregateKeys(entity, q).length > 0;
|
|
523
|
+
}
|
|
524
|
+
/**
|
|
525
|
+
* The relation aggregates one query reads: the ones its projection carries, plus any its `$where` or
|
|
526
|
+
* `$sort` names, which a read materializes whether or not it answers with them.
|
|
527
|
+
*/
|
|
528
|
+
aggregateKeys(entity, q) {
|
|
529
|
+
const meta = getMeta(entity);
|
|
530
|
+
const projected = normalizeScalarFieldSelection(meta, asSelectMap(q.$select), q.$exclude);
|
|
531
|
+
const named = [...Object.keys(q.$where ?? {}), ...Object.keys(q.$sort ?? {})];
|
|
532
|
+
return [...new Set([...projected, ...named])].flatMap((key) => {
|
|
533
|
+
const spec = aggregateOf(meta.fields[key]);
|
|
534
|
+
return spec ? [[key, spec]] : [];
|
|
535
|
+
});
|
|
536
|
+
}
|
|
537
|
+
/**
|
|
538
|
+
* The stages a relation aggregate a query names needs: the correlated lookup that reads the related
|
|
539
|
+
* rows - narrowed, ordered and capped as the field declared - ending in the tally or total it wants,
|
|
540
|
+
* and the `$addFields` that puts the value on the document under the field's own name.
|
|
541
|
+
*
|
|
542
|
+
* The same spec the SQL dialects render as a correlated subquery: a relation aggregate is data, so a
|
|
543
|
+
* document engine builds it out of stages rather than being refused a language it does not speak.
|
|
544
|
+
*/
|
|
545
|
+
aggregateFieldStages(meta, aggregates) {
|
|
546
|
+
const stages = [];
|
|
547
|
+
const temps = [];
|
|
548
|
+
for (const [key, spec] of aggregates) {
|
|
549
|
+
const temp = `${REL_TEMP_PREFIX}${key}`;
|
|
550
|
+
stages.push(...this.aggregateStages(meta, spec, temp, key));
|
|
551
|
+
temps.push(temp);
|
|
552
|
+
}
|
|
553
|
+
return { stages, temps };
|
|
554
|
+
}
|
|
555
|
+
/**
|
|
556
|
+
* One relation aggregate on the document under `field`: the correlated lookup that reads the related
|
|
557
|
+
* rows - narrowed, ordered and capped as the spec says - ending in the tally or total it wants, and
|
|
558
|
+
* the `$addFields` reading that back, `0` or `null` where the lookup matched nothing.
|
|
559
|
+
*
|
|
560
|
+
* Every aggregate MongoDB answers is built here: a `$count` a query asks for, an ordering by one, and
|
|
561
|
+
* a field a `computed` declares, which is the same spec the SQL dialects render as one subquery.
|
|
562
|
+
*/
|
|
563
|
+
aggregateStages(meta, spec, temp, field) {
|
|
564
|
+
const relOpts = relationOf(meta, spec.relation);
|
|
565
|
+
const query = spec.query ?? {};
|
|
566
|
+
const tail = [
|
|
567
|
+
...(query.$sort ? [{ $sort: this.sort(relOpts.entity(), query.$sort) }] : []),
|
|
568
|
+
...this.pagerStages(query),
|
|
569
|
+
spec.field
|
|
570
|
+
? {
|
|
571
|
+
$group: {
|
|
572
|
+
_id: null,
|
|
573
|
+
[AGGREGATE_VALUE_ALIAS]: { [spec.op]: `$${this.columnOf(getMeta(relOpts.entity()), spec.field)}` },
|
|
574
|
+
},
|
|
575
|
+
}
|
|
576
|
+
: { $count: AGGREGATE_VALUE_ALIAS },
|
|
577
|
+
];
|
|
578
|
+
return [
|
|
579
|
+
this.relationLookup(meta, relOpts, query.$where ?? {}, temp, tail),
|
|
580
|
+
{ $addFields: { [field]: this.tally(temp, spec.op) } },
|
|
581
|
+
];
|
|
509
582
|
}
|
|
510
|
-
/**
|
|
511
|
-
|
|
512
|
-
|
|
583
|
+
/**
|
|
584
|
+
* The value a lookup left in `temp`, which holds no row at all where nothing matched: `0` for the
|
|
585
|
+
* aggregates that count something, and `null` for the ones with no value to report.
|
|
586
|
+
*/
|
|
587
|
+
tally(temp, op = '$count') {
|
|
588
|
+
const empty = op === '$count' || op === '$sum' ? 0 : null;
|
|
589
|
+
return { $ifNull: [{ $arrayElemAt: [`$${temp}.${AGGREGATE_VALUE_ALIAS}`, 0] }, empty] };
|
|
513
590
|
}
|
|
514
591
|
/**
|
|
515
592
|
* The lookups reading each to-many a query populates, and the tally of each `$count`, onto the fields
|
|
@@ -523,12 +600,11 @@ export class MongoDialect extends AbstractDialect {
|
|
|
523
600
|
for (const relKey of getRelationRequestSummary(meta, q.$populate).toManyKeys) {
|
|
524
601
|
stages.push(...this.toManyLookup(meta, relKey, parseRelationAtKey(relKey, q.$populate).query, temps));
|
|
525
602
|
}
|
|
526
|
-
for (const { relKey,
|
|
603
|
+
for (const { relKey, where } of countedRelations(meta, q.$count)) {
|
|
527
604
|
const temp = `${REL_TEMP_PREFIX}count_${relKey}`;
|
|
528
605
|
temps.push(temp);
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
});
|
|
606
|
+
const spec = { relation: relKey, op: '$count', query: { $where: where } };
|
|
607
|
+
stages.push(...this.aggregateStages(meta, spec, temp, `${COUNT_RESULT_KEY}.${relKey}`));
|
|
532
608
|
}
|
|
533
609
|
return temps.length ? [...stages, { $unset: temps }] : stages;
|
|
534
610
|
}
|
|
@@ -595,6 +671,7 @@ export class MongoDialect extends AbstractDialect {
|
|
|
595
671
|
pathOf(meta, key) {
|
|
596
672
|
const dot = key.indexOf('.');
|
|
597
673
|
if (dot < 0) {
|
|
674
|
+
assertReadable(meta, key);
|
|
598
675
|
return this.columnOf(meta, key);
|
|
599
676
|
}
|
|
600
677
|
return this.columnOf(meta, key.slice(0, dot)) + key.slice(dot);
|
|
@@ -602,8 +679,12 @@ export class MongoDialect extends AbstractDialect {
|
|
|
602
679
|
aggregationPipeline(entity, q, opts) {
|
|
603
680
|
// Lookups that a relation condition needs come first, then the match that reads them, then the
|
|
604
681
|
// temporary fields are dropped so they never reach the caller.
|
|
682
|
+
// Every relation aggregate the query reads comes first: a `$match`, a `$sort` and the projection all
|
|
683
|
+
// name it as a field of the document, which is what these stages make true.
|
|
684
|
+
const aggregates = this.aggregateFieldStages(getMeta(entity), this.aggregateKeys(entity, q));
|
|
605
685
|
const { stages, filter, unset } = this.whereWithRelations(entity, q.$where, opts);
|
|
606
686
|
return [
|
|
687
|
+
...aggregates.stages,
|
|
607
688
|
...stages,
|
|
608
689
|
...(hasKeys(filter) ? [{ $match: filter }] : []),
|
|
609
690
|
...(unset.length ? [{ $unset: unset }] : []),
|
|
@@ -611,6 +692,7 @@ export class MongoDialect extends AbstractDialect {
|
|
|
611
692
|
sort: this.sort(entity, q.$sort, q.$populate),
|
|
612
693
|
pager: this.pagerStages(q),
|
|
613
694
|
}),
|
|
695
|
+
...(aggregates.temps.length ? [{ $unset: aggregates.temps }] : []),
|
|
614
696
|
];
|
|
615
697
|
}
|
|
616
698
|
/** The `$skip`/`$limit` stages of a page, each checked: `/http` hands a page over untyped. */
|
|
@@ -1113,3 +1195,18 @@ export class MongoDialect extends AbstractDialect {
|
|
|
1113
1195
|
function sortDirection(value) {
|
|
1114
1196
|
return value === 'desc' || value === -1 ? -1 : 1;
|
|
1115
1197
|
}
|
|
1198
|
+
/** The relation aggregate a field computes, where it computes one rather than writing SQL. */
|
|
1199
|
+
function aggregateOf(field) {
|
|
1200
|
+
return field?.computed instanceof RelationAggregate ? field.computed.spec : undefined;
|
|
1201
|
+
}
|
|
1202
|
+
/**
|
|
1203
|
+
* A `computed` field writing SQL is refused wherever a query names it, since no document engine
|
|
1204
|
+
* evaluates SQL and answering with the property name would hand back `undefined` for every row. One
|
|
1205
|
+
* computing a relation aggregate is read: `aggregateFieldStages` builds it.
|
|
1206
|
+
*/
|
|
1207
|
+
function assertReadable(meta, key) {
|
|
1208
|
+
const field = meta.fields[key];
|
|
1209
|
+
if (field?.computed && !aggregateOf(field)) {
|
|
1210
|
+
throw new TypeError(`cannot read '${meta.entity.name}.${key}' on MongoDB: a 'computed' field writing SQL is not something a document engine evaluates`);
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
@@ -18,8 +18,8 @@ export declare class MongodbQuerier extends AbstractQuerier {
|
|
|
18
18
|
*/
|
|
19
19
|
private readCursor;
|
|
20
20
|
/**
|
|
21
|
-
* Whether a read needs stages a `find` cursor cannot express: a lookup to populate, count, filter
|
|
22
|
-
*
|
|
21
|
+
* Whether a read needs stages a `find` cursor cannot express: a lookup to populate, count, filter or
|
|
22
|
+
* order by a relation, one to build a relation aggregate, and the grouping `$distinct` is.
|
|
23
23
|
*/
|
|
24
24
|
private readsThroughPipeline;
|
|
25
25
|
private buildScalarProjection;
|
|
@@ -68,15 +68,16 @@ export class MongodbQuerier extends AbstractQuerier {
|
|
|
68
68
|
: this.buildFindCursor(entity, q, opts);
|
|
69
69
|
}
|
|
70
70
|
/**
|
|
71
|
-
* Whether a read needs stages a `find` cursor cannot express: a lookup to populate, count, filter
|
|
72
|
-
*
|
|
71
|
+
* Whether a read needs stages a `find` cursor cannot express: a lookup to populate, count, filter or
|
|
72
|
+
* order by a relation, one to build a relation aggregate, and the grouping `$distinct` is.
|
|
73
73
|
*/
|
|
74
74
|
readsThroughPipeline(entity, q) {
|
|
75
75
|
return (!!q.$distinct ||
|
|
76
76
|
hasKeys(q.$count) ||
|
|
77
77
|
populatesRelations(getMeta(entity), q.$populate) ||
|
|
78
78
|
this.dialect.constrainsRelations(entity, q.$where) ||
|
|
79
|
-
this.dialect.sortsRelations(entity, q.$sort)
|
|
79
|
+
this.dialect.sortsRelations(entity, q.$sort) ||
|
|
80
|
+
this.dialect.readsAggregates(entity, q));
|
|
80
81
|
}
|
|
81
82
|
buildScalarProjection(entity, q) {
|
|
82
83
|
return this.dialect.select(entity, q.$select, q.$exclude);
|
package/dist/type/dialect.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { EntityMeta, UpdatePayload } from './entity.js';
|
|
2
|
-
import type { Query, QueryConflictPaths, QueryOptions, QueryPage, QuerySearch } from './query.js';
|
|
3
|
-
import type { QueryAggMap, QueryAggregate, QueryGroupMap } from './queryAggregate.js';
|
|
2
|
+
import type { Query, QueryConflictPaths, QueryOptions, QueryPage, QuerySearch, RelationQuery } from './query.js';
|
|
3
|
+
import type { QueryAggMap, QueryAggregate, QueryAggregateOp, QueryGroupMap } from './queryAggregate.js';
|
|
4
4
|
import type { Type } from './utility.js';
|
|
5
5
|
/**
|
|
6
6
|
* comparison options.
|
|
@@ -208,7 +208,34 @@ export interface SqlQueryDialect {
|
|
|
208
208
|
* Default: '?' for MySQL/MariaDB/SQLite, '$n' for PostgreSQL.
|
|
209
209
|
*/
|
|
210
210
|
placeholder(index: number): string;
|
|
211
|
+
/**
|
|
212
|
+
* A relation aggregate as a correlated subquery, correlated to the row under `prefix`: what a field
|
|
213
|
+
* declaring `computed: (user) => user.resources.count()` renders as, wherever a clause names it.
|
|
214
|
+
*/
|
|
215
|
+
appendRelationAggregate<E>(ctx: QueryContext, entity: Type<E>, aggregate: RelationAggregateSpec, prefix: string): void;
|
|
211
216
|
}
|
|
217
|
+
/**
|
|
218
|
+
* The aggregates a relation reads, spelled as the query language spells them, so a `computed` field, an
|
|
219
|
+
* aggregate query and MongoDB's own `$group` all name one the same way. `$count` and `$sum` are the two
|
|
220
|
+
* a row change turns into a delta.
|
|
221
|
+
*/
|
|
222
|
+
export type RelationAggregateOp = QueryAggregateOp;
|
|
223
|
+
/** What a relation aggregate reads: how many rows, or one of the target's columns. */
|
|
224
|
+
export type RelationAggregateProjection = {
|
|
225
|
+
readonly op: '$count';
|
|
226
|
+
readonly field?: never;
|
|
227
|
+
} | {
|
|
228
|
+
readonly op: Exclude<RelationAggregateOp, '$count'>;
|
|
229
|
+
readonly field: string;
|
|
230
|
+
};
|
|
231
|
+
/** A relation aggregate as a `computed` field holds it: what it reads, off which relation, filtered how. */
|
|
232
|
+
export type RelationAggregateSpec = RelationAggregateProjection & {
|
|
233
|
+
readonly relation: string;
|
|
234
|
+
/** Which of the related rows it reads, and the page it caps them to, as the field declared them. */
|
|
235
|
+
readonly query?: RelationSubqueryQuery;
|
|
236
|
+
};
|
|
237
|
+
/** The rows a relation subquery reads: which ones, in what order, and the page capping them. */
|
|
238
|
+
export type RelationSubqueryQuery = Pick<RelationQuery, '$where' | '$sort' | '$limit' | '$skip'>;
|
|
212
239
|
/**
|
|
213
240
|
* Supported SQL dialect identifiers.
|
|
214
241
|
*/
|
package/dist/type/entity.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { EnumValues, ForeignKeyAction, IndexType } from '../schema/types.js';
|
|
2
|
-
import type { FilterOptions } from './query.js';
|
|
3
|
-
import type { ColumnRef, QueryRaw } from './queryRaw.js';
|
|
2
|
+
import type { FilterOptions, RelationQuery } from './query.js';
|
|
3
|
+
import type { ColumnRef, QueryRaw, RelationAggregate } from './queryRaw.js';
|
|
4
4
|
import type { QueryWhere } from './queryWhere.js';
|
|
5
5
|
import type { Except, IsMany, Json, Scalar, Type, Unpacked } from './utility.js';
|
|
6
6
|
import type { VectorDistance, VectorIndexOptions, VectorIndexType } from './vector.js';
|
|
@@ -22,6 +22,15 @@ export type FieldKey<E> = {
|
|
|
22
22
|
}[Key<E>];
|
|
23
23
|
/** The relation names of an entity: every key but its fields and its methods, so the two sets cannot drift. */
|
|
24
24
|
export type RelationKey<E> = Exclude<Key<E>, FieldKey<E> | MethodKey<E>>;
|
|
25
|
+
/**
|
|
26
|
+
* To-one relations only: a parent holds many rows of a to-many, so there is no single value to order it
|
|
27
|
+
* by, and joining one in would duplicate the parent instead. Order those inside `$populate`.
|
|
28
|
+
*/
|
|
29
|
+
export type ToOneRelationKey<E> = {
|
|
30
|
+
[K in RelationKey<E>]: IsMany<E[K]> extends true ? never : K;
|
|
31
|
+
}[RelationKey<E>];
|
|
32
|
+
/** The relation names a parent holds many rows of: what a populated query fills, and what an aggregate reads. */
|
|
33
|
+
export type ToManyRelationKey<E> = Exclude<RelationKey<E>, ToOneRelationKey<E>>;
|
|
25
34
|
/** Whether `T` carries the `Json` brand, read off its marker key: a primitive matches `Json<infer P>` too. */
|
|
26
35
|
type IsJson<T> = '__json' extends keyof T ? true : false;
|
|
27
36
|
/** The payload `P` of a branded `Json<P>`, or `never` for any other type. */
|
|
@@ -201,8 +210,11 @@ export type FieldOptions<V = TsTypeOf<FieldType>, E = unknown> = {
|
|
|
201
210
|
/**
|
|
202
211
|
* An expression the database computes, never written: spliced into each read, or with `stored` a
|
|
203
212
|
* generated column, `computed: (user) => raw`${user.first} || ' ' || ${user.last}``.
|
|
213
|
+
*
|
|
214
|
+
* A relation aggregate is the other form, `computed: (user) => user.resources.count()`, read as the
|
|
215
|
+
* subquery a `$count` reads. Both resolve to SQL at registration, so everything downstream sees one.
|
|
204
216
|
*/
|
|
205
|
-
readonly computed?:
|
|
217
|
+
readonly computed?: ComputedSql<E>;
|
|
206
218
|
/** Whether {@link FieldOptions.computed} is a generated column rather than spliced into each read; no query changes either way. */
|
|
207
219
|
readonly stored?: boolean;
|
|
208
220
|
readonly updatable?: boolean;
|
|
@@ -258,7 +270,22 @@ export type FieldOptionsFor<V, E = unknown> = (FieldOptions<NonNullable<V>, E> &
|
|
|
258
270
|
}) | (FieldOptions<NonNullable<V>, E> & {
|
|
259
271
|
readonly references: EntityGetter;
|
|
260
272
|
readonly type?: TypeFor<V>;
|
|
273
|
+
}) | AggregateOptionsFor<V, E>;
|
|
274
|
+
/**
|
|
275
|
+
* A field a relation aggregate computes: the aggregate types it, so it declares no `type`, and only the
|
|
276
|
+
* two a row change turns into a delta - `count` and `sum` - may be `stored`.
|
|
277
|
+
*/
|
|
278
|
+
type AggregateOptionsFor<V, E> = Except<FieldOptions<NonNullable<V>, E>, 'computed' | 'stored' | 'type'> & ({
|
|
279
|
+
readonly computed: AggregateReading<E, V, boolean>;
|
|
280
|
+
readonly stored?: false;
|
|
281
|
+
} | {
|
|
282
|
+
readonly computed: AggregateReading<E, V, true>;
|
|
283
|
+
readonly stored: true;
|
|
261
284
|
});
|
|
285
|
+
/** An aggregate reading what the property holds, bivariant the way {@link EntitySql} is. */
|
|
286
|
+
type AggregateReading<E, V, S extends boolean> = {
|
|
287
|
+
agg(refs: ComputedRefs<E>): RelationAggregate<null extends V ? NonNullable<V> | null : NonNullable<V>, S>;
|
|
288
|
+
}['agg'];
|
|
262
289
|
/** The entity a relation points at: `Company` for `company?: Company` and `companies?: Company[]` alike. */
|
|
263
290
|
export type RelationTarget<V> = Extract<Unpacked<V>, object>;
|
|
264
291
|
/**
|
|
@@ -360,6 +387,66 @@ export type RefMap<E, F extends keyof E = FieldKey<E>> = {
|
|
|
360
387
|
export type EntitySql<E> = QueryRaw | {
|
|
361
388
|
sql(refs: RefMap<E>): QueryRaw;
|
|
362
389
|
}['sql'];
|
|
390
|
+
/** The fields of `C` a `sum` or an `avg` can add up. */
|
|
391
|
+
type NumericKey<C> = {
|
|
392
|
+
readonly [K in FieldKey<C>]-?: [NonNullable<C[K]>] extends [number | bigint] ? K : never;
|
|
393
|
+
}[FieldKey<C>];
|
|
394
|
+
/** One field of `C`, read off its refs: `(item) => item.amount`. */
|
|
395
|
+
type PickRef<C, K extends keyof C> = (refs: RefMap<C>) => ColumnRef<K & string>;
|
|
396
|
+
/**
|
|
397
|
+
* A relation as a `computed` field reads it, its aggregates typed against the related entity. `count`
|
|
398
|
+
* and `sum` are the two a row change turns into a delta, so they alone may be `stored`; the rest read
|
|
399
|
+
* as a subquery and are `null` where the relation holds no row.
|
|
400
|
+
*/
|
|
401
|
+
export type RelationRef<C> = {
|
|
402
|
+
count(q?: AggregateFilter<C>): RelationAggregate<number, true>;
|
|
403
|
+
count(q: AggregatePage<C>): RelationAggregate<number, false>;
|
|
404
|
+
sum<K extends NumericKey<C>>(pick: PickRef<C, K>, q?: AggregateFilter<C>): RelationAggregate<NonNullable<C[K]>, true>;
|
|
405
|
+
sum<K extends NumericKey<C>>(pick: PickRef<C, K>, q: AggregateTopRows<C>): RelationAggregate<NonNullable<C[K]>, false>;
|
|
406
|
+
min<K extends FieldKey<C>>(pick: PickRef<C, K>, q?: AggregateRows<C>): RelationAggregate<NonNullable<C[K]> | null, false>;
|
|
407
|
+
max<K extends FieldKey<C>>(pick: PickRef<C, K>, q?: AggregateRows<C>): RelationAggregate<NonNullable<C[K]> | null, false>;
|
|
408
|
+
avg<K extends NumericKey<C>>(pick: PickRef<C, K>, q?: AggregateRows<C>): RelationAggregate<number | null, false>;
|
|
409
|
+
};
|
|
410
|
+
/**
|
|
411
|
+
* What an aggregate reads of the related rows. The predicate is an {@link EntityPredicate} rather than a
|
|
412
|
+
* full `$where` so that `stored: true` changes no call site: a trigger sees one row, and can evaluate
|
|
413
|
+
* nothing that traverses a relation or opens a subquery.
|
|
414
|
+
*/
|
|
415
|
+
export type AggregateFilter<C> = {
|
|
416
|
+
readonly $where?: EntityPredicate<C>;
|
|
417
|
+
};
|
|
418
|
+
/**
|
|
419
|
+
* A tally capped to a page of the related rows, as `count` itself takes one. It needs no `$sort` and
|
|
420
|
+
* accepts none: an order picks *which* rows a page holds, never how many.
|
|
421
|
+
*/
|
|
422
|
+
export type AggregatePage<C> = AggregateFilter<C> & Pick<RelationQuery<C>, '$limit' | '$skip'>;
|
|
423
|
+
/**
|
|
424
|
+
* The rows a value aggregate reads where it reads only some of them - "the five largest" - which only
|
|
425
|
+
* an order defines, so `$sort` and `$limit` come together. Keyed off the relation read they are, so
|
|
426
|
+
* the page an aggregate takes and the page a `$populate` takes cannot drift apart.
|
|
427
|
+
*/
|
|
428
|
+
export type AggregateTopRows<C> = AggregateFilter<C> & Required<Pick<RelationQuery<C>, '$sort' | '$limit'>> & Pick<RelationQuery<C>, '$skip'>;
|
|
429
|
+
/** Either of those, for the aggregates that are never `stored` and so need no second signature. */
|
|
430
|
+
export type AggregateRows<C> = AggregateFilter<C> | AggregateTopRows<C>;
|
|
431
|
+
/** What a `computed` callback reads: the entity's fields as columns, its to-many relations as aggregates. */
|
|
432
|
+
export type ComputedRefs<E, F extends keyof E = FieldKey<E>, R extends keyof E = ToManyRelationKey<E>> = RefMap<E, F> & {
|
|
433
|
+
readonly [K in R]-?: RelationRef<RelationTarget<E[K]>>;
|
|
434
|
+
};
|
|
435
|
+
/** A relation aggregate a definition writes, bivariant the way {@link EntitySql} is. */
|
|
436
|
+
export type EntityAggregate<E> = {
|
|
437
|
+
agg(refs: ComputedRefs<E>): RelationAggregate;
|
|
438
|
+
}['agg'];
|
|
439
|
+
/**
|
|
440
|
+
* SQL a `computed` field writes. One callback shape for every arm, aggregate or not: overload
|
|
441
|
+
* resolution picks a contextual parameter type per arm only while they agree on one.
|
|
442
|
+
*/
|
|
443
|
+
export type ComputedSql<E> = QueryRaw | {
|
|
444
|
+
sql(refs: ComputedRefs<E>): QueryRaw;
|
|
445
|
+
}['sql'];
|
|
446
|
+
/** The value a field's options declare it holds, where an aggregate is what declares it. */
|
|
447
|
+
export type AggregateValue<O> = O extends {
|
|
448
|
+
readonly computed: (...args: never[]) => RelationAggregate<infer V>;
|
|
449
|
+
} ? V : never;
|
|
363
450
|
/** A predicate DDL can hold: the entity's own fields, without a relation, `$text` or a sub-query. */
|
|
364
451
|
export type EntityPredicate<E> = QueryWhere<E> & {
|
|
365
452
|
readonly [K in RelationKey<E>]?: never;
|
package/dist/type/query.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { FieldKey, JsonFieldPaths, RelationKey, RelationTarget, WrittenId } from './entity.js';
|
|
1
|
+
import type { FieldKey, JsonFieldPaths, RelationKey, RelationTarget, ToManyRelationKey, WrittenId } from './entity.js';
|
|
2
2
|
import type { QueryLock } from './queryLock.js';
|
|
3
3
|
import type { QueryRaw } from './queryRaw.js';
|
|
4
4
|
import type { QueryWhere } from './queryWhere.js';
|
|
@@ -116,15 +116,6 @@ export type QuerySortDirection = -1 | 1 | 'asc' | 'desc';
|
|
|
116
116
|
* Accepted value for a field in `$sort` - either a direction or a vector similarity search.
|
|
117
117
|
*/
|
|
118
118
|
export type QuerySortValue = QuerySortDirection | QueryVectorSearch;
|
|
119
|
-
/**
|
|
120
|
-
* To-one relations only: a parent holds many rows of a to-many, so there is no single value to order
|
|
121
|
-
* it by, and joining one in would duplicate the parent instead. Order those inside `$populate`.
|
|
122
|
-
*/
|
|
123
|
-
type ToOneRelationKey<E> = {
|
|
124
|
-
[K in RelationKey<E>]: IsMany<E[K]> extends true ? never : K;
|
|
125
|
-
}[RelationKey<E>];
|
|
126
|
-
/** The relation names a parent holds many rows of, which a populated query fills with a list. */
|
|
127
|
-
type ToManyRelationKey<E> = Exclude<RelationKey<E>, ToOneRelationKey<E>>;
|
|
128
119
|
/**
|
|
129
120
|
* Ordering parents by how many rows a to-many relation holds - "the ten users with the most posts".
|
|
130
121
|
* The tally is computed per parent as a correlated count, never by loading the rows.
|
package/dist/type/queryRaw.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { QueryContext, SqlQueryDialect } from './dialect.js';
|
|
1
|
+
import type { QueryContext, RelationAggregateSpec, SqlQueryDialect } from './dialect.js';
|
|
2
2
|
import type { Type } from './utility.js';
|
|
3
3
|
/** What a `raw` callback receives. See {@link QueryRawFn}. */
|
|
4
4
|
export type QueryRawRenderOptions = {
|
|
@@ -44,3 +44,21 @@ export declare class ColumnRef<K extends string = string> extends QueryRaw {
|
|
|
44
44
|
readonly key: K;
|
|
45
45
|
constructor(key: K, value: QueryRawFn);
|
|
46
46
|
}
|
|
47
|
+
/**
|
|
48
|
+
* A relation aggregate as SQL, read off a `computed` field's refs: `(user) => user.resources.count()`.
|
|
49
|
+
* It renders as the correlated subquery a `$count` reads, so a field holding one is read, filtered and
|
|
50
|
+
* sorted like any other.
|
|
51
|
+
*
|
|
52
|
+
* `V` is the value it reads and `Storable` whether a trigger could keep it, both carried in phantom
|
|
53
|
+
* fields so the aggregate a field declares decides the property's type and refuses `stored: true` on
|
|
54
|
+
* one no delta can maintain.
|
|
55
|
+
*/
|
|
56
|
+
export declare class RelationAggregate<V = unknown, Storable extends boolean = boolean> extends QueryRaw {
|
|
57
|
+
/** What it reads, kept beside the SQL so a read decodes the value the way the target's field does. */
|
|
58
|
+
readonly spec: RelationAggregateSpec;
|
|
59
|
+
private readonly __value;
|
|
60
|
+
private readonly __storable;
|
|
61
|
+
constructor(
|
|
62
|
+
/** What it reads, kept beside the SQL so a read decodes the value the way the target's field does. */
|
|
63
|
+
spec: RelationAggregateSpec, value: QueryRawFn);
|
|
64
|
+
}
|
package/dist/type/queryRaw.js
CHANGED
|
@@ -30,3 +30,21 @@ export class ColumnRef extends QueryRaw {
|
|
|
30
30
|
this.key = key;
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
|
+
/**
|
|
34
|
+
* A relation aggregate as SQL, read off a `computed` field's refs: `(user) => user.resources.count()`.
|
|
35
|
+
* It renders as the correlated subquery a `$count` reads, so a field holding one is read, filtered and
|
|
36
|
+
* sorted like any other.
|
|
37
|
+
*
|
|
38
|
+
* `V` is the value it reads and `Storable` whether a trigger could keep it, both carried in phantom
|
|
39
|
+
* fields so the aggregate a field declares decides the property's type and refuses `stored: true` on
|
|
40
|
+
* one no delta can maintain.
|
|
41
|
+
*/
|
|
42
|
+
export class RelationAggregate extends QueryRaw {
|
|
43
|
+
spec;
|
|
44
|
+
constructor(
|
|
45
|
+
/** What it reads, kept beside the SQL so a read decodes the value the way the target's field does. */
|
|
46
|
+
spec, value) {
|
|
47
|
+
super(value);
|
|
48
|
+
this.spec = spec;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -2,8 +2,8 @@ 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
4
|
import { VECTOR_INDEX_TYPES } from '../type/vector.js';
|
|
5
|
-
import { isDatabaseWritten } from './field.util.js';
|
|
6
|
-
import { entityName,
|
|
5
|
+
import { getFieldKeys, isDatabaseWritten } from './field.util.js';
|
|
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. */
|
|
8
8
|
export function filterFieldKeys(meta, payload, callbackKey) {
|
|
9
9
|
return getKeys(payload).filter((key) => {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type ColumnFamily, type EntityMeta, type FieldOptions } from '../type/index.js';
|
|
1
|
+
import { type ColumnFamily, type EntityMeta, type FieldKey, type FieldOptions, type RelationAggregateSpec } from '../type/index.js';
|
|
2
2
|
/** The family of a logical field type, or `undefined` where it names none. */
|
|
3
3
|
export declare function columnFamily(type: unknown): ColumnFamily | undefined;
|
|
4
4
|
/**
|
|
@@ -12,6 +12,12 @@ export declare function isIntegerColumn(field: Pick<FieldOptions, 'type' | 'colu
|
|
|
12
12
|
* because an inlined field has no column to name, while a stored one is read like any other.
|
|
13
13
|
*/
|
|
14
14
|
export declare function isInlinedExpression<F extends FieldOptions>(field: F): field is F & Required<Pick<F, 'computed'>>;
|
|
15
|
+
/**
|
|
16
|
+
* The relation aggregate a field computes, where it computes one rather than writing SQL: what it
|
|
17
|
+
* reads, off which relation, narrowed and capped how. Every engine renders it from this - a correlated
|
|
18
|
+
* subquery on SQL, a lookup on MongoDB - so both read the same declaration rather than parsing SQL.
|
|
19
|
+
*/
|
|
20
|
+
export declare function aggregateOf(field: FieldOptions | undefined): RelationAggregateSpec | undefined;
|
|
15
21
|
/**
|
|
16
22
|
* Whether the database supplies this field's value, so no insert or update may write it: a stored
|
|
17
23
|
* computed column *is* a real column, read like one, but writing to it is an error on every engine.
|
|
@@ -25,3 +31,11 @@ export declare function isSoleIdField<E>(meta: EntityMeta<E>, field: FieldOption
|
|
|
25
31
|
* states its width. The one answer the create statement and the diff both read.
|
|
26
32
|
*/
|
|
27
33
|
export declare function isAutoIncrement(field: FieldOptions, isPrimaryKey: boolean): boolean;
|
|
34
|
+
/**
|
|
35
|
+
* The fields a read answers with where it names none. A relation aggregate is left out unless it asks
|
|
36
|
+
* for `eager: true`: it reads the related rows, which is what a relation does, and a relation is loaded
|
|
37
|
+
* only when a query asks for it. Naming one in `$select` reads it, whatever the default.
|
|
38
|
+
*/
|
|
39
|
+
export declare function getFieldKeys<E>(fields: {
|
|
40
|
+
[K in FieldKey<E>]?: FieldOptions;
|
|
41
|
+
}): FieldKey<E>[];
|