uql-orm 0.66.0 → 0.67.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 (193) hide show
  1. package/dist/browser/querier/httpQuerier.js +1 -8
  2. package/dist/browser/uql-browser.min.js.map +5 -5
  3. package/dist/bunSql/bunSql.util.d.ts +2 -6
  4. package/dist/bunSql/bunSql.util.js +2 -6
  5. package/dist/bunSql/bunSqlQuerier.d.ts +2 -5
  6. package/dist/bunSql/bunSqlQuerier.js +2 -5
  7. package/dist/cockroachdb/cockroachDialect.d.ts +4 -13
  8. package/dist/cockroachdb/cockroachDialect.js +4 -13
  9. package/dist/context/context.browser.js +2 -10
  10. package/dist/context/context.d.ts +4 -17
  11. package/dist/context/context.js +4 -17
  12. package/dist/dialect/abstractDialect.d.ts +4 -19
  13. package/dist/dialect/abstractDialect.js +2 -20
  14. package/dist/dialect/abstractSqlDialect.d.ts +47 -212
  15. package/dist/dialect/abstractSqlDialect.js +68 -222
  16. package/dist/dialect/aliases.d.ts +2 -12
  17. package/dist/dialect/aliases.js +4 -12
  18. package/dist/dialect/hydrateColumn.d.ts +2 -6
  19. package/dist/dialect/hydrateColumn.js +3 -13
  20. package/dist/dialect/jsonArrayElemMatchUtils.d.ts +1 -7
  21. package/dist/dialect/jsonArrayElemMatchUtils.js +1 -7
  22. package/dist/dialect/jsonSql.d.ts +6 -27
  23. package/dist/dialect/jsonSql.js +6 -27
  24. package/dist/dialect/mergeSqlDialect.d.ts +4 -22
  25. package/dist/dialect/mergeSqlDialect.js +4 -22
  26. package/dist/dialect/mysqlLikeSqlDialect.d.ts +11 -37
  27. package/dist/dialect/mysqlLikeSqlDialect.js +35 -51
  28. package/dist/dialect/pgLikeSqlDialect.d.ts +8 -22
  29. package/dist/dialect/pgLikeSqlDialect.js +36 -39
  30. package/dist/dialect/queryContext.d.ts +4 -22
  31. package/dist/dialect/queryContext.js +4 -22
  32. package/dist/dialect/queryJoins.d.ts +3 -12
  33. package/dist/dialect/queryJoins.js +3 -12
  34. package/dist/dialect/vectorCast.d.ts +2 -12
  35. package/dist/dialect/vectorCast.js +3 -19
  36. package/dist/dialect/vectorSqlDialect.d.ts +8 -38
  37. package/dist/dialect/vectorSqlDialect.js +7 -38
  38. package/dist/entity/decorator/bag.d.ts +6 -19
  39. package/dist/entity/decorator/bag.js +6 -22
  40. package/dist/entity/decorator/entity.d.ts +2 -7
  41. package/dist/entity/decorator/entity.js +2 -7
  42. package/dist/entity/decorator/members.d.ts +7 -30
  43. package/dist/entity/decorator/members.js +3 -12
  44. package/dist/entity/metadata/definition.d.ts +2 -18
  45. package/dist/entity/metadata/definition.js +6 -28
  46. package/dist/http/handler.d.ts +2 -14
  47. package/dist/index.d.ts +3 -1
  48. package/dist/index.js +3 -1
  49. package/dist/libsql/libsqlDialect.d.ts +1 -8
  50. package/dist/libsql/libsqlDialect.js +1 -8
  51. package/dist/maria/mariaDialect.d.ts +3 -5
  52. package/dist/maria/mariaDialect.js +5 -5
  53. package/dist/maria/mariadbQuerier.js +2 -2
  54. package/dist/maria/mariadbQuerierPool.js +1 -6
  55. package/dist/migrate/builder/migrationBuilder.js +3 -19
  56. package/dist/migrate/builder/splitSqlStatements.d.ts +1 -14
  57. package/dist/migrate/builder/splitSqlStatements.js +2 -22
  58. package/dist/migrate/builder/types.d.ts +2 -15
  59. package/dist/migrate/cli-config.js +2 -11
  60. package/dist/migrate/cli.js +2 -7
  61. package/dist/migrate/codegen/entityCodeGenerator.d.ts +0 -15
  62. package/dist/migrate/codegen/entityCodeGenerator.js +15 -44
  63. package/dist/migrate/codegen/fieldOptionsSource.d.ts +1 -8
  64. package/dist/migrate/codegen/fieldOptionsSource.js +3 -22
  65. package/dist/migrate/ddl/indexDdl.d.ts +2 -5
  66. package/dist/migrate/ddl/indexDdl.js +2 -5
  67. package/dist/migrate/ddl/pgIndexDdl.d.ts +3 -13
  68. package/dist/migrate/ddl/pgIndexDdl.js +3 -13
  69. package/dist/migrate/generator/definitionToNode.d.ts +2 -9
  70. package/dist/migrate/generator/definitionToNode.js +3 -17
  71. package/dist/migrate/generator/indexNodeToSchema.d.ts +2 -3
  72. package/dist/migrate/generator/indexNodeToSchema.js +2 -3
  73. package/dist/migrate/generator/mongoCommand.d.ts +1 -8
  74. package/dist/migrate/generator/mongoSchemaGenerator.d.ts +1 -8
  75. package/dist/migrate/generator/mongoSchemaGenerator.js +1 -8
  76. package/dist/migrate/introspection/abstractSqlSchemaIntrospector.d.ts +6 -26
  77. package/dist/migrate/introspection/abstractSqlSchemaIntrospector.js +9 -41
  78. package/dist/migrate/introspection/baseSqlIntrospector.js +0 -1
  79. package/dist/migrate/introspection/mongoIntrospector.d.ts +3 -1
  80. package/dist/migrate/introspection/mongoIntrospector.js +48 -46
  81. package/dist/migrate/introspection/mssqlIntrospector.d.ts +4 -4
  82. package/dist/migrate/introspection/mssqlIntrospector.js +18 -27
  83. package/dist/migrate/introspection/mysqlIntrospector.d.ts +7 -2
  84. package/dist/migrate/introspection/mysqlIntrospector.js +16 -14
  85. package/dist/migrate/introspection/postgresIntrospector.d.ts +24 -9
  86. package/dist/migrate/introspection/postgresIntrospector.js +68 -59
  87. package/dist/migrate/introspection/sqliteIntrospector.d.ts +1 -1
  88. package/dist/migrate/introspection/sqliteIntrospector.js +8 -10
  89. package/dist/migrate/migrator.d.ts +9 -53
  90. package/dist/migrate/migrator.js +32 -65
  91. package/dist/migrate/schemaGenerator.d.ts +16 -66
  92. package/dist/migrate/schemaGenerator.js +21 -74
  93. package/dist/mongo/mongoDialect.d.ts +21 -53
  94. package/dist/mongo/mongoDialect.js +25 -70
  95. package/dist/mongo/mongodbQuerier.d.ts +5 -8
  96. package/dist/mongo/mongodbQuerier.js +31 -65
  97. package/dist/mssql/mssqlDialect.d.ts +8 -34
  98. package/dist/mssql/mssqlDialect.js +37 -51
  99. package/dist/mssql/mssqlQuerier.d.ts +37 -4
  100. package/dist/mssql/mssqlQuerier.js +2 -2
  101. package/dist/mssql/mssqlWireTypes.d.ts +2 -14
  102. package/dist/mssql/mssqlWireTypes.js +2 -14
  103. package/dist/nestjs/uqlModule.js +2 -7
  104. package/dist/pglite/pgliteQuerier.d.ts +1 -9
  105. package/dist/pglite/pgliteQuerierPool.d.ts +4 -26
  106. package/dist/pglite/pgliteQuerierPool.js +3 -18
  107. package/dist/postgres/abstractPgQuerierPool.d.ts +1 -8
  108. package/dist/postgres/abstractPgQuerierPool.js +1 -8
  109. package/dist/postgres/pgNumericTypes.d.ts +3 -26
  110. package/dist/postgres/pgNumericTypes.js +3 -26
  111. package/dist/postgres/postgresDialect.d.ts +4 -10
  112. package/dist/postgres/postgresDialect.js +4 -10
  113. package/dist/querier/abstractQuerier.d.ts +35 -103
  114. package/dist/querier/abstractQuerier.js +105 -201
  115. package/dist/querier/abstractSharedHandleQuerierPool.d.ts +3 -17
  116. package/dist/querier/abstractSharedHandleQuerierPool.js +3 -17
  117. package/dist/querier/abstractSqlQuerier.d.ts +15 -36
  118. package/dist/querier/abstractSqlQuerier.js +49 -131
  119. package/dist/schema/canonicalType.d.ts +3 -21
  120. package/dist/schema/canonicalType.js +22 -67
  121. package/dist/schema/dependencyGraph.d.ts +2 -8
  122. package/dist/schema/dependencyGraph.js +2 -32
  123. package/dist/schema/index.d.ts +1 -25
  124. package/dist/schema/index.js +0 -26
  125. package/dist/schema/indexColumns.d.ts +1 -8
  126. package/dist/schema/indexColumns.js +1 -8
  127. package/dist/schema/indexDifferences.d.ts +7 -40
  128. package/dist/schema/indexDifferences.js +6 -31
  129. package/dist/schema/schemaAST.d.ts +8 -175
  130. package/dist/schema/schemaAST.js +13 -365
  131. package/dist/schema/schemaASTBuilder.d.ts +2 -24
  132. package/dist/schema/schemaASTBuilder.js +2 -30
  133. package/dist/schema/schemaASTDiffer.d.ts +6 -46
  134. package/dist/schema/schemaASTDiffer.js +8 -56
  135. package/dist/schema/types.d.ts +5 -61
  136. package/dist/schema/types.js +3 -6
  137. package/dist/sqlite/abstractSqliteQuerier.d.ts +1 -8
  138. package/dist/sqlite/localSqliteQuerierPool.d.ts +1 -7
  139. package/dist/sqlite/localSqliteQuerierPool.js +1 -7
  140. package/dist/sqlite/nodeSqliteQuerierPool.d.ts +2 -7
  141. package/dist/sqlite/nodeSqliteQuerierPool.js +2 -7
  142. package/dist/sqlite/sqliteDialect.d.ts +5 -20
  143. package/dist/sqlite/sqliteDialect.js +29 -35
  144. package/dist/turso/tursoDialect.d.ts +4 -6
  145. package/dist/turso/tursoDialect.js +4 -6
  146. package/dist/turso/tursoLocalQuerierPool.d.ts +1 -7
  147. package/dist/turso/tursoLocalQuerierPool.js +1 -7
  148. package/dist/turso/tursoQuerierPool.d.ts +2 -6
  149. package/dist/turso/tursoQuerierPool.js +2 -6
  150. package/dist/turso/tursoSessionQuerier.d.ts +1 -7
  151. package/dist/turso/tursoSessionQuerier.js +1 -7
  152. package/dist/type/dialect.d.ts +42 -94
  153. package/dist/type/dialect.js +3 -13
  154. package/dist/type/entity.d.ts +163 -534
  155. package/dist/type/entity.js +26 -9
  156. package/dist/type/logger.d.ts +2 -14
  157. package/dist/type/migration.d.ts +9 -38
  158. package/dist/type/querier.d.ts +9 -28
  159. package/dist/type/querierPool.d.ts +4 -26
  160. package/dist/type/query.d.ts +19 -73
  161. package/dist/type/query.js +2 -7
  162. package/dist/type/queryAggregate.d.ts +18 -98
  163. package/dist/type/queryRaw.d.ts +1 -8
  164. package/dist/type/queryRaw.js +1 -8
  165. package/dist/type/queryWhere.d.ts +13 -61
  166. package/dist/type/universalQuerier.d.ts +18 -105
  167. package/dist/type/utility.d.ts +12 -24
  168. package/dist/type/vector.d.ts +8 -38
  169. package/dist/type/vector.js +1 -1
  170. package/dist/type/wire.d.ts +2 -5
  171. package/dist/util/dialect.util.d.ts +9 -27
  172. package/dist/util/dialect.util.js +10 -27
  173. package/dist/util/field.util.d.ts +2 -31
  174. package/dist/util/field.util.js +3 -43
  175. package/dist/util/fieldOption.util.d.ts +7 -15
  176. package/dist/util/fieldOption.util.js +1 -1
  177. package/dist/util/filters.util.d.ts +2 -5
  178. package/dist/util/filters.util.js +2 -5
  179. package/dist/util/logger.d.ts +2 -6
  180. package/dist/util/logger.js +2 -6
  181. package/dist/util/object.util.d.ts +2 -6
  182. package/dist/util/object.util.js +1 -5
  183. package/dist/util/raw.d.ts +3 -23
  184. package/dist/util/relationQuery.util.d.ts +3 -14
  185. package/dist/util/relationQuery.util.js +3 -14
  186. package/dist/util/rowKey.util.d.ts +2 -10
  187. package/dist/util/rowKey.util.js +2 -10
  188. package/dist/util/sql.util.d.ts +6 -37
  189. package/dist/util/sql.util.js +13 -73
  190. package/dist/util/sqlLiteral.d.ts +2 -13
  191. package/dist/util/sqlLiteral.js +8 -13
  192. package/dist/util/string.util.js +0 -2
  193. package/package.json +4 -4
