uql-orm 0.56.0 → 0.58.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (110) hide show
  1. package/README.md +7 -9
  2. package/dist/browser/uql-browser.min.js +2 -2
  3. package/dist/browser/uql-browser.min.js.map +4 -4
  4. package/dist/cockroachdb/cockroachDialect.d.ts +5 -2
  5. package/dist/cockroachdb/cockroachDialect.js +2 -10
  6. package/dist/d1/d1SqliteDialect.d.ts +1 -0
  7. package/dist/d1/d1SqliteDialect.js +2 -0
  8. package/dist/dialect/abstractSqlDialect.d.ts +196 -33
  9. package/dist/dialect/abstractSqlDialect.js +410 -203
  10. package/dist/dialect/aliases.d.ts +10 -7
  11. package/dist/dialect/aliases.js +12 -7
  12. package/dist/dialect/hydrateColumn.d.ts +8 -2
  13. package/dist/dialect/hydrateColumn.js +33 -1
  14. package/dist/dialect/jsonSql.d.ts +13 -5
  15. package/dist/dialect/jsonSql.js +24 -7
  16. package/dist/dialect/mysqlLikeSqlDialect.d.ts +31 -3
  17. package/dist/dialect/mysqlLikeSqlDialect.js +57 -5
  18. package/dist/dialect/pgLikeSqlDialect.d.ts +20 -20
  19. package/dist/dialect/pgLikeSqlDialect.js +23 -48
  20. package/dist/dialect/pgVectorMetrics.d.ts +13 -0
  21. package/dist/dialect/pgVectorMetrics.js +17 -0
  22. package/dist/dialect/queryContext.d.ts +3 -7
  23. package/dist/dialect/queryContext.js +13 -8
  24. package/dist/dialect/queryJoins.d.ts +8 -4
  25. package/dist/dialect/queryJoins.js +26 -11
  26. package/dist/dialect/vectorSqlDialect.d.ts +2 -2
  27. package/dist/dialect/vectorSqlDialect.js +2 -3
  28. package/dist/entity/decorator/bag.d.ts +2 -2
  29. package/dist/entity/decorator/entity.d.ts +8 -9
  30. package/dist/entity/decorator/entity.js +6 -7
  31. package/dist/entity/decorator/members.d.ts +7 -6
  32. package/dist/entity/decorator/members.js +2 -1
  33. package/dist/entity/metadata/definition.d.ts +16 -11
  34. package/dist/entity/metadata/definition.js +54 -42
  35. package/dist/http/handler.d.ts +2 -2
  36. package/dist/http/handler.js +0 -1
  37. package/dist/maria/mariaDialect.d.ts +13 -6
  38. package/dist/maria/mariaDialect.js +29 -9
  39. package/dist/migrate/cli.d.ts +2 -3
  40. package/dist/migrate/cli.js +2 -2
  41. package/dist/migrate/codegen/entityCodeGenerator.js +6 -4
  42. package/dist/migrate/codegen/entityTypes.d.ts +1 -1
  43. package/dist/migrate/codegen/entityTypes.js +4 -3
  44. package/dist/migrate/codegen/indexDecoratorSource.d.ts +5 -4
  45. package/dist/migrate/codegen/indexDecoratorSource.js +17 -13
  46. package/dist/migrate/codegen/sourceLiteral.d.ts +2 -0
  47. package/dist/migrate/codegen/sourceLiteral.js +4 -0
  48. package/dist/migrate/ddl/index.d.ts +1 -5
  49. package/dist/migrate/ddl/index.js +14 -25
  50. package/dist/migrate/ddl/indexDdl.d.ts +11 -2
  51. package/dist/migrate/ddl/indexDdl.js +17 -1
  52. package/dist/migrate/ddl/mssqlIndexDdl.d.ts +10 -0
  53. package/dist/migrate/ddl/mssqlIndexDdl.js +10 -0
  54. package/dist/migrate/ddl/mysqlIndexDdl.d.ts +10 -17
  55. package/dist/migrate/ddl/mysqlIndexDdl.js +16 -27
  56. package/dist/migrate/ddl/pgIndexDdl.d.ts +18 -8
  57. package/dist/migrate/ddl/pgIndexDdl.js +29 -12
  58. package/dist/migrate/generator/mongoSchemaGenerator.d.ts +3 -3
  59. package/dist/migrate/migrator.d.ts +4 -4
  60. package/dist/migrate/schemaGenerator.d.ts +8 -8
  61. package/dist/migrate/schemaGenerator.js +5 -7
  62. package/dist/migrate/schemaGeneratorAsync.d.ts +2 -3
  63. package/dist/mongo/mongoDialect.d.ts +31 -18
  64. package/dist/mongo/mongoDialect.js +146 -104
  65. package/dist/mongo/mongodbQuerier.d.ts +10 -17
  66. package/dist/mongo/mongodbQuerier.js +31 -106
  67. package/dist/mssql/mssqlDialect.d.ts +16 -0
  68. package/dist/mssql/mssqlDialect.js +26 -4
  69. package/dist/mysql/mysqlDialect.d.ts +2 -0
  70. package/dist/mysql/mysqlDialect.js +4 -0
  71. package/dist/querier/abstractQuerier.d.ts +20 -36
  72. package/dist/querier/abstractQuerier.js +35 -129
  73. package/dist/querier/abstractQuerierPool.d.ts +2 -2
  74. package/dist/querier/abstractSqlQuerier.d.ts +4 -17
  75. package/dist/querier/abstractSqlQuerier.js +40 -50
  76. package/dist/schema/canonicalType.js +4 -4
  77. package/dist/schema/indexDifferences.js +4 -4
  78. package/dist/schema/schemaASTBuilder.d.ts +3 -3
  79. package/dist/schema/schemaASTBuilder.js +32 -3
  80. package/dist/schema/schemaASTDiffer.js +5 -5
  81. package/dist/sqlite/sqliteDialect.d.ts +20 -1
  82. package/dist/sqlite/sqliteDialect.js +38 -7
  83. package/dist/turso/tursoDialect.d.ts +2 -0
  84. package/dist/turso/tursoDialect.js +2 -0
  85. package/dist/type/config.d.ts +3 -3
  86. package/dist/type/dialect.d.ts +4 -5
  87. package/dist/type/entity.d.ts +110 -69
  88. package/dist/type/migration.d.ts +7 -7
  89. package/dist/type/migratorDialect.d.ts +4 -0
  90. package/dist/type/querier.d.ts +6 -6
  91. package/dist/type/querierPool.d.ts +2 -2
  92. package/dist/type/query.d.ts +41 -72
  93. package/dist/type/query.js +10 -5
  94. package/dist/type/queryAggregate.d.ts +43 -34
  95. package/dist/type/queryAggregate.js +1 -1
  96. package/dist/type/queryWhere.d.ts +12 -9
  97. package/dist/type/universalQuerier.d.ts +4 -4
  98. package/dist/util/dialect.util.d.ts +4 -4
  99. package/dist/util/dialect.util.js +24 -15
  100. package/dist/util/field.util.d.ts +5 -0
  101. package/dist/util/field.util.js +19 -0
  102. package/dist/util/object.util.d.ts +2 -0
  103. package/dist/util/object.util.js +4 -0
  104. package/dist/util/relationQuery.util.d.ts +12 -65
  105. package/dist/util/relationQuery.util.js +27 -81
  106. package/dist/util/rowKey.util.d.ts +1 -11
  107. package/dist/util/rowKey.util.js +1 -13
  108. package/package.json +1 -1
  109. package/dist/querier/relationCount.d.ts +0 -16
  110. package/dist/querier/relationCount.js +0 -121
