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
@@ -2,7 +2,7 @@ import { COUNT_ALIAS } from '../dialect/aliases.js';
2
2
  import { hasRequiredJoin } from '../dialect/queryJoins.js';
3
3
  import { fieldOf, getMeta, idOf, namesKey, soleIdOf } from '../entity/index.js';
4
4
  import { AbstractQuerier, enrichError } from '../querier/index.js';
5
- import { clone, getKeys, getRelationRequestSummary, getSoftDeleteValue, hasKeys, idOnlyQuery, isPagedQuery, populatesRelations, queryChildrenOf, throwNoPendingTransaction, throwPendingTransaction, withoutSoftDeleteFilter, } from '../util/index.js';
5
+ import { clone, getKeys, getSoftDeleteValue, hasKeys, idOnlyQuery, isPagedQuery, populatesRelations, throwNoPendingTransaction, throwPendingTransaction, withoutSoftDeleteFilter, } from '../util/index.js';
6
6
  /**
7
7
  * `$limit: 0` asks for no rows, the way it does on every SQL dialect - but MongoDB reads `limit(0)`
8
8
  * as *unlimited*, so a read that passed it straight to the driver came back with the whole
@@ -11,12 +11,6 @@ import { clone, getKeys, getRelationRequestSummary, getSoftDeleteValue, hasKeys,
11
11
  function asksForNoRows(q) {
12
12
  return q.$limit === 0;
13
13
  }
14
- /**
15
- * What MongoDB accepts in one pipeline. Bisected against a real server: 1000 top-level stages are
16
- * accepted and 1001 refused (`Pipeline length must be no longer than 1000 stages`), and a
17
- * `$unionWith`'s own sub-pipeline stages do not count toward it.
18
- */
19
- const MAX_PIPELINE_STAGES = 1000;
20
14
  export class MongodbQuerier extends AbstractQuerier {
21
15
  dialect;
22
16
  conn;
@@ -34,109 +28,20 @@ export class MongodbQuerier extends AbstractQuerier {
34
28
  });
35
29
  }
36
30
  async internalFindMany(entity, q, opts) {
37
- this.dialect.assertNoLock(q);
38
31
  if (asksForNoRows(q)) {
39
32
  return [];
40
33
  }
41
34
  return this.timed('internalFindMany', undefined, async () => {
42
- const meta = getMeta(entity);
43
- const vectorSort = this.dialect.extractVectorSort(q.$sort);
44
- let documents;
45
- if (vectorSort) {
46
- const pipeline = this.buildVectorPipeline(entity, q, vectorSort, opts);
47
- documents = await this.runPipeline(entity, meta, pipeline);
48
- // to-many relations need their own query, exactly as in the non-vector path
49
- await this.fillToManyRelations(entity, documents, q.$populate);
50
- }
51
- else {
52
- // A relation condition needs `$lookup`, so it forces the aggregation path just like populating
53
- // one does - and so does ordering by a relation, which reads what a lookup produced, and
54
- // `$distinct`, which is a `$group`. A plain `find` cursor can express none of the four.
55
- if (q.$distinct ||
56
- populatesRelations(meta, q.$populate) ||
57
- this.dialect.constrainsRelations(entity, q.$where) ||
58
- this.dialect.sortsRelations(entity, q.$sort)) {
59
- const pipeline = this.dialect.aggregationPipeline(entity, q, opts);
60
- documents = await this.runPipeline(entity, meta, pipeline);
61
- await this.fillToManyRelations(entity, documents, q.$populate);
62
- }
63
- else {
64
- const cursor = this.buildFindCursor(entity, q, opts);
65
- documents = await this.execute(() => cursor.toArray());
66
- documents = this.dialect.normalizeIds(meta, documents);
67
- }
68
- }
69
- return documents;
35
+ const cursor = this.readCursor(entity, q, opts);
36
+ return this.dialect.normalizeIds(getMeta(entity), await this.execute(() => cursor.toArray()));
70
37
  });
71
38
  }
