uql-orm 0.36.1 → 0.37.1

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 (54) hide show
  1. package/dist/browser/http/http.d.ts +1 -1
  2. package/dist/browser/querier/httpQuerier.d.ts +12 -7
  3. package/dist/browser/querier/httpQuerier.js +6 -1
  4. package/dist/browser/type/clientQuerier.d.ts +10 -23
  5. package/dist/browser/uql-browser.min.js +2 -2
  6. package/dist/browser/uql-browser.min.js.map +7 -7
  7. package/dist/cockroachdb/cockroachDialect.d.ts +8 -1
  8. package/dist/cockroachdb/cockroachDialect.js +12 -0
  9. package/dist/dialect/abstractSqlDialect.d.ts +31 -4
  10. package/dist/dialect/abstractSqlDialect.js +64 -15
  11. package/dist/dialect/aliases.d.ts +30 -0
  12. package/dist/dialect/aliases.js +32 -0
  13. package/dist/dialect/jsonSql.d.ts +0 -20
  14. package/dist/dialect/jsonSql.js +0 -20
  15. package/dist/dialect/mysqlLikeSqlDialect.d.ts +6 -0
  16. package/dist/dialect/mysqlLikeSqlDialect.js +20 -1
  17. package/dist/dialect/pgLikeSqlDialect.d.ts +2 -0
  18. package/dist/dialect/pgLikeSqlDialect.js +4 -1
  19. package/dist/dialect/queryJoins.d.ts +6 -0
  20. package/dist/dialect/queryJoins.js +13 -0
  21. package/dist/http/contract.d.ts +0 -7
  22. package/dist/http/handler.d.ts +2 -2
  23. package/dist/http/handler.js +1 -1
  24. package/dist/http/query.js +3 -2
  25. package/dist/mongo/mongoDialect.d.ts +15 -4
  26. package/dist/mongo/mongoDialect.js +65 -16
  27. package/dist/mongo/mongodbQuerier.d.ts +13 -2
  28. package/dist/mongo/mongodbQuerier.js +46 -2
  29. package/dist/postgres/postgresDialect.d.ts +7 -0
  30. package/dist/postgres/postgresDialect.js +13 -0
  31. package/dist/querier/abstractQuerier.d.ts +34 -18
  32. package/dist/querier/abstractQuerier.js +43 -30
  33. package/dist/querier/abstractQuerierPool.d.ts +9 -7
  34. package/dist/querier/abstractQuerierPool.js +6 -0
  35. package/dist/querier/abstractSqlQuerier.d.ts +30 -2
  36. package/dist/querier/abstractSqlQuerier.js +61 -8
  37. package/dist/querier/relationCount.d.ts +16 -0
  38. package/dist/querier/relationCount.js +104 -0
  39. package/dist/sqlite/sqliteDialect.d.ts +6 -1
  40. package/dist/sqlite/sqliteDialect.js +12 -1
  41. package/dist/type/dialect.d.ts +10 -3
  42. package/dist/type/index.d.ts +1 -0
  43. package/dist/type/index.js +1 -0
  44. package/dist/type/querier.d.ts +21 -14
  45. package/dist/type/query.d.ts +77 -16
  46. package/dist/type/query.js +17 -0
  47. package/dist/type/universalQuerier.d.ts +85 -48
  48. package/dist/type/wire.d.ts +37 -0
  49. package/dist/type/wire.js +1 -0
  50. package/dist/util/dialect.util.d.ts +6 -0
  51. package/dist/util/dialect.util.js +15 -0
  52. package/dist/util/relationQuery.util.d.ts +9 -0
  53. package/dist/util/relationQuery.util.js +13 -0
  54. package/package.json +4 -4
@@ -1,9 +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
4
  import { resolveQueryJoins, resolveSortableJoin } from '../dialect/queryJoins.js';
4
5
  import { getMeta } from '../entity/index.js';
5
6
  import { QueryRaw } from '../type/queryRaw.js';
6
- import { asSelectMap, assertAggregateColumns, assertNonNegativeInteger, buildQueryWhereAsMap, fillOnFields, filterFieldKeys, getKeys, getRelationRequestSummary, hasKeys, isJsonUpdateOp, isOperatorMap, isOperatorObject, isVectorSearch, normalizeScalarFieldSelection, parseGroupMap, parseRelationSize, someKey, } from '../util/index.js';
7
+ import { asSelectMap, assertAggregateColumns, assertNonNegativeInteger, buildQueryWhereAsMap, fillOnFields, filterFieldKeys, getKeys, getRelationRequestSummary, hasKeys, isJsonUpdateOp, isOperatorMap, isOperatorObject, isVectorSearch, normalizeScalarFieldSelection, parseGroupMap, parseRelationSize, parseSortByCount, someKey, } from '../util/index.js';
7
8
  /** Default {@link DialectFeatures} for MongoDB; shared by {@link MongoDialect} and its schema generator. */
