uql-orm 0.68.1 → 0.70.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/dist/browser/querier/httpQuerier.d.ts +7 -7
  2. package/dist/browser/type/clientQuerier.d.ts +5 -5
  3. package/dist/browser/uql-browser.min.js +2 -2
  4. package/dist/browser/uql-browser.min.js.map +6 -6
  5. package/dist/cockroachdb/cockroachDialect.js +2 -2
  6. package/dist/dialect/abstractDialect.d.ts +8 -2
  7. package/dist/dialect/abstractDialect.js +17 -1
  8. package/dist/dialect/abstractSqlDialect.d.ts +42 -4
  9. package/dist/dialect/abstractSqlDialect.js +164 -48
  10. package/dist/dialect/aliases.d.ts +15 -4
  11. package/dist/dialect/aliases.js +15 -4
  12. package/dist/dialect/mysqlLikeSqlDialect.js +3 -2
  13. package/dist/dialect/pgLikeSqlDialect.js +1 -0
  14. package/dist/dialect/queryJoins.d.ts +8 -1
  15. package/dist/dialect/queryJoins.js +33 -10
  16. package/dist/entity/decorator/members.d.ts +44 -2
  17. package/dist/entity/decorator/members.js +0 -5
  18. package/dist/entity/metadata/definition.d.ts +8 -3
  19. package/dist/entity/metadata/definition.js +10 -3
  20. package/dist/migrate/codegen/entityCodeGenerator.js +4 -2
  21. package/dist/migrate/introspection/postgresIntrospector.d.ts +6 -0
  22. package/dist/migrate/introspection/postgresIntrospector.js +7 -1
  23. package/dist/migrate/migrator.d.ts +2 -1
  24. package/dist/migrate/migrator.js +5 -3
  25. package/dist/mongo/mongoDialect.d.ts +49 -19
  26. package/dist/mongo/mongoDialect.js +238 -81
  27. package/dist/mongo/mongodbQuerier.d.ts +2 -4
  28. package/dist/mongo/mongodbQuerier.js +16 -14
  29. package/dist/mssql/mssqlDialect.js +3 -2
  30. package/dist/postgres/postgresDialect.js +2 -2
  31. package/dist/querier/abstractQuerier.d.ts +16 -11
  32. package/dist/querier/abstractQuerier.js +28 -8
  33. package/dist/querier/abstractQuerierPool.d.ts +9 -9
  34. package/dist/querier/abstractSqlQuerier.d.ts +1 -1
  35. package/dist/querier/abstractSqlQuerier.js +3 -3
  36. package/dist/sqlite/sqliteDialect.js +1 -0
  37. package/dist/turso/tursoDialect.d.ts +1 -1
  38. package/dist/turso/tursoDialect.js +6 -2
  39. package/dist/type/dialect.d.ts +35 -2
  40. package/dist/type/entity.d.ts +120 -4
  41. package/dist/type/migration.d.ts +7 -0
  42. package/dist/type/query.d.ts +7 -10
  43. package/dist/type/queryAggregate.d.ts +77 -42
  44. package/dist/type/queryAggregate.js +4 -21
  45. package/dist/type/queryRaw.d.ts +19 -1
  46. package/dist/type/queryRaw.js +18 -0
  47. package/dist/type/universalQuerier.d.ts +9 -9
  48. package/dist/util/dialect.util.d.ts +6 -2
  49. package/dist/util/dialect.util.js +21 -7
  50. package/dist/util/field.util.d.ts +15 -1
  51. package/dist/util/field.util.js +18 -1
  52. package/dist/util/object.util.d.ts +1 -4
  53. package/dist/util/object.util.js +0 -3
  54. package/dist/util/raw.d.ts +2 -2
  55. package/dist/util/raw.js +29 -2
  56. package/package.json +1 -1
