uql-orm 0.68.1 → 0.70.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 (56) hide show
  1. package/dist/browser/querier/httpQuerier.d.ts +7 -7
  2. package/dist/browser/type/clientQuerier.d.ts +5 -5
  3. package/dist/browser/uql-browser.min.js +2 -2
  4. package/dist/browser/uql-browser.min.js.map +6 -6
  5. package/dist/cockroachdb/cockroachDialect.js +2 -2
  6. package/dist/dialect/abstractDialect.d.ts +8 -2
  7. package/dist/dialect/abstractDialect.js +17 -1
  8. package/dist/dialect/abstractSqlDialect.d.ts +42 -4
  9. package/dist/dialect/abstractSqlDialect.js +164 -48
  10. package/dist/dialect/aliases.d.ts +15 -4
  11. package/dist/dialect/aliases.js +15 -4
  12. package/dist/dialect/mysqlLikeSqlDialect.js +3 -2
  13. package/dist/dialect/pgLikeSqlDialect.js +1 -0
  14. package/dist/dialect/queryJoins.d.ts +8 -1
  15. package/dist/dialect/queryJoins.js +33 -10
  16. package/dist/entity/decorator/members.d.ts +44 -2
  17. package/dist/entity/decorator/members.js +0 -5
  18. package/dist/entity/metadata/definition.d.ts +8 -3
  19. package/dist/entity/metadata/definition.js +10 -3
  20. package/dist/migrate/codegen/entityCodeGenerator.js +4 -2
  21. package/dist/migrate/introspection/postgresIntrospector.d.ts +6 -0
  22. package/dist/migrate/introspection/postgresIntrospector.js +7 -1
  23. package/dist/migrate/migrator.d.ts +2 -1
  24. package/dist/migrate/migrator.js +5 -3
  25. package/dist/mongo/mongoDialect.d.ts +49 -19
  26. package/dist/mongo/mongoDialect.js +238 -81
  27. package/dist/mongo/mongodbQuerier.d.ts +2 -4
  28. package/dist/mongo/mongodbQuerier.js +16 -14
  29. package/dist/mssql/mssqlDialect.js +3 -2
  30. package/dist/postgres/postgresDialect.js +2 -2
  31. package/dist/querier/abstractQuerier.d.ts +16 -11
  32. package/dist/querier/abstractQuerier.js +28 -8
  33. package/dist/querier/abstractQuerierPool.d.ts +9 -9
  34. package/dist/querier/abstractSqlQuerier.d.ts +1 -1
  35. package/dist/querier/abstractSqlQuerier.js +3 -3
  36. package/dist/sqlite/sqliteDialect.js +1 -0
  37. package/dist/turso/tursoDialect.d.ts +1 -1
  38. package/dist/turso/tursoDialect.js +6 -2
  39. package/dist/type/dialect.d.ts +35 -2
  40. package/dist/type/entity.d.ts +120 -4
  41. package/dist/type/migration.d.ts +7 -0
  42. package/dist/type/query.d.ts +7 -10
  43. package/dist/type/queryAggregate.d.ts +77 -42
  44. package/dist/type/queryAggregate.js +4 -21
  45. package/dist/type/queryRaw.d.ts +19 -1
  46. package/dist/type/queryRaw.js +18 -0
  47. package/dist/type/universalQuerier.d.ts +9 -9
  48. package/dist/util/dialect.util.d.ts +6 -2
  49. package/dist/util/dialect.util.js +21 -7
  50. package/dist/util/field.util.d.ts +15 -1
  51. package/dist/util/field.util.js +18 -1
  52. package/dist/util/object.util.d.ts +1 -4
  53. package/dist/util/object.util.js +0 -3
  54. package/dist/util/raw.d.ts +2 -2
  55. package/dist/util/raw.js +29 -2
  56. package/package.json +1 -1
@@ -1,7 +1,8 @@
1
- import type { FieldKey } from './entity.js';
1
+ import type { FieldKey, RelationKey, RelationTarget } from './entity.js';
2
2
  import type { QueryPager, QuerySelect, QuerySortDirection } from './query.js';
3
+ import type { QueryRaw } from './queryRaw.js';
3
4
  import type { QueryWhere, QueryWhereFieldValue } from './queryWhere.js';
4
- import type { RejectKeys } from './utility.js';
5
+ import type { IsMany, RejectKeys } from './utility.js';
5
6
  /** The columns `$group` names by a literal `true`, so an uninferred `$group`, its own constraint, names none. */