@@ -1,15 +1,9 @@
1
- import { assertSoleId, getMeta, idOf, namesKey, relationOf, soleIdOf } from '../entity/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';
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';
3
3
  import { enrichError } from './queryError.js';
4
4
  /**
5
- * Rejects a nullish primary key before it reaches a statement. The by-id methods reduce to
6
- * `{ $where: { id } }`, and a key compared to `undefined` is *no filter*, so an unchecked one
7
- * addresses the whole table. An entity declares its id optional, which puts `undefined` inside
8
- * `IdValue<E>`, and the HTTP layer reaches these methods with parsed JSON regardless, so the guard
9
- * belongs at runtime.
10
- *
11
- * Its callers are all `async` so this surfaces as a rejection on every one of them: a guard that
12
- * threw synchronously from some and rejected from others would escape a caller's `.catch()`.
5
+ * Refuses a nullish id, which would reduce to no filter at all, and a composite id missing a column,
6
+ * which would address every row agreeing on the rest. Callers are `async`, so it always rejects.
13
7
  */
14
8
  function assertIdValue(entity, id) {
15
9
  if (id === undefined || id === null) {
@@ -29,13 +23,7 @@ function assertIdValue(entity, id) {
29
23
  throw new TypeError(`'${entity.name}' is addressed by an object carrying every key of its primary key (${ids.join(', ')}); missing ${missing.join(', ')}.`);
30
24
  }
31
25
  }
32
- /**
33
- * The one column a write path matches a parent's key against.
34
- *
35
- * Every caller reaches its parents through a sole id - `soleIdOf` refused a composite before any of
36
- * them - so the parent contributes exactly one column here, whether the relation goes through a
37
- * junction or straight to the child.
38
- */
26
+ /** The column a write matches a parent's sole key against, on the junction or the child. */
39
27
  function soleParentColumn(relOpts) {
40
28
  return parentJoins(relOpts, 1)[0].joined;
41
29
  }
@@ -100,12 +88,7 @@ export class AbstractQuerier {
100
88
  }
101
89
  });