@@ -1,4 +1,5 @@
1
- import type { EntityData, EntityId, ExtraOptions, FieldKey, 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';
1
+ import type { AbstractDialect } from '../dialect/abstractDialect.js';
2
+ import type { EntityData, EntityId, EntityWrite, ExtraOptions, FieldKey, Querier, Query, QueryAggMap, QueryAggregate, QueryAggregateResult, QueryConflictPaths, QueryFilter, QueryFindResult, QueryGroupMap, QueryOneProjected, QueryOptions, QueryPage, QueryProjected, QuerySearch, PrimaryKey, QueryUpdateResult, QueryUpsertOneResult, QueryUpsertManyResult, RelationKey, TransactionOptions, Type, UpdatePayload, UpdateWrite, WrittenId } from '../type/index.js';
2
3
  import { LoggerWrapper } from '../util/index.js';
3
4
  /** Base class for all database queriers. */
4
5
  export declare abstract class AbstractQuerier implements Querier {
@@ -15,6 +16,7 @@ export declare abstract class AbstractQuerier implements Querier {
15
16
  */
16
17
  protected released: boolean;
17
18
  protected readonly logger: LoggerWrapper;
19
+ abstract readonly dialect: AbstractDialect;
18
20
  constructor(extra?: ExtraOptions | undefined);
19
21
  protected validateProjectionQuery<E extends object>(entity: Type<E>, q: Query<E>): void;
20
22
  private validateProjectionQueryRecursive;
@@ -73,21 +75,24 @@ export declare abstract class AbstractQuerier implements Querier {
73
75
  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>[]>;
74
76
  /** Abstract outright: nothing is shared to do around it. See {@link UniversalQuerier.estimatedCount}. */
75
77
  abstract estimatedCount<E extends object>(entity: Type<E>): Promise<number>;
76
- insertOne<E extends object>(entity: Type<E>, payload: EntityData<E>): Promise<WrittenId<E> | undefined>;
78
+ insertOne<E extends object>(entity: Type<E>, payload: EntityWrite<E>): Promise<WrittenId<E> | undefined>;
77
79
  /**
78
80
  * The `onInsert` values are filled here, before the write, so the after hooks and the ids read the
79
81
  * same rows the statement wrote.
80
82
  */
81
- insertMany<E extends object>(entity: Type<E>, payload: EntityData<E>[]): Promise<(WrittenId<E> | undefined)[]>;
83
+ insertMany<E extends object>(entity: Type<E>, payload: EntityWrite<E>[]): Promise<(WrittenId<E> | undefined)[]>;
82
84
  /** Writes `rows`, and onto each one the key the database generated for it, where it can tell. */
83
85
  protected abstract internalInsertMany<E extends object>(entity: Type<E>, rows: EntityData<E>[]): Promise<void>;
84
- updateOneById<E extends object>(entity: Type<E>, id: EntityId<E>, payload: UpdatePayload<E>, opts?: QueryOptions): Promise<number>;
86
+ updateOneById<E extends object>(entity: Type<E>, id: EntityId<E>, payload: UpdateWrite<E>, opts?: QueryOptions): Promise<number>;
85
87
  /** Settles the rows first where the update cascades, so a payload changing what `$where` reads still names them. */
86
- updateMany<E extends object>(entity: Type<E>, q: QuerySearch<E>, payload: UpdatePayload<E>, opts?: QueryOptions): Promise<number>;
88
+ updateMany<E extends object>(entity: Type<E>, q: QuerySearch<E>, payload: UpdateWrite<E>, opts?: QueryOptions): Promise<number>;
87
89
  /** The UPDATE, skipped where the payload writes no column, reporting `unwritten` instead. */
88
90
  private updateColumns;
89
- /** Whether a write has to name the rows `q` matches by their ids: no engine pages or orders an update or delete. */
90
- protected settlesWrite<E extends object>(_entity: Type<E>, q: QuerySearch<E>): boolean;
91
+ /**
92
+ * Whether a write has to name the rows `q` matches by their ids: no engine pages or orders an update or
93
+ * delete, and one without {@link DialectFeatures.correlatedWrites} cannot read a relation in its filter.
94
+ */
95
+ protected settlesWrite<E extends object>(entity: Type<E>, q: QuerySearch<E>): boolean;
91
96
  /** The ids `q` matches, in its own order and page. */
92
97
  protected settleIds<E extends object>(entity: Type<E>, q: QuerySearch<E>, opts?: QueryOptions): Promise<EntityId<E>[]>;
93
98
  /** Runs one UPDATE over `q`, which names its rows by id wherever {@link updateMany} settled them. */
@@ -95,8 +100,8 @@ export declare abstract class AbstractQuerier implements Querier {
95
100
  restoreOneById<E extends object>(entity: Type<E>, id: EntityId<E>): Promise<number>;
96
101
  restoreMany<E extends object>(entity: Type<E>, q: QuerySearch<E>): Promise<number>;
97
102
  /** Fires `beforeUpsert`/`afterUpsert`: which branch a row takes is the database's to decide, so neither the insert's nor the update's pair fits. */
98
- upsertOne<E extends object>(entity: Type<E>, conflictPaths: QueryConflictPaths<E>, payload: EntityData<E>): Promise<QueryUpsertOneResult<E>>;
99
- upsertMany<E extends object>(entity: Type<E>, conflictPaths: QueryConflictPaths<E>, payload: EntityData<E>[]): Promise<QueryUpsertManyResult<E>>;
103
+ upsertOne<E extends object>(entity: Type<E>, conflictPaths: QueryConflictPaths<E>, payload: EntityWrite<E>): Promise<QueryUpsertOneResult<E>>;
104
+ upsertMany<E extends object>(entity: Type<E>, conflictPaths: QueryConflictPaths<E>, payload: EntityWrite<E>[]): Promise<QueryUpsertManyResult<E>>;
100
105
  protected abstract internalUpsertOne<E extends object>(entity: Type<E>, conflictPaths: QueryConflictPaths<E>, payload: EntityData<E>): Promise<QueryUpdateResult>;
101
106
  protected abstract internalUpsertMany<E extends object>(entity: Type<E>, conflictPaths: QueryConflictPaths<E>, payload: EntityData<E>[]): Promise<QueryUpdateResult>;
102
107
  deleteOneById<E extends object>(entity: Type<E>, id: EntityId<E>, opts?: QueryOptions): Promise<number>;
@@ -107,13 +112,13 @@ export declare abstract class AbstractQuerier implements Querier {
107
112
  }, opts?: QueryOptions): Promise<number>;
108
113
  /** Runs one DELETE (or soft-delete stamp) over `q`, which names its rows by id wherever {@link deleteMany} settled them. */
109
114
  protected abstract internalDeleteMany<E extends object>(entity: Type<E>, q: QuerySearch<E>, opts?: QueryOptions): Promise<number>;
110
- saveOne<E extends object>(entity: Type<E>, payload: EntityData<E>): Promise<WrittenId<E> | undefined>;
115
+ saveOne<E extends object>(entity: Type<E>, payload: EntityWrite<E>): Promise<WrittenId<E> | undefined>;
111
116
  /**
112
117
  * Whether a row names its key decides its statement, never whether the row exists: a named row
113
118
  * upserts on that key, so a stale id is written rather than silently missed, and an unnamed one
114
119
  * inserts. A composite is always named. The hooks follow the statement: a named row fires the upsert pair.
115
120
  */
116
- saveMany<E extends object>(entity: Type<E>, payload: EntityData<E>[]): Promise<(WrittenId<E> | undefined)[]>;
121
+ saveMany<E extends object>(entity: Type<E>, payload: EntityWrite<E>[]): Promise<(WrittenId<E> | undefined)[]>;
117
122
  /** Writes each inserted row's relations, one set of statements per relation whatever the number of rows. */
118
123
  protected insertRelations<E extends object>(entity: Type<E>, rows: E[]): Promise<void>;
119
124
  /** `EntityId` because a settled composite row is an object, which {@link childrenOf} reads each foreign key column out of. */
@@ -1,5 +1,5 @@
1
1
  import { assertSoleId, getMeta, idOf, namesKey, relationOf } from '../entity/index.js';
2
- import { cascadesOnDelete, childrenOf, clone, entityName, fillOnFields, filterFieldKeys, filterPersistableRelationKeys, forEachRequestedRelation, getKeys, getRelationRequestSummary, idOnlyQuery, isPagedQuery, isScalarId, LoggerWrapper, parentJoins, queryLoggerFor, parseRelationAtKey, parseRelationQueryValue, rowKey, runHooks, someKey, targetKeyColumns, whereIds, withoutSoftDeleteFilter, } from '../util/index.js';
2
+ import { cascadesOnDelete, childrenOf, clone, entityName, fillOnFields, filterFieldKeys, filterPersistableRelationKeys, forEachRequestedRelation, getKeys, getRelationRequestSummary, idOnlyQuery, isPagedQuery, hasKeys, isScalarId, LoggerWrapper, parentJoins, queryLoggerFor, parseRelationAtKey, parseRelationQueryValue, rowKey, runHooks, someKey, targetKeyColumns, whereIds, withoutSoftDeleteFilter, } from '../util/index.js';
3
3
  import { enrichError } from './queryError.js';
4
4
  /**
5
5
  * Refuses a nullish id, which would reduce to no filter at all, and a composite id missing a column,
@@ -27,6 +27,17 @@ function assertIdValue(entity, id) {
27
27
  function soleParentColumn(relOpts) {
28
28
  return parentJoins(relOpts, 1)[0].joined;
29
29
  }
30
+ /**
31
+ * A bulk write names the rows it changes: a `$where`, or a `$limit` capping how many it reaches. The
32
+ * caller's own clauses are what count - a filter the entity adds, soft delete's, would otherwise make
33
+ * every table look narrowed, which is the case this exists to catch.
34
+ */
35
+ function assertNamesRows(entity, method, q, opts) {
36
+ if (opts?.unfiltered || hasKeys(q?.$where) || q?.$limit !== undefined) {
37
+ return;
38
+ }
39
+ throw new TypeError(`'${method}' over '${entity.name}' names no rows, so it would address every one: pass '{ unfiltered: true }' to mean it`);
40
+ }
30
41
  /**
31
42
  * The id each written row is named by, in payload order. Read off the rows as written, so a key the
32
43
  * database generated or the ORM filled is there, and a composite is named by every column of it.
@@ -183,6 +194,7 @@ export class AbstractQuerier {
183
194
  }
184
195
  /** Settles the rows first where the update cascades, so a payload changing what `$where` reads still names them. */
185
196
  async updateMany(entity, q, payload, opts) {
197
+ assertNamesRows(entity, 'updateMany', q, opts);
186
198
  const meta = getMeta(entity);
187
199
  return this.hooked(entity, 'Update', [payload], async ([row]) => {
188
200
  fillOnFields(meta, [row], 'onUpdate');
@@ -206,9 +218,13 @@ export class AbstractQuerier {
206
218
  const writes = filterFieldKeys(getMeta(entity), row, 'onUpdate').length > 0;
207
219
  return writes ? this.internalUpdateMany(entity, q, row, opts) : unwritten;
208
220
  }
209
- /** Whether a write has to name the rows `q` matches by their ids: no engine pages or orders an update or delete. */
210
- settlesWrite(_entity, q) {
211
- return isPagedQuery(q);
221
+ /**
222
+ * Whether a write has to name the rows `q` matches by their ids: no engine pages or orders an update or
223
+ * delete, and one without {@link DialectFeatures.correlatedWrites} cannot read a relation in its filter.
224
+ */
225
+ settlesWrite(entity, q) {
226
+ const { dialect } = this;
227
+ return isPagedQuery(q) || (!dialect.features.correlatedWrites && dialect.constrainsRelations(entity, q.$where));
212
228
  }
213
229
  /** The ids `q` matches, in its own order and page. */
214
230
  async settleIds(entity, q, opts) {
@@ -254,6 +270,7 @@ export class AbstractQuerier {
254
270
  }
255
271
  async deleteMany(entityOrQuery, qOrOpts, maybeOpts) {
256
272
  const [entity, q, opts] = this.resolveEntityQuery(entityOrQuery, qOrOpts, maybeOpts);
273
+ assertNamesRows(entity, 'deleteMany', q, opts);
257
274
  const meta = getMeta(entity);
258
275
  const cascades = cascadesOnDelete(meta);
259
276
  const watched = this.hasHook(entity, 'beforeDelete') || this.hasHook(entity, 'afterDelete');
@@ -350,8 +367,7 @@ export class AbstractQuerier {
350
367
  const relOpts = relationOf(meta, relKey);
351
368
  const relEntity = relOpts.entity();
352
369
  const target = relOpts.through ? relOpts.through() : relEntity;
353
- const where = childrenOf(parentJoins(relOpts, meta.ids.length), ids);
354
- await this.deleteMany(target, { $where: where }, opts);
370
+ await this.deleteMany(target, { $where: childrenOf(parentJoins(relOpts, meta.ids.length), ids) }, opts);
355
371
  }
356
372
  }
357
373
  /**
@@ -480,8 +496,12 @@ export class AbstractQuerier {
480
496
  * the write filled in - a generated key, an `onInsert` value - without it landing on the caller's.
481
497
  */
482
498
  async hooked(entity, event, payloads, write) {
483
- await this.emitHook(entity, `before${event}`, payloads);
484
- const rows = clone(payloads);
499
+ // The one place a caller's write becomes the row the rest of the library handles. They are the
500
+ // same object: a write is the entity's data without the keys the database fills, which
501
+ // TypeScript cannot relate across an entity it has not resolved.
502
+ const asRows = payloads;
503
+ await this.emitHook(entity, `before${event}`, asRows);
504
+ const rows = clone(asRows);
485
505
  const result = await write(rows);
486
506
  await this.emitHook(entity, `after${event}`, rows);
487
507
  return result;
@@ -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, QueryUpsertOneResult, QueryUpsertManyResult, RelationKey, TransactionOptions, Type, UpdatePayload, WrittenId } from '../type/index.js';
2
+ import type { EntityWrite, EntityId, ExtraOptions, FieldKey, PoolRunOptions, Querier, QuerierPool, QueryAggMap, QueryAggregate, QueryAggregateResult, QueryConflictPaths, QueryFilter, QueryFindResult, QueryGroupMap, QueryOneProjected, QueryOptions, QueryPage, QueryProjected, QuerySearch, QueryUpsertOneResult, QueryUpsertManyResult, RelationKey, TransactionOptions, Type, UpdateWrite, 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
  */
@@ -37,14 +37,14 @@ export declare abstract class AbstractQuerierPool<Q extends Querier, D extends A
37
37
  exists<E extends object>(entity: Type<E>, q?: QueryFilter<E>, opts?: QueryOptions): Promise<boolean>;
38
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
39
  estimatedCount<E extends object>(entity: Type<E>): Promise<number>;
40
- insertOne<E extends object>(entity: Type<E>, payload: EntityData<E>): Promise<WrittenId<E> | undefined>;
41
- insertMany<E extends object>(entity: Type<E>, payload: EntityData<E>[]): Promise<(WrittenId<E> | undefined)[]>;
42
- updateOneById<E extends object>(entity: Type<E>, id: EntityId<E>, payload: UpdatePayload<E>, opts?: QueryOptions): Promise<number>;
43
- updateMany<E extends object>(entity: Type<E>, q: QuerySearch<E>, payload: UpdatePayload<E>, opts?: QueryOptions): Promise<number>;
44
- upsertOne<E extends object>(entity: Type<E>, conflictPaths: QueryConflictPaths<E>, payload: EntityData<E>): Promise<QueryUpsertOneResult<E>>;
45
- upsertMany<E extends object>(entity: Type<E>, conflictPaths: QueryConflictPaths<E>, payload: EntityData<E>[]): Promise<QueryUpsertManyResult<E>>;
46
- saveOne<E extends object>(entity: Type<E>, payload: EntityData<E>): Promise<WrittenId<E> | undefined>;
47
- saveMany<E extends object>(entity: Type<E>, payload: EntityData<E>[]): Promise<(WrittenId<E> | undefined)[]>;
40
+ insertOne<E extends object>(entity: Type<E>, payload: EntityWrite<E>): Promise<WrittenId<E> | undefined>;
41
+ insertMany<E extends object>(entity: Type<E>, payload: EntityWrite<E>[]): Promise<(WrittenId<E> | undefined)[]>;
42
+ updateOneById<E extends object>(entity: Type<E>, id: EntityId<E>, payload: UpdateWrite<E>, opts?: QueryOptions): Promise<number>;
43
+ updateMany<E extends object>(entity: Type<E>, q: QuerySearch<E>, payload: UpdateWrite<E>, opts?: QueryOptions): Promise<number>;
44
+ upsertOne<E extends object>(entity: Type<E>, conflictPaths: QueryConflictPaths<E>, payload: EntityWrite<E>): Promise<QueryUpsertOneResult<E>>;
45
+ upsertMany<E extends object>(entity: Type<E>, conflictPaths: QueryConflictPaths<E>, payload: EntityWrite<E>[]): Promise<QueryUpsertManyResult<E>>;
46
+ saveOne<E extends object>(entity: Type<E>, payload: EntityWrite<E>): Promise<WrittenId<E> | undefined>;
47
+ saveMany<E extends object>(entity: Type<E>, payload: EntityWrite<E>[]): Promise<(WrittenId<E> | undefined)[]>;
48
48
  deleteOneById<E extends object>(entity: Type<E>, id: EntityId<E>, opts?: QueryOptions): Promise<number>;
49
49
  deleteMany<E extends object>(entity: Type<E>, q: QuerySearch<E>, opts?: QueryOptions): Promise<number>;
50
50
  restoreOneById<E extends object>(entity: Type<E>, id: EntityId<E>): Promise<number>;
@@ -80,7 +80,7 @@ export declare abstract class AbstractSqlQuerier extends AbstractQuerier impleme
80
80
  */
81
81
  private hydrateFields;
82
82
  /**
83
- * Runs a statement whose one row carries a {@link COUNT_ALIAS} column. `Number` because `COUNT(*)` is BIGINT and
83
+ * Runs a statement whose one row carries a {@link AGGREGATE_VALUE_ALIAS} column. `Number` because `COUNT(*)` is BIGINT and
84
84
  * a caller supplying their own `types` replaces the decoding the pools do at the wire; `?? 0` because
85
85
  * a catalog that does not know the table answers with no row, which is nothing counted.
86
86
  */
@@ -1,4 +1,4 @@
1
- import { COUNT_ALIAS, TOTAL_ALIAS } from '../dialect/aliases.js';
1
+ import { AGGREGATE_VALUE_ALIAS, TOTAL_ALIAS } from '../dialect/aliases.js';
2
2
  import { decodeColumn } from '../dialect/hydrateColumn.js';
3
3
  import { getMeta, namesKey } from '../entity/index.js';
4
4
  import { COUNT_RESULT_KEY } from '../type/index.js';
@@ -290,13 +290,13 @@ export class AbstractSqlQuerier extends AbstractQuerier {
290
290
  }
291
291
  }
292
292
  /**
293
- * Runs a statement whose one row carries a {@link COUNT_ALIAS} column. `Number` because `COUNT(*)` is BIGINT and
293
+ * Runs a statement whose one row carries a {@link AGGREGATE_VALUE_ALIAS} column. `Number` because `COUNT(*)` is BIGINT and
294
294
  * a caller supplying their own `types` replaces the decoding the pools do at the wire; `?? 0` because
295
295
  * a catalog that does not know the table answers with no row, which is nothing counted.
296
296
  */
297
297
  async runCount(build) {
298
298
  const [row] = await this.query(build);
299
- return Number(row?.[COUNT_ALIAS] ?? 0);
299
+ return Number(row?.[AGGREGATE_VALUE_ALIAS] ?? 0);
300
300
  }
301
301
  async internalCount(entity, q, opts) {
302
302
  return this.runCount((ctx) => this.dialect.count(ctx, entity, q, opts));
@@ -19,6 +19,7 @@ export const SQLITE_FEATURES = {
19
19
  stringSizing: 'text',
20
20
  supportsUnsigned: false,
21
21
  serverSideCursors: false,
22
+ correlatedWrites: true,
22
23
  rowLocks: false,
23
24
  rowLockWithWindow: true,
24
25
  rowLockOf: true,
@@ -5,6 +5,6 @@ import type { SqlDialectFeatures } from '../type/index.js';
5
5
  * `ORDER BY` inside an aggregate, which its Rust engine lacks. Imports no driver.
6
6
  */
7
7
  export declare class TursoDialect extends LibsqlDialect {
8
- /** The Rust engine takes no `ORDER BY` inside an aggregate. */
8
+ /** The Rust engine takes no `ORDER BY` inside an aggregate, nor a subquery reading the table a write changes. */
9
9
  readonly features: SqlDialectFeatures;
10
10
  }
@@ -5,6 +5,10 @@ import { SQLITE_FEATURES } from '../sqlite/sqliteDialect.js';
5
5
  * `ORDER BY` inside an aggregate, which its Rust engine lacks. Imports no driver.
6
6
  */
7
7
  export class TursoDialect extends LibsqlDialect {
8
- /** The Rust engine takes no `ORDER BY` inside an aggregate. */
9
- features = { ...SQLITE_FEATURES, orderedJsonAggregates: false };
8
+ /** The Rust engine takes no `ORDER BY` inside an aggregate, nor a subquery reading the table a write changes. */
9
+ features = {
10
+ ...SQLITE_FEATURES,
11
+ orderedJsonAggregates: false,
12
+ correlatedWrites: false,
13
+ };
10
14
  }
@@ -1,6 +1,6 @@
1
1
  import type { EntityMeta, UpdatePayload } from './entity.js';
2
- import type { Query, QueryConflictPaths, QueryOptions, QueryPage, QuerySearch } from './query.js';
3
- import type { QueryAggMap, QueryAggregate, QueryGroupMap } from './queryAggregate.js';
2
+ import type { Query, QueryConflictPaths, QueryOptions, QueryPage, QuerySearch, RelationQuery } from './query.js';
3
+ import type { QueryAggMap, QueryAggregate, QueryAggregateOp, QueryGroupMap } from './queryAggregate.js';
4
4
  import type { Type } from './utility.js';
5
5
  /**
6
6
  * comparison options.
@@ -122,6 +122,12 @@ export interface DialectFeatures {
122
122
  * answer, not the driver's: node-`pg` streams on its own and keeps doing so.
123
123
  */
124
124
  readonly serverSideCursors: boolean;
125
+ /**
126
+ * Whether an `UPDATE` or `DELETE` can read a relation in its filter. False on MongoDB, whose filter
127
+ * hosts no lookup, and on Turso's engine, which cannot resolve the written table inside a subquery:
128
+ * such a write reads the ids of the rows it names first.
129
+ */
130
+ readonly correlatedWrites: boolean;
125
131
  }
126
132
  /** What a SQL engine can do beyond {@link DialectFeatures}, read where a statement is built. */
127
133
  export interface SqlDialectFeatures extends DialectFeatures {
@@ -208,7 +214,34 @@ export interface SqlQueryDialect {
208
214
  * Default: '?' for MySQL/MariaDB/SQLite, '$n' for PostgreSQL.
209
215
  */
210
216
  placeholder(index: number): string;
217
+ /**
218
+ * A relation aggregate as a correlated subquery, correlated to the row under `prefix`: what a field
219
+ * declaring `computed: (user) => user.resources.count()` renders as, wherever a clause names it.
220
+ */
221
+ appendRelationAggregate<E>(ctx: QueryContext, entity: Type<E>, aggregate: RelationAggregateSpec, prefix: string): void;
211
222
  }
223
+ /**
224
+ * The aggregates a relation reads, spelled as the query language spells them, so a `computed` field, an
225
+ * aggregate query and MongoDB's own `$group` all name one the same way. `$count` and `$sum` are the two
226
+ * a row change turns into a delta.
227
+ */
228
+ export type RelationAggregateOp = QueryAggregateOp;
229
+ /** What a relation aggregate reads: how many rows, or one of the target's columns. */
230
+ export type RelationAggregateProjection = {
231
+ readonly op: '$count';
232
+ readonly field?: never;
233
+ } | {
234
+ readonly op: Exclude<RelationAggregateOp, '$count'>;
235
+ readonly field: string;
236
+ };
237
+ /** A relation aggregate as a `computed` field holds it: what it reads, off which relation, filtered how. */
238
+ export type RelationAggregateSpec = RelationAggregateProjection & {
239
+ readonly relation: string;
240
+ /** Which of the related rows it reads, and the page it caps them to, as the field declared them. */
241
+ readonly query?: RelationSubqueryQuery;
242
+ };
243
+ /** The rows a relation subquery reads: which ones, in what order, and the page capping them. */
244
+ export type RelationSubqueryQuery = Pick<RelationQuery, '$where' | '$sort' | '$limit' | '$skip'>;
212
245
  /**
213
246
  * Supported SQL dialect identifiers.
214
247
  */
@@ -1,8 +1,8 @@
1
1
  import type { EnumValues, ForeignKeyAction, IndexType } from '../schema/types.js';
2
- import type { FilterOptions } from './query.js';
3
- import type { ColumnRef, QueryRaw } from './queryRaw.js';
2
+ import type { FilterOptions, RelationQuery } from './query.js';
3
+ import type { ColumnRef, QueryRaw, RelationAggregate } from './queryRaw.js';
4
4
  import type { QueryWhere } from './queryWhere.js';
5
- import type { Except, IsMany, Json, Scalar, Type, Unpacked } from './utility.js';
5
+ import type { Except, IsMany, Json, Scalar, Type, Unpacked, Writable } from './utility.js';
6
6
  import type { VectorDistance, VectorIndexOptions, VectorIndexType } from './vector.js';
7
7
  /** Brands the property an entity is identified by, where it is not `id`, `_id` or `uuid`. */
8
8
  export declare const idKey: unique symbol;
@@ -20,8 +20,35 @@ export type Key<E> = keyof E & string;
20
20
  export type FieldKey<E> = {
21
21
  readonly [K in keyof E]-?: [NonNullable<E[K]>] extends [Scalar | readonly Scalar[] | Json | readonly Json[]] ? K : never;
22
22
  }[Key<E>];
23
+ /**
24
+ * Whether `A` and `B` are the same type, `readonly` included - which no conditional sees, since
25
+ * assignability ignores the modifier. Two identical generic signatures compare equal only when their
26
+ * deferred bodies do.
27
+ */
28
+ type IfEquals<A, B, Yes, No> = (<T>() => T extends A ? 1 : 2) extends <T>() => (T extends B ? 1 : 2) ? Yes : No;
29
+ /**
30
+ * The fields a caller writes: every one the class does not declare `readonly`. A field the database
31
+ * writes - a relation aggregate, a stored generated column, a trigger-kept stamp - is `readonly`, and
32
+ * its value never reaches the database, so a write payload leaves it out rather than dropping it.
33
+ */
34
+ export type WritableKey<E> = {
35
+ readonly [K in FieldKey<E>]-?: IfEquals<Pick<E, K>, Writable<Pick<E, K>>, K, never>;
36
+ }[FieldKey<E>];
37
+ /** A whole-record write as a caller supplies one: {@link EntityData} without the fields it cannot write. */
38
+ export type EntityWrite<E> = EntityData<E, WritableKey<E>>;
39
+ /** A partial write as a caller supplies one: {@link UpdatePayload} without them. */
40
+ export type UpdateWrite<E, Raw = QueryRaw> = UpdatePayload<E, Raw, WritableKey<E>>;
23
41
  /** The relation names of an entity: every key but its fields and its methods, so the two sets cannot drift. */
24
42
  export type RelationKey<E> = Exclude<Key<E>, FieldKey<E> | MethodKey<E>>;
43
+ /**
44
+ * To-one relations only: a parent holds many rows of a to-many, so there is no single value to order it
45
+ * by, and joining one in would duplicate the parent instead. Order those inside `$populate`.
46
+ */
47
+ export type ToOneRelationKey<E> = {
48
+ [K in RelationKey<E>]: IsMany<E[K]> extends true ? never : K;
49
+ }[RelationKey<E>];
50
+ /** The relation names a parent holds many rows of: what a populated query fills, and what an aggregate reads. */
51
+ export type ToManyRelationKey<E> = Exclude<RelationKey<E>, ToOneRelationKey<E>>;
25
52
  /** Whether `T` carries the `Json` brand, read off its marker key: a primitive matches `Json<infer P>` too. */
26
53
  type IsJson<T> = '__json' extends keyof T ? true : false;
27
54
  /** The payload `P` of a branded `Json<P>`, or `never` for any other type. */
@@ -201,8 +228,11 @@ export type FieldOptions<V = TsTypeOf<FieldType>, E = unknown> = {
201
228
  /**
202
229
  * An expression the database computes, never written: spliced into each read, or with `stored` a
203
230
  * generated column, `computed: (user) => raw`${user.first} || ' ' || ${user.last}``.
231
+ *
232
+ * A relation aggregate is the other form, `computed: (user) => user.resources.count()`, read as the
233
+ * subquery a `$count` reads. Both resolve to SQL at registration, so everything downstream sees one.
204
234
  */
205
- readonly computed?: EntitySql<E>;
235
+ readonly computed?: ComputedSql<E>;
206
236
  /** Whether {@link FieldOptions.computed} is a generated column rather than spliced into each read; no query changes either way. */
207
237
  readonly stored?: boolean;
208
238
  readonly updatable?: boolean;
@@ -255,10 +285,36 @@ export type TsTypeOf<T> = T extends StringConstructor ? string : T extends Numbe
255
285
  */
256
286
  export type FieldOptionsFor<V, E = unknown> = (FieldOptions<NonNullable<V>, E> & {
257
287
  readonly type: TypeFor<V>;
288
+ readonly isId: true;
258
289
  }) | (FieldOptions<NonNullable<V>, E> & {
290
+ readonly type: TypeFor<V>;
291
+ } & DeclaresNotNull<V>) | (FieldOptions<NonNullable<V>, E> & {
259
292
  readonly references: EntityGetter;
260
293
  readonly type?: TypeFor<V>;
294
+ } & DeclaresNotNull<V>) | AggregateOptionsFor<V, E>;
295
+ /**
296
+ * A column holds `null` unless `nullable: false` says otherwise, and a read hydrates one, so a property
297
+ * that does not admit it says so here. The decorators state the same rule the other way round, against
298
+ * the property they are applied to; a key needs neither, being NOT NULL on every engine.
299
+ */
300
+ type DeclaresNotNull<V> = null extends V ? unknown : {
301
+ readonly nullable: false;
302
+ };
303
+ /**
304
+ * A field a relation aggregate computes: the aggregate types it, so it declares no `type`, and only the
305
+ * two a row change turns into a delta - `count` and `sum` - may be `stored`.
306
+ */
307
+ type AggregateOptionsFor<V, E> = Except<FieldOptions<NonNullable<V>, E>, 'computed' | 'stored' | 'type'> & ({
308
+ readonly computed: AggregateReading<E, V, boolean>;
309
+ readonly stored?: false;
310
+ } | {
311
+ readonly computed: AggregateReading<E, V, true>;
312
+ readonly stored: true;
261
313
  });
314
+ /** An aggregate reading what the property holds, bivariant the way {@link EntitySql} is. */
315
+ type AggregateReading<E, V, S extends boolean> = {
316
+ agg(refs: ComputedRefs<E>): RelationAggregate<null extends V ? NonNullable<V> | null : NonNullable<V>, S>;
317
+ }['agg'];
262
318
  /** The entity a relation points at: `Company` for `company?: Company` and `companies?: Company[]` alike. */
263
319
  export type RelationTarget<V> = Extract<Unpacked<V>, object>;
264
320
  /**
@@ -360,6 +416,66 @@ export type RefMap<E, F extends keyof E = FieldKey<E>> = {
360
416
  export type EntitySql<E> = QueryRaw | {
361
417
  sql(refs: RefMap<E>): QueryRaw;
362
418
  }['sql'];
419
+ /** The fields of `C` a `sum` or an `avg` can add up. */
420
+ type NumericKey<C> = {
421
+ readonly [K in FieldKey<C>]-?: [NonNullable<C[K]>] extends [number | bigint] ? K : never;
422
+ }[FieldKey<C>];
423
+ /** One field of `C`, read off its refs: `(item) => item.amount`. */
424
+ type PickRef<C, K extends keyof C> = (refs: RefMap<C>) => ColumnRef<K & string>;
425
+ /**
426
+ * A relation as a `computed` field reads it, its aggregates typed against the related entity. `count`
427
+ * and `sum` are the two a row change turns into a delta, so they alone may be `stored`; the rest read
428
+ * as a subquery and are `null` where the relation holds no row.
429
+ */
430
+ export type RelationRef<C> = {
431
+ count(q?: AggregateFilter<C>): RelationAggregate<number, true>;
432
+ count(q: AggregatePage<C>): RelationAggregate<number, false>;
433
+ sum<K extends NumericKey<C>>(pick: PickRef<C, K>, q?: AggregateFilter<C>): RelationAggregate<NonNullable<C[K]>, true>;
434
+ sum<K extends NumericKey<C>>(pick: PickRef<C, K>, q: AggregateTopRows<C>): RelationAggregate<NonNullable<C[K]>, false>;
435
+ min<K extends FieldKey<C>>(pick: PickRef<C, K>, q?: AggregateRows<C>): RelationAggregate<NonNullable<C[K]> | null, false>;
436
+ max<K extends FieldKey<C>>(pick: PickRef<C, K>, q?: AggregateRows<C>): RelationAggregate<NonNullable<C[K]> | null, false>;
437
+ avg<K extends NumericKey<C>>(pick: PickRef<C, K>, q?: AggregateRows<C>): RelationAggregate<number | null, false>;
438
+ };
439
+ /**
440
+ * What an aggregate reads of the related rows. The predicate is an {@link EntityPredicate} rather than a
441
+ * full `$where` so that `stored: true` changes no call site: a trigger sees one row, and can evaluate
442
+ * nothing that traverses a relation or opens a subquery.
443
+ */
444
+ export type AggregateFilter<C> = {
445
+ readonly $where?: EntityPredicate<C>;
446
+ };
447
+ /**
448
+ * A tally capped to a page of the related rows, as `count` itself takes one. It needs no `$sort` and
449
+ * accepts none: an order picks *which* rows a page holds, never how many.
450
+ */
451
+ export type AggregatePage<C> = AggregateFilter<C> & Pick<RelationQuery<C>, '$limit' | '$skip'>;
452
+ /**
453
+ * The rows a value aggregate reads where it reads only some of them - "the five largest" - which only
454
+ * an order defines, so `$sort` and `$limit` come together. Keyed off the relation read they are, so
455
+ * the page an aggregate takes and the page a `$populate` takes cannot drift apart.
456
+ */
457
+ export type AggregateTopRows<C> = AggregateFilter<C> & Required<Pick<RelationQuery<C>, '$sort' | '$limit'>> & Pick<RelationQuery<C>, '$skip'>;
458
+ /** Either of those, for the aggregates that are never `stored` and so need no second signature. */
459
+ export type AggregateRows<C> = AggregateFilter<C> | AggregateTopRows<C>;
460
+ /** What a `computed` callback reads: the entity's fields as columns, its to-many relations as aggregates. */
461
+ export type ComputedRefs<E, F extends keyof E = FieldKey<E>, R extends keyof E = ToManyRelationKey<E>> = RefMap<E, F> & {
462
+ readonly [K in R]-?: RelationRef<RelationTarget<E[K]>>;
463
+ };
464
+ /** A relation aggregate a definition writes, bivariant the way {@link EntitySql} is. */
465
+ export type EntityAggregate<E> = {
466
+ agg(refs: ComputedRefs<E>): RelationAggregate;
467
+ }['agg'];
468
+ /**
469
+ * SQL a `computed` field writes. One callback shape for every arm, aggregate or not: overload
470
+ * resolution picks a contextual parameter type per arm only while they agree on one.
471
+ */
472
+ export type ComputedSql<E> = QueryRaw | {
473
+ sql(refs: ComputedRefs<E>): QueryRaw;
474
+ }['sql'];
475
+ /** The value a field's options declare it holds, where an aggregate is what declares it. */
476
+ export type AggregateValue<O> = O extends {
477
+ readonly computed: (...args: never[]) => RelationAggregate<infer V>;
478
+ } ? V : never;
363
479
  /** A predicate DDL can hold: the entity's own fields, without a relation, `$text` or a sub-query. */
364
480
  export type EntityPredicate<E> = QueryWhere<E> & {
365
481
  readonly [K in RelationKey<E>]?: never;
@@ -9,6 +9,13 @@ import type { EntityMeta, EntityWhereMeta, FieldOptions, IndexColumnSchema, Logg
9
9
  */
10
10
  export interface MigrationDefinition<Q extends Querier = SqlQuerier> {
11
11
  readonly name?: string;
12
+ /**
13
+ * `false` runs this migration outside a transaction, for a statement an engine refuses inside one -
14
+ * `CREATE INDEX CONCURRENTLY` on Postgres, the index a busy table needs. The cost is the rollback: a
15
+ * failure part-way leaves the statements before it applied and the migration unlogged. MongoDB
16
+ * creates collections outside any transaction already, so it changes nothing there.
17
+ */
18
+ readonly transaction?: boolean;
12
19
  up(querier: Q): Promise<void>;
13
20
  down(querier: Q): Promise<void>;
14
21
  }
@@ -1,4 +1,4 @@
1
- import type { FieldKey, JsonFieldPaths, RelationKey, RelationTarget, WrittenId } from './entity.js';
1
+ import type { FieldKey, JsonFieldPaths, RelationKey, RelationTarget, ToManyRelationKey, WrittenId } from './entity.js';
2
2
  import type { QueryLock } from './queryLock.js';
3
3
  import type { QueryRaw } from './queryRaw.js';
4
4
  import type { QueryWhere } from './queryWhere.js';
@@ -16,6 +16,12 @@ export type QueryOptions = {
16
16
  * already-deleted rows are removed too. No effect on entities without a soft-delete field.
17
17
  */
18
18
  hardDelete?: boolean;
19
+ /**
20
+ * `updateMany`/`deleteMany` only: address every row of the table on purpose. Without it a bulk write
21
+ * that names none - no `$where` and no `$limit` - is refused, since a forgotten filter and the whole
22
+ * table look alike. The entity's own filters never count as naming one.
23
+ */
24
+ unfiltered?: boolean;
19
25
  /**
20
26
  * prefix the query with this.
21
27
  */
@@ -116,15 +122,6 @@ export type QuerySortDirection = -1 | 1 | 'asc' | 'desc';
116
122
  * Accepted value for a field in `$sort` - either a direction or a vector similarity search.
117
123
  */
118
124
  export type QuerySortValue = QuerySortDirection | QueryVectorSearch;
119
- /**
120
- * To-one relations only: a parent holds many rows of a to-many, so there is no single value to order
121
- * it by, and joining one in would duplicate the parent instead. Order those inside `$populate`.
122
- */
123
- type ToOneRelationKey<E> = {
124
- [K in RelationKey<E>]: IsMany<E[K]> extends true ? never : K;
125
- }[RelationKey<E>];
126
- /** The relation names a parent holds many rows of, which a populated query fills with a list. */
127
- type ToManyRelationKey<E> = Exclude<RelationKey<E>, ToOneRelationKey<E>>;
128
125
  /**
129
126
  * Ordering parents by how many rows a to-many relation holds - "the ten users with the most posts".
130
127
  * The tally is computed per parent as a correlated count, never by loading the rows.