uql-orm 0.68.0 → 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.
Files changed (37) hide show
  1. package/dist/browser/http/http.js +5 -8
  2. package/dist/browser/querier/httpQuerier.d.ts +9 -9
  3. package/dist/browser/uql-browser.min.js +2 -2
  4. package/dist/browser/uql-browser.min.js.map +9 -8
  5. package/dist/d1/d1Querier.d.ts +5 -5
  6. package/dist/d1/d1QuerierPool.d.ts +3 -3
  7. package/dist/dialect/abstractSqlDialect.d.ts +31 -4
  8. package/dist/dialect/abstractSqlDialect.js +99 -21
  9. package/dist/dialect/aliases.d.ts +8 -0
  10. package/dist/dialect/aliases.js +8 -0
  11. package/dist/entity/decorator/members.d.ts +29 -1
  12. package/dist/entity/decorator/members.js +0 -5
  13. package/dist/entity/metadata/definition.js +10 -3
  14. package/dist/http/query.d.ts +10 -2
  15. package/dist/http/query.js +26 -1
  16. package/dist/migrate/introspection/postgresIntrospector.d.ts +6 -0
  17. package/dist/migrate/introspection/postgresIntrospector.js +7 -1
  18. package/dist/mongo/mongoDialect.d.ts +29 -3
  19. package/dist/mongo/mongoDialect.js +111 -14
  20. package/dist/mongo/mongodbQuerier.d.ts +2 -2
  21. package/dist/mongo/mongodbQuerier.js +4 -3
  22. package/dist/type/dialect.d.ts +29 -2
  23. package/dist/type/entity.d.ts +94 -7
  24. package/dist/type/query.d.ts +30 -34
  25. package/dist/type/queryRaw.d.ts +19 -1
  26. package/dist/type/queryRaw.js +18 -0
  27. package/dist/type/queryWhere.d.ts +20 -20
  28. package/dist/type/universalQuerier.d.ts +10 -10
  29. package/dist/type/wire.d.ts +9 -0
  30. package/dist/util/dialect.util.js +2 -2
  31. package/dist/util/field.util.d.ts +15 -1
  32. package/dist/util/field.util.js +18 -1
  33. package/dist/util/object.util.d.ts +1 -4
  34. package/dist/util/object.util.js +0 -3
  35. package/dist/util/raw.d.ts +2 -2
  36. package/dist/util/raw.js +29 -2
  37. package/package.json +2 -2
@@ -1,10 +1,10 @@
1
1
  import { ObjectId } from 'mongodb';
2
2
  import { AbstractDialect } from '../dialect/abstractDialect.js';