72
- /**
73
- * Every parent's own bounded page. One `$unionWith` per parent after the first, so the whole page is
74
- * one round trip - measured ~6x faster than a query each (11.0 ms -> 1.9 ms at 50 parents, 87.5 ms
75
- * -> 14.1 ms at 500), because `execute` serializes on the session and a query each is N round trips
76
- * rather than N concurrent ones.
77
- *
78
- * Both arms return documents with their own relations already filled, so this only chooses between
79
- * them: leaving that to the caller once meant the arm that fills its own did it twice.
80
- * [The design](../../../../architecture/populate-limits.md).
81
- */
82
- async internalFindManyPerParent(entity, q, { joins, parents }) {
83
- const queries = parents.map((parent) => queryChildrenOf(q, joins, parent));
84
- // A vector sort is not a degraded fallback here, it is the only expressible form: `$vectorSearch`
85
- // has to be the first stage of a pipeline, so it cannot be one of N `$unionWith` branches. Read a
86
- // parent at a time it stays correct, because `buildVectorSearchStage` passes the query's `$where`
87
- // - which carries this parent's key - into the search as its filter, so each parent gets its own
88
- // nearest rather than a share of the collection's.
89
- if (this.dialect.extractVectorSort(q.$sort)) {
90
- return this.readEachInTurn(entity, queries);
91
- }
92
- const pipelines = queries.map((it) => this.dialect.aggregationPipeline(entity, it));
93
- // Counted, not estimated: the leading branch's own length grows with every `$lookup` a populate
94
- // adds, so a fixed parent budget would let a richer query overflow at the server instead.
95
- const stages = pipelines[0].length + pipelines.length - 1;
96
- return stages > MAX_PIPELINE_STAGES
97
- ? this.readEachInTurn(entity, queries)
98
- : this.readInOnePipeline(entity, q, pipelines);
99
- }
100
- /** Every parent's page as one `$unionWith` pipeline. */
101
- async readInOnePipeline(entity, q, pipelines) {
102
- const meta = getMeta(entity);
103
- const [first, ...rest] = pipelines;
104
- const documents = await this.runPipeline(entity, meta, [
105
- ...first,
106
- ...rest.map((pipeline) => ({ $unionWith: { coll: meta.name, pipeline } })),
107
- ]);
108
- await this.fillToManyRelations(entity, documents, q.$populate);
109
- return documents;
110
- }
111
- /** A query each, for what one pipeline cannot carry. `internalFindMany` fills its own relations. */
112
- async readEachInTurn(entity, queries) {
113
- const documents = [];
114
- for (const query of queries) {
115
- documents.push(...(await this.internalFindMany(entity, query)));
116
- }
117
- return documents;
118
- }
119
39
  async *internalFindManyStream(entity, q, opts) {
120
40
  if (asksForNoRows(q)) {
121
41
  return;
122
42
  }
123
43
  const meta = getMeta(entity);
124
- const { joinableKeys, toManyKeys } = getRelationRequestSummary(meta, q.$populate);
125
- if (joinableKeys.length || toManyKeys.length) {
126
- const parts = [];
127
- if (joinableKeys.length)
128
- parts.push(`joinable: ${joinableKeys.join(', ')}`);
129
- if (toManyKeys.length)
130
- parts.push(`toMany: ${toManyKeys.join(', ')}`);
131
- throw new TypeError(`findManyStream does not load relations on MongoDB (${parts.join('; ')}). Use findMany with $populate (or legacy relation keys in $select) so aggregation and fill logic can run.`);
132
- }
133
- // An ordering that names a relation reads a field only a `$lookup` produces, and a stream has no
134
- // pipeline to add one: MongoDB ranks every document equal and hands back natural order. `findMany`
135
- // takes the aggregation path for exactly this case, so it is the one that can honor the clause.
136
- if (this.dialect.sortsRelations(entity, q.$sort)) {
137
- throw new TypeError('findManyStream does not order by a relation on MongoDB. Use findMany, whose aggregation pipeline adds the $lookup the ordering reads.');
138
- }
139
- const cursor = this.buildFindCursor(entity, q, opts);
44
+ const cursor = this.readCursor(entity, q, opts);
140
45
  try {
141
46
  for await (const doc of cursor) {
142
47
  const [normalized] = this.dialect.normalizeIds(meta, [doc]);
@@ -147,6 +52,32 @@ export class MongodbQuerier extends AbstractQuerier {
147
52
  throw enrichError(err, this.logger, 'internalFindManyStream');
148
53
  }
149
54
  }
55
+ /**
56
+ * The cursor a read runs on: the aggregation pipeline for a clause only a stage can express - a
57
+ * lookup, a grouping, a vector search - and the plain `find` cursor for everything else. One routing
58
+ * for a read and a stream alike, so both load the same relations.
59
+ */
60
+ readCursor(entity, q, opts) {
61
+ this.dialect.assertNoLock(q);
62
+ const vectorSort = this.dialect.extractVectorSort(q.$sort);
63
+ const pipeline = vectorSort
64
+ ? this.buildVectorPipeline(entity, q, vectorSort, opts)
65
+ : this.readsThroughPipeline(entity, q) && this.dialect.aggregationPipeline(entity, q, opts);
66
+ return pipeline
67
+ ? this.collection(entity).aggregate(pipeline, { session: this.session })
68
+ : this.buildFindCursor(entity, q, opts);
69
+ }
70
+ /**
71
+ * Whether a read needs stages a `find` cursor cannot express: a lookup to populate, count, filter
72
+ * or order by a relation, and the grouping `$distinct` is.
73
+ */
74
+ readsThroughPipeline(entity, q) {
75
+ return (!!q.$distinct ||
76
+ hasKeys(q.$count) ||
77
+ populatesRelations(getMeta(entity), q.$populate) ||
78
+ this.dialect.constrainsRelations(entity, q.$where) ||
79
+ this.dialect.sortsRelations(entity, q.$sort));
80
+ }
150
81
  buildScalarProjection(entity, q) {
151
82
  return this.dialect.select(entity, q.$select, q.$exclude);
152
83
  }
@@ -174,11 +105,6 @@ export class MongodbQuerier extends AbstractQuerier {
174
105
  }
175
106
  return cursor;
176
107
  }
177
- /** Execute an aggregation pipeline and normalize `_id` → `id`. */
178
- async runPipeline(entity, meta, pipeline) {
179
- const documents = await this.execute((session) => this.collection(entity).aggregate(pipeline, { session }).toArray());
180
- return this.dialect.normalizeIds(meta, documents);
181
- }
182
108
  /**
183
109
  * Build an aggregation pipeline for vector similarity search.
184
110
  * `$vectorSearch` is always the first stage; `$where` is merged into its `filter`.
@@ -191,7 +117,7 @@ export class MongodbQuerier extends AbstractQuerier {
191
117
  // that follow treat it like any other - and a query with no projection keeps its own columns.
192
118
  ...(scoreAlias ? [{ $addFields: { [scoreAlias]: { $meta: 'vectorSearchScore' } } }] : []),
193
119
  // `$vectorSearch` has already applied `$limit`, so the pager is its own.
194
- ...this.dialect.readStages(entity, q, opts, {
120
+ ...this.dialect.readStages(entity, q, {
195
121
  sort: this.dialect.sort(entity, vectorSort.regularSort, q.$populate),
196
122
  project: scoreAlias ? { [scoreAlias]: 1 } : undefined,
197
123
  }),
@@ -200,7 +126,6 @@ export class MongodbQuerier extends AbstractQuerier {
200
126
  async internalAggregate(entity, q, opts) {
201
127
  return this.timed('internalAggregate', undefined, async () => {
202
128
  const pipeline = this.dialect.buildAggregateStages(entity, q, opts);
203
- // oxlint-disable-next-line typescript/no-explicit-any -- aggregate result type matches QueryAggregateResult at runtime but TS can't verify
204
129
  return this.execute((session) => this.collection(entity).aggregate(pipeline, { session }).toArray());
205
130
  });
206
131
  }
@@ -1,3 +1,4 @@
1
+ import { type RelationRows } from '../dialect/abstractSqlDialect.js';
1
2
  import { MergeSqlDialect } from '../dialect/mergeSqlDialect.js';
2
3
  import type { DialectFeatures, EntityMeta, FieldOptions, InsertIdSource, Query, QueryContext, QueryOptions, QueryPager, QuerySizeComparisonOps, Type, VectorDistance, VectorMetric } from '../type/index.js';
3
4
  /**
@@ -65,6 +66,21 @@ export declare class MsSqlDialect extends MergeSqlDialect {
65
66
  * with room for the sign and the point.
66
67
  */
67
68
  protected selectFieldExpr(escapedColumn: string, field: FieldOptions): string;
69
+ /**
70
+ * The rows read as they are, `FOR JSON PATH` making the array. It nests a dotted key and leaves a
71
+ * null out, which is what unflattening a row with a joined column does, so a row with none keeps its
72
+ * nulls. `JSON_QUERY` keeps the array JSON inside a parent's own `FOR JSON`.
73
+ */
74
+ protected appendRelationArray(ctx: QueryContext, rows: RelationRows): void;
75
+ /**
76
+ * What JSON would round or misread crosses it as text: a number exactly, style 3 keeping a float's
77
+ * 17 digits, bytes as hex, and a date in UTC with its offset, which is how `tedious` reads one.
78
+ */
79
+ protected readonly carriedFields: {
80
+ numeric: (expr: string) => string;
81
+ blob: (expr: string) => string;
82
+ date: (expr: string) => string;
83
+ };
68
84
  /** Named parameters, which `tedious` binds by name rather than by position. */
69
85
  placeholder(index: number): string;
70
86
  /** `OUTPUT` reads the written row out of the `INSERTED` pseudo-table rather than `RETURNING` it. */
@@ -1,4 +1,6 @@
1
- import { COUNT_ALIAS, JSON_ELEM_ALIAS_PREFIX } from '../dialect/aliases.js';
1
+ import { relationTermKey } from '../dialect/abstractSqlDialect.js';
2
+ import { COUNT_ALIAS, JSON_ELEM_ALIAS } from '../dialect/aliases.js';
3
+ import { BYTES_PREFIX } from '../dialect/hydrateColumn.js';
2
4
  import { jsonPath } from '../dialect/jsonSql.js';
3
5
  import { MergeSqlDialect } from '../dialect/mergeSqlDialect.js';
4
6
  import { getMeta } from '../entity/index.js';
@@ -127,6 +129,26 @@ export class MsSqlDialect extends MergeSqlDialect {
127
129
  const exactDecimal = field.type === String && fieldOptionsToCanonical(field).category === 'decimal';
128
130
  return exactDecimal ? `CONVERT(NVARCHAR(41), ${escapedColumn})` : escapedColumn;
129
131
  }
132
+ /**
133
+ * The rows read as they are, `FOR JSON PATH` making the array. It nests a dotted key and leaves a
134
+ * null out, which is what unflattening a row with a joined column does, so a row with none keeps its
135
+ * nulls. `JSON_QUERY` keeps the array JSON inside a parent's own `FOR JSON`.
136
+ */
137
+ appendRelationArray(ctx, rows) {
138
+ const rowsCtx = ctx.createFragment();
139
+ const { terms } = this.read(rowsCtx, rows.entity, rows.query, { alias: rows.alias, json: true }, rows.joins);
140
+ const nulls = terms.some((term) => relationTermKey(term).includes('.')) ? '' : ', INCLUDE_NULL_VALUES';
141
+ ctx.append(`JSON_QUERY(COALESCE((${rowsCtx.sql} FOR JSON PATH${nulls}), '[]'))`);
142
+ }
143
+ /**
144
+ * What JSON would round or misread crosses it as text: a number exactly, style 3 keeping a float's
145
+ * 17 digits, bytes as hex, and a date in UTC with its offset, which is how `tedious` reads one.
146
+ */
147
+ carriedFields = {
148
+ numeric: (expr) => `CONVERT(VARCHAR(40), ${expr}, 3)`,
149
+ blob: (expr) => `${this.escape(BYTES_PREFIX)} + CONVERT(VARCHAR(MAX), ${expr}, 2)`,
150
+ date: (expr) => `CONVERT(VARCHAR(33), CAST(${expr} AS DATETIMEOFFSET), 127)`,
151
+ };
130
152
  /** Named parameters, which `tedious` binds by name rather than by position. */
131
153
  placeholder(index) {
132
154
  return `@p${index}`;
@@ -293,12 +315,12 @@ export class MsSqlDialect extends MergeSqlDialect {
293
315
  : `JSON_VALUE(${alias}.${this.#elem.value}, ${jsonPath(field)})`;
294
316
  }
295
317
  jsonAll(ctx, jsonField, value) {
296
- const alias = ctx.nextAlias(JSON_ELEM_ALIAS_PREFIX);
318
+ const alias = ctx.claimAlias(JSON_ELEM_ALIAS);
297
319
  const conditions = value.map((val) => `EXISTS (SELECT 1 FROM OPENJSON(${jsonField}) ${alias} WHERE ${alias}.${this.#elem.value} = ${this.jsonScalarParam(ctx, val)})`);
298
320
  return `(${conditions.join(' AND ')})`;
299
321
  }
300
322
  jsonSize(ctx, jsonField, value) {
301
- const alias = ctx.nextAlias(JSON_ELEM_ALIAS_PREFIX);
323
+ const alias = ctx.claimAlias(JSON_ELEM_ALIAS);
302
324
  return this.buildFragment(ctx, (fragmentCtx) => this.buildSizeComparison(fragmentCtx, () => fragmentCtx.append(`(SELECT COUNT(*) FROM OPENJSON(${jsonField}) ${alias})`), value));
303
325
  }
304
326
  /** `JSON_MODIFY` takes one path per call, so several keys chain into one expression. */
@@ -323,7 +345,7 @@ export class MsSqlDialect extends MergeSqlDialect {
323
345
  * remove-by-value. `JSON_QUERY` is what marks the rebuilt text as JSON rather than a string.
324
346
  */
325
347
  jsonPullKey(ctx, expr, escapedCol, key, value) {
326
- const alias = ctx.nextAlias(JSON_ELEM_ALIAS_PREFIX);
348
+ const alias = ctx.claimAlias(JSON_ELEM_ALIAS);
327
349
  const val = `${alias}.${this.#elem.value}`;
328
350
  // `OPENJSON` hands back a string element unquoted and a null one as SQL NULL, so each survivor is
329
351
  // re-encoded from its reported `type` before the array is put back together - concatenated raw,
@@ -6,4 +6,6 @@ export declare class MySqlDialect extends MysqlLikeSqlDialect {
6
6
  * "subject to removal in a future version"; aliasing the inserted row (8.0.19+) is its replacement.
7
7
  */
8
8
  protected readonly upsertNewRowAlias = "_uql_new";
9
+ /** A `SET_VAR` hint, which MySQL reads only in a statement's first `SELECT`. */
10
+ protected applySettings(sql: string, settings: readonly string[]): string;
9
11
  }
@@ -7,4 +7,8 @@ export class MySqlDialect extends MysqlLikeSqlDialect {
7
7
  * "subject to removal in a future version"; aliasing the inserted row (8.0.19+) is its replacement.
8
8
  */
9
9
  upsertNewRowAlias = UPSERT_NEW_ROW_ALIAS;
10
+ /** A `SET_VAR` hint, which MySQL reads only in a statement's first `SELECT`. */
11
+ applySettings(sql, settings) {
12
+ return sql.replace('SELECT ', `SELECT /*+ ${settings.map((setting) => `SET_VAR(${setting})`).join(' ')} */ `);
13
+ }
10
14
  }
@@ -1,5 +1,5 @@
1
- import type { EntityData, EntityId, ExtraOptions, FieldKey, IdValue, Querier, Query, QueryAggMap, QueryAggregate, QueryAggregateResult, QueryConflictPaths, QueryFilter, QueryFindResult, QueryGroupMap, QueryOneProjected, QueryOptions, QueryPage, QueryPopulate, QueryProjected, QuerySearch, QueryStreamProjected, PrimaryKey, QueryUpdateResult, QueryUpsertOneResult, QueryUpsertManyResult, RawRow, RelationKey, TransactionOptions, Type, UpdatePayload, WrittenId } from '../type/index.js';
2
- import { LoggerWrapper, type ParentJoin, type ParentPartition } from '../util/index.js';
1
+ import type { EntityData, EntityId, ExtraOptions, FieldKey, IdValue, Querier, Query, QueryAggMap, QueryAggregate, QueryAggregateResult, QueryConflictPaths, QueryFilter, QueryFindResult, QueryGroupMap, QueryOneProjected, QueryOptions, QueryPage, QueryProjected, QuerySearch, PrimaryKey, QueryUpdateResult, QueryUpsertOneResult, QueryUpsertManyResult, RelationKey, TransactionOptions, Type, UpdatePayload, WrittenId } from '../type/index.js';
2
+ import { LoggerWrapper } from '../util/index.js';
3
3
  /** Base class for all database queriers. */
4
4
  export declare abstract class AbstractQuerier implements Querier {
5
5
  readonly extra?: ExtraOptions | undefined;
@@ -44,31 +44,20 @@ export declare abstract class AbstractQuerier implements Querier {
44
44
  $entity: Type<E>;
45
45
  }, opts?: QueryOptions): Promise<QueryFindResult<E, S, V, X, P, C>[]>;
46
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>(entity: Type<E>, q: QueryProjected<E, S, V, X, P, C>, opts?: QueryOptions): Promise<QueryFindResult<E, S, V, X, P, C>[]>;
47
- protected abstract internalFindMany<E extends object>(entity: Type<E>, q: Query<E>, opts?: QueryOptions): Promise<E[]>;
48
47
  /**
49
- * Stream records as an async iterable.
50
- * Supports both entity-as-argument and entity-as-field patterns.
51
- *
52
- * **SQL:** Joinable relations (e.g. m1 / one-to-one) are still emitted in the streamed SQL; **to-many**
53
- * relations are not filled (no second query) - requesting them throws a clear `TypeError`.
54
- *
55
- * **MongoDB:** Relation loading uses aggregation + follow-up queries in `findMany`; **streams use a plain
56
- * find cursor**, so any requested relation keys in `$select` / `$populate` throw a `TypeError`.
57
- *
58
- * No `afterLoad` hooks on streamed rows.
48
+ * The rows matching `q`, each populated relation and `$count` read with them in the same statement
49
+ * or pipeline. [The design](../../../../architecture/relations-in-one-statement.md).
59
50
  */
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>(q: QueryStreamProjected<E, S, V, X, P> & {
61
- $entity: Type<E>;
62
- }, opts?: QueryOptions): AsyncIterable<QueryFindResult<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>(entity: Type<E>, q: QueryStreamProjected<E, S, V, X, P>, opts?: QueryOptions): AsyncIterable<QueryFindResult<E, S, V, X, P>>;
51
+ protected abstract internalFindMany<E extends object>(entity: Type<E>, q: Query<E>, opts?: QueryOptions): Promise<E[]>;
64
52
  /**
65
- * The children of every parent in `parents`, at most `$limit` each after `$skip` - what a to-many
66
- * `$populate` carrying either one means. One statement, not one per parent.
67
- *
68
- * Abstract rather than defaulted: a default would be N queries, which is the N+1 that batched
69
- * population exists to prevent, and it would be invisible to whichever backend forgot to override.
53
+ * Stream records as an async iterable, in both the entity-as-argument and entity-as-field patterns.
54
+ * Each row streams with its populated relations and counts, read by the same statement or pipeline
55
+ * `findMany` runs. No `afterLoad` hooks on streamed rows.
70
56
  */
71
- protected abstract internalFindManyPerParent<E extends object>(entity: Type<E>, q: Query<E>, partition: ParentPartition): Promise<E[]>;
57
+ 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>(q: QueryProjected<E, S, V, X, P, C> & {
58
+ $entity: Type<E>;
59
+ }, opts?: QueryOptions): AsyncIterable<QueryFindResult<E, S, V, X, P, C>>;
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>>;
72
61
  protected abstract internalFindManyStream<E extends object>(entity: Type<E>, q: Query<E>, opts?: QueryOptions): AsyncIterable<E>;
73
62
  /**
74
63
  * Find multiple records and return both the records and total count.
@@ -157,19 +146,6 @@ export declare abstract class AbstractQuerier implements Querier {
157
146
  * inserts. A composite is always named. The hooks follow the statement: a named row fires the upsert pair.
158
147
  */
159
148
  saveMany<E extends object>(entity: Type<E>, payload: EntityData<E>[]): Promise<(WrittenId<E> | undefined)[]>;
160
- protected fillToManyRelations<E>(entity: Type<E>, payload: E[], populate?: QueryPopulate<E>): Promise<void>;
161
- private fillToManyThroughRelation;
162
- private fillToManyOneToMany;
163
- /**
164
- * The children of a whole page of parents, however the relation asked for them: one bounded branch
165
- * per parent when it wants a share of its own, otherwise a single flat statement over an `IN (...)`
166
- * list, which is both correct and cheaper.
167
- *
168
- * The one place that decision is made - a one-to-many and the junction of a many-to-many differ in
169
- * what they query, never in how the page is spread over its parents.
170
- */
171
- private findChildrenOf;
172
- protected putChildrenInParents<E>(parents: E[], children: RawRow[], joins: readonly ParentJoin[], relKey: keyof E & string): void;
173
149
  protected insertRelations<E extends object>(entity: Type<E>, payload: E[]): Promise<void>;
174
150
  protected updateRelations<E extends object>(entity: Type<E>, q: QuerySearch<E>, payload: UpdatePayload<E>, opts?: QueryOptions): Promise<void>;
175
151
  /**
@@ -207,6 +183,14 @@ export declare abstract class AbstractQuerier implements Querier {
207
183
  transaction<T>(callback: () => Promise<T>, opts?: TransactionOptions): Promise<T>;
208
184
  /** Whether anything at all - a global listener or the entity itself - handles `event`. */
209
185
  private hasHook;
186
+ /** Whether an `afterLoad` listens on the entity a read returns or on any relation it populated. */
187
+ private listensForLoad;
188
+ /**
189
+ * `afterLoad` for every row a read loaded, a populated relation's before the rows holding them, so a
190
+ * parent's hook sees its children as their own hooks left them. Rows are walked only where a hook
191
+ * listens.
192
+ */
193
+ private emitLoaded;
210
194
  /**
211
195
  * The ids of `rows`, read back by the columns an upsert matched them on, for a statement that could
212
196
  * not report them in payload order. A row that no read row matches, or that two do, keeps
@@ -1,7 +1,6 @@
1
1
  import { assertSoleId, getMeta, idOf, namesKey, relationOf, soleIdOf } from '../entity/index.js';
2
- import { asSelectMap, childrenOf, clone, dataKeyed, entityName, fillOnFields, filterPersistableRelationKeys, forEachRequestedRelation, getKeys, getRelationRequestSummary, idOnlyQuery, isScalarId, joinedColumns, keyColumns, LoggerWrapper, isBoundedPerParent, parentJoins, queryChildrenOfAll, queryLoggerFor, parseRelationAtKey, parseRelationQueryValue, rowKey, runHooks, someKey, targetKeyColumns, whereIds, withoutSoftDeleteFilter, } from '../util/index.js';
2
+ import { childrenOf, clone, entityName, fillOnFields, filterPersistableRelationKeys, forEachRequestedRelation, getKeys, getRelationRequestSummary, idOnlyQuery, isScalarId, LoggerWrapper, parentJoins, queryLoggerFor, parseRelationAtKey, parseRelationQueryValue, rowKey, runHooks, someKey, targetKeyColumns, whereIds, withoutSoftDeleteFilter, } from '../util/index.js';
3
3
  import { enrichError } from './queryError.js';
4
- import { fillRelationCounts, withIdForCounts } from './relationCount.js';
5
4
  /**
6
5
  * Rejects a nullish primary key before it reaches a statement. The by-id methods reduce to
7
6
  * `{ $where: { id } }`, and a key compared to `undefined` is *no filter*, so an unchecked one
@@ -130,14 +129,11 @@ export class AbstractQuerier {
130
129
  async findMany(entityOrQuery, maybeQueryOrOpts, maybeOpts) {
131
130
  const [entity, q, opts] = this.resolveEntityQuery(entityOrQuery, maybeQueryOrOpts, maybeOpts);
132
131
  this.validateProjectionQuery(entity, q);
133
- const founds = await this.internalFindMany(entity, withIdForCounts(entity, q), opts);
132
+ const founds = await this.internalFindMany(entity, q, opts);
134
133
  // Guarded here rather than only inside: awaiting a call that returns at once still costs every read
135
- // a promise and a turn of the microtask queue, and most reads count nothing and hook nothing.
136
- if (q.$count) {
137
- await fillRelationCounts(this, entity, founds, q.$count);
138
- }
139
- if (this.hasHook(entity, 'afterLoad')) {
140
- await this.emitHook(entity, 'afterLoad', founds);
134
+ // a promise and a turn of the microtask queue, and most reads hook nothing.
135
+ if (this.listensForLoad(entity, q.$populate)) {
136
+ await this.emitLoaded(entity, founds, q.$populate);
141
137
  }
142
138
  return founds;
143
139
  }
@@ -149,9 +145,10 @@ export class AbstractQuerier {
149
145
  async findManyAndCount(entityOrQuery, maybeQueryOrOpts, maybeOpts) {
150
146
  const [entity, q, opts] = this.resolveEntityQuery(entityOrQuery, maybeQueryOrOpts, maybeOpts);
151
147
  this.validateProjectionQuery(entity, q);
152
- const [founds, count] = await this.internalFindManyAndCount(entity, withIdForCounts(entity, q), opts);
153
- await fillRelationCounts(this, entity, founds, q.$count);
154
- await this.emitHook(entity, 'afterLoad', founds);
148
+ const [founds, count] = await this.internalFindManyAndCount(entity, q, opts);
149
+ if (this.listensForLoad(entity, q.$populate)) {
150
+ await this.emitLoaded(entity, founds, q.$populate);
151
+ }
155
152
  return [founds, count];
156
153
  }
157
154
  /**
@@ -342,123 +339,6 @@ export class AbstractQuerier {
342
339
  await (toInsert.length && toUpsert.length ? this.transaction(write) : write());
343
340
  return ids;
344
341
  }
345
- async fillToManyRelations(entity, payload, populate) {
346
- if (!payload.length) {
347
- return;
348
- }
349
- const meta = getMeta(entity);
350
- const relKeys = getRelationRequestSummary(meta, populate).toManyKeys;
351
- for (const relKey of relKeys) {
352
- const relOpts = relationOf(meta, relKey);
353
- const relEntity = relOpts.entity();
354
- const relationQuery = clone(parseRelationAtKey(relKey, populate).query);
355
- if (relOpts.through) {
356
- await this.fillToManyThroughRelation(payload, meta, relKey, relOpts, relationQuery);
357
- }
358
- else if (relOpts.cardinality === '1m') {
359
- await this.fillToManyOneToMany(payload, meta, relKey, relOpts, relationQuery, relEntity);
360
- }
361
- }
362
- }
363
- async fillToManyThroughRelation(payload, meta, relKey, relOpts, relationQuery) {
364
- const joins = parentJoins(relOpts, meta.ids.length);
365
- const [targetColumn] = targetKeyColumns(relOpts, meta.ids.length);
366
- const throughEntity = relOpts.through();
367
- const throughMeta = getMeta(throughEntity);
368
- const targetRelKey = getKeys(throughMeta.relations).find((key) => throughMeta.relations[key]?.references.some(({ local }) => local === targetColumn));
369
- if (!targetRelKey) {
370
- // Asserted rather than assumed: used as a key regardless, it spells the literal string
371
- // `undefined`, and the statement asks the junction for a relation of that name.
372
- throw new TypeError(`'${meta.name}.${relKey}' goes through '${throughMeta.name}', which declares no relation on its ` +
373
- `'${targetColumn}' column. Give it one, so the target's rows can be read through it.`);
374
- }
375
- // A relation query names the target's columns, not the junction's, so its projection and filter
376
- // belong on the populate below, resolved against the entity that has them. Spread onto the
377
- // junction query instead they asked `ItemTag` for `Tag`'s columns and failed with "no such
378
- // column".
379
- //
380
- // Ordering and paging split the other way: they describe the statement with one row per pairing,
381
- // which is the junction's. Left on the populate they reached a to-one join, which rejects all
382
- // four by name - so a many-to-many carrying any of them threw rather than paging.
383
- //
384
- // Those four are not a coincidence: they are exactly the clauses a joined relation rejects, for
385
- // the same reason - each needs a statement with many rows per parent, which only the junction's
386
- // is. The `satisfies` ties the two lists together, so a fifth clause added there fails to compile
387
- // here rather than quietly staying on the populate and throwing again.
388
- const { $sort, $limit, $skip, $distinct, ...targetQuery } = relationQuery;
389
- const junctionClauses = {
390
- $limit,
391
- $skip,
392
- $distinct,
393
- // Qualified by the relation that reaches them, since the columns it names are the target's.
394
- $sort: $sort && { [targetRelKey]: $sort },
395
- };
396
- const junctionQuery = {
397
- $select: joinedColumns(joins),
398
- ...junctionClauses,
399
- $populate: {
400
- [targetRelKey]: {
401
- ...targetQuery,
402
- $required: true,
403
- },
404
- },
405
- };
406
- const throughFounds = await this.findChildrenOf(throughEntity, junctionQuery, joins, payload, meta.fields);
407
- // The junction's own columns carried onto the target's row, which is where `putChildrenInParents`
408
- // reads them back from - a junction row holds the parent's key under `joined`, not under `parent`.
409
- const founds = throughFounds.map((it) => ({
410
- ...it[targetRelKey],
411
- ...Object.fromEntries(joins.map(({ joined }) => [joined, it[joined]])),
412
- }));
413
- this.putChildrenInParents(payload, founds, joins, relKey);
414
- }
415
- async fillToManyOneToMany(payload, meta, relKey, relOpts, relationQuery, relEntity) {
416
- const joins = parentJoins(relOpts, meta.ids.length);
417
- // The FK is what putChildrenInParents groups on, so it outlives the relation's projection
418
- // either way: added to a whitelisting `$select` (the raw-array form has nothing to augment),
419
- // dropped from a subtractive `$exclude`. `relationQuery` is already a clone.
420
- const select = asSelectMap(relationQuery.$select);
421
- const exclude = relationQuery.$exclude;
422
- for (const { joined } of joins) {
423
- if (select && !select[joined]) {
424
- select[joined] = true;
425
- }
426
- delete exclude?.[joined];
427
- }
428
- this.putChildrenInParents(payload, await this.findChildrenOf(relEntity, relationQuery, joins, payload, meta.fields), joins, relKey);
429
- }
430
- /**
431
- * The children of a whole page of parents, however the relation asked for them: one bounded branch
432
- * per parent when it wants a share of its own, otherwise a single flat statement over an `IN (...)`
433
- * list, which is both correct and cheaper.
434
- *
435
- * The one place that decision is made - a one-to-many and the junction of a many-to-many differ in
436
- * what they query, never in how the page is spread over its parents.
437
- */
438
- async findChildrenOf(entity, query, joins, parents, parentFields) {
439
- const founds = isBoundedPerParent(query)
440
- ? await this.internalFindManyPerParent(entity, query, { joins, parents, parentFields })
441
- : await this.findMany(entity, queryChildrenOfAll(query, joins, parents));
442
- // Read back as rows rather than as the entity they hydrate to: what follows regroups them by the
443
- // join columns, which a projected entity type does not carry.
444
- return founds;
445
- }
446
- putChildrenInParents(parents, children, joins, relKey) {
447
- const childrenByParentId = dataKeyed();
448
- // Every joined column, so two children agreeing on one column of a composite key are not
449
- // gathered under the same parent. Both column lists are read once, not once per row.
450
- const joinedKeys = keyColumns(joins, 'joined');
451
- const parentKeys = keyColumns(joins, 'parent');
452
- for (const child of children) {
453
- (childrenByParentId[rowKey(child, joinedKeys)] ??= []).push(child);
454
- }
455
- for (const parent of parents) {
456
- // `[]` rather than nothing for a parent with no children: a populated to-many is a list the
457
- // caller asked for, so it maps and counts without a guard, and its type can say so. An
458
- // unpopulated one stays absent, which is what tells the two apart.
459
- parent[relKey] = (childrenByParentId[rowKey(parent, parentKeys)] ?? []);
460
- }
461
- }
462
342
  async insertRelations(entity, payload) {
463
343
  const meta = getMeta(entity);
464
344
  const entries = payload.reduce((acc, it) => {
@@ -625,6 +505,32 @@ export class AbstractQuerier {
625
505
  hasHook(entity, event) {
626
506
  return (this.extra?.listeners?.some((listener) => listener[event]) || (getMeta(entity).hooks?.[event]?.length ?? 0) > 0);
627
507
  }
508
+ /** Whether an `afterLoad` listens on the entity a read returns or on any relation it populated. */
509
+ listensForLoad(entity, populate) {
510
+ if (this.hasHook(entity, 'afterLoad')) {
511
+ return true;
512
+ }
513
+ const meta = getMeta(entity);
514
+ return getRelationRequestSummary(meta, populate).requestedKeys.some((relKey) => this.listensForLoad(relationOf(meta, relKey).entity(), parseRelationAtKey(relKey, populate).query.$populate));
515
+ }
516
+ /**
517
+ * `afterLoad` for every row a read loaded, a populated relation's before the rows holding them, so a
518
+ * parent's hook sees its children as their own hooks left them. Rows are walked only where a hook
519
+ * listens.
520
+ */
521
+ async emitLoaded(entity, rows, populate) {
522
+ const meta = getMeta(entity);
523
+ for (const relKey of getRelationRequestSummary(meta, populate).requestedKeys) {
524
+ const relEntity = relationOf(meta, relKey).entity();
525
+ const relPopulate = parseRelationAtKey(relKey, populate).query.$populate;
526
+ if (this.listensForLoad(relEntity, relPopulate)) {
527
+ // A to-many holds a list and a to-one a row, which `flatMap` takes alike; an absent one adds none.
528
+ const loaded = rows.flatMap((row) => row[relKey] ?? []);
529
+ await this.emitLoaded(relEntity, loaded, relPopulate);
530
+ }
531
+ }
532
+ await this.emitHook(entity, 'afterLoad', rows);
533
+ }
628
534
  /**
629
535
  * The ids of `rows`, read back by the columns an upsert matched them on, for a statement that could
630
536
  * not report them in payload order. A row that no read row matches, or that two do, keeps
@@ -1,5 +1,5 @@
1
1
  import type { AbstractDialect } from '../dialect/index.js';
2
- import type { EntityData, EntityId, ExtraOptions, FieldKey, PoolRunOptions, Querier, QuerierPool, QueryAggMap, QueryAggregate, QueryAggregateResult, QueryConflictPaths, QueryFilter, QueryFindResult, QueryGroupMap, QueryOneProjected, QueryOptions, QueryPage, QueryProjected, QuerySearch, QueryStreamProjected, QueryUpsertOneResult, QueryUpsertManyResult, RelationKey, TransactionOptions, Type, UpdatePayload, WrittenId } from '../type/index.js';
2
+ import type { EntityData, EntityId, ExtraOptions, FieldKey, PoolRunOptions, Querier, QuerierPool, QueryAggMap, QueryAggregate, QueryAggregateResult, QueryConflictPaths, QueryFilter, QueryFindResult, QueryGroupMap, QueryOneProjected, QueryOptions, QueryPage, QueryProjected, QuerySearch, QueryUpsertOneResult, QueryUpsertManyResult, RelationKey, TransactionOptions, Type, UpdatePayload, WrittenId } 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
  */
@@ -31,7 +31,7 @@ export declare abstract class AbstractQuerierPool<Q extends Querier, D extends A
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: QueryStreamProjected<E, S, V, X, P>, opts?: QueryOptions): AsyncGenerator<QueryFindResult<E, S, V, X, P>>;
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, const C extends RelationKey<E> = never>(entity: Type<E>, q: QueryProjected<E, S, V, X, P, C>, opts?: QueryOptions): AsyncGenerator<QueryFindResult<E, S, V, X, P, C>>;
35
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
36
  count<E extends object>(entity: Type<E>, q?: QueryPage<E>, opts?: QueryOptions): Promise<number>;
37
37
  exists<E extends object>(entity: Type<E>, q?: QueryFilter<E>, opts?: QueryOptions): Promise<boolean>;