@@ -9,8 +9,8 @@ export const COUNT_RESULT_KEY = '_count';
9
9
  * Declared beside the type they describe so the two cannot drift, and `satisfies` fails the build
10
10
  * rather than the runtime if a clause is ever renamed.
11
11
  *
12
- * `$lock` belongs to no group on purpose: it is the one clause neither a wire query nor a relation's
13
- * query accepts, so leaving it out is what excludes it from both.
12
+ * `$lock` is only in {@link QUERY_STATEMENT_CLAUSES}: neither a wire query nor a relation's query
13
+ * accepts it.
14
14
  */
15
15
  export const QUERY_OBJECT_CLAUSES = [
16
16
  '$select',
@@ -20,9 +20,8 @@ export const QUERY_OBJECT_CLAUSES = [
20
20
  '$sort',
21
21
  ];
22
22
  /**
23
- * Object clauses only the statement itself takes, never a relation's own query - the mirror of
24
- * `$lock`, which neither takes. Counting a relation is batched over the rows a read returned, and a
25
- * populated relation's rows are assembled after that, so there is nothing for a nested one to count.
23
+ * Object clauses only the statement itself takes: a populated relation's rows keep their declared type,
24
+ * so a `$count` inside one would have no `_count` to land in.
26
25
  */
27
26
  export const QUERY_ROOT_OBJECT_CLAUSES = ['$count'];
28
27
  export const QUERY_NUMBER_CLAUSES = ['$skip', '$limit'];
@@ -33,3 +32,9 @@ export const QUERY_NUMBER_CLAUSES = ['$skip', '$limit'];
33
32
  */
34
33
  export const QUERY_ROOT_NUMBER_CLAUSES = ['$candidates'];
35
34
  export const QUERY_BOOLEAN_CLAUSES = ['$distinct'];
35
+ /** The clauses that describe the statement, which a populated relation's own query refuses by name. */
36
+ export const QUERY_STATEMENT_CLAUSES = [
37
+ '$lock',
38
+ ...QUERY_ROOT_OBJECT_CLAUSES,
39
+ ...QUERY_ROOT_NUMBER_CLAUSES,
40
+ ];
@@ -1,9 +1,9 @@
1
1
  import type { FieldKey } from './entity.js';
2
- import type { QueryPager, QuerySortDirection } from './query.js';
2
+ import type { QueryPager, QuerySelect, QuerySortDirection } from './query.js';
3
3
  import type { QueryWhere, QueryWhereFieldValue } from './queryWhere.js';
4
4
  /**
5
5
  * Maps the offending keys to `never`, turning an excess key into a compile error; resolves to
6
- * `unknown` (an inert intersection member) when there are none. Needed because `$group`/`$agg` are
6
+ * `unknown` (an inert intersection member) when there are none. Needed because `$group`/`$select` are
7
7
  * captured as whole maps, and TypeScript skips excess-property checking on a naked type parameter.
8
8
  * A find captures key sets instead, where an unknown key fails the capture's own constraint.
9
9
  * @internal
@@ -21,7 +21,7 @@ type GroupedKeys<G> = {
21
21
  }[keyof G];
22
22
  /**
23
23
  * The keys `T` declares by name, or `never` when `T` is only an index signature - which is what an
24
- * uninferred `$agg` is, and what would otherwise make every key look like a declared alias.
24
+ * uninferred `$select` is, and what would otherwise make every key look like a declared alias.
25
25
  * @internal
26
26
  */
27
27
  type NamedKeys<T> = string extends keyof T ? never : keyof T;
@@ -37,7 +37,7 @@ export type QueryAggregateOp = (typeof QUERY_AGGREGATE_OPS)[number];
37
37
  export declare function isQueryAggregateOp(op: string): op is QueryAggregateOp;
38
38
  /**
39
39
  * DISTINCT-qualified aggregate ops, each mapped to the base op it applies to a field's distinct
40
- * values: `$countDistinct` → `COUNT(DISTINCT col)`, and likewise `$sumDistinct`/`$avgDistinct`. Flat
40
+ * values: `$countDistinct` -> `COUNT(DISTINCT col)`, and likewise `$sumDistinct`/`$avgDistinct`. Flat
41
41
  * (not a nested `{ $distinct }` argument) so the op is self-documenting and greppable. `$min`/`$max`
42
42
  * are omitted: DISTINCT is a no-op for them.
43
43
  */
@@ -57,8 +57,20 @@ export declare function resolveAggregateOp(key: string): {
57
57
  op: QueryAggregateOp;
58
58
  distinct: boolean;
59
59
  };
60
+ /**
61
+ * Exactly one key of `T` with its value; every other key is forbidden (`never`). `Pick`, not `Record`,
62
+ * so the chosen key stays linked to `T`'s own property and renames follow it through.
63
+ */
64
+ type ExactlyOne<T> = {
65
+ [K in keyof T]: Readonly<Pick<T, K>> & Partial<Readonly<Record<Exclude<keyof T, K>, never>>>;
66
+ }[keyof T];
67
+ /**
68
+ * One field named as a key - `{ amount: true }` - the way a statement names every field, so an editor
69
+ * rename reaches it where a string never would. `F` narrows which fields qualify.
70
+ */
71
+ export type QueryFieldRef<E, F extends keyof E = FieldKey<E>> = ExactlyOne<Required<QuerySelect<E, F, true>>>;
60
72
  /** The argument of an aggregate function: a field, or `'*'` (only meaningful for `COUNT(*)`). */
61
- export type QueryAggregateArg<E> = FieldKey<E> | '*';
73
+ export type QueryAggregateArg<E> = QueryFieldRef<E> | '*';
62
74
  /**
63
75
  * Fields `SUM`/`AVG` can total. Restricted to numeric columns because the result is declared
64
76
  * `number`: totalling a text or date column is either an engine error or a coercion, and neither
@@ -81,21 +93,17 @@ type TotallingOp = OpsOf<'$sum' | '$avg' | '$sumDistinct' | '$avgDistinct'>;
81
93
  * Every aggregate op mapped to the argument it accepts: `$count` a field or `'*'` (`COUNT(*)`),
82
94
  * the totalling ops a numeric field, `$min`/`$max`/`$countDistinct` any field.
83
95
  */
84
- type QueryAggregateArgMap<E> = Record<'$count', QueryAggregateArg<E>> & Record<TotallingOp, NumericFieldKey<E>> & Record<Exclude<AggregateOp, '$count' | TotallingOp>, FieldKey<E>>;
85
- /** Exactly one key of `T`: the chosen op with its value; every other op key is forbidden (`never`). */
86
- type ExactlyOne<T> = {
87
- [K in keyof T]: Readonly<Record<K, T[K]>> & Partial<Readonly<Record<Exclude<keyof T, K>, never>>>;
88
- }[keyof T];
96
+ type QueryAggregateArgMap<E> = Record<'$count', QueryAggregateArg<E>> & Record<TotallingOp, QueryFieldRef<E, NumericFieldKey<E>>> & Record<Exclude<AggregateOp, '$count' | TotallingOp>, QueryFieldRef<E>>;
89
97
  /**
90
98
  * An aggregate function applied to a field. Exactly one operation per entry (a second op is a
91
99
  * compile error). Only `$count` accepts `'*'` (i.e. `COUNT(*)`); every other op requires a field.
92
100
  * DISTINCT variants are flat ops (`$countDistinct`/`$sumDistinct`/`$avgDistinct`) taking a field.
93
101
  *
94
- * @example { $count: '*' } → COUNT(*)
95
- * @example { $countDistinct: 'id' } → COUNT(DISTINCT "id")
96
- * @example { $sum: 'amount' } → SUM("amount")
97
- * @example { $sumDistinct: 'amount' } → SUM(DISTINCT "amount")
98
- * @example { $avg: 'age' } → AVG("age")
102
+ * @example { $count: '*' } -> COUNT(*)
103
+ * @example { $countDistinct: { id: true } } -> COUNT(DISTINCT "id")
104
+ * @example { $sum: { amount: true } } -> SUM("amount")
105
+ * @example { $sumDistinct: { amount: true } } -> SUM(DISTINCT "amount")
106
+ * @example { $avg: { age: true } } -> AVG("age")
99
107
  */
100
108
  export type QueryAggregateFn<E> = ExactlyOne<QueryAggregateArgMap<E>>;
101
109
  /** A single-key `{ [op]: unknown }` shape for each op in `Ops`, matched to infer that op's result. */
@@ -109,16 +117,14 @@ type CountingOp = OpsOf<'$count' | '$countDistinct'>;
109
117
  /**
110
118
  * Group-by columns: an object mapping entity field keys to `true`, exactly like {@link QuerySelect}.
111
119
  * Typed against the entity, so a typo'd column is a compile error. Compute aggregate columns with
112
- * {@link QueryAggMap} (the `$agg` key), not here.
120
+ * {@link QueryAggMap} (the `$select` key), not here.
113
121
  *
114
122
  * @example
115
123
  * ```ts
116
- * { status: true } // → GROUP BY "status"
124
+ * { status: true } // -> GROUP BY "status"
117
125
  * ```
118
126
  */
119
- export type QueryGroupMap<E> = {
120
- readonly [K in FieldKey<E>]?: true;
121
- };
127
+ export type QueryGroupMap<E> = Readonly<QuerySelect<E, FieldKey<E>, true>>;
122
128
  /**
123
129
  * Computed aggregate columns: an object mapping your chosen output alias to an aggregate function.
124
130
  * Alias names are free (you are naming new columns); the aggregated field reference inside each
@@ -126,8 +132,8 @@ export type QueryGroupMap<E> = {
126
132
  *
127
133
  * @example
128
134
  * ```ts
129
- * { count: { $count: '*' }, avgAge: { $avg: 'age' } }
130
- * // → COUNT(*) AS "count", AVG("age") AS "avgAge"
135
+ * { count: { $count: '*' }, avgAge: { $avg: { age: true } } }
136
+ * // -> COUNT(*) AS "count", AVG("age") AS "avgAge"
131
137
  * ```
132
138
  */
133
139
  export type QueryAggMap<E> = {
@@ -151,7 +157,7 @@ type QueryAggregateFnResult<E, Fn> = Fn extends FnWithOp<CountingOp> ? number :
151
157
  readonly $min: infer F;
152
158
  } | {
153
159
  readonly $max: infer F;
154
- } ? FieldValueType<E, F> | null : unknown;
160
+ } ? FieldValueType<E, keyof F> | null : unknown;
155
161
  /**
156
162
  * Flattens an intersection into a single object literal for readable editor hovers.
157
163
  * @internal
@@ -166,17 +172,15 @@ type Simplify<T> = {
166
172
  * Grouped columns come from {@link GroupedKeys}, not `keyof G`, so a `$group` the compiler could
167
173
  * not read contributes none rather than all of them.
168
174
  */