3
- import { COUNT_ALIAS, REL_NESTED_KEY, REL_TEMP_PREFIX, sortCountField } from '../dialect/aliases.js';
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: COUNT_ALIAS }];
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.tallyLookup(meta, relOpts, {}, temp), { $addFields: { [temp]: this.tally(temp) } });
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
- /** The correlated lookup counting a relation's rows, which `where` narrows, into `temp`. */
507
- tallyLookup(meta, relOpts, where, temp) {
508
- return this.relationLookup(meta, relOpts, where, temp, [{ $count: COUNT_ALIAS }]);
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
- /** The tally a lookup left in `temp`, which holds no row at all where nothing matched: a zero. */
511
- tally(temp) {
512
- return { $ifNull: [{ $arrayElemAt: [`$${temp}.${COUNT_ALIAS}`, 0] }, 0] };
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, relation, where } of countedRelations(meta, q.$count)) {
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
- stages.push(this.tallyLookup(meta, relation, where, temp), {
530
- $addFields: { [`${COUNT_RESULT_KEY}.${relKey}`]: this.tally(temp) },
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
- * or order by a relation, and the grouping `$distinct` is.
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
- * or order by a relation, and the grouping `$distinct` is.
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);
@@ -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
  */
@@ -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. */
@@ -84,7 +93,7 @@ export type JsonUpdateOp<T = unknown> = {
84
93
  */
85
94
  type JsonUpdateOpFor<V, T = UnwrapJson<NonNullable<V>>> = [T] extends [never] ? never : IsMany<T> extends true ? never : JsonUpdateOp<T>;
86
95
  /** What an update takes beyond the value: `null` to clear an optional member, `raw` SQL, and JSON operators. */
87
- type UpdateExtra<V> = (undefined extends V ? null : never) | QueryRaw | JsonUpdateOpFor<V>;
96
+ type UpdateExtra<V, Raw> = (undefined extends V ? null : never) | Raw | JsonUpdateOpFor<V>;
88
97
  /**
89
98
  * What a whole-record write persists: the fields and relations with their declared optionality, a
90
99
  * related row's alike, and no methods. Two mapped types, since asking each key costs a conditional.
@@ -97,10 +106,10 @@ export type EntityData<E, F extends keyof E = FieldKey<E>, R extends keyof E = R
97
106
  /** A relation's value as its rows' {@link EntityData}. */
98
107
  type RelationData<V> = V extends readonly (infer T)[] ? EntityData<T>[] : V extends object ? EntityData<V> : never;
99
108
  /** {@link EntityData} made partial, each member also taking its {@link UpdateExtra}. */
100
- export type UpdatePayload<E, F extends keyof E = FieldKey<E>, R extends keyof E = RelationKey<E>> = {
101
- [P in F]?: E[P] | UpdateExtra<E[P]>;
109
+ export type UpdatePayload<E, Raw = QueryRaw, F extends keyof E = FieldKey<E>, R extends keyof E = RelationKey<E>> = {
110
+ [P in F]?: E[P] | UpdateExtra<E[P], Raw>;
102
111
  } & {
103
- [P in R]?: E[P] | RelationData<E[P]> | UpdateExtra<E[P]>;
112
+ [P in R]?: E[P] | RelationData<E[P]> | UpdateExtra<E[P], Raw>;
104
113
  };
105
114
  /** The key's name where the entity states it, by the `idKey` brand or a conventional name; `never` otherwise. */
106
115
  export type NamedIdKey<E> = E extends {
@@ -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?: EntitySql<E>;
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;
@@ -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';
@@ -37,7 +37,7 @@ export type QuerySelect<E, F extends keyof E = FieldKey<E>, V = BooleanLike> = {
37
37
  * Accepted `$select` value: a field map, or raw SQL projections built with `raw()`
38
38
  * (e.g. ``[raw`*`, raw`LOG10(points)`.as('score')]``). The raw form is SQL-only.
39
39
  */
40
- export type QuerySelectValue<E> = QuerySelect<E> | readonly QueryRaw[];
40
+ export type QuerySelectValue<E, Raw = QueryRaw> = QuerySelect<E> | readonly Raw[];
41
41
  /**
42
42
  * Fields to exclude from the query result - `{ name: true }` blacklists fields.
43
43
  * Mutually exclusive with positive field selections in `$select`.
@@ -46,8 +46,8 @@ export type QueryExclude<E> = QuerySelect<E>;
46
46
  /**
47
47
  * relation population map.
48
48
  */
49
- export type QueryPopulate<E, R extends keyof E = RelationKey<E>> = {
50
- [K in R]?: BooleanLike | QueryPopulateRelationOptions<E[K]>;
49
+ export type QueryPopulate<E, Raw = QueryRaw, R extends keyof E = RelationKey<E>> = {
50
+ [K in R]?: BooleanLike | QueryPopulateRelationOptions<E[K], Raw>;
51
51
  };
52
52
  /**
53
53
  * The key a read carries its relation tallies under. One spelling for the type and the runtime that
@@ -59,8 +59,8 @@ export declare const COUNT_RESULT_KEY = "_count";
59
59
  * which ones count: a correlated count in the read's own statement, so no related row is loaded. Comes
60
60
  * back under `_count`, which keeps it clear of a relation of the same name `$populate` filled.
61
61
  */
62
- export type QueryCount<E, R extends keyof E = ToManyRelationKey<E>> = {
63
- [K in R]?: BooleanLike | QueryFilter<RelationTarget<E[K]>>;
62
+ export type QueryCount<E, Raw = QueryRaw, R extends keyof E = ToManyRelationKey<E>> = {
63
+ [K in R]?: BooleanLike | QueryFilter<RelationTarget<E[K]>, Raw>;
64
64
  };
65
65
  /**
66
66
  * query conflict paths - subset of field keys used to detect upsert conflicts.
@@ -69,7 +69,7 @@ export type QueryConflictPaths<E> = QuerySelect<E, FieldKey<E>, true>;
69
69
  /**
70
70
  * Options to populate a relation declared as `V`, by its cardinality.
71
71
  */
72
- export type QueryPopulateRelationOptions<V> = IsMany<V> extends true ? RelationQuery<RelationTarget<V>> : QueryUnique<RelationTarget<V>> & {
72
+ export type QueryPopulateRelationOptions<V, Raw = QueryRaw> = IsMany<V> extends true ? RelationQuery<RelationTarget<V>, Raw> : QueryUnique<RelationTarget<V>, Raw> & {
73
73
  $required?: boolean;
74
74
  };
75
75
  /**
@@ -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.
@@ -158,24 +149,24 @@ export type QueryPager = {
158
149
  /**
159
150
  * Which rows a statement addresses.
160
151
  */
161
- export type QueryFilter<E> = {
152
+ export type QueryFilter<E, Raw = QueryRaw> = {
162
153
  /**
163
154
  * filtering options.
164
155
  */
165
- $where?: QueryWhere<E>;
156
+ $where?: QueryWhere<E, Raw>;
166
157
  };
167
158
  /**
168
159
  * A filter plus the page `count` takes. No `$sort`: ordering picks *which* rows a page holds, never
169
160
  * how many, so a count that accepted one would promise an influence it cannot have.
170
161
  */
171
- export type QueryPage<E> = QueryFilter<E> & QueryPager;
162
+ export type QueryPage<E, Raw = QueryRaw> = QueryFilter<E, Raw> & QueryPager;
172
163
  /**
173
164
  * A filter plus the ordering and page `updateMany`/`deleteMany` take. Both settle the
174
165
  * rows they address with a SELECT first, so the page is portable rather than MySQL-only, and a
175
166
  * vector `$sort` is as valid here as on a read: it ranks the settle query's rows, which has the
176
167
  * projection list to hold the distance. `$lock` stays off these, declared on {@link Query} instead.
177
168
  */
178
- export type QuerySearch<E> = QueryPage<E> & {
169
+ export type QuerySearch<E, Raw = QueryRaw> = QueryPage<E, Raw> & {
179
170
  /**
180
171
  * sorting options.
181
172
  */
@@ -184,21 +175,21 @@ export type QuerySearch<E> = QueryPage<E> & {
184
175
  /**
185
176
  * query options.
186
177
  */
187
- export type Query<E> = {
178
+ export type Query<E, Raw = QueryRaw> = {
188
179
  /**
189
180
  * field selection - `{ name: true }` whitelists fields, or raw SQL projections
190
181
  * (``[raw`LOG10(points)`.as('score')]``, SQL dialects only - MongoDB rejects the raw-array form).
191
182
  * Mutually exclusive with `$exclude`.
192
183
  */
193
- $select?: QuerySelectValue<E>;
184
+ $select?: QuerySelectValue<E, Raw>;
194
185
  /**
195
186
  * relation population options.
196
187
  */
197
- $populate?: QueryPopulate<E>;
188
+ $populate?: QueryPopulate<E, Raw>;
198
189
  /**
199
190
  * how many rows each named relation holds, under `_count` on every row. See {@link QueryCount}.
200
191
  */
201
- $count?: QueryCount<E>;
192
+ $count?: QueryCount<E, Raw>;
202
193
  /**
203
194
  * field exclusion - `{ name: true }` blacklists fields. Mutually exclusive with positive `$select`.
204
195
  * Keys a relation is assembled from (a joined row's primary key, a to-many's foreign key) are kept
@@ -227,7 +218,7 @@ export type Query<E> = {
227
218
  /**
228
219
  * filtering options.
229
220
  */
230
- $where?: QueryWhere<E>;
221
+ $where?: QueryWhere<E, Raw>;
231
222
  /**
232
223
  * Index from where start the search
233
224
  */
@@ -237,6 +228,11 @@ export type Query<E> = {
237
228
  */
238
229
  $limit?: number;
239
230
  };
231
+ /**
232
+ * A {@link Query} as it travels as JSON, which a `raw` SQL fragment cannot: what the browser client takes,
233
+ * and what an RPC contract (tRPC, oRPC, TanStack Start) declares as its input.
234
+ */
235
+ export type WireQuery<E> = Query<E, never>;
240
236
  /**
241
237
  * `Query`'s clauses grouped by the shape of their value, for the wire parser and the relation query
242
238
  * check alike; `satisfies` keeps them in step with `Query`.
@@ -262,36 +258,36 @@ type RelationClause = (typeof QUERY_OBJECT_CLAUSES | typeof QUERY_NUMBER_CLAUSES
262
258
  * A populated relation's own query: the clause groups its runtime check accepts, so the two cannot
263
259
  * drift, and a clause added to {@link Query} stays off it until it joins one of them.
264
260
  */
265
- export type RelationQuery<E = object> = Pick<Query<E>, RelationClause> & {
261
+ export type RelationQuery<E = object, Raw = QueryRaw> = Pick<Query<E, Raw>, RelationClause> & {
266
262
  $required?: boolean;
267
263
  };
268
264
  /**
269
265
  * options to get a single record.
270
266
  */
271
- export type QueryOne<E> = Except<Query<E>, '$limit'>;
267
+ export type QueryOne<E, Raw = QueryRaw> = Except<Query<E, Raw>, '$limit'>;
272
268
  /**
273
269
  * options to get an unique record.
274
270
  */
275
- export type QueryUnique<E> = Pick<QueryOne<E>, '$select' | '$exclude' | '$populate' | '$where'>;
271
+ export type QueryUnique<E, Raw = QueryRaw> = Pick<QueryOne<E, Raw>, '$select' | '$exclude' | '$populate' | '$where'>;
276
272
  /**
277
273
  * The clauses that shape a row, captured as key sets rather than maps: a naked type parameter skips
278
274
  * excess-property checks, while a key set fails its own constraint on a typo.
279
275
  * @internal
280
276
  */
281
- type QueryProjection<E, S extends FieldKey<E>, V, X extends FieldKey<E>, P extends RelationKey<E>, C extends RelationKey<E>> = {
282
- $select?: QuerySelect<E, S, V> | readonly QueryRaw[];
277
+ type QueryProjection<E, S extends FieldKey<E>, V, X extends FieldKey<E>, P extends RelationKey<E>, C extends RelationKey<E>, Raw = QueryRaw> = {
278
+ $select?: QuerySelect<E, S, V> | readonly Raw[];
283
279
  $exclude?: QuerySelect<E, X, V>;
284
- $populate?: QueryPopulate<E, P>;
285
- $count?: QueryCount<E, C & ToManyRelationKey<E>>;
280
+ $populate?: QueryPopulate<E, Raw, P>;
281
+ $count?: QueryCount<E, Raw, C & ToManyRelationKey<E>>;
286
282
  };
287
283
  /**
288
284
  * A {@link Query} whose projection is captured, so {@link QueryFindResult} can shape the row.
289
285
  */
290
- export type QueryProjected<E, S extends FieldKey<E>, V, X extends FieldKey<E>, P extends RelationKey<E>, C extends RelationKey<E> = never> = Query<E> & QueryProjection<E, S, V, X, P, C>;
286
+ export type QueryProjected<E, S extends FieldKey<E>, V, X extends FieldKey<E>, P extends RelationKey<E>, C extends RelationKey<E> = never, Raw = QueryRaw> = Query<E, Raw> & QueryProjection<E, S, V, X, P, C, Raw>;
291
287
  /**
292
288
  * A {@link QueryOne} whose projection is captured, so {@link QueryFindResult} can shape the row.
293
289
  */
294
- export type QueryOneProjected<E, S extends FieldKey<E>, V, X extends FieldKey<E>, P extends RelationKey<E>, C extends RelationKey<E> = never> = QueryOne<E> & QueryProjection<E, S, V, X, P, C>;
290
+ export type QueryOneProjected<E, S extends FieldKey<E>, V, X extends FieldKey<E>, P extends RelationKey<E>, C extends RelationKey<E> = never, Raw = QueryRaw> = QueryOne<E, Raw> & QueryProjection<E, S, V, X, P, C, Raw>;
295
291
  /**
296
292
  * The keys a query comes back with, as the runtime projects them: a positive `$select`'s, or every
297
293
  * field minus what `$select` or `$exclude` subtracts, plus the populated relations.
@@ -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
+ }