6
7
  type GroupedKeys<G> = {
7
8
  [K in keyof G]: G[K] extends true ? K : never;
@@ -23,22 +24,14 @@ export type QueryAggregateOp = (typeof QUERY_AGGREGATE_OPS)[number];
23
24
  */
24
25
  export declare function isQueryAggregateOp(op: string): op is QueryAggregateOp;
25
26
  /**
26
- * DISTINCT-qualified aggregate ops, each mapped to the base op it applies to a field's distinct
27
- * values: `$countDistinct` -> `COUNT(DISTINCT col)`, and likewise `$sumDistinct`/`$avgDistinct`. Flat
28
- * (not a nested `{ $distinct }` argument) so the op is self-documenting and greppable. `$min`/`$max`
29
- * are omitted: DISTINCT is a no-op for them.
27
+ * `$countDistinct` -> `COUNT(DISTINCT col)`. Flat (not a nested `{ $distinct }` argument) so the op is
28
+ * self-documenting and greppable. The other ops have no DISTINCT variant: `$sum`/`$avg` over deduplicated
29
+ * values is rarely what totalling or averaging means, and DISTINCT is a no-op for `$min`/`$max`.
30
30
  */
31
- declare const QUERY_AGGREGATE_DISTINCT_OP_BASE: {
32
- readonly $countDistinct: '$count';
33
- readonly $sumDistinct: '$sum';
34
- readonly $avgDistinct: '$avg';
35
- };
36
- /** DISTINCT-qualified aggregate operators (the keys of {@link QUERY_AGGREGATE_DISTINCT_OP_BASE}). */
37
- export type QueryAggregateDistinctOp = keyof typeof QUERY_AGGREGATE_DISTINCT_OP_BASE;
31
+ export type QueryAggregateDistinctOp = '$countDistinct';
38
32
  /**
39
- * Resolve an aggregate op key into its base op and whether it is DISTINCT-qualified. A flat distinct
40
- * op resolves to its base op with `distinct: true`; a plain op to `distinct: false`. Throws otherwise
41
- * (`$min`/`$max` have no distinct variant).
33
+ * Resolve an aggregate op key into its base op and whether it is DISTINCT-qualified: `$countDistinct`
34
+ * resolves to `$count` with `distinct: true`, a plain op to itself with `distinct: false`. Throws otherwise.
42
35
  */
43
36
  export declare function resolveAggregateOp(key: string): {
44
37
  op: QueryAggregateOp;
@@ -59,9 +52,8 @@ export type QueryFieldRef<E, F extends keyof E = FieldKey<E>> = ExactlyOne<Requi
59
52
  /** The argument of an aggregate function: a field, or `'*'` (only meaningful for `COUNT(*)`). */
60
53
  export type QueryAggregateArg<E> = QueryFieldRef<E> | '*';
61
54
  /**
62
- * Fields `SUM`/`AVG` can total. Restricted to numeric columns because the result is declared
63
- * `number`: totalling a text or date column is either an engine error or a coercion, and neither
64
- * produces the value the signature promises.
55
+ * Fields `SUM`/`AVG` can total. Restricted to numeric columns because totalling a text or date one is
56
+ * either an engine error or a coercion, and neither produces the value the signature promises.
65
57
  */
66
58
  type NumericFieldKey<E> = {
67
59
  readonly [K in FieldKey<E>]: [NonNullable<E[K]>] extends [number | bigint] ? K : never;
@@ -74,8 +66,12 @@ type AggregateOp = QueryAggregateOp | QueryAggregateDistinctOp;
74
66
  * where `Extract` would quietly drop the renamed member and leave the subset wrong but valid.
75
67
  */
76
68
  type OpsOf<K extends AggregateOp> = K;
69
+ /** Ops that add a column up, which keeps the column's own type: a `bigint` column totals to a `bigint`. */
70
+ type SummingOp = OpsOf<'$sum'>;
71
+ /** Ops that mean a column, which the engine floats, so the result is a `number` however wide the column. */
72
+ type AveragingOp = OpsOf<'$avg'>;
77
73
  /** Ops that total a column, so their argument has to be numeric. */
78
- type TotallingOp = OpsOf<'$sum' | '$avg' | '$sumDistinct' | '$avgDistinct'>;
74
+ type TotallingOp = SummingOp | AveragingOp;
79
75
  /**
80
76
  * Every aggregate op mapped to the argument it accepts: `$count` a field or `'*'` (`COUNT(*)`),
81
77
  * the totalling ops a numeric field, `$min`/`$max`/`$countDistinct` any field.
@@ -83,19 +79,60 @@ type TotallingOp = OpsOf<'$sum' | '$avg' | '$sumDistinct' | '$avgDistinct'>;
83
79
  type QueryAggregateArgMap<E> = Record<'$count', QueryAggregateArg<E>> & Record<TotallingOp, QueryFieldRef<E, NumericFieldKey<E>>> & Record<Exclude<AggregateOp, '$count' | TotallingOp>, QueryFieldRef<E>>;
84
80
  /**
85
81
  * An aggregate over one field, exactly one op per entry: `{ $sum: { amount: true } }` is `SUM("amount")`,
86
- * `{ $countDistinct: { id: true } }` is `COUNT(DISTINCT "id")`, and only `$count` takes `'*'`.
82
+ * `{ $countDistinct: { id: true } }` is `COUNT(DISTINCT "id")`, and only `$count` takes `'*'`. Its own
83
+ * `$where` narrows the rows it reads to those matching, over the entity's own fields.
87
84
  */
88
- export type QueryAggregateFn<E> = ExactlyOne<QueryAggregateArgMap<E>>;
89
- /** A single-key `{ [op]: unknown }` shape for each op in `Ops`, matched to infer that op's result. */
90
- type FnWithOp<Ops extends string> = {
85
+ export type QueryAggregateFn<E> = ExactlyOne<QueryAggregateArgMap<E>> & {
86
+ readonly $where?: QueryAggregateWhere<E>;
87
+ };
88
+ /** What an aggregate's own `$where` reads: the entity's fields, since a relation there is a subquery inside it. */
89
+ type QueryAggregateWhere<E> = QueryWhere<E, QueryRaw, FieldKey<E>>;
90
+ /** An aggregate's `$where` naming a key it does not have, refused: a captured `$select` skips the excess-property check. */
91
+ type AggregateWhereKeys<E, Fn> = Fn extends {
92
+ readonly $where: infer W;
93
+ } ? {
94
+ readonly $where: RejectKeys<Exclude<keyof W, keyof QueryAggregateWhere<E>>>;
95
+ } : unknown;
96
+ /** A single-key `{ [op]: Arg }` shape for each op in `Ops`, matched to infer that op's result or its argument. */
97
+ type FnWithOp<Ops extends string, Arg = unknown> = {
91
98
  [K in Ops]: {
92
- readonly [P in K]: unknown;
99
+ readonly [P in K]: Arg;
93
100
  };
94
101
  }[Ops];
95
102
  /** Ops that count rows. Alone among the ops they answer `0`, never NULL, over an empty group. */
96
103
  type CountingOp = OpsOf<'$count' | '$countDistinct'>;
97
- /** The columns to group by, `{ status: true }`, typed against the entity like `$select`. */
98
- export type QueryGroupMap<E> = Readonly<QuerySelect<E, FieldKey<E>, true>>;
104
+ /** Ops that read back as the column they aggregate, rather than widening or floating it. */
105
+ type ColumnTypedOp = SummingOp | OpsOf<'$min' | '$max'>;
106
+ /**
107
+ * A field a group key reads, by the path to it: `{ orderId: true }`, or through a to-one relation,
108
+ * `{ transaction: { orderId: true } }`. A to-many is `never`, since joining one multiplies the rows.
109
+ */
110
+ export type QueryGroupRef<E, K extends keyof E = FieldKey<E> | RelationKey<E>> = ExactlyOne<Required<{
111
+ [P in K]: P extends RelationKey<E> ? IsMany<E[P]> extends true ? never : QueryGroupRef<RelationTarget<E[P]>> : true;
112
+ }>>;
113
+ /** The columns to group by: a field switched on, `{ status: true }`, or an alias for a field a path reads. */
114
+ export type QueryGroupMap<E> = {
115
+ readonly [key: string]: true | QueryGroupRef<E>;
116
+ };
117
+ /**
118
+ * A captured `$group` against its schema: a field key takes `true` and keeps its link, any other key is an
119
+ * alias for a path. A key switched on that is no field is refused by name, which the intersection alone lets by.
120
+ */
121
+ type QueryGroupSchema<E, G> = Readonly<QuerySelect<E, FieldKey<E>, true>> & {
122
+ readonly [K in Exclude<NamedKeys<G>, FieldKey<E>>]: QueryGroupRef<E>;
123
+ } & RejectKeys<Exclude<GroupedKeys<G>, FieldKey<E>>>;
124
+ /**
125
+ * The value a path reads: the field's own type, or through a relation, `null` where the row points nowhere.
126
+ * `any` answers `unknown`: TypeScript checks a deferred type by instantiating it with `any`, which would
127
+ * walk every relation of the entity on every aggregate call, quadrupling what one costs to check.
128
+ */
129
+ type GroupRefValue<E, Ref> = 0 extends 1 & Ref ? unknown : {
130
+ [K in keyof Ref & keyof E]: Ref[K] extends true ? E[K] : NonNullable<GroupRefLeaf<RelationTarget<E[K]>, Ref[K]>> | null;
131
+ }[keyof Ref & keyof E];
132
+ /** The field at the end of a path, whose type {@link GroupRefValue} widens with `null`; `any` as it does. */
133
+ type GroupRefLeaf<E, Ref> = 0 extends 1 & Ref ? unknown : {
134
+ [K in keyof Ref & keyof E]: Ref[K] extends true ? E[K] : GroupRefLeaf<RelationTarget<E[K]>, Ref[K]>;
135
+ }[keyof Ref & keyof E];
99
136
  /** Computed columns by the alias each is read back under: `{ count: { $count: '*' }, avgAge: { $avg: { age: true } } }`. */
100
137
  export type QueryAggMap<E> = {
101
138
  readonly [alias: string]: QueryAggregateFn<E>;
@@ -103,14 +140,11 @@ export type QueryAggMap<E> = {
103
140
  /** The entity type of an aggregated field reference `F`, or `unknown` if it is not a known field. */
104
141
  type FieldValueType<E, F> = F extends keyof E ? E[F] : unknown;
105
142
  /**
106
- * A computed column's type: a count is a `number`; every other aggregate is `null` over no rows, a
107
- * total a `number` (exact to 2^53, `raw` beyond) and `$min`/`$max` the field's own type.
143
+ * A computed column's type: a count is a `number`; every other aggregate is `null` over no rows, a mean
144
+ * a `number` whatever it read, and a total, a `$min` or a `$max` the column's own type - so a `bigint`
145
+ * column totals to a `bigint`, which is what the driver decodes rather than rounding through a float.
108
146
  */
109
- type QueryAggregateFnResult<E, Fn> = Fn extends FnWithOp<CountingOp> ? number : Fn extends FnWithOp<TotallingOp> ? number | null : Fn extends {
110
- readonly $min: infer F;
111
- } | {
112
- readonly $max: infer F;
113
- } ? FieldValueType<E, keyof F> | null : unknown;
147
+ type QueryAggregateFnResult<E, Fn> = Fn extends FnWithOp<CountingOp> ? number : Fn extends FnWithOp<AveragingOp> ? number | null : Fn extends FnWithOp<ColumnTypedOp, infer F> ? FieldValueType<E, keyof F> | null : unknown;
114
148
  /**
115
149
  * Flattens an intersection into a single object literal for readable editor hovers.
116
150
  * @internal
@@ -118,8 +152,10 @@ type QueryAggregateFnResult<E, Fn> = Fn extends FnWithOp<CountingOp> ? number :
118
152
  type Simplify<T> = {
119
153
  [K in keyof T]: T[K];
120
154
  } & {};
121
- /** An aggregate's row: each grouped column with its entity type, each computed one with its aggregate's. */
155
+ /** An aggregate's row: each grouped column with its entity type, each alias with its path's, each computed one with its aggregate's. */
122
156
  export type QueryAggregateResult<E, G, A> = Simplify<Pick<E, GroupedKeys<G> & FieldKey<E>> & {
157
+ -readonly [K in Exclude<NamedKeys<G>, FieldKey<E>>]: GroupRefValue<E, G[K]>;
158
+ } & {
123
159
  -readonly [K in keyof A]: QueryAggregateFnResult<E, A[K]>;
124
160
  }>;
125
161
  /** A `HAVING` as the dialects read it, erased; {@link QueryAggregate.$having} is where it is typed. `{ count: { $gt: 5 } }` */
@@ -136,19 +172,18 @@ export type QueryAggregate<E, G extends QueryGroupMap<E> = QueryGroupMap<E>, A e
136
172
  */
137
173
  readonly $where?: QueryWhere<E>;
138
174
  /**
139
- * Columns to group by - `{ status: true }`, typed against the entity like `$select`. A computed
140
- * aggregate wrongly placed here (it belongs in `$select`) is rejected via {@link RejectKeys}, since
141
- * `$group` is captured as a generic and a bare generic skips excess-property checking. The captured
142
- * map meets its schema, {@link QueryGroupMap}, so each key keeps its link to the entity property.
175
+ * Columns to group by: `{ status: true }`, or an alias for a to-one relation's field by the path to
176
+ * it, `{ orderId: { transaction: { orderId: true } } }`. The captured map meets its schema, so a field
177
+ * key keeps its link to the entity property, and a typo matches neither form.
143
178
  */
144
- readonly $group?: G & QueryGroupMap<E> & RejectKeys<Exclude<keyof G, FieldKey<E>>>;
179
+ readonly $group?: G & QueryGroupSchema<E, G>;
145
180
  /**
146
181
  * The computed columns by alias, the captured map meeting its schema so field keys stay linked. An alias
147
182
  * repeating a `$group` column is refused, since both would come back under one name.
148
183
  */
149
184
  readonly $select?: A & {
150
- readonly [K in keyof A]: QueryAggregateFn<E>;
151
- } & RejectKeys<NamedKeys<A> & GroupedKeys<G>>;
185
+ readonly [K in keyof A]: QueryAggregateFn<E> & AggregateWhereKeys<E, A[K]>;
186
+ } & RejectKeys<NamedKeys<A> & NamedKeys<G>>;
152
187
  /** Filtering after grouping, by a result column, each value typed as that column is. */
153
188
  readonly $having?: {
154
189
  readonly [K in keyof QueryAggregateResult<E, G, A>]?: QueryWhereFieldValue<QueryAggregateResult<E, G, A>[K]>;
@@ -7,29 +7,12 @@ export function isQueryAggregateOp(op) {
7
7
  return QUERY_AGGREGATE_OPS.includes(op);
8
8
  }
9
9
  /**
10
- * DISTINCT-qualified aggregate ops, each mapped to the base op it applies to a field's distinct
11
- * values: `$countDistinct` -> `COUNT(DISTINCT col)`, and likewise `$sumDistinct`/`$avgDistinct`. Flat
12
- * (not a nested `{ $distinct }` argument) so the op is self-documenting and greppable. `$min`/`$max`
13
- * are omitted: DISTINCT is a no-op for them.
14
- */
15
- const QUERY_AGGREGATE_DISTINCT_OP_BASE = {
16
- $countDistinct: '$count',
17
- $sumDistinct: '$sum',
18
- $avgDistinct: '$avg',
19
- };
20
- /** Whether `key` is a DISTINCT-qualified aggregate operator (narrows for a cast-free base lookup). */
21
- function isQueryAggregateDistinctOp(key) {
22
- // `Object.hasOwn`, not `key in`: the latter matches inherited members like `toString`.
23
- return Object.hasOwn(QUERY_AGGREGATE_DISTINCT_OP_BASE, key);
24
- }
25
- /**
26
- * Resolve an aggregate op key into its base op and whether it is DISTINCT-qualified. A flat distinct
27
- * op resolves to its base op with `distinct: true`; a plain op to `distinct: false`. Throws otherwise
28
- * (`$min`/`$max` have no distinct variant).
10
+ * Resolve an aggregate op key into its base op and whether it is DISTINCT-qualified: `$countDistinct`
11
+ * resolves to `$count` with `distinct: true`, a plain op to itself with `distinct: false`. Throws otherwise.
29
12
  */
30
13
  export function resolveAggregateOp(key) {
31
- if (isQueryAggregateDistinctOp(key)) {
32
- return { op: QUERY_AGGREGATE_DISTINCT_OP_BASE[key], distinct: true };
14
+ if (key === '$countDistinct') {
15
+ return { op: '$count', distinct: true };
33
16
  }
34
17
  if (isQueryAggregateOp(key)) {
35
18
  return { op: key, distinct: false };
@@ -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
+ }
@@ -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
+ }
@@ -1,4 +1,4 @@
1
- import type { EntityData, EntityId, FieldKey, RelationKey, UpdatePayload, WrittenId } from './entity.js';
1
+ import type { EntityId, EntityWrite, FieldKey, RelationKey, UpdateWrite, WrittenId } from './entity.js';
2
2
  import type { QueryConflictPaths, QueryFilter, QueryFindResult, QueryOneProjected, QueryOptions, QueryPage, QueryProjected, QuerySearch, QueryUpsertOneResult, QueryUpsertManyResult } from './query.js';
3
3
  import type { QueryAggMap, QueryAggregate, QueryAggregateResult, QueryGroupMap } from './queryAggregate.js';
4
4
  import type { Type } from './utility.js';
@@ -31,9 +31,9 @@ export interface SharedQuerier<W extends QuerierTransport, O, DO = O> {
31
31
  /** Whether any record matches: a count capped at one row, so the engine stops at the first match. */
32
32
  exists<E extends object>(entity: Type<E>, q?: QueryFilter<E, QuerierRaw<W>>, opts?: O): QuerierResult<W, boolean>;
33
33
  /** Update the record with the given primary key; resolves to the number of affected rows. */
34
- updateOneById<E extends object>(entity: Type<E>, id: EntityId<E>, payload: UpdatePayload<E, QuerierRaw<W>>, opts?: O): QuerierResult<W, number>;
34
+ updateOneById<E extends object>(entity: Type<E>, id: EntityId<E>, payload: UpdateWrite<E, QuerierRaw<W>>, opts?: O): QuerierResult<W, number>;
35
35
  /** Update the records matching the query; resolves to the number of affected rows. */
36
- updateMany<E extends object>(entity: Type<E>, q: QuerySearch<E, QuerierRaw<W>>, payload: UpdatePayload<E, QuerierRaw<W>>, opts?: O): QuerierResult<W, number>;
36
+ updateMany<E extends object>(entity: Type<E>, q: QuerySearch<E, QuerierRaw<W>>, payload: UpdateWrite<E, QuerierRaw<W>>, opts?: O): QuerierResult<W, number>;
37
37
  /**
38
38
  * delete or SoftDelete a record.
39
39
  * @param entity the entity to persist on
@@ -59,31 +59,31 @@ export interface UniversalQuerier extends SharedQuerier<'server', QueryOptions>
59
59
  */
60
60
  findManyStream<E extends object, const S extends FieldKey<E> = never, const V = true, const X extends FieldKey<E> = never, const P extends RelationKey<E> = never, const C extends RelationKey<E> = never>(entity: Type<E>, q: QueryProjected<E, S, V, X, P, C>, opts?: QueryOptions): AsyncIterable<QueryFindResult<E, S, V, X, P, C>>;
61
61
  /** Insert a record and resolve to its id. See {@link UniversalQuerier.insertMany}. */
62
- insertOne<E extends object>(entity: Type<E>, payload: EntityData<E>): Promise<WrittenId<E> | undefined>;
62
+ insertOne<E extends object>(entity: Type<E>, payload: EntityWrite<E>): Promise<WrittenId<E> | undefined>;
63
63
  /**
64
64
  * Insert records in as few statements as the bind limit allows, resolving to their ids in payload order.
65
65
  * Ids are exact everywhere but MySQL, which infers them from its header and reports `undefined` rather
66
66
  * than a guess where it cannot: a batch naming some keys, or a key that is not `AUTO_INCREMENT`.
67
67
  */
68
- insertMany<E extends object>(entity: Type<E>, payload: EntityData<E>[]): Promise<(WrittenId<E> | undefined)[]>;
68
+ insertMany<E extends object>(entity: Type<E>, payload: EntityWrite<E>[]): Promise<(WrittenId<E> | undefined)[]>;
69
69
  /** Insert or update a record by its conflict paths; resolves to its id and whether it was created. */
70
- upsertOne<E extends object>(entity: Type<E>, conflictPaths: QueryConflictPaths<E>, payload: EntityData<E>): Promise<QueryUpsertOneResult<E>>;
70
+ upsertOne<E extends object>(entity: Type<E>, conflictPaths: QueryConflictPaths<E>, payload: EntityWrite<E>): Promise<QueryUpsertOneResult<E>>;
71
71
  /** Insert or update records by their conflict paths; resolves to their ids in payload order. */
72
- upsertMany<E extends object>(entity: Type<E>, conflictPaths: QueryConflictPaths<E>, payload: EntityData<E>[]): Promise<QueryUpsertManyResult<E>>;
72
+ upsertMany<E extends object>(entity: Type<E>, conflictPaths: QueryConflictPaths<E>, payload: EntityWrite<E>[]): Promise<QueryUpsertManyResult<E>>;
73
73
  /**
74
74
  * insert or update a record.
75
75
  * @param entity the entity to persist on
76
76
  * @param payload the data to be persisted
77
77
  * @return the ID
78
78
  */
79
- saveOne<E extends object>(entity: Type<E>, payload: EntityData<E>): Promise<WrittenId<E> | undefined>;
79
+ saveOne<E extends object>(entity: Type<E>, payload: EntityWrite<E>): Promise<WrittenId<E> | undefined>;
80
80
  /**
81
81
  * Insert or update records.
82
82
  * @param entity the entity to persist on
83
83
  * @param payload the data to be persisted
84
84
  * @return the IDs
85
85
  */
86
- saveMany<E extends object>(entity: Type<E>, payload: EntityData<E>[]): Promise<(WrittenId<E> | undefined)[]>;
86
+ saveMany<E extends object>(entity: Type<E>, payload: EntityWrite<E>[]): Promise<(WrittenId<E> | undefined)[]>;
87
87
  /**
88
88
  * Restore soft-deleted records (sets the soft-delete field back to `null`). Throws if the
89
89
  * entity has no soft-delete field.
@@ -104,9 +104,11 @@ export declare function applyFilters<E>(meta: EntityMeta<E>, whereMap: QueryWher
104
104
  /**
105
105
  * Parsed entry from a `$group` map - either a raw group key or an aggregate function call.
106
106
  */
107
- export type ParsedGroupEntry = {
107
+ export type ParsedGroupEntry<E = object> = {
108
108
  readonly kind: 'key';
109
109
  readonly alias: string;
110
+ /** The field it reads, behind the to-one relations leading to it: `['transaction', 'orderId']`. */
111
+ readonly path: readonly string[];
110
112
  } | {
111
113
  readonly kind: 'fn';
112
114
  readonly alias: string;
@@ -114,6 +116,8 @@ export type ParsedGroupEntry = {
114
116
  readonly fieldRef: string;
115
117
  /** `true` for a flat distinct op (`$countDistinct`, ...) -> `COUNT(DISTINCT field)`. */
116
118
  readonly distinct: boolean;
119
+ /** The rows it reads, where not all of the statement's. */
120
+ readonly where?: QueryWhere<E>;
117
121
  };
118
122
  /**
119
123
  * The `$size` of a relation condition, `{ comments: { $size: { $gte: 2 } } }`, or `undefined` where it
@@ -130,7 +134,7 @@ export declare function parseSortByCount(val: unknown): unknown;
130
134
  * Parse the `$group` (grouped columns) and `$select` (computed aggregates) maps into structured
131
135
  * entries consumable by any dialect. Grouped columns come first, then computed columns.
132
136
  */
133
- export declare function parseGroupMap<E>(group?: QueryGroupMap<E>, select?: QueryAggMap<E>): ParsedGroupEntry[];
137
+ export declare function parseGroupMap<E>(group?: QueryGroupMap<E>, select?: QueryAggMap<E>): ParsedGroupEntry<E>[];
134
138
  /**
135
139
  * Whether `value` is a map of comparison operators rather than a value to compare against. Only a
136
140
  * plain object qualifies: `Date`, `QueryRaw`, `Uint8Array` and arrays are all `typeof 'object'`, and
@@ -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, getFieldKeys, getKeys, hasKeys, isOperatorObject, isScalarId, isRecord, isWhereMap, someKey, } from './object.util.js';
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) => {
@@ -344,22 +344,36 @@ export function parseGroupMap(group, select) {
344
344
  const entries = [];
345
345
  const groupMap = group ?? {};
346
346
  for (const alias of getKeys(groupMap)) {
347
- if (groupMap[alias]) {
348
- entries.push({ kind: 'key', alias });
347
+ const ref = groupMap[alias];
348
+ if (ref) {
349
+ entries.push({ kind: 'key', alias, path: ref === true ? [alias] : groupRefPath(alias, ref) });
349
350
  }
350
351
  }
351
352
  if (!select) {
352
353
  return entries;
353
354
  }
354
355
  for (const alias of getKeys(select)) {
355
- const fnEntry = select[alias];
356
- const key = getKeys(fnEntry)[0];
356
+ const { $where: where } = select[alias];
357
+ const call = select[alias];
358
+ const key = getKeys(call).find((name) => name !== '$where');
359
+ if (key === undefined) {
360
+ throw new TypeError(`aggregate '${alias}' names no op, only a $where`);
361
+ }
357
362
  // Flat DISTINCT ops (`$countDistinct`, ...) normalize to their base op + a `distinct` flag.
358
363
  const { op, distinct } = resolveAggregateOp(key);
359
- entries.push({ kind: 'fn', alias, op, fieldRef: aggregateFieldRef(alias, fnEntry[key]), distinct });
364
+ const fieldRef = aggregateFieldRef(alias, call[key]);
365
+ entries.push({ kind: 'fn', alias, op, fieldRef, distinct, ...(where && hasKeys(where) ? { where } : {}) });
360
366
  }
361
367
  return entries;
362
368
  }
369
+ /** The path a group key's `{ transaction: { orderId: true } }` names, one key at each level. */
370
+ function groupRefPath(alias, ref) {
371
+ const [key, ...rest] = isRecord(ref) ? getKeys(ref) : [];
372
+ if (!isRecord(ref) || key === undefined || rest.length) {
373
+ throw new TypeError(`$group '${alias}' names one field by the path to it: got ${JSON.stringify(ref)}`);
374
+ }
375
+ return ref[key] === true ? [key] : [key, ...groupRefPath(alias, ref[key])];
376
+ }
363
377
  /** The column an aggregate reads: `'*'`, or the one field its `{ field: true }` names. */
364
378
  function aggregateFieldRef(alias, arg) {
365
379
  const [field, ...rest] = arg === '*' ? [arg] : namedKeys(arg);
@@ -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>[];
@@ -1,4 +1,4 @@
1
- import { COLUMN_TYPES, } from '../type/index.js';
1
+ import { COLUMN_TYPES, RelationAggregate, } from '../type/index.js';
2
2
  import { getKeys } from './object.util.js';
3
3
  // Constructors and type strings in one map: a logical type is either, and every caller asks the same
4
4
  // question of both.
@@ -41,6 +41,15 @@ export function isIntegerColumn(field) {
41
41
  export function isInlinedExpression(field) {
42
42
  return field.computed !== undefined && field.stored !== true;
43
43
  }
44
+ /**
45
+ * The relation aggregate a field computes, where it computes one rather than writing SQL: what it
46
+ * reads, off which relation, narrowed and capped how. Every engine renders it from this - a correlated
47
+ * subquery on SQL, a lookup on MongoDB - so both read the same declaration rather than parsing SQL.
48
+ */
49
+ export function aggregateOf(field) {
50
+ const computed = field?.computed;
51
+ return computed instanceof RelationAggregate ? computed.spec : undefined;
52
+ }
44
53
  /**
45
54
  * Whether the database supplies this field's value, so no insert or update may write it: a stored
46
55
  * computed column *is* a real column, read like one, but writing to it is an error on every engine.
@@ -62,3 +71,11 @@ export function isAutoIncrement(field, isPrimaryKey) {
62
71
  return field.autoIncrement;
63
72
  return isPrimaryKey && columnFamily(field.type) === 'numeric' && !field.onInsert && !field.references;
64
73
  }
74
+ /**
75
+ * The fields a read answers with where it names none. A relation aggregate is left out unless it asks
76
+ * for `eager: true`: it reads the related rows, which is what a relation does, and a relation is loaded
77
+ * only when a query asks for it. Naming one in `$select` reads it, whatever the default.
78
+ */
79
+ export function getFieldKeys(fields) {
80
+ return getKeys(fields).filter((field) => fields[field].eager ?? !aggregateOf(fields[field]));
81
+ }
@@ -1,4 +1,4 @@
1
- import type { EntityMeta, FieldKey, FieldOptions } from '../type/index.js';
1
+ import type { EntityMeta } from '../type/index.js';
2
2
  export declare function throwPendingTransaction(): never;
3
3
  export declare function throwNoPendingTransaction(): never;
4
4
  export declare function clone<T>(value: T): T;
@@ -24,9 +24,6 @@ export declare function definedEntries<K extends string, V>(record: Partial<Reco
24
24
  * out reached for three different fallbacks, `?? ''` among them, and named nothing at all.
25
25
  */
26
26
  export declare function entityName<E>(meta: EntityMeta<E>): string;
27
- export declare function getFieldKeys<E>(fields: {
28
- [K in FieldKey<E>]?: FieldOptions;
29
- }): FieldKey<E>[];
30
27
  /**
31
28
  * Whether `value` addresses a row by itself rather than naming columns: every primitive, and the
32
29
  * object ids a driver deals in (`ObjectId`, `Date`, bytes). Only a plain object names columns, which
@@ -59,9 +59,6 @@ export function definedEntries(record) {
59
59
  export function entityName(meta) {
60
60
  return meta.name ?? meta.entity.name;
61
61
  }
62
- export function getFieldKeys(fields) {
63
- return getKeys(fields).filter((field) => fields[field].eager ?? true);
64
- }
65
62
  /**
66
63
  * Whether `value` addresses a row by itself rather than naming columns: every primitive, and the
67
64
  * object ids a driver deals in (`ObjectId`, `Date`, bytes). Only a plain object names columns, which
@@ -1,4 +1,4 @@
1
- import { type EntitySql, type EntityWhere, type EntityWhereMeta, QueryRaw, type QueryRawFn, type RefMap, type Type } from '../type/index.js';
1
+ import { type EntitySql, type EntityWhere, type EntityWhereMeta, QueryRaw, type QueryRawFn, type ComputedRefs, type RefMap, type Type } from '../type/index.js';
2
2
  /**
3
3
  * Raw SQL, where an interpolated value binds, a `refs` field renders its column, and a `raw` renders
4
4
  * in place: `raw`GREATEST(0, ${user.credits} - ${amount})``. A callback writes whatever it writes, so
@@ -16,7 +16,7 @@ export declare function refs<E>(entity: Type<E>): RefMap<E>;
16
16
  * The refs a definition's callbacks read: an index's, a check's, a computed field's. A member decorator
17
17
  * sees no class, so these name no entity and resolve against the one rendering them.
18
18
  */
19
- export declare function memberRefs<E>(): RefMap<E>;
19
+ export declare function memberRefs<E>(): ComputedRefs<E>;
20
20
  /** SQL a definition writes, a callback's refs read off {@link memberRefs}. */
21
21
  export declare function entitySql<E>(sql: EntitySql<E>): QueryRaw;
22
22
  /** A definition's predicate, its callback resolved the way {@link entitySql} resolves one. */
package/dist/util/raw.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { getMeta } from '../entity/metadata/definition.js';
2
- import { ColumnRef, QueryRaw, } from '../type/index.js';
2
+ import { ColumnRef, QueryRaw, RelationAggregate, } from '../type/index.js';
3
3
  import { isInlinedExpression } from './field.util.js';
4
4
  export function raw(value, ...rest) {
5
5
  if (!isTemplateStrings(value)) {
@@ -27,7 +27,7 @@ export function raw(value, ...rest) {
27
27
  export function refs(entity) {
28
28
  return new Proxy({}, { get: (_, key) => columnRef(entity, String(key)) });
29
29
  }
30
- const MEMBER_REFS = new Proxy({}, { get: (_, key) => columnRef(undefined, String(key)) });
30
+ const MEMBER_REFS = new Proxy({}, { get: (_, key) => memberRef(String(key)) });
31
31
  /**
32
32
  * The refs a definition's callbacks read: an index's, a check's, a computed field's. A member decorator
33
33
  * sees no class, so these name no entity and resolve against the one rendering them.
@@ -43,6 +43,33 @@ export function entitySql(sql) {
43
43
  export function entityWhere(where) {
44
44
  return typeof where === 'function' ? where(memberRefs()) : where;
45
45
  }
46
+ /**
47
+ * One member as a definition reads it: a {@link ColumnRef} where it names a field, and the same object
48
+ * answering `count`, `sum`, `min`, `max` and `avg` where it names a to-many. One runtime object, since
49
+ * a member decorator sees no class and so cannot know which the key is; the types keep them apart.
50
+ */
51
+ function memberRef(relation) {
52
+ const over = (op) => (pick, q) => relationAggregate({ relation, op, field: pick(memberRefs()).key, ...(q && { query: q }) });
53
+ return Object.assign(columnRef(undefined, relation), {
54
+ count: (q) => relationAggregate({ relation, op: '$count', ...(q && { query: q }) }),
55
+ sum: over('$sum'),
56
+ min: over('$min'),
57
+ max: over('$max'),
58
+ avg: over('$avg'),
59
+ });
60
+ }
61
+ /**
62
+ * A relation aggregate as SQL: the dialect writes the same correlated subquery a `$count` reads,
63
+ * correlated to whichever alias the clause naming the field is rendering under.
64
+ */
65
+ function relationAggregate(spec) {
66
+ return new RelationAggregate(spec, (opts) => {
67
+ if (!opts.entity) {
68
+ throw new TypeError(`'${spec.relation}' was read off a definition's refs, so it renders only inside its entity's SQL`);
69
+ }
70
+ opts.dialect.appendRelationAggregate(opts.ctx, opts.entity, spec, opts.prefix);
71
+ });
72
+ }
46
73
  /** One field as SQL, against its own entity or, read off a definition, the entity rendering it. */
47
74
  function columnRef(entity, key) {
48
75
  return new ColumnRef(key, (opts) => {
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "homepage": "https://uql-orm.dev",
4
4
  "description": "The JSON-native TypeScript ORM for Bun, Browsers, Edge, Deno, Node, Workers. Supports PostgreSQL, PGlite, MySQL, MariaDB, SQLite, CockroachDB, SQL Server, Turso, Neon, Cloudflare D1 and MongoDB. Queries are plain JSON, typed to the leaf.",
5
5
  "license": "MIT",
6
- "version": "0.68.1",
6
+ "version": "0.70.0",
7
7
  "type": "module",
8
8
  "engines": {
9
9
  "node": ">=24"