169
- export type QueryAggregateResult<E, G, A> = Simplify<{
170
- -readonly [K in GroupedKeys<G> & FieldKey<E>]: E[K];
171
- } & {
175
+ export type QueryAggregateResult<E, G, A> = Simplify<Pick<E, GroupedKeys<G> & FieldKey<E>> & {
172
176
  -readonly [K in keyof A]: QueryAggregateFnResult<E, A[K]>;
173
177
  }>;
174
178
  /**
175
- * Erased runtime shape of a HAVING clause (alias → comparison), consumed by the dialect builders.
179
+ * Erased runtime shape of a HAVING clause (alias -> comparison), consumed by the dialect builders.
176
180
  * Values are `unknown` because the SQL is built generically; the typed, per-column value checking
177
181
  * lives in {@link QueryAggregate.$having}.
178
182
  *
179
- * @example { count: { $gt: 5 } } → HAVING COUNT(*) > 5
183
+ * @example { count: { $gt: 5 } } -> HAVING COUNT(*) > 5
180
184
  */
181
185
  export type QueryHavingMap = {
182
186
  readonly [alias: string]: QueryWhereFieldValue<unknown> | undefined;
@@ -190,7 +194,7 @@ export type QueryHavingMap = {
190
194
  * querier.aggregate(User, {
191
195
  * $where: { deletedAt: { $isNull: true } },
192
196
  * $group: { status: true },
193
- * $agg: { count: { $count: '*' }, avgAge: { $avg: 'age' } },
197
+ * $select: { count: { $count: '*' }, avgAge: { $avg: { age: true } } },
194
198
  * $having: { count: { $gt: 5 } },
195
199
  * $sort: { count: -1 },
196
200
  * });
@@ -203,17 +207,22 @@ export type QueryAggregate<E, G extends QueryGroupMap<E> = QueryGroupMap<E>, A e
203
207
  readonly $where?: QueryWhere<E>;
204
208
  /**
205
209
  * Columns to group by - `{ status: true }`, typed against the entity like `$select`. A computed
206
- * aggregate wrongly placed here (it belongs in `$agg`) is rejected via {@link Reject}, since
207
- * `$group` is captured as a generic and a bare generic skips excess-property checking.
210
+ * aggregate wrongly placed here (it belongs in `$select`) is rejected via {@link Reject}, since
211
+ * `$group` is captured as a generic and a bare generic skips excess-property checking. The captured
212
+ * map meets its schema, {@link QueryGroupMap}, so each key keeps its link to the entity property.
208
213
  */
209
- readonly $group?: G & Reject<Exclude<keyof G, FieldKey<E>>>;
214
+ readonly $group?: G & QueryGroupMap<E> & Reject<Exclude<keyof G, FieldKey<E>>>;
210
215
  /**
211
- * Computed aggregate columns - `{ count: { $count: '*' }, avgAge: { $avg: 'age' } }`.
216
+ * Computed aggregate columns - `{ count: { $count: '*' }, avgAge: { $avg: { age: true } } }`. The
217
+ * captured map meets its schema over the same aliases, as `$group` does, so field keys stay linked
218
+ * (a `Record<keyof A, ...>` spelling of the same type breaks the inference of `A`).
212
219
  *
213
220
  * An alias repeating a `$group` column is rejected: both would be emitted under that one name,
214
221
  * leaving the driver to keep whichever it read last.
215
222
  */
216
- readonly $agg?: A & Reject<NamedKeys<A> & GroupedKeys<G>>;
223
+ readonly $select?: A & {
224
+ readonly [K in keyof A]: QueryAggregateFn<E>;
225
+ } & Reject<NamedKeys<A> & GroupedKeys<G>>;
217
226
  /**
218
227
  * Post-aggregation filtering, applied after grouping (SQL `HAVING`, MongoDB post-group `$match`).
219
228
  * Keyed by the result columns (grouped columns + computed aliases), and each value is typed to that
@@ -8,7 +8,7 @@ export function isQueryAggregateOp(op) {
8
8
  }
9
9
  /**
10
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
11
+ * values: `$countDistinct` -> `COUNT(DISTINCT col)`, and likewise `$sumDistinct`/`$avgDistinct`. Flat
12
12
  * (not a nested `{ $distinct }` argument) so the op is self-documenting and greppable. `$min`/`$max`
13
13
  * are omitted: DISTINCT is a no-op for them.
14
14
  */
@@ -1,4 +1,5 @@
1
1
  import type { FieldKey, JsonFieldPaths, JsonFieldPathValue, RelationKey, RelationTarget } from './entity.js';
2
+ import type { QuerySelect } from './query.js';
2
3
  import type { QueryRaw } from './queryRaw.js';
3
4
  import type { ExpandScalar, IsMany, QueryComparableScalar, Scalar } from './utility.js';
4
5
  import type { QueryVectorQuery } from './vector.js';
@@ -11,9 +12,9 @@ export type QueryTextSearchOptions<E> = {
11
12
  */
12
13
  $value: string;
13
14
  /**
14
- * list of fields to search on.
15
+ * the fields to search, `{ title: true, body: true }`, in the order a MySQL `FULLTEXT` index lists them.
15
16
  */
16
- $fields?: FieldKey<E>[];
17
+ $fields?: QuerySelect<E>;
17
18
  /**
18
19
  * Postgres text-search configuration (e.g. `'english'`), applied to both the document and the
19
20
  * query. Defaults to the server's `default_text_search_config`. Ignored by other dialects.
@@ -27,17 +28,19 @@ export type QueryTextSearchOptions<E> = {
27
28
  * filtered via nested typed objects; dotted relation paths are not supported (the dialects throw
28
29
  * for non-JSON dotted keys).
29
30
  *
30
- * One mapped type over the three key sets rather than three intersected, for the reason
31
- * {@link QuerySortMap} is: the sets are disjoint, and an assignability check against an
32
- * intersection is repeated per constituent, which every `$where` in a codebase pays. The root
33
- * operators stay a separate member - they are a fixed shape, not keyed off the entity.
31
+ * Fields and relations share one mapped type over `K extends keyof E`, which keeps each key linked to
32
+ * its property (see {@link QuerySelect}). JSON paths are not keys of `E`, so they are a second member,
33
+ * and only where the entity has one: an empty member would switch off the weak-type check that
34
+ * rejects `$where: 1`.
34
35
  *
35
36
  * An object and nothing else: in a union with ids or lists, TypeScript reports a wrong value against
36
37
  * the whole `$where` instead of the key holding it. Ids are `{ id: 1 }`, or the by-id methods.
37
38
  */
38
- export type QueryWhere<E> = QueryWhereRootOperator<E> & {
39
- [K in FieldKey<E> | RelationKey<E> | JsonFieldPaths<E>]?: K extends FieldKey<E> ? QueryWhereFieldValue<E[K]> : K extends RelationKey<E> ? QueryWhere<RelationTarget<E[K]>> | QueryRelationSizeFilter : QueryWhereFieldValue<JsonFieldPathValue<E, K & string>>;
40
- };
39
+ export type QueryWhere<E, K extends keyof E = FieldKey<E> | RelationKey<E>> = QueryWhereRootOperator<E> & {
40
+ [P in K]?: P extends FieldKey<E> ? QueryWhereFieldValue<E[P]> : QueryWhere<RelationTarget<E[P]>> | QueryRelationSizeFilter;
41
+ } & ([JsonFieldPaths<E>] extends [never] ? unknown : {
42
+ [P in JsonFieldPaths<E>]?: QueryWhereFieldValue<JsonFieldPathValue<E, P>>;
43
+ });
41
44
  /**
42
45
  * Filter a to-many relation by its row count.
43
46
  * @example { users: { $size: 2 } }
@@ -1,5 +1,5 @@
1
1
  import type { EntityData, EntityId, FieldKey, RelationKey, UpdatePayload, WrittenId } from './entity.js';
2
- import type { QueryConflictPaths, QueryFilter, QueryFindResult, QueryOneProjected, QueryOptions, QueryPage, QueryProjected, QuerySearch, QueryStreamProjected, QueryUpsertOneResult, QueryUpsertManyResult } from './query.js';
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';
5
5
  import type { QuerierCountedResult, QuerierResult, QuerierTransport } from './wire.js';
@@ -97,14 +97,14 @@ export interface SharedQuerier<W extends QuerierTransport, O, DO = O> {
97
97
  */
98
98
  export interface UniversalQuerier extends SharedQuerier<'server', QueryOptions> {
99
99
  /**
100
- * streams the records matching the given search parameters as an async iterable.
101
- * Does not fill relations or fire lifecycle hooks - designed for high-performance
100
+ * streams the records matching the given search parameters as an async iterable, each with the
101
+ * relations and counts `findMany` reads. Fires no lifecycle hooks, and holds one row at a time, for
102
102
  * bulk reads (ETL, exports, migrations).
103
103
  * @param entity the target entity
104
104
  * @param q the criteria options
105
105
  * @return an async iterable of records
106
106
  */
107
- 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>(entity: Type<E>, q: QueryStreamProjected<E, S, V, X, P>, opts?: QueryOptions): AsyncIterable<QueryFindResult<E, S, V, X, P>>;
107
+ 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>>;
108
108
  /**
109
109
  * Insert a single record and return its ID (provided, `onInsert`-generated, or
110
110
  * database-generated - see {@link UniversalQuerier.insertMany} for the exact semantics).
@@ -128,7 +128,7 @@ export type ParsedGroupEntry = {
128
128
  readonly alias: string;
129
129
  readonly op: QueryAggregateOp;
130
130
  readonly fieldRef: string;
131
- /** `true` for a flat distinct op (`$countDistinct`, ...) → `COUNT(DISTINCT field)`. */
131
+ /** `true` for a flat distinct op (`$countDistinct`, ...) -> `COUNT(DISTINCT field)`. */
132
132
  readonly distinct: boolean;
133
133
  };
134
134
  /**
@@ -145,10 +145,10 @@ export declare function parseRelationSize(val: unknown): number | QuerySizeCompa
145
145
  */
146
146
  export declare function parseSortByCount(val: unknown): unknown;
147
147
  /**
148
- * Parse the `$group` (grouped columns) and `$agg` (computed aggregates) maps into structured
148
+ * Parse the `$group` (grouped columns) and `$select` (computed aggregates) maps into structured
149
149
  * entries consumable by any dialect. Grouped columns come first, then computed columns.
150
150
  */
151
- export declare function parseGroupMap<E>(group?: QueryGroupMap<E>, agg?: QueryAggMap<E>): ParsedGroupEntry[];
151
+ export declare function parseGroupMap<E>(group?: QueryGroupMap<E>, select?: QueryAggMap<E>): ParsedGroupEntry[];
152
152
  /**
153
153
  * Whether `value` is a map of comparison operators rather than a value to compare against. Only a
154
154
  * plain object qualifies: `Date`, `QueryRaw`, `Uint8Array` and arrays are all `typeof 'object'`, and
@@ -165,7 +165,7 @@ export declare function isOperatorMap(value: unknown): value is Record<string, u
165
165
  export declare function assertNonNegativeInteger(value: number, clause: string): number;
166
166
  /**
167
167
  * Rejects a `$having`/`$sort` key naming something an aggregate does not emit. Its rows are its
168
- * `$group` columns and its `$agg` aliases; anything else is a value that is not there. Shared so
168
+ * `$group` columns and its `$select` aliases; anything else is a value that is not there. Shared so
169
169
  * SQL and MongoDB refuse the same query with the same words.
170
170
  */
171
171
  export declare function throwUnknownAggregateColumn(key: string, clause: string): never;
@@ -3,7 +3,7 @@ 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
5
  import { isDatabaseWritten } from './field.util.js';
6
- import { entityName, getFieldKeys, getKeys, hasKeys, isScalarId, isWhereMap, someKey } from './object.util.js';
6
+ import { entityName, getFieldKeys, getKeys, hasKeys, isScalarId, isRecord, isWhereMap, someKey, } from './object.util.js';
7
7
  export function filterFieldKeys(meta, payload, callbackKey) {
8
8
  return getKeys(payload).filter((key) => {
9
9
  const fieldOpts = meta.fields[key];
@@ -355,10 +355,10 @@ export function parseSortByCount(val) {
355
355
  return val.$count;
356
356
  }
357
357
  /**
358
- * Parse the `$group` (grouped columns) and `$agg` (computed aggregates) maps into structured
358
+ * Parse the `$group` (grouped columns) and `$select` (computed aggregates) maps into structured
359
359
  * entries consumable by any dialect. Grouped columns come first, then computed columns.
360
360
  */
361
- export function parseGroupMap(group, agg) {
361
+ export function parseGroupMap(group, select) {
362
362
  const entries = [];
363
363
  const groupMap = group ?? {};
364
364
  for (const alias of getKeys(groupMap)) {
@@ -366,22 +366,30 @@ export function parseGroupMap(group, agg) {
366
366
  entries.push({ kind: 'key', alias });
367
367
  }
368
368
  }
369
- if (!agg) {
369
+ if (!select) {
370
370
  return entries;
371
371
  }
372
- for (const alias of getKeys(agg)) {
373
- const fnEntry = agg[alias];
372
+ for (const alias of getKeys(select)) {
373
+ const fnEntry = select[alias];
374
374
  const key = getKeys(fnEntry)[0];
375
375
  // Flat DISTINCT ops (`$countDistinct`, ...) normalize to their base op + a `distinct` flag.
376
376
  const { op, distinct } = resolveAggregateOp(key);
377
- const fieldRef = fnEntry[key];
378
- if (fieldRef === undefined) {
379
- throw TypeError(`empty aggregate function for: ${alias}`);
380
- }
381
- entries.push({ kind: 'fn', alias, op, fieldRef, distinct });
377
+ entries.push({ kind: 'fn', alias, op, fieldRef: aggregateFieldRef(alias, fnEntry[key]), distinct });
382
378
  }
383
379
  return entries;
384
380
  }
381
+ /** The column an aggregate reads: `'*'`, or the one field its `{ field: true }` names. */
382
+ function aggregateFieldRef(alias, arg) {
383
+ const [field, ...rest] = arg === '*' ? [arg] : namedKeys(arg);
384
+ if (field === undefined || rest.length) {
385
+ throw new TypeError(`aggregate '${alias}' takes one field as { field: true }, or '*': got ${JSON.stringify(arg)}`);
386
+ }
387
+ return field;
388
+ }
389
+ /** The keys a `{ key: true }` map switches on; anything that is not such a map switches on none. */
390
+ function namedKeys(map) {
391
+ return isRecord(map) ? getKeys(map).filter((key) => map[key]) : [];
392
+ }
385
393
  /**
386
394
  * Whether `value` is a map of comparison operators rather than a value to compare against. Only a
387
395
  * plain object qualifies: `Date`, `QueryRaw`, `Uint8Array` and arrays are all `typeof 'object'`, and
@@ -410,11 +418,11 @@ export function assertNonNegativeInteger(value, clause) {
410
418
  }
411
419
  /**
412
420
  * Rejects a `$having`/`$sort` key naming something an aggregate does not emit. Its rows are its
413
- * `$group` columns and its `$agg` aliases; anything else is a value that is not there. Shared so
421
+ * `$group` columns and its `$select` aliases; anything else is a value that is not there. Shared so
414
422
  * SQL and MongoDB refuse the same query with the same words.
415
423
  */
416
424
  export function throwUnknownAggregateColumn(key, clause) {
417
- throw new TypeError(`cannot ${clause} by '${key}': it is neither a $group column nor an $agg alias`);
425
+ throw new TypeError(`cannot ${clause} by '${key}': it is neither a $group column nor a $select alias`);
418
426
  }
419
427
  /** {@link throwUnknownAggregateColumn} over every key of a clause, for backends that check up front. */
420
428
  export function assertAggregateColumns(clauseMap, emitted, clause) {
@@ -430,8 +438,9 @@ export function assertAggregateColumns(clauseMap, emitted, clause) {
430
438
  * where neither says, rather than guessed: every engine answers a guess with an error of its own.
431
439
  */
432
440
  export function textSearchFields(meta, search) {
433
- if (search.$fields?.length) {
434
- return search.$fields;
441
+ const named = namedKeys(search.$fields);
442
+ if (named.length) {
443
+ return named;
435
444
  }
436
445
  const fulltext = (meta.indexes ?? []).filter((index) => index.type === 'fulltext');
437
446
  if (fulltext.length === 1) {
@@ -22,6 +22,11 @@ export declare const COLUMN_TYPES_BY_FAMILY: {
22
22
  };
23
23
  /** The family of a logical field type, or `undefined` where it names none. */
24
24
  export declare function columnFamily(type: unknown): ColumnFamily | undefined;
25
+ /**
26
+ * Whether a field's column holds whole numbers: a declared integer type, or a `Number` or `BigInt`
27
+ * with no scale, which every engine here stores as BIGINT.
28
+ */
29
+ export declare function isIntegerColumn(field: Pick<FieldOptions, 'type' | 'columnType' | 'precision' | 'scale'>): boolean;
25
30
  /**
26
31
  * Whether the field's expression is spliced into each statement that reads it, rather than stored.
27
32
  * Every read site asks this - the DDL skip, the projection, the `$where` and `ORDER BY` operands -
@@ -46,6 +46,25 @@ for (const family of getKeys(COLUMN_TYPES_BY_FAMILY)) {
46
46
  export function columnFamily(type) {
47
47
  return FAMILY_OF.get(typeof type === 'string' ? type.toLowerCase() : type);
48
48
  }
49
+ /** The numeric column types that hold whole numbers. */
50
+ const INTEGER_COLUMN_TYPES = new Set([
51
+ 'int',
52
+ 'integer',
53
+ 'tinyint',
54
+ 'smallint',
55
+ 'bigint',
56
+ ]);
57
+ /**
58
+ * Whether a field's column holds whole numbers: a declared integer type, or a `Number` or `BigInt`
59
+ * with no scale, which every engine here stores as BIGINT.
60
+ */
61
+ export function isIntegerColumn(field) {
62
+ const type = field.columnType ?? field.type;
63
+ if (typeof type === 'string') {
64
+ return INTEGER_COLUMN_TYPES.has(type.toLowerCase());
65
+ }
66
+ return type === BigInt || (type === Number && !field.precision && !field.scale);
67
+ }
49
68
  /**
50
69
  * Whether the field's expression is spliced into each statement that reads it, rather than stored.
51
70
  * Every read site asks this - the DDL skip, the projection, the `$where` and `ORDER BY` operands -
@@ -19,6 +19,8 @@ export declare function someValue(obj: object, pred: (value: unknown) => boolean
19
19
  export declare function isOperatorObject(value: unknown): value is Record<string, unknown>;
20
20
  /** Whether every key of the non-empty object `value` is an operator (no plain field names mixed in). */
21
21
  export declare function isOperatorOnlyObject(value: unknown): value is Record<string, unknown>;
22
+ /** Whether `value` is an object that is not an array, whose keys can be read. */
23
+ export declare function isRecord(value: unknown): value is Record<string, unknown>;
22
24
  export declare function getKeys<T extends object>(obj: T): (keyof T & string)[];
23
25
  /** The entries of `record` holding a value: a key declared but left `undefined` is no entry at all. */
24
26
  export declare function definedEntries<K extends string, V>(record: Partial<Record<K, V>>): [K, V][];
@@ -49,6 +49,10 @@ export function isOperatorObject(value) {
49
49
  export function isOperatorOnlyObject(value) {
50
50
  return hasKeys(value) && !Array.isArray(value) && !someKey(value, (key) => !isOperatorKey(key));
51
51
  }
52
+ /** Whether `value` is an object that is not an array, whose keys can be read. */
53
+ export function isRecord(value) {
54
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
55
+ }
52
56
  export function getKeys(obj) {
53
57
  return obj ? Object.keys(obj) : [];
54
58
  }
@@ -1,4 +1,4 @@
1
- import type { EntityMeta, FieldMeta, Except, Query, QueryPopulate, RelationKey, RelationMeta } from '../type/index.js';
1
+ import type { EntityMeta, QueryCount, QueryPopulate, RelationKey, RelationMeta, RelationQuery, QueryWhere } from '../type/index.js';
2
2
  export type RelationRequestSummary<E> = {
3
3
  readonly requestedKeys: readonly RelationKey<E>[];
4
4
  readonly joinableKeys: readonly RelationKey<E>[];
@@ -32,67 +32,10 @@ export declare function parentJoins(relOpts: Pick<RelationMeta, 'references' | '
32
32
  * which is the mistake this module exists to prevent.
33
33
  */
34
34
  export declare function targetKeyColumns(relOpts: Pick<RelationMeta, 'references'>, parentKeyCount: number): string[];
35
- /** `{ joined column: true }`: the projection or grouping that keeps a parent's key on the rows read. */
36
- export declare function joinedColumns(joins: readonly ParentJoin[]): Record<string, true>;
37
- /**
38
- * One side's columns: `'parent'` for the columns a parent is keyed by, `'joined'` for the ones a
39
- * child or tally row carries that key in. Matching the two halves means agreeing on every column, so
40
- * each side is read through `joins` rather than through the parent's own key list - the same set only
41
- * for a to-many, and silently a different one otherwise. `keyof ParentJoin` is what keeps the two
42
- * sides from being spelled apart.
43
- *
44
- * Lifted out of `joins` once per relation, not once per row: {@link rowKey} takes the list and reads
45
- * each row itself, so a page of children costs one key each and nothing else.
46
- */
47
- export declare function keyColumns(joins: readonly ParentJoin[], side: keyof ParentJoin): string[];
48
- /**
49
- * `{ joined column: every parent's value for it }`, the filter that fetches a whole page of parents'
50
- * children in one statement.
51
- *
52
- * A composite over-selects, because the lists are independent and a pairing no parent has can still
53
- * match. Regrouping the rows keys on every column, so those rows find no parent and are dropped -
54
- * cheaper than the row-value comparison no engine spells the same way.
55
- */
56
- export declare function parentsIn(joins: readonly ParentJoin[], parents: readonly unknown[]): Record<string, unknown[]>;
57
- /**
58
- * The parents a bounded to-many read fans out over: the rows themselves, the columns matching them to
59
- * their children.
60
- */
61
- export type ParentPartition = {
62
- readonly joins: readonly ParentJoin[];
63
- readonly parents: readonly unknown[];
64
- /** The parent's own fields: a `LATERAL` row source has to spell its key column's type. */
65
- readonly parentFields: Readonly<Record<string, FieldMeta | undefined>>;
66
- };
67
- /**
68
- * Whether a to-many's own query asks for a share *per parent* rather than a slice of the whole page.
69
- * Only `$limit`/`$skip` do: without one, a single flat statement over an `IN (...)` list is both
70
- * correct and cheaper.
71
- */
72
- export declare function isBoundedPerParent(query: Pick<RelationQuery, '$limit' | '$skip'>): boolean;
73
- /**
74
- * `query` narrowed to one parent's children: what a single branch of a bounded per-parent read asks
75
- * for. Shared by the backends so how the parent's filter merges into the relation's own is decided
76
- * once - both spelled it out, and a rule that ever needs more than a spread would have to change twice.
77
- */
78
- export declare function queryChildrenOf<E>(query: Query<E>, joins: readonly ParentJoin[], parent: unknown): Query<E>;
79
- /**
80
- * `query` narrowed to the children of a whole page of parents, which is the flat read a relation with
81
- * no share of its own takes. Over-selects on a composite key exactly as {@link parentsIn} does.
82
- */
83
- export declare function queryChildrenOfAll<E>(query: Query<E>, joins: readonly ParentJoin[], parents: readonly unknown[]): Query<E>;
84
- /**
85
- * `query` with `filter` merged into its own `$where`: the one rule for narrowing a relation's query to
86
- * the parents it is being read for, whether the filter names their keys as values or, for a correlated
87
- * shape, as a reference to a row source.
88
- */
89
- export declare function queryNarrowedTo<E>(query: Query<E>, filter: Record<string, unknown>): Query<E>;
90
35
  /**
91
36
  * The `$where` naming exactly the children of the rows `parentIds` identifies: an `IN` over the one
92
- * column a single key contributes, an OR of key maps for several.
93
- *
94
- * Exact, unlike {@link parentsIn}: a read absorbs over-selection by regrouping its rows, and a write
95
- * has nothing to regroup - a pairing no parent has would delete a child of a parent that survives.
37
+ * column a single key contributes, an OR of whole key maps for several - lists of each column apart
38
+ * would pair values no parent has, and a delete would take a child of a parent that survives.
96
39
  */
97
40
  export declare function childrenOf(joins: readonly ParentJoin[], parentIds: readonly unknown[]): Record<string, unknown>;
98
41
  /**
@@ -107,11 +50,15 @@ export type JoinedRelationRejectedKey = (typeof JOINED_RELATION_REJECTIONS)[numb
107
50
  export declare function getRelationRequestSummary<E>(meta: EntityMeta<E>, populate?: QueryPopulate<E>): RelationRequestSummary<E>;
108
51
  /** True when `$populate` includes at least one relation key. */
109
52
  export declare function populatesRelations<E>(meta: EntityMeta<E>, populate?: QueryPopulate<E>): boolean;
110
- export type RelationQuery<E extends object = object> = Except<Query<E>, StatementOnlyClause> & {
111
- $required?: boolean;
112
- };
113
- /** The clauses that describe the statement rather than what a query selects. */
114
- type StatementOnlyClause = '$lock' | '$candidates';
53
+ /**
54
+ * Each relation a `$count` tallies, and the filter narrowing what it counts: the target's, whose type
55
+ * only the metadata knows this far down, as for a relation filter reaching the same subquery.
56
+ */
57
+ export declare function countedRelations<E>(meta: EntityMeta<E>, counts: QueryCount<E> | undefined): {
58
+ readonly relKey: RelationKey<E>;
59
+ readonly relation: RelationMeta;
60
+ readonly where: QueryWhere<object>;
61
+ }[];
115
62
  export type ParsedRelationQuery<E extends object = object> = {
116
63
  query: RelationQuery<E>;
117
64
  required: boolean;