102
90
  }
103
- /**
104
- * Resolves `[entity, query, opts]` for the dual call pattern: `(entity, q, opts)` (entity argument)
105
- * vs `(query, opts)` (entity via the query's `$entity` field). Generic in the query `Q` because it
106
- * only ever reads `$entity`: pinning it to one statement's shape made every caller launder its own
107
- * through a cast, which is how a read query's `$sort` used to reach a write's.
108
- */
91
+ /** `[entity, query, opts]` from either call form, `(entity, q, opts)` or `({ $entity, ...q }, opts)`. */
109
92
  resolveEntityQuery(entityOrQuery, maybeQueryOrOpts, maybeOpts) {
110
93
  if (typeof entityOrQuery === 'function' && entityOrQuery.prototype) {
111
94
  return [entityOrQuery, maybeQueryOrOpts ?? {}, maybeOpts];
@@ -163,16 +146,11 @@ export class AbstractQuerier {
163
146
  }
164
147
  async count(entityOrQuery, maybeQueryOrOpts, maybeOpts) {
165
148
  const [entity, q, opts] = this.resolveEntityQuery(entityOrQuery, maybeQueryOrOpts, maybeOpts);
166
- if (q.$skip !== undefined || q.$limit !== undefined) {
167
- const meta = getMeta(entity);
168
- const rows = await this.internalFindMany(entity, idOnlyQuery(meta, { $where: q.$where, $skip: q.$skip, $limit: q.$limit }), opts);
169
- return rows.length;
170
- }
171
149
  return this.internalCount(entity, q, opts);
172
150
  }
173
151
  async exists(entityOrQuery, maybeQueryOrOpts, maybeOpts) {
174
152
  const [entity, q, opts] = this.resolveEntityQuery(entityOrQuery, maybeQueryOrOpts, maybeOpts);
175
- return (await this.count(entity, { $where: q.$where, $limit: 1 }, opts)) > 0;
153
+ return (await this.internalCount(entity, { $where: q.$where, $limit: 1 }, opts)) > 0;
176
154
  }
177
155
  /**
178
156
  * Run an aggregate query.
@@ -203,12 +181,41 @@ export class AbstractQuerier {
203
181
  assertIdValue(entity, id);
204
182
  return this.updateMany(entity, { $where: whereIds(getMeta(entity), id) }, payload, opts);
205
183
  }
184
+ /** Settles the rows first where the update cascades, so a payload changing what `$where` reads still names them. */
206
185
  async updateMany(entity, q, payload, opts) {
207
- return this.hooked(entity, 'Update', [payload], ([row]) => {
208
- fillOnFields(getMeta(entity), [row], 'onUpdate');
209
- return this.internalUpdateMany(entity, q, row, opts);
186
+ const meta = getMeta(entity);
187
+ return this.hooked(entity, 'Update', [payload], async ([row]) => {
188
+ fillOnFields(meta, [row], 'onUpdate');
189
+ const relKeys = filterPersistableRelationKeys(meta, row, 'persist');
190
+ if (!relKeys.length && !this.settlesWrite(entity, q)) {
191
+ return this.updateColumns(entity, q, row, opts, 0);
192
+ }
193
+ const ids = await this.settleIds(entity, q, opts);
194
+ if (!ids.length) {
195
+ return 0;
196
+ }
197
+ const changes = await this.updateColumns(entity, { $where: whereIds(meta, ids) }, row, opts, ids.length);
198
+ for (const relKey of relKeys) {
199
+ await this.saveRelation(entity, relKey, ids.map((id) => ({ id, value: row[relKey] })), true);
200
+ }
201
+ return changes;
210
202
  });
211
203
  }
204
+ /** The UPDATE, skipped where the payload writes no column, reporting `unwritten` instead. */
205
+ async updateColumns(entity, q, row, opts, unwritten) {
206
+ const writes = filterFieldKeys(getMeta(entity), row, 'onUpdate').length > 0;
207
+ return writes ? this.internalUpdateMany(entity, q, row, opts) : unwritten;
208
+ }
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);
212
+ }
213
+ /** The ids `q` matches, in its own order and page. */
214
+ async settleIds(entity, q, opts) {
215
+ const meta = getMeta(entity);
216
+ const rows = await this.internalFindMany(entity, idOnlyQuery(meta, q), opts);
217
+ return rows.map((row) => idOf(meta, row));
218
+ }
212
219
  async restoreOneById(entity, id) {
213
220
  assertIdValue(entity, id);
214
221
  return this.restoreMany(entity, { $where: whereIds(getMeta(entity), id) });
@@ -223,12 +230,7 @@ export class AbstractQuerier {
223
230
  filters: { softDelete: false },
224
231
  });
225
232
  }
226
- /**
227
- * `beforeUpsert`/`afterUpsert` rather than the insert's or the update's pair: the database decides
228
- * which branch each row takes as the statement runs, so neither of those could be fired honestly -
229
- * but the upsert itself is a fact known before and after, and a row written with no hook at all
230
- * was how an `@Id({ onInsert })` or an audit trail silently skipped this path.
231
- */
233
+ /** 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. */
232
234
  async upsertOne(entity, conflictPaths, payload) {
233
235
  const meta = getMeta(entity);
234
236
  return this.hooked(entity, 'Upsert', [payload], async (rows) => {
@@ -252,40 +254,29 @@ export class AbstractQuerier {
252
254
  }
253
255
  async deleteMany(entityOrQuery, qOrOpts, maybeOpts) {
254
256
  const [entity, q, opts] = this.resolveEntityQuery(entityOrQuery, qOrOpts, maybeOpts);
255
- const doomed = await this.findDoomed(entity, q, opts);
256
- if (doomed?.length === 0) {
257
+ const meta = getMeta(entity);
258
+ const cascades = cascadesOnDelete(meta);
259
+ const watched = this.hasHook(entity, 'beforeDelete') || this.hasHook(entity, 'afterDelete');
260
+ if (!watched && !cascades && !this.settlesWrite(entity, q)) {
261
+ return this.internalDeleteMany(entity, q, opts);
262
+ }
263
+ // A hard delete takes already-soft-deleted rows too, so reading them back has to see them.
264
+ const readOpts = opts?.hardDelete ? { ...opts, filters: withoutSoftDeleteFilter(opts.filters) } : opts;
265
+ // A hook receives the rows themselves; the ids read off them name the same rows a second read might not.
266
+ const doomed = watched ? await this.internalFindMany(entity, q, readOpts) : [];
267
+ const ids = watched ? doomed.map((row) => idOf(meta, row)) : await this.settleIds(entity, q, readOpts);
268
+ if (!ids.length) {
257
269
  return 0;
258
270
  }
259
- // Where a snapshot was taken, the statement names those rows rather than resolving `q` a second
260
- // time. Two reads of one `$limit` with no total order are free to disagree, which would fire the
261
- // hooks for one row and delete another; naming them also spares the second read.
262
- let target = q;
263
- if (doomed) {
264
- const meta = getMeta(entity);
265
- const ids = doomed.map((it) => idOf(meta, it));
266
- target = { $where: whereIds(meta, ids) };
271
+ await this.emitHook(entity, 'beforeDelete', doomed);
272
+ // Children first: they hold the foreign key, which a schema without `ON DELETE CASCADE` enforces.
273
+ if (cascades) {
274
+ await this.deleteRelations(entity, ids, opts);
267
275
  }
268
- await this.emitHook(entity, 'beforeDelete', doomed ?? []);
269
- const changes = await this.internalDeleteMany(entity, target, opts);
270
- await this.emitHook(entity, 'afterDelete', doomed ?? []);
276
+ const changes = await this.internalDeleteMany(entity, { $where: whereIds(meta, ids) }, opts);
277
+ await this.emitHook(entity, 'afterDelete', doomed);
271
278
  return changes;
272
279
  }
273
- /**
274
- * The rows a delete is about to take, loaded only when a hook or listener is there to receive
275
- * them: the round trip is pure overhead for the (common) delete nobody is watching, and
276
- * `internalDeleteMany` has its own fast path that never reads the rows at all.
277
- *
278
- * `undefined` means nobody was watching, which is not the same as the empty array meaning nothing
279
- * matched - the caller deletes by `q` for the first and skips the statement entirely for the second.
280
- */
281
- async findDoomed(entity, q, opts) {
282
- if (!this.hasHook(entity, 'beforeDelete') && !this.hasHook(entity, 'afterDelete')) {
283
- return undefined;
284
- }
285
- // A hard delete takes already-soft-deleted rows too, so reading them back has to see them.
286
- const findOpts = opts?.hardDelete ? { ...opts, filters: withoutSoftDeleteFilter(opts.filters) } : opts;
287
- return this.internalFindMany(entity, q, findOpts);
288
- }
289
280
  async saveOne(entity, payload) {
290
281
  const [id] = await this.saveMany(entity, [payload]);
291
282
  return id;
@@ -339,39 +330,18 @@ export class AbstractQuerier {
339
330
  await (toInsert.length && toUpsert.length ? this.transaction(write) : write());
340
331
  return ids;
341
332
  }
342
- async insertRelations(entity, payload) {
343
- const meta = getMeta(entity);
344
- const entries = payload.reduce((acc, it) => {
345
- const relKeys = filterPersistableRelationKeys(meta, it, 'persist');
346
- if (relKeys.length > 0)
347
- acc.push({ it, relKeys });
348
- return acc;
349
- }, []);
350
- if (!entries.length)
351
- return;
352
- const idKey = soleIdOf(meta, 'saving a relation');
353
- await Promise.all(entries.map(({ it, relKeys }) => Promise.all(relKeys.map((relKey) => this.saveRelation(entity, [it[idKey]], it[relKey], relKey)))));
354
- }
355
- async updateRelations(entity, q, payload, opts) {
333
+ /** Writes each inserted row's relations, one set of statements per relation whatever the number of rows. */
334
+ async insertRelations(entity, rows) {
356
335
  const meta = getMeta(entity);
357
- const relKeys = filterPersistableRelationKeys(meta, payload, 'persist');
358
- if (!relKeys.length) {
359
- return;
360
- }
361
- const idKey = soleIdOf(meta, 'saving a relation');
362
- const founds = await this.findMany(entity, idOnlyQuery(meta, q), opts);
363
- const ids = founds.map((found) => found[idKey]);
364
- if (!ids.length) {
365
- return;
366
- }
367
- for (const relKey of relKeys) {
368
- await this.saveRelation(entity, ids, payload[relKey], relKey, true);
336
+ const [idKey] = meta.ids;
337
+ for (const relKey of filterPersistableRelationKeys(meta, meta.relations, 'persist')) {
338
+ const writes = rows.flatMap((row) => (row[relKey] == null ? [] : [{ id: row[idKey], value: row[relKey] }]));
339
+ if (writes.length) {
340
+ await this.saveRelation(entity, relKey, writes, false);
341
+ }
369
342
  }
370
343
  }
371
- /**
372
- * `EntityId` because a settled set names composite rows as objects, which is also what the parent's
373
- * own delete takes - and what {@link childrenOf} reads each child's foreign key columns out of.
374
- */
344
+ /** `EntityId` because a settled composite row is an object, which {@link childrenOf} reads each foreign key column out of. */
375
345
  async deleteRelations(entity, ids, opts) {
376
346
  const meta = getMeta(entity);
377
347
  const relKeys = filterPersistableRelationKeys(meta, meta.relations, 'delete');
@@ -385,101 +355,53 @@ export class AbstractQuerier {
385
355
  }
386
356
  }
387
357
  /**
388
- * Persists `relValue` against every id in `ids`, which an update hands the whole page of rows it
389
- * settled: the value is the same for all of them, so the statements are per relation rather than per
390
- * row wherever the cardinality allows it.
358
+ * Writes each parent's value into one relation. The parent owns what it points at: an update replaces
359
+ * it, and a `null` only clears it.
391
360
  */
392
- async saveRelation(entity, ids, relValue, relKey, isUpdate) {
361
+ async saveRelation(entity, relKey, writes, isUpdate) {
393
362
  const meta = getMeta(entity);
394
- const relOpts = relationOf(meta, relKey);
395
- // Here rather than only in the callers below: writing the parent's key into a child is one column
396
- // per key, so a composite takes a statement per parent. `soleParentColumn` and the sole
397
- // `targetKeyColumns` under it read the *first* pair, which is a real column of a wrong pairing
398
- // unless this has run - and this method is `protected`, so a caller can arrive without them.
363
+ // Writing the parent's key into a child is one column per key, and the helpers below read the first pair.
399
364
  assertSoleId(meta, 'saving a relation');
365
+ const relOpts = relationOf(meta, relKey);
400
366
  const relEntity = relOpts.entity();
401
- switch (relOpts.cardinality) {
402
- case '1m':
403
- case 'mm':
404
- return this.saveToMany(relOpts, relEntity, ids, relValue, isUpdate);
405
- case '11':
406
- return this.saveOneToOne(relEntity, relOpts, ids, relValue, isUpdate);
407
- case 'm1':
408
- if (relValue)
409
- return this.saveManyToOne(entity, relEntity, relOpts, ids, relValue);
367
+ if (relOpts.cardinality === 'm1') {
368
+ return this.saveManyToOne(entity, relEntity, relOpts.references[0].local, writes);
410
369
  }
411
- }
412
- async saveToMany(relOpts, relEntity, ids, relPayload, isUpdate) {
413
- const { through } = relOpts;
414
- if (through) {
415
- const localField = soleParentColumn(relOpts);
416
- const [targetColumn] = targetKeyColumns(relOpts, 1);
417
- const throughEntity = through();
418
- if (isUpdate) {
419
- await this.deleteMany(throughEntity, { $where: { [localField]: ids } });
420
- }
421
- if (relPayload) {
422
- // Saved per parent on purpose: each one owns its copies of the children, and saving them once
423
- // would link every parent to a single shared row instead.
424
- for (const id of ids) {
425
- const savedIds = await this.saveMany(relEntity, relPayload);
426
- // A link needs the target's id, and a driver that cannot report one (a MySQL batch mixing
427
- // supplied and generated keys) would otherwise write a row pointing at `undefined`.
428
- if (savedIds.some((relId) => relId === undefined)) {
429
- throw new TypeError(`'${relEntity.name}' rows saved through '${throughEntity.name}' reported no id, so they cannot be linked. ` +
430
- 'Insert them with their own ids, or save the relation in its own statement.');
431
- }
432
- await this.insertMany(throughEntity, savedIds.map((relId) => ({ [localField]: id, [targetColumn]: relId })));
433
- }
434
- }
370
+ const holder = relOpts.through ? relOpts.through() : relEntity;
371
+ const parentColumn = soleParentColumn(relOpts);
372
+ if (isUpdate) {
373
+ const ids = writes.map(({ id }) => id);
374
+ await this.deleteMany(holder, { $where: { [parentColumn]: ids } });
375
+ }
376
+ // Each parent gets its own copies, so a row listed for two parents is written twice.
377
+ const children = writes.flatMap(({ id, value }) => [value ?? []].flat().map((row) => ({ id, row })));
378
+ if (!children.length) {
435
379
  return;
436
380
  }
437
- const foreignField = soleParentColumn(relOpts);
438
- if (isUpdate) {
439
- await this.deleteMany(relEntity, { $where: { [foreignField]: ids } });
381
+ if (!relOpts.through) {
382
+ await this.saveMany(relEntity, children.map(({ id, row }) => ({ ...row, [parentColumn]: id })));
383
+ return;
440
384
  }
441
- if (relPayload) {
442
- await this.saveMany(relEntity, ids.flatMap((id) => relPayload.map((it) => ({ ...it, [foreignField]: id }))));
385
+ const savedIds = await this.saveMany(relEntity, children.map(({ row }) => row));
386
+ // A link needs the target's id, which a MySQL batch mixing supplied and generated keys cannot report.
387
+ if (savedIds.includes(undefined)) {
388
+ throw new TypeError(`'${relEntity.name}' rows saved through '${holder.name}' reported no id, so they cannot be linked. ` +
389
+ 'Insert them with their own ids, or save the relation in its own statement.');
443
390
  }
391
+ const [targetColumn] = targetKeyColumns(relOpts, 1);
392
+ await this.insertMany(holder, children.map(({ id }, index) => ({ [parentColumn]: id, [targetColumn]: savedIds[index] })));
444
393
  }
445
- async saveOneToOne(relEntity, relOpts, ids, relPayload, isUpdate) {
446
- const foreignField = soleParentColumn(relOpts);
447
- // The same rule a to-many follows: the parent owns its child, so an update replaces it. Without
448
- // this the old row stayed behind and a one-to-one `$populate` had two rows to choose from.
449
- if (relPayload === null || isUpdate) {
450
- await this.deleteMany(relEntity, { $where: { [foreignField]: ids } });
451
- if (relPayload === null) {
452
- return;
453
- }
454
- }
455
- await this.saveMany(relEntity, ids.map((id) => ({ ...relPayload, [foreignField]: id })));
456
- }
457
- async saveManyToOne(entity, relEntity, relOpts, ids, relPayload) {
458
- // Not `soleParentColumn`: a many-to-one points the other way, so this is the *parent's* own column
459
- // holding the child's id - the one place `references[0].local` does not name a key of the parent.
460
- const localField = relOpts.references[0].local;
461
- // Per parent: each gets its own reference row, so each `SET` carries a different value.
462
- for (const id of ids) {
463
- const referenceId = await this.insertOne(relEntity, relPayload);
464
- await this.updateOneById(entity, id, { [localField]: referenceId });
394
+ /** Each parent gets its own referenced row, and its own column pointing at it. */
395
+ async saveManyToOne(entity, relEntity, localColumn, writes) {
396
+ const pointing = writes.filter(({ value }) => value);
397
+ const referenceIds = await this.insertMany(relEntity, pointing.map(({ value }) => value));
398
+ for (const [index, { id }] of pointing.entries()) {
399
+ await this.updateOneById(entity, id, { [localColumn]: referenceIds[index] });
465
400
  }
466
401
  }
467
402
  /**
468
- * Runs `callback` in a transaction: begin, commit on success, roll back on failure.
469
- *
470
- * The single place that sequence is written; everything else delegates here, because both subtleties
471
- * below were got wrong by code that hand-rolled it:
472
- *
473
- * - `beginTransaction` connects before it begins, so a refused connection lands in the catch with no
474
- * transaction open. `rollbackTransaction` is a no-op there rather than an error, which is why a
475
- * wrong password no longer surfaces as a transaction-state error.
476
- * - A rollback that fails too is a consequence of the original failure, not news, so it must not
477
- * replace it either.
478
- *
479
- * The connection is **not** released here: whoever took it from the pool gives it back, through
480
- * {@link QuerierPool.transaction}, {@link QuerierPool.withQuerier} or `await using`. Releasing a
481
- * connection this method never acquired is what forced every caller to know whether it still owned
482
- * one afterwards.
403
+ * Runs `callback` in a transaction, joining one already open. A rollback that fails is logged, never
404
+ * thrown over the original error, and the connection stays with whoever acquired it.
483
405
  */
484
406
  async transaction(callback, opts) {
485
407
  if (this.hasOpenTransaction) {
@@ -578,27 +500,13 @@ export class AbstractQuerier {
578
500
  }
579
501
  await runHooks(entity, event, payloads, { querier: this });
580
502
  }
581
- /**
582
- * Runs `task` after everything already queued on this querier, one at a time.
583
- *
584
- * @remarks Not re-entrant: only one task runs at a time, so a serialized method awaited from inside
585
- * another one would wait for a task queued behind itself. Callers below keep their `serialize` calls
586
- * sequential rather than nested.
587
- */
503
+ /** Runs `task` after everything already queued, one at a time. Not re-entrant: never nest `serialize` calls. */
588
504
  serialize(task) {
589
505
  const res = this.taskQueue.then(task);
590
506
  this.taskQueue = res.catch(() => { });
591
507
  return res;
592
508
  }
593
- /**
594
- * Runs `task`, logs `query` with how long it took, and tags any error it throws with that query.
595
- *
596
- * A method rather than the `@Log()` decorator it replaces. A standard-spec method decorator works by
597
- * returning a replacement function, and a replacement cannot carry the original's type parameters, so
598
- * decorating `internalFindMany<E extends Document>` made its signature unresolvable. Most of the query
599
- * surface is generic like that, and wrapping at the call site costs one line while keeping the
600
- * signature intact.
601
- */
509
+ /** Runs `task`, logs `query` with its duration, and tags a failure with it: a method, since a decorator would lose the generics. */
602
510
  async timed(query, values, task) {
603
511
  const startTime = performance.now();
604
512
  try {
@@ -612,12 +520,8 @@ export class AbstractQuerier {
612
520
  }
613
521
  }
614
522
  /**
615
- * Rolls back an unfinished transaction, then hands the connection back.
616
- *
617
- * @remarks Refusing to release was the opposite of safe: the throw came *before* the connection went
618
- * back, so it destroyed the error that got here and cost the pool a connection with a live `BEGIN` on
619
- * it. It is also the only option `await using` can reach, which calls `Symbol.asyncDispose` with no
620
- * arguments and discards what it returns.
523
+ * Rolls back an unfinished transaction, then hands the connection back, discarding it if the rollback
524
+ * failed. Never throws first, since `await using` has no other way to release.
621
525
  */
622
526
  async release() {
623
527
  let discard = false;
@@ -2,23 +2,9 @@ import type { AbstractSqlDialect } from '../dialect/index.js';
2
2
  import type { SqlQuerier } from '../type/index.js';
3
3
  import { AbstractSqlQuerierPool } from './abstractSqlQuerierPool.js';
4
4
  /**
5
- * Base pool for a handle opened once and kept for the pool's lifetime: the one connection every local
6
- * SQLite driver, the embedded Turso engine and PGlite give per database, or libSQL's client.
7
- *
8
- * The handle is shared, but each acquisition gets its own querier, so transaction state stays per unit
9
- * of work. On a single connection that state is not *isolated*, which is the one way these differ from
10
- * a real pool: two queriers cannot hold independent transactions, and a unit of work that needs one needs
11
- * its own pool and therefore its own database. libSQL's client opens a session per transaction instead.
12
- *
13
- * What a second `BEGIN` then does is the engine's, not this class's: SQLite and the embedded Turso
14
- * engine both refuse it ("cannot start a transaction within a transaction"), while PGlite accepts it
15
- * into the transaction already open - see {@link PgliteQuerierPool}, which is why that one is worth
16
- * saying out loud.
17
- *
18
- * Subclasses supply only how to open the handle and how to wrap it.
19
- *
20
- * @remarks Deliberately not re-exported from `querier/index.ts`, which the root entry point re-exports:
21
- * only the driver entries that open a single handle need this, and each imports it by path.
5
+ * A pool over one handle kept for its lifetime: a local SQLite file, the embedded Turso engine, PGlite, or a
6
+ * libSQL client. Each acquisition gets its own querier, but on one connection two cannot hold separate
7
+ * transactions: a unit of work needing its own needs its own pool. Imported by path, not from `querier/index.ts`.
22
8
  */
23
9
  export declare abstract class AbstractSharedHandleQuerierPool<DB extends {
24
10
  close(): unknown;
@@ -1,22 +1,8 @@
1
1
  import { AbstractSqlQuerierPool } from './abstractSqlQuerierPool.js';
2
2
  /**
3
- * Base pool for a handle opened once and kept for the pool's lifetime: the one connection every local
4
- * SQLite driver, the embedded Turso engine and PGlite give per database, or libSQL's client.
5
- *
6
- * The handle is shared, but each acquisition gets its own querier, so transaction state stays per unit
7
- * of work. On a single connection that state is not *isolated*, which is the one way these differ from
8
- * a real pool: two queriers cannot hold independent transactions, and a unit of work that needs one needs
9
- * its own pool and therefore its own database. libSQL's client opens a session per transaction instead.
10
- *
11
- * What a second `BEGIN` then does is the engine's, not this class's: SQLite and the embedded Turso
12
- * engine both refuse it ("cannot start a transaction within a transaction"), while PGlite accepts it
13
- * into the transaction already open - see {@link PgliteQuerierPool}, which is why that one is worth
14
- * saying out loud.
15
- *
16
- * Subclasses supply only how to open the handle and how to wrap it.
17
- *
18
- * @remarks Deliberately not re-exported from `querier/index.ts`, which the root entry point re-exports:
19
- * only the driver entries that open a single handle need this, and each imports it by path.
3
+ * A pool over one handle kept for its lifetime: a local SQLite file, the embedded Turso engine, PGlite, or a
4
+ * libSQL client. Each acquisition gets its own querier, but on one connection two cannot hold separate
5
+ * transactions: a unit of work needing its own needs its own pool. Imported by path, not from `querier/index.ts`.
20
6
  */
21
7
  export class AbstractSharedHandleQuerierPool extends AbstractSqlQuerierPool {
22
8
  /**
@@ -1,5 +1,5 @@
1
1
  import type { AbstractSqlDialect } from '../dialect/index.js';
2
- import type { EntityData, ExtraOptions, Query, QueryAggMap, QueryAggregate, QueryAggregateResult, QueryConflictPaths, QueryFilter, QueryGroupMap, QueryOptions, QuerySearch, QueryUpdateResult, SqlQuerier, TransactionOptions, Type, UpdatePayload } from '../type/index.js';
2
+ import type { EntityData, ExtraOptions, Query, QueryAggMap, QueryAggregate, QueryAggregateResult, QueryConflictPaths, QueryPage, QueryGroupMap, QueryOptions, QuerySearch, QueryUpdateResult, SqlQuerier, TransactionOptions, Type, UpdatePayload } from '../type/index.js';
3
3
  import type { BuildUpdateResultPayload } from '../util/sql.util.js';
4
4
  import { AbstractQuerier } from './abstractQuerier.js';
5
5
  export declare abstract class AbstractSqlQuerier extends AbstractQuerier implements SqlQuerier {
@@ -34,46 +34,27 @@ export declare abstract class AbstractSqlQuerier extends AbstractQuerier impleme
34
34
  protected lazyConnect(): Promise<void>;
35
35
  all<T>(query: string, values?: unknown[]): Promise<T[]>;
36
36
  run(query: string, values?: unknown[]): Promise<QueryUpdateResult>;
37
+ /** The rows of a statement the dialect builds. */
38
+ private query;
39
+ /** Runs a statement the dialect builds. */
40
+ private exec;
37
41
  /**
38
- * `$lock` outside a transaction is always a bug, and a silent one. Every engine accepts
39
- * `SELECT ... FOR UPDATE` in autocommit and then releases the lock as the statement commits,
40
- * before the caller has seen a row: the SQL is correct, nothing is omitted, and no layer below
41
- * this one can tell. The dialect cannot check it either, being stateless and shared by every
42
- * connection of the pool, so this is the only place it can be caught.
43
- *
44
- * The capability check runs first on purpose: "this engine has no row locks" is the more
45
- * actionable answer, and on SQLite it is the answer either way.
42
+ * Refuses a `$lock` the engine lacks, then one outside a transaction, where the lock would drop as the
43
+ * statement commits; only the querier knows whether one is open.
46
44
  */
47
45
  protected assertLockable<E>(entity: Type<E>, q: Query<E>): void;
48
46
  /**
49
- * Run the `SET`s that tune an ANN index for this query, and refuse the ones that would not apply.
50
- *
51
- * Same shape as {@link assertLockable} and for the same reason: a `SET LOCAL` outside a transaction
52
- * is accepted, applies to nothing, and leaves the query running at the engine's default recall -
53
- * correct SQL, silently untuned. Only the querier knows whether a transaction is open.
54
- *
55
- * The statements go through `internalRun`, sharing this querier's single connection with the query
56
- * they precede; `SET LOCAL` then expires with the transaction, so nothing is left behind.
47
+ * Runs the `SET`s tuning an ANN index for the query on its connection, refusing where they would apply to
48
+ * nothing: a `SET LOCAL` outside a transaction.
57
49
  */
58
50
  private applyVectorTuning;
59
51
  protected internalFindMany<E extends object>(entity: Type<E>, q: Query<E>, opts?: QueryOptions): Promise<E[]>;
52
+ /** Every row `q` matches past its page, deduplicated where it reads `$distinct`. */
53
+ private countUnpaged;
60
54
  /**
61
- * How to count when the total cannot ride along in the read's own `COUNT(*) OVER ()` column, or
62
- * `undefined` when it can. Two clauses rule the window out: `$distinct`, because a window counts
63
- * before the deduplication and so overstates the page, and `$lock` on an engine that refuses the
64
- * pair outright. Both then cost a second statement; only the counting differs.
65
- */
66
- private countedSeparately;
67
- /**
68
- * One statement for both: the page carries its own unpaged total in an extra column. An empty page
69
- * has no row to carry it, which is the one case still needing a count of its own - a `$skip` past
70
- * the end, or a filter nothing matched.
71
- *
72
- * A `$required` relation needs no special case: the window counts what the INNER JOIN left, which
73
- * is exactly the total a caller of a filtered read is asking for. A `$lock` is the one clause an
74
- * engine may refuse to have in the same statement, which {@link AbstractSqlDialect.supportsWindowWithRowLock}
75
- * answers; where it does, the total comes from a count of its own. A `$distinct` read needs one
76
- * too, and a deduplicating one: see {@link AbstractSqlDialect.countDistinct}.
55
+ * The page and its unpaged total in one statement, a window column on each row, and a count of its own for
56
+ * an empty page. A window counts before `DISTINCT`, and the Postgres family refuses one beside a `$lock`,
57
+ * so those count apart.
77
58
  */
78
59
  protected internalFindManyAndCount<E extends object>(entity: Type<E>, q: Query<E>, opts?: QueryOptions): Promise<[E[], number]>;
79
60
  private selectRows;
@@ -104,13 +85,11 @@ export declare abstract class AbstractSqlQuerier extends AbstractQuerier impleme
104
85
  * a catalog that does not know the table answers with no row, which is nothing counted.
105
86
  */
106
87
  private runCount;
107
- protected internalCount<E extends object>(entity: Type<E>, q?: QueryFilter<E>, opts?: QueryOptions): Promise<number>;
88
+ protected internalCount<E extends object>(entity: Type<E>, q: QueryPage<E>, opts?: QueryOptions): Promise<number>;
108
89
  estimatedCount<E extends object>(entity: Type<E>): Promise<number>;
109
90
  protected 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>[]>;
110
91
  internalInsertMany<E extends object>(entity: Type<E>, rows: EntityData<E>[]): Promise<void>;
111
92
  internalUpdateMany<E extends object>(entity: Type<E>, q: QuerySearch<E>, payload: UpdatePayload<E>, opts?: QueryOptions): Promise<number>;
112
- /** The ids matching `q`, in `q`'s own order and page, so a write can name the rows it settled on. */
113
- private settleIds;
114
93
  protected internalUpsertOne<E extends object>(entity: Type<E>, conflictPaths: QueryConflictPaths<E>, payload: EntityData<E>): Promise<QueryUpdateResult>;
115
94
  protected internalUpsertMany<E extends object>(entity: Type<E>, conflictPaths: QueryConflictPaths<E>, payload: EntityData<E>[]): Promise<QueryUpdateResult>;
116
95
  private runUpsert;