8
9
  export const mongoDialectFeatures = {
9
10
  explicitJsonCast: false,
@@ -27,10 +28,6 @@ export class MongoDialect extends AbstractDialect {
27
28
  // The MongoDB driver reports the exact `_id` of every inserted document (`insertedIds`).
28
29
  insertIdSource = 'returning';
29
30
  static ID_KEY = '_id';
30
- /** Temporary lookup fields for relation conditions, dropped with `$unset` after the `$match`. */
31
- static REL_TEMP_PREFIX = '__uql_rel_';
32
- static REL_COUNT_KEY = 'n';
33
- static REL_NESTED_KEY = '__uql_target';
34
31
  static VECTOR_INDEX_TYPES = new Set(['vectorSearch', 'hnsw', 'ivfflat', 'vector']);
35
32
  /** Atlas rejects a `$vectorSearch` asking for more candidates than this. */
36
33
  static MAX_NUM_CANDIDATES = 10_000;
@@ -142,10 +139,10 @@ export class MongoDialect extends AbstractDialect {
142
139
  const relOpts = meta.relations[relKey];
143
140
  const relEntity = relOpts.entity();
144
141
  const relMeta = getMeta(relEntity);
145
- const temp = `${MongoDialect.REL_TEMP_PREFIX}${lookups.temps.length}`;
142
+ const temp = `${REL_TEMP_PREFIX}${lookups.temps.length}`;
146
143
  const sizeVal = parseRelationSize(val);
147
144
  // `$count` for a size test, `$limit: 1` for existence: neither returns the matched documents.
148
- const tail = sizeVal === undefined ? [{ $limit: 1 }] : [{ $count: MongoDialect.REL_COUNT_KEY }];
145
+ const tail = sizeVal === undefined ? [{ $limit: 1 }] : [{ $count: COUNT_ALIAS }];
149
146
  // Scope first, render once - merging the target's filters into an already-rendered filter would
150
147
  // leave their own keys unmapped. The caller's filter bypass is deliberately *not* passed down:
151
148
  // `withDeleted()` or `hardDelete` on the parent must not un-hide trashed rows of the target, the
@@ -153,7 +150,18 @@ export class MongoDialect extends AbstractDialect {
153
150
  const targetCondition = (sizeVal === undefined ? val : {});
154
151
  const targetScope = this.renderFilter(relEntity, this.scopedWhereMap(relMeta, targetCondition), opts);
155
152
  lookups.temps.push(temp);
156
- lookups.stages.push(relOpts.cardinality === 'mm' && relOpts.through
153
+ lookups.stages.push(this.relationLookup(meta, relOpts, relMeta, relEntity, targetScope, temp, tail, opts));
154
+ return sizeVal === undefined
155
+ ? { [`${temp}.0`]: { $exists: true } }
156
+ : { $expr: this.compareRelationCount(temp, sizeVal) };
157
+ }
158
+ /**
159
+ * The correlated `$lookup` for one relation, as `temp`: through its junction for a ManyToMany, or
160
+ * straight at the target otherwise. `tail` decides what the lookup leaves behind - a row to test
161
+ * for existence, or a `$count` - so a filter and an ordering build the same stage.
162
+ */
163
+ relationLookup(meta, relOpts, relMeta, relEntity, targetScope, temp, tail, opts) {
164
+ return relOpts.cardinality === 'mm' && relOpts.through
157
165
  ? this.junctionLookup(relOpts, relMeta, relEntity, targetScope, temp, tail, opts)
158
166
  : {
159
167
  $lookup: {
@@ -162,10 +170,7 @@ export class MongoDialect extends AbstractDialect {
162
170
  pipeline: [...(hasKeys(targetScope) ? [{ $match: targetScope }] : []), ...tail],
163
171
  as: temp,
164
172
  },
165
- });
166
- return sizeVal === undefined
167
- ? { [`${temp}.0`]: { $exists: true } }
168
- : { $expr: this.compareRelationCount(temp, sizeVal) };
173
+ };
169
174
  }
170
175
  /**
171
176
  * ManyToMany counts/tests junction rows, so the target is reached from inside the junction's own
@@ -175,7 +180,7 @@ export class MongoDialect extends AbstractDialect {
175
180
  const throughEntity = relOpts.through();
176
181
  const throughMeta = getMeta(throughEntity);
177
182
  const junctionScope = this.renderFilter(throughEntity, this.scopedWhereMap(throughMeta, {}), opts);
178
- const nested = MongoDialect.REL_NESTED_KEY;
183
+ const nested = REL_NESTED_KEY;
179
184
  return {
180
185
  $lookup: {
181
186
  from: this.resolveTableName(throughMeta),
@@ -204,7 +209,7 @@ export class MongoDialect extends AbstractDialect {
204
209
  * the `$ifNull` fallback to 0, so `{ $size: 0 }` matches parents with no related row at all.
205
210
  */
206
211
  compareRelationCount(temp, sizeVal) {
207
- const count = { $ifNull: [{ $arrayElemAt: [`$${temp}.${MongoDialect.REL_COUNT_KEY}`, 0] }, 0] };
212
+ const count = { $ifNull: [{ $arrayElemAt: [`$${temp}.${COUNT_ALIAS}`, 0] }, 0] };
208
213
  if (typeof sizeVal === 'number') {
209
214
  return { $eq: [count, sizeVal] };
210
215
  }
@@ -397,10 +402,47 @@ export class MongoDialect extends AbstractDialect {
397
402
  // one: ordering by a relation nothing looked up reads a field that is not there, which MongoDB
398
403
  // ranks as all-equal rather than rejecting. The SQL dialects can add the join themselves.
399
404
  const relPath = `${path}${key}`;
405
+ const countDirection = parseSortByCount(value);
406
+ if (countDirection !== undefined) {
407
+ // The tally rides on a field {@link sortCountStages} adds, which only the queried entity's
408
+ // own pipeline has: a nested one is built inside its parent's `$lookup`, where there is no
409
+ // parent document left to hang it off.
410
+ if (path) {
411
+ throw new TypeError(`$sort by '${relPath}.$count' is only supported on the queried entity`);
412
+ }
413
+ out[sortCountField(key)] = sortDirection(countDirection);
414
+ continue;
415
+ }
400
416
  const { join, sort: relationSort } = resolveSortableJoin(relation, relPath, value, joins, `cannot $sort by relation '${relPath}' on MongoDB unless it is populated: only $populate adds its fields to the document`);
401
417
  this.collectSort(join.meta, relationSort, joins, `${relPath}.`, out);
402
418
  }
403
419
  }
420
+ /**
421
+ * The stages a `$sort` by a relation's size needs: one correlated `$lookup` tallying the relation
422
+ * per parent, and the `$set` that lifts the tally onto the document as the field the `$sort` then
423
+ * orders by. A parent with no related row gets no lookup result at all, which is a zero.
424
+ */
425
+ sortCountStages(entity, sort, opts) {
426
+ const meta = getMeta(entity);
427
+ const stages = [];
428
+ const fields = [];
429
+ for (const [key, value] of Object.entries(sort ?? {})) {
430
+ const relOpts = meta.relations[key];
431
+ if (!relOpts || parseSortByCount(value) === undefined) {
432
+ continue;
433
+ }
434
+ const relEntity = relOpts.entity();
435
+ const relMeta = getMeta(relEntity);
436
+ const temp = sortCountField(key);
437
+ const targetScope = this.renderFilter(relEntity, this.scopedWhereMap(relMeta, {}), opts);
438
+ const tail = [{ $count: COUNT_ALIAS }];
439
+ stages.push(this.relationLookup(meta, relOpts, relMeta, relEntity, targetScope, temp, tail, opts), {
440
+ $addFields: { [temp]: { $ifNull: [{ $arrayElemAt: [`$${temp}.${COUNT_ALIAS}`, 0] }, 0] } },
441
+ });
442
+ fields.push(temp);
443
+ }
444
+ return { stages, fields };
445
+ }
404
446
  /** Whether a `$sort` reads a relation, which is what forces the lookups to run before it. */
405
447
  sortsRelations(entity, sort) {
406
448
  if (!sort) {
@@ -459,7 +501,10 @@ export class MongoDialect extends AbstractDialect {
459
501
  readStages(entity, q, opts, extra = {}) {
460
502
  const meta = getMeta(entity);
461
503
  const joins = resolveQueryJoins(meta, q);
462
- const lookups = this.lookupStages(meta, joins, undefined, opts);
504
+ // The tally an ordering by a relation's size reads, and the field it parks it on: both belong
505
+ // with the lookups, since the `$sort` right after them is what they exist for.
506
+ const counted = this.sortCountStages(entity, q.$sort, opts);
507
+ const lookups = [...this.lookupStages(meta, joins, undefined, opts), ...counted.stages];
463
508
  const sort = hasKeys(extra.sort) ? [{ $sort: extra.sort }] : [];
464
509
  const pager = extra.pager ?? [];
465
510
  // Merged into the query's own projection rather than standing in for one: a query that asked
@@ -471,10 +516,14 @@ export class MongoDialect extends AbstractDialect {
471
516
  // which is the one way this differs from a SQL join. Taken back out once the `$sort` that needed
472
517
  // it has run, so ordering by an unpopulated relation costs the same nothing it does there.
473
518
  const sortOnly = [...joins.values()].filter((join) => !join.projected).map((join) => join.path);
474
- const unset = sortOnly.length ? [{ $unset: sortOnly }] : [];
519
+ const dropped = [...sortOnly, ...counted.fields];
520
+ const unset = dropped.length ? [{ $unset: dropped }] : [];
475
521
  // The grouping collapses rows onto the columns it projects, which leaves nothing for an ordering
476
522
  // that reads a lookup those columns do not carry. Refused rather than answered all-equal, and in
477
523
  // the same terms the SQL dialects refuse `SELECT DISTINCT` ordered by an unselected column.
524
+ if (q.$distinct && counted.fields.length) {
525
+ throw new TypeError(`cannot $sort by a relation's $count with $distinct: the grouping keeps only the columns it projects`);
526
+ }
478
527
  if (q.$distinct && sortOnly.length) {
479
528
  throw new TypeError(`cannot $sort by relation '${sortOnly[0]}' with $distinct unless '${sortOnly[0]}' is populated: the grouping keeps only the columns it projects`);
480
529
  }
@@ -1,6 +1,6 @@
1
1
  import type { Document, MongoClient } from 'mongodb';
2
2
  import { AbstractQuerier } from '../querier/index.js';
3
- import type { EntityData, ExtraOptions, IdValue, PrimaryKey, Query, QueryAggMap, QueryAggregate, QueryAggregateResult, QueryConflictPaths, QueryGroupMap, QueryOptions, QuerySearch, TransactionOptions, Type, UpdatePayload } from '../type/index.js';
3
+ import type { EntityData, ExtraOptions, IdValue, PrimaryKey, Query, QueryAggMap, QueryAggregate, QueryAggregateResult, QueryConflictPaths, QueryFilter, QueryGroupMap, QueryOptions, QuerySearch, TransactionOptions, Type, UpdatePayload } from '../type/index.js';
4
4
  import type { MongoDialect } from './mongoDialect.js';
5
5
  export declare class MongodbQuerier extends AbstractQuerier {
6
6
  readonly dialect: MongoDialect;
@@ -22,7 +22,18 @@ export declare class MongodbQuerier extends AbstractQuerier {
22
22
  */
23
23
  private buildVectorPipeline;
24
24
  protected internalAggregate<E extends Document, G extends QueryGroupMap<E>, A extends QueryAggMap<E>>(entity: Type<E>, q: QueryAggregate<E, G, A>, opts?: QueryOptions): Promise<QueryAggregateResult<E, G, A>[]>;
25
- protected internalCount<E extends Document>(entity: Type<E>, qm?: QuerySearch<E>, opts?: QueryOptions): Promise<number>;
25
+ /**
26
+ * A `$required` relation drops parents that have no match, and `$distinct` collapses them, so both
27
+ * totals have to be taken after the stage that does it - which only the read pipeline builds. Every
28
+ * other query counts through {@link internalCount}, which needs no pipeline of its own.
29
+ */
30
+ protected internalFindManyAndCount<E extends Document>(entity: Type<E>, q: Query<E>, opts?: QueryOptions): Promise<[E[], number]>;
31
+ protected internalCount<E extends Document>(entity: Type<E>, qm?: QueryFilter<E>, opts?: QueryOptions): Promise<number>;
32
+ /**
33
+ * The collection's metadata count, which the driver exposes as its own call and which takes no
34
+ * filter - the reason {@link UniversalQuerier.estimatedCount} takes none either.
35
+ */
36
+ estimatedCount<E extends Document>(entity: Type<E>): Promise<number>;
26
37
  /**
27
38
  * The ids matching `q`, in `q`'s own order and page, so a write can name the rows it settled on.
28
39
  * Built from the read pipeline rather than stages assembled here: that dropped `$sort`/`$limit` on
@@ -1,6 +1,16 @@
1
+ import { COUNT_ALIAS } from '../dialect/aliases.js';
2
+ import { hasRequiredJoin } from '../dialect/queryJoins.js';
1
3
  import { getMeta } from '../entity/index.js';
2
4
  import { AbstractQuerier, enrichError } from '../querier/index.js';
3
5
  import { clone, getKeys, getRelationRequestSummary, getSoftDeleteValue, hasKeys, idOnlyQuery, isPagedQuery, populatesRelations, throwNoPendingTransaction, throwPendingTransaction, withoutSoftDeleteFilter, } from '../util/index.js';
6
+ /**
7
+ * `$limit: 0` asks for no rows, the way it does on every SQL dialect - but MongoDB reads `limit(0)`
8
+ * as *unlimited*, so a read that passed it straight to the driver came back with the whole
9
+ * collection. The reads answer it here instead, since no cursor can express it.
10
+ */
11
+ function asksForNoRows(q) {
12
+ return q.$limit === 0;
13
+ }
4
14
  export class MongodbQuerier extends AbstractQuerier {
5
15
  dialect;
6
16
  conn;
@@ -19,6 +29,9 @@ export class MongodbQuerier extends AbstractQuerier {
19
29
  }
20
30
  async internalFindMany(entity, q, opts) {
21
31
  this.dialect.assertNoLock(q);
32
+ if (asksForNoRows(q)) {
33
+ return [];
34
+ }
22
35
  return this.timed('internalFindMany', undefined, async () => {
23
36
  const meta = getMeta(entity);
24
37
  const vectorSort = this.dialect.extractVectorSort(q.$sort);
@@ -51,6 +64,9 @@ export class MongodbQuerier extends AbstractQuerier {
51
64
  });
52
65
  }
53
66
  async *internalFindManyStream(entity, q, opts) {
67
+ if (asksForNoRows(q)) {
68
+ return;
69
+ }
54
70
  const meta = getMeta(entity);
55
71
  const { joinableKeys, toManyKeys } = getRelationRequestSummary(meta, q.$populate);
56
72
  if (joinableKeys.length || toManyKeys.length) {
@@ -99,6 +115,7 @@ export class MongodbQuerier extends AbstractQuerier {
99
115
  if (q.$skip) {
100
116
  cursor.skip(q.$skip);
101
117
  }
118
+ // Only a positive limit reaches the driver; `asksForNoRows` took the zero.
102
119
  if (q.$limit) {
103
120
  cursor.limit(q.$limit);
104
121
  }
@@ -134,14 +151,34 @@ export class MongodbQuerier extends AbstractQuerier {
134
151
  return this.execute((session) => this.collection(entity).aggregate(pipeline, { session }).toArray());
135
152
  });
136
153
  }
154
+ /**
155
+ * A `$required` relation drops parents that have no match, and `$distinct` collapses them, so both
156
+ * totals have to be taken after the stage that does it - which only the read pipeline builds. Every
157
+ * other query counts through {@link internalCount}, which needs no pipeline of its own.
158
+ */
159
+ async internalFindManyAndCount(entity, q, opts) {
160
+ if (!q.$distinct && !hasRequiredJoin(getMeta(entity), q)) {
161
+ return super.internalFindManyAndCount(entity, q, opts);
162
+ }
163
+ const { $sort: _sort, $skip: _skip, $limit: _limit, ...unpaged } = q;
164
+ const [founds, counted] = await Promise.all([
165
+ this.internalFindMany(entity, q, opts),
166
+ this.execute((session) => this.collection(entity)
167
+ .aggregate([...this.dialect.aggregationPipeline(entity, unpaged, opts), { $count: COUNT_ALIAS }], { session })
168
+ .toArray()),
169
+ ]);
170
+ return [founds, counted[0]?.[COUNT_ALIAS] ?? 0];
171
+ }
137
172
  async internalCount(entity, qm = {}, opts) {
138
173
  return this.timed('internalCount', undefined, async () => {
139
174
  if (this.dialect.constrainsRelations(entity, qm.$where)) {
140
175
  const { stages, filter } = this.dialect.whereWithRelations(entity, qm.$where, opts);
141
176
  const [counted] = await this.execute((session) => this.collection(entity)
142
- .aggregate([...stages, { $match: filter }, { $count: 'n' }], { session })
177
+ .aggregate([...stages, { $match: filter }, { $count: COUNT_ALIAS }], {
178
+ session,
179
+ })
143
180
  .toArray());
144
- return counted?.n ?? 0;
181
+ return counted?.[COUNT_ALIAS] ?? 0;
145
182
  }
146
183
  const filter = this.dialect.where(entity, qm.$where, opts);
147
184
  return this.execute((session) => this.collection(entity).countDocuments(filter, {
@@ -149,6 +186,13 @@ export class MongodbQuerier extends AbstractQuerier {
149
186
  }));
150
187
  });
151
188
  }
189
+ /**
190
+ * The collection's metadata count, which the driver exposes as its own call and which takes no
191
+ * filter - the reason {@link UniversalQuerier.estimatedCount} takes none either.
192
+ */
193
+ async estimatedCount(entity) {
194
+ return this.timed('estimatedCount', undefined, async () => this.execute((session) => this.collection(entity).estimatedDocumentCount({ session })));
195
+ }
152
196
  /**
153
197
  * The ids matching `q`, in `q`'s own order and page, so a write can name the rows it settled on.
154
198
  * Built from the read pipeline rather than stages assembled here: that dropped `$sort`/`$limit` on
@@ -11,4 +11,11 @@ export declare class PostgresDialect extends PgLikeSqlDialect {
11
11
  readonly dialectName: SqlDialectName;
12
12
  readonly vectorExtension: string | undefined;
13
13
  upsert<E>(ctx: QueryContext, entity: Type<E>, conflictPaths: QueryConflictPaths<E>, payload: E | E[]): void;
14
+ /**
15
+ * `to_regclass` rather than a `::regclass` cast: it answers `NULL` for a table that does not exist
16
+ * (mid-migration, say) where the cast throws. `GREATEST` because a table nothing has analyzed yet
17
+ * carries `reltuples = -1`, Postgres' "no statistic" (not `NULL`, and not `0`) since PG 14 - handed
18
+ * back raw it would read as a negative row count.
19
+ */
20
+ estimatedCount<E>(ctx: QueryContext, entity: Type<E>): void;
14
21
  }
@@ -1,4 +1,6 @@
1
+ import { COUNT_ALIAS } from '../dialect/aliases.js';
1
2
  import { PgLikeSqlDialect } from '../dialect/pgLikeSqlDialect.js';
3
+ import { getMeta } from '../entity/index.js';
2
4
  /**
3
5
  * PostgreSQL dialect. For node-pg use PgDialect. Neon, Bun SQL, and Cockroach use driver-specific
4
6
  * subclasses. Shared Postgres-wire AST/quoting/JSONB/full-text-search/vector-search logic
@@ -13,4 +15,15 @@ export class PostgresDialect extends PgLikeSqlDialect {
13
15
  // The xmax system column is 0 for a newly inserted row and non-zero for an updated one (MVCC).
14
16
  super.upsert(ctx, entity, conflictPaths, payload, `, (xmax = 0) AS ${this.escapeId('_created')}`);
15
17
  }
18
+ /**
19
+ * `to_regclass` rather than a `::regclass` cast: it answers `NULL` for a table that does not exist
20
+ * (mid-migration, say) where the cast throws. `GREATEST` because a table nothing has analyzed yet
21
+ * carries `reltuples = -1`, Postgres' "no statistic" (not `NULL`, and not `0`) since PG 14 - handed
22
+ * back raw it would read as a negative row count.
23
+ */
24
+ estimatedCount(ctx, entity) {
25
+ ctx.append(`SELECT GREATEST(reltuples, 0)::bigint ${this.escapeId(COUNT_ALIAS, true)} FROM pg_class WHERE oid = to_regclass(`);
26
+ ctx.addValue(this.escapedTableName(getMeta(entity)));
27
+ ctx.append(')');
28
+ }
16
29
  }
@@ -1,4 +1,4 @@
1
- import type { EntityData, ExtraOptions, FieldKey, IdValue, Querier, Query, QueryAggMap, QueryAggregate, QueryAggregateResult, QueryConflictPaths, QueryFindResult, QueryGroupMap, QueryOneProjected, QueryOptions, QueryPopulate, QueryProjected, QuerySearch, QueryUpdateResult, RawRow, RelationKey, TransactionOptions, Type, UpdatePayload } from '../type/index.js';
1
+ import type { EntityData, ExtraOptions, FieldKey, IdValue, Querier, Query, QueryAggMap, QueryAggregate, QueryAggregateResult, QueryConflictPaths, QueryFilter, QueryFindResult, QueryGroupMap, QueryOneProjected, QueryOptions, QueryPage, QueryPopulate, QueryProjected, QuerySearch, QueryStreamProjected, QueryUpdateResult, RawRow, RelationKey, TransactionOptions, Type, UpdatePayload } from '../type/index.js';
2
2
  import { LoggerWrapper } from '../util/index.js';
3
3
  /**
4
4
  * Base class for all database queriers.
@@ -30,23 +30,23 @@ export declare abstract class AbstractQuerier implements Querier {
30
30
  protected resolveEntityQuery<E extends object, Q extends object>(entityOrQuery: Type<E> | (Q & {
31
31
  $entity: Type<E>;
32
32
  }), maybeQueryOrOpts?: Q | QueryOptions, maybeOpts?: QueryOptions): [Type<E>, Q, QueryOptions | undefined];
33
- findOneById<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>, id: IdValue<E>, q?: QueryOneProjected<E, S, V, X, P>, opts?: QueryOptions): Promise<QueryFindResult<E, S, V, X, P> | undefined>;
33
+ findOneById<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>, id: IdValue<E>, q?: QueryOneProjected<E, S, V, X, P, C>, opts?: QueryOptions): Promise<QueryFindResult<E, S, V, X, P, C> | undefined>;
34
34
  /**
35
35
  * Find a single record matching the query.
36
36
  * Supports both entity-as-argument and entity-as-field patterns.
37
37
  */
38
- findOne<E extends object, const S extends FieldKey<E> = never, const V = true, const X extends FieldKey<E> = never, const P extends RelationKey<E> = never>(q: QueryOneProjected<E, S, V, X, P> & {
38
+ findOne<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>(q: QueryOneProjected<E, S, V, X, P, C> & {
39
39
  $entity: Type<E>;
40
- }, opts?: QueryOptions): Promise<QueryFindResult<E, S, V, X, P> | undefined>;
41
- findOne<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: QueryOneProjected<E, S, V, X, P>, opts?: QueryOptions): Promise<QueryFindResult<E, S, V, X, P> | undefined>;
40
+ }, opts?: QueryOptions): Promise<QueryFindResult<E, S, V, X, P, C> | undefined>;
41
+ findOne<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: QueryOneProjected<E, S, V, X, P, C>, opts?: QueryOptions): Promise<QueryFindResult<E, S, V, X, P, C> | undefined>;
42
42
  /**
43
43
  * Find multiple records matching the query.
44
44
  * Supports both entity-as-argument and entity-as-field patterns.
45
45
  */
46
- findMany<E extends object, const S extends FieldKey<E> = never, const V = true, const X extends FieldKey<E> = never, const P extends RelationKey<E> = never>(q: QueryProjected<E, S, V, X, P> & {
46
+ findMany<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>(q: QueryProjected<E, S, V, X, P, C> & {
47
47
  $entity: Type<E>;
48
- }, opts?: QueryOptions): Promise<QueryFindResult<E, S, V, X, P>[]>;
49
- findMany<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: QueryProjected<E, S, V, X, P>, opts?: QueryOptions): Promise<QueryFindResult<E, S, V, X, P>[]>;
48
+ }, opts?: QueryOptions): Promise<QueryFindResult<E, S, V, X, P, C>[]>;
49
+ findMany<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): Promise<QueryFindResult<E, S, V, X, P, C>[]>;
50
50
  protected abstract internalFindMany<E extends object>(entity: Type<E>, q: Query<E>, opts?: QueryOptions): Promise<E[]>;
51
51
  /**
52
52
  * Stream records as an async iterable.
@@ -60,33 +60,49 @@ export declare abstract class AbstractQuerier implements Querier {
60
60
  *
61
61
  * No `afterLoad` hooks on streamed rows.
62
62
  */
63
- 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>(q: QueryProjected<E, S, V, X, P> & {
63
+ 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>(q: QueryStreamProjected<E, S, V, X, P> & {
64
64
  $entity: Type<E>;
65
65
  }, opts?: QueryOptions): AsyncIterable<QueryFindResult<E, S, V, X, P>>;
66
- 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: QueryProjected<E, S, V, X, P>, opts?: QueryOptions): AsyncIterable<QueryFindResult<E, S, V, X, P>>;
66
+ 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>>;
67
67
  protected abstract internalFindManyStream<E extends object>(entity: Type<E>, q: Query<E>, opts?: QueryOptions): AsyncIterable<E>;
68
68
  /**
69
69
  * Find multiple records and return both the records and total count.
70
70
  * Supports both entity-as-argument and entity-as-field patterns.
71
71
  */
72
- findManyAndCount<E extends object, const S extends FieldKey<E> = never, const V = true, const X extends FieldKey<E> = never, const P extends RelationKey<E> = never>(q: QueryProjected<E, S, V, X, P> & {
72
+ findManyAndCount<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>(q: QueryProjected<E, S, V, X, P, C> & {
73
73
  $entity: Type<E>;
74
- }, opts?: QueryOptions): Promise<[QueryFindResult<E, S, V, X, P>[], number]>;
75
- findManyAndCount<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: QueryProjected<E, S, V, X, P>, opts?: QueryOptions): Promise<[QueryFindResult<E, S, V, X, P>[], number]>;
74
+ }, opts?: QueryOptions): Promise<[QueryFindResult<E, S, V, X, P, C>[], number]>;
75
+ findManyAndCount<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): Promise<[QueryFindResult<E, S, V, X, P, C>[], number]>;
76
76
  /**
77
- * Count records matching the query.
78
- * Supports both entity-as-argument and entity-as-field patterns.
77
+ * The page and how many rows the filter matched beyond it. Two statements here, which is what a
78
+ * backend with no way to answer both at once is left with; SQL overrides it to answer from one.
79
+ */
80
+ protected internalFindManyAndCount<E extends object>(entity: Type<E>, q: Query<E>, opts?: QueryOptions): Promise<[E[], number]>;
81
+ /**
82
+ * Count records matching the query, in both the entity-as-argument and entity-as-field patterns.
83
+ * A `$skip`/`$limit` settles the matching ids and counts those, rather than scanning every match;
84
+ * `$sort` never reaches that SELECT, since it changes which rows a page holds, never how many.
79
85
  */
80
- count<E extends object>(entity: Type<E>, q?: QuerySearch<E>, opts?: QueryOptions): Promise<number>;
81
- count<E extends object>(q: QuerySearch<E> & {
86
+ count<E extends object>(entity: Type<E>, q?: QueryPage<E>, opts?: QueryOptions): Promise<number>;
87
+ count<E extends object>(q: QueryPage<E> & {
82
88
  $entity: Type<E>;
83
89
  }, opts?: QueryOptions): Promise<number>;
84
- protected abstract internalCount<E extends object>(entity: Type<E>, q: QuerySearch<E>, opts?: QueryOptions): Promise<number>;
90
+ protected abstract internalCount<E extends object>(entity: Type<E>, q: QueryFilter<E>, opts?: QueryOptions): Promise<number>;
91
+ /**
92
+ * Whether anything matches, in both the entity-as-argument and entity-as-field patterns. A count
93
+ * capped at one row, so the engine stops at the first match instead of scanning every other one.
94
+ */
95
+ exists<E extends object>(entity: Type<E>, q?: QueryFilter<E>, opts?: QueryOptions): Promise<boolean>;
96
+ exists<E extends object>(q: QueryFilter<E> & {
97
+ $entity: Type<E>;
98
+ }, opts?: QueryOptions): Promise<boolean>;
85
99
  /**
86
100
  * Run an aggregate query.
87
101
  */
88
102
  aggregate<E extends object, const G extends QueryGroupMap<E>, const A extends QueryAggMap<E>>(entity: Type<E>, q: QueryAggregate<E, G, A>, opts?: QueryOptions): Promise<QueryAggregateResult<E, G, A>[]>;
89
103
  protected abstract internalAggregate<E extends object, G extends QueryGroupMap<E>, A extends QueryAggMap<E>>(entity: Type<E>, q: QueryAggregate<E, G, A>, opts?: QueryOptions): Promise<QueryAggregateResult<E, G, A>[]>;
104
+ /** Abstract outright: nothing is shared to do around it. See {@link UniversalQuerier.estimatedCount}. */
105
+ abstract estimatedCount<E extends object>(entity: Type<E>): Promise<number>;
90
106
  insertOne<E extends object>(entity: Type<E>, payload: EntityData<E>): Promise<IdValue<E> | undefined>;
91
107
  insertMany<E extends object>(entity: Type<E>, payload: EntityData<E>[]): Promise<IdValue<E>[]>;
92
108
  protected abstract internalInsertMany<E extends object>(entity: Type<E>, payload: EntityData<E>[]): Promise<IdValue<E>[]>;
@@ -1,6 +1,7 @@
1
1
  import { getMeta } from '../entity/index.js';
2
- import { asSelectMap, augmentWhere, clone, filterPersistableRelationKeys, forEachRequestedRelation, getKeys, getRelationRequestSummary, idOnlyQuery, LoggerWrapper, parseRelationAtKey, parseRelationQueryValue, runHooks, someKey, withoutSoftDeleteFilter, } from '../util/index.js';
2
+ import { asSelectMap, augmentWhere, clone, filterPersistableRelationKeys, forEachRequestedRelation, getKeys, getRelationRequestSummary, idOnlyQuery, LoggerWrapper, parentKeyColumn, parseRelationAtKey, parseRelationQueryValue, runHooks, someKey, targetKeyColumn, withoutSoftDeleteFilter, } from '../util/index.js';
3
3
  import { enrichError } from './queryError.js';
4
+ import { fillRelationCounts, withIdForCounts } from './relationCount.js';
4
5
  /**
5
6
  * Rejects a nullish primary key before it reaches a statement. The by-id methods reduce to
6
7
  * `{ $where: id }`, and a nullish `$where` is *no filter*, so an unchecked one addresses the whole
@@ -92,7 +93,8 @@ export class AbstractQuerier {
92
93
  async findMany(entityOrQuery, maybeQueryOrOpts, maybeOpts) {
93
94
  const [entity, q, opts] = this.resolveEntityQuery(entityOrQuery, maybeQueryOrOpts, maybeOpts);
94
95
  this.validateProjectionQuery(entity, q);
95
- const founds = await this.internalFindMany(entity, q, opts);
96
+ const founds = await this.internalFindMany(entity, withIdForCounts(entity, q), opts);
97
+ await fillRelationCounts(this, entity, founds, q.$count);
96
98
  await this.emitHook(entity, 'afterLoad', founds);
97
99
  return founds;
98
100
  }
@@ -104,18 +106,34 @@ export class AbstractQuerier {
104
106
  async findManyAndCount(entityOrQuery, maybeQueryOrOpts, maybeOpts) {
105
107
  const [entity, q, opts] = this.resolveEntityQuery(entityOrQuery, maybeQueryOrOpts, maybeOpts);
106
108
  this.validateProjectionQuery(entity, q);
107
- const { $sort: _, $limit: _l, $skip: _s, ...qCount } = q;
108
- const [founds, count] = await Promise.all([
109
- this.internalFindMany(entity, q, opts),
110
- this.internalCount(entity, qCount, opts),
111
- ]);
109
+ const [founds, count] = await this.internalFindManyAndCount(entity, withIdForCounts(entity, q), opts);
110
+ await fillRelationCounts(this, entity, founds, q.$count);
112
111
  await this.emitHook(entity, 'afterLoad', founds);
113
112
  return [founds, count];
114
113
  }
115
- count(entityOrQuery, maybeQueryOrOpts, maybeOpts) {
114
+ /**
115
+ * The page and how many rows the filter matched beyond it. Two statements here, which is what a
116
+ * backend with no way to answer both at once is left with; SQL overrides it to answer from one.
117
+ */
118
+ async internalFindManyAndCount(entity, q, opts) {
119
+ return Promise.all([
120
+ this.internalFindMany(entity, q, opts),
121
+ this.internalCount(entity, { $where: q.$where }, opts),
122
+ ]);
123
+ }
124
+ async count(entityOrQuery, maybeQueryOrOpts, maybeOpts) {
116
125
  const [entity, q, opts] = this.resolveEntityQuery(entityOrQuery, maybeQueryOrOpts, maybeOpts);
126
+ if (q.$skip !== undefined || q.$limit !== undefined) {
127
+ const meta = getMeta(entity);
128
+ const rows = await this.internalFindMany(entity, idOnlyQuery(meta, { $where: q.$where, $skip: q.$skip, $limit: q.$limit }), opts);
129
+ return rows.length;
130
+ }
117
131
  return this.internalCount(entity, q, opts);
118
132
  }
133
+ async exists(entityOrQuery, maybeQueryOrOpts, maybeOpts) {
134
+ const [entity, q, opts] = this.resolveEntityQuery(entityOrQuery, maybeQueryOrOpts, maybeOpts);
135
+ return (await this.count(entity, { $where: q.$where, $limit: 1 }, opts)) > 0;
136
+ }
119
137
  /**
120
138
  * Run an aggregate query.
121
139
  */
@@ -200,13 +218,12 @@ export class AbstractQuerier {
200
218
  const toInsert = [];
201
219
  const toUpdate = [];
202
220
  const existingIds = [];
203
- const idKey = (meta.id ?? 'id');
204
221
  for (const it of payload) {
205
- const id = it[idKey];
222
+ const id = it[meta.id];
206
223
  if (!id) {
207
224
  toInsert.push(it);
208
225
  }
209
- else if (!someKey(it, (key) => key !== idKey)) {
226
+ else if (!someKey(it, (key) => key !== meta.id)) {
210
227
  existingIds.push(id);
211
228
  }
212
229
  else {
@@ -216,9 +233,9 @@ export class AbstractQuerier {
216
233
  const [insertedIds, updatedIds] = await Promise.all([
217
234
  toInsert.length ? this.insertMany(entity, toInsert) : [],
218
235
  Promise.all(toUpdate.map(async (it) => {
219
- const id = it[idKey];
236
+ const id = it[meta.id];
220
237
  const data = { ...it };
221
- delete data[idKey];
238
+ delete data[meta.id];
222
239
  await this.updateOneById(entity, id, data);
223
240
  return id;
224
241
  })),
@@ -246,10 +263,10 @@ export class AbstractQuerier {
246
263
  }
247
264
  }
248
265
  async fillToManyThroughRelation(payload, meta, relKey, relOpts, relationQuery) {
249
- const localField = relOpts.references[0].local;
266
+ const localField = parentKeyColumn(relOpts);
250
267
  const throughEntity = relOpts.through();
251
268
  const throughMeta = getMeta(throughEntity);
252
- const targetRelKey = getKeys(throughMeta.relations).find((key) => throughMeta.relations[key]?.references.some(({ local }) => local === relOpts.references[1].local));
269
+ const targetRelKey = getKeys(throughMeta.relations).find((key) => throughMeta.relations[key]?.references.some(({ local }) => local === targetKeyColumn(relOpts)));
253
270
  const ids = payload.map((it) => it[meta.id]);
254
271
  // A relation query names the target's columns, not the join table's, so the projection and the
255
272
  // filter belong on the populate below - resolved there against the entity that has them. Spread
@@ -278,7 +295,7 @@ export class AbstractQuerier {
278
295
  this.putChildrenInParents(payload, founds, meta.id, localField, relKey);
279
296
  }
280
297
  async fillToManyOneToMany(payload, meta, relKey, relOpts, relationQuery, relEntity) {
281
- const foreignField = relOpts.references[0].foreign;
298
+ const foreignField = parentKeyColumn(relOpts);
282
299
  // The FK is what putChildrenInParents groups on, so it outlives the relation's projection
283
300
  // either way: added to a whitelisting `$select` (the raw-array form has nothing to augment),
284
301
  // dropped from a subtractive `$exclude`. `relationQuery` is already a clone.
@@ -343,15 +360,9 @@ export class AbstractQuerier {
343
360
  if (!relOpts)
344
361
  continue;
345
362
  const relEntity = relOpts.entity();
346
- const localField = relOpts.references[0].local;
347
- if (relOpts.through) {
348
- const throughEntity = relOpts.through();
349
- await this.deleteMany(throughEntity, { $where: { [localField]: ids } }, opts);
350
- }
351
- else {
352
- const foreignField = relOpts.references[0].foreign;
353
- await this.deleteMany(relEntity, { $where: { [foreignField]: ids } }, opts);
354
- }
363
+ const parentKey = parentKeyColumn(relOpts);
364
+ const target = relOpts.through ? relOpts.through() : relEntity;
365
+ await this.deleteMany(target, { $where: { [parentKey]: ids } }, opts);
355
366
  }
356
367
  }
357
368
  /**
@@ -378,9 +389,9 @@ export class AbstractQuerier {
378
389
  }
379
390
  }
380
391
  async saveToMany(relOpts, relEntity, ids, relPayload, isUpdate) {
381
- const { references, through } = relOpts;
392
+ const { through } = relOpts;
382
393
  if (through) {
383
- const localField = references[0].local;
394
+ const localField = parentKeyColumn(relOpts);
384
395
  const throughEntity = through();
385
396
  if (isUpdate) {
386
397
  await this.deleteMany(throughEntity, { $where: { [localField]: ids } });
@@ -390,12 +401,12 @@ export class AbstractQuerier {
390
401
  // would link every parent to a single shared row instead.
391
402
  for (const id of ids) {
392
403
  const savedIds = await this.saveMany(relEntity, relPayload);
393
- await this.insertMany(throughEntity, savedIds.map((relId) => ({ [references[0].local]: id, [references[1].local]: relId })));
404
+ await this.insertMany(throughEntity, savedIds.map((relId) => ({ [localField]: id, [targetKeyColumn(relOpts)]: relId })));
394
405
  }
395
406
  }
396
407
  return;
397
408
  }
398
- const foreignField = references[0].foreign;
409
+ const foreignField = parentKeyColumn(relOpts);
399
410
  if (isUpdate) {
400
411
  await this.deleteMany(relEntity, { $where: { [foreignField]: ids } });
401
412
  }
@@ -404,7 +415,7 @@ export class AbstractQuerier {
404
415
  }
405
416
  }
406
417
  async saveOneToOne(relEntity, relOpts, ids, relPayload) {
407
- const foreignField = relOpts.references[0].foreign;
418
+ const foreignField = parentKeyColumn(relOpts);
408
419
  if (relPayload === null) {
409
420
  await this.deleteMany(relEntity, { $where: { [foreignField]: ids } });
410
421
  return;
@@ -412,6 +423,8 @@ export class AbstractQuerier {
412
423
  await this.saveMany(relEntity, ids.map((id) => ({ ...relPayload, [foreignField]: id })));
413
424
  }
414
425
  async saveManyToOne(entity, relEntity, relOpts, ids, relPayload) {
426
+ // Not `parentKeyColumn`: a many-to-one points the other way, so this is the *parent's* own column
427
+ // holding the child's id - the one place `references[0].local` does not name a key of the parent.
415
428
  const localField = relOpts.references[0].local;
416
429
  // Per parent: each gets its own reference row, so each `SET` carries a different value.
417
430
  for (const id of ids) {
@@ -1,5 +1,5 @@
1
1
  import type { AbstractDialect } from '../dialect/index.js';
2
- import type { EntityData, ExtraOptions, FieldKey, IdValue, PoolRunOptions, Querier, QuerierPool, QueryAggMap, QueryAggregate, QueryAggregateResult, QueryConflictPaths, QueryFindResult, QueryGroupMap, QueryOneProjected, QueryOptions, QueryProjected, QuerySearch, QueryUpdateResult, RelationKey, TransactionOptions, Type, UpdatePayload } from '../type/index.js';
2
+ import type { EntityData, ExtraOptions, FieldKey, IdValue, PoolRunOptions, Querier, QuerierPool, QueryAggMap, QueryAggregate, QueryAggregateResult, QueryConflictPaths, QueryFilter, QueryFindResult, QueryGroupMap, QueryOneProjected, QueryOptions, QueryPage, QueryProjected, QuerySearch, QueryStreamProjected, QueryUpdateResult, RelationKey, TransactionOptions, Type, UpdatePayload } from '../type/index.js';
3
3
  /**
4
4
  * Base pool: dialect id and behavior come only from the `dialect` instance (see {@link QuerierPool}).
5
5
  */
@@ -24,17 +24,19 @@ export declare abstract class AbstractQuerierPool<Q extends Querier, D extends A
24
24
  withQuerier<T>(callback: (querier: Q) => Promise<T>, opts?: PoolRunOptions): Promise<T>;
25
25
  /** Run `fn` under `context` (an enclosing {@link withContext}) when provided, else run it as-is. */
26
26
  private runScoped;
27
- findOneById<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>, id: IdValue<E>, q?: QueryOneProjected<E, S, V, X, P>, opts?: QueryOptions): Promise<QueryFindResult<E, S, V, X, P> | undefined>;
28
- findOne<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: QueryOneProjected<E, S, V, X, P>, opts?: QueryOptions): Promise<QueryFindResult<E, S, V, X, P> | undefined>;
29
- findMany<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: QueryProjected<E, S, V, X, P>, opts?: QueryOptions): Promise<QueryFindResult<E, S, V, X, P>[]>;
27
+ findOneById<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>, id: IdValue<E>, q?: QueryOneProjected<E, S, V, X, P, C>, opts?: QueryOptions): Promise<QueryFindResult<E, S, V, X, P, C> | undefined>;
28
+ findOne<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: QueryOneProjected<E, S, V, X, P, C>, opts?: QueryOptions): Promise<QueryFindResult<E, S, V, X, P, C> | undefined>;
29
+ findMany<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): Promise<QueryFindResult<E, S, V, X, P, C>[]>;
30
30
  /**
31
31
  * The connection outlives the call here: it is held until the iterator is drained or closed by a
32
32
  * `break`/`throw`. Abandoning the iterator instead leaks it until GC, so consume it in a `for await`.
33
33
  */
34
- 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: QueryProjected<E, S, V, X, P>, opts?: QueryOptions): AsyncGenerator<QueryFindResult<E, S, V, X, P>>;
35
- findManyAndCount<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: QueryProjected<E, S, V, X, P>, opts?: QueryOptions): Promise<[QueryFindResult<E, S, V, X, P>[], number]>;
36
- count<E extends object>(entity: Type<E>, q?: QuerySearch<E>, opts?: QueryOptions): Promise<number>;
34
+ 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): AsyncGenerator<QueryFindResult<E, S, V, X, P>>;
35
+ findManyAndCount<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): Promise<[QueryFindResult<E, S, V, X, P, C>[], number]>;
36
+ count<E extends object>(entity: Type<E>, q?: QueryPage<E>, opts?: QueryOptions): Promise<number>;
37
+ exists<E extends object>(entity: Type<E>, q?: QueryFilter<E>, opts?: QueryOptions): Promise<boolean>;
37
38
  aggregate<E extends object, const G extends QueryGroupMap<E>, const A extends QueryAggMap<E>>(entity: Type<E>, q: QueryAggregate<E, G, A>, opts?: QueryOptions): Promise<QueryAggregateResult<E, G, A>[]>;
39
+ estimatedCount<E extends object>(entity: Type<E>): Promise<number>;
38
40
  insertOne<E extends object>(entity: Type<E>, payload: EntityData<E>): Promise<IdValue<E> | undefined>;
39
41
  insertMany<E extends object>(entity: Type<E>, payload: EntityData<E>[]): Promise<IdValue<E>[]>;
40
42
  updateOneById<E extends object>(entity: Type<E>, id: IdValue<E>, payload: UpdatePayload<E>, opts?: QueryOptions): Promise<number>;