uql-orm 0.53.0 → 0.55.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 (158) hide show
  1. package/README.md +2 -2
  2. package/dist/browser/uql-browser.min.js.map +2 -2
  3. package/dist/bunSql/bunSql.util.d.ts +3 -14
  4. package/dist/bunSql/bunSql.util.js +33 -56
  5. package/dist/bunSql/bunSqlQuerier.d.ts +3 -6
  6. package/dist/bunSql/bunSqlQuerier.js +7 -13
  7. package/dist/bunSql/bunSqlQuerierPool.d.ts +10 -5
  8. package/dist/bunSql/bunSqlQuerierPool.js +25 -10
  9. package/dist/cockroachdb/cockroachDialect.d.ts +2 -5
  10. package/dist/cockroachdb/cockroachDialect.js +2 -5
  11. package/dist/cockroachdb/crdbQuerierPool.d.ts +1 -3
  12. package/dist/cockroachdb/crdbQuerierPool.js +0 -4
  13. package/dist/cockroachdb/index.d.ts +0 -1
  14. package/dist/cockroachdb/index.js +0 -1
  15. package/dist/d1/d1SqliteDialect.d.ts +5 -0
  16. package/dist/d1/d1SqliteDialect.js +7 -0
  17. package/dist/dialect/abstractSqlDialect.d.ts +15 -9
  18. package/dist/dialect/abstractSqlDialect.js +47 -54
  19. package/dist/dialect/hydrateColumn.js +2 -2
  20. package/dist/dialect/mergeSqlDialect.d.ts +2 -2
  21. package/dist/dialect/mergeSqlDialect.js +0 -4
  22. package/dist/dialect/mysqlLikeSqlDialect.d.ts +0 -3
  23. package/dist/dialect/mysqlLikeSqlDialect.js +0 -3
  24. package/dist/dialect/pgLikeSqlDialect.d.ts +5 -0
  25. package/dist/dialect/pgLikeSqlDialect.js +12 -2
  26. package/dist/entity/decorator/members.d.ts +3 -10
  27. package/dist/entity/index.d.ts +1 -1
  28. package/dist/entity/index.js +1 -1
  29. package/dist/entity/metadata/definition.d.ts +3 -1
  30. package/dist/entity/metadata/definition.js +8 -4
  31. package/dist/libsql/index.d.ts +0 -1
  32. package/dist/libsql/index.js +0 -1
  33. package/dist/libsql/libsqlQuerierPool.d.ts +3 -5
  34. package/dist/libsql/libsqlQuerierPool.js +2 -5
  35. package/dist/maria/mariadbQuerier.d.ts +0 -3
  36. package/dist/maria/mariadbQuerier.js +6 -7
  37. package/dist/maria/mariadbQuerierPool.js +4 -7
  38. package/dist/migrate/builder/migrationBuilder.js +0 -4
  39. package/dist/migrate/ddl/index.d.ts +3 -3
  40. package/dist/migrate/ddl/index.js +3 -3
  41. package/dist/migrate/migrator.d.ts +3 -7
  42. package/dist/migrate/migrator.js +3 -7
  43. package/dist/migrate/schemaGenerator.d.ts +0 -13
  44. package/dist/migrate/schemaGenerator.js +0 -13
  45. package/dist/migrate/storage/databaseStorage.d.ts +2 -2
  46. package/dist/migrate/storage/databaseStorage.js +2 -2
  47. package/dist/mongo/index.d.ts +0 -1
  48. package/dist/mongo/index.js +0 -1
  49. package/dist/mongo/mongoDialect.d.ts +0 -1
  50. package/dist/mongo/mongoDialect.js +3 -14
  51. package/dist/mongo/mongodbQuerier.d.ts +3 -5
  52. package/dist/mongo/mongodbQuerier.js +23 -23
  53. package/dist/mongo/mongodbQuerierPool.d.ts +2 -2
  54. package/dist/mongo/mongodbQuerierPool.js +2 -2
  55. package/dist/mssql/mssqlDialect.d.ts +5 -5
  56. package/dist/mssql/mssqlDialect.js +11 -8
  57. package/dist/mssql/mssqlQuerier.d.ts +13 -6
  58. package/dist/mssql/mssqlQuerier.js +30 -75
  59. package/dist/mssql/mssqlQuerierPool.js +2 -0
  60. package/dist/mssql/mssqlWireTypes.d.ts +3 -4
  61. package/dist/mssql/mssqlWireTypes.js +5 -8
  62. package/dist/mysql/index.d.ts +0 -1
  63. package/dist/mysql/index.js +0 -1
  64. package/dist/mysql/mysql2Querier.d.ts +1 -4
  65. package/dist/mysql/mysql2Querier.js +0 -3
  66. package/dist/mysql/mysql2QuerierPool.d.ts +2 -2
  67. package/dist/mysql/mysql2QuerierPool.js +5 -3
  68. package/dist/neon/index.d.ts +0 -2
  69. package/dist/neon/index.js +0 -2
  70. package/dist/neon/neonQuerierPool.d.ts +2 -4
  71. package/dist/neon/neonQuerierPool.js +2 -6
  72. package/dist/pglite/index.d.ts +0 -1
  73. package/dist/pglite/index.js +0 -1
  74. package/dist/pglite/pgliteQuerier.d.ts +3 -3
  75. package/dist/pglite/pgliteQuerier.js +1 -1
  76. package/dist/pglite/pgliteQuerierPool.d.ts +7 -2
  77. package/dist/pglite/pgliteQuerierPool.js +16 -6
  78. package/dist/postgres/abstractPgQuerierPool.d.ts +8 -12
  79. package/dist/postgres/abstractPgQuerierPool.js +8 -6
  80. package/dist/postgres/index.d.ts +0 -1
  81. package/dist/postgres/index.js +0 -1
  82. package/dist/postgres/pgNumericTypes.d.ts +5 -5
  83. package/dist/postgres/pgNumericTypes.js +11 -7
  84. package/dist/postgres/pgQuerier.d.ts +22 -4
  85. package/dist/postgres/pgQuerier.js +29 -2
  86. package/dist/postgres/pgQuerierPool.d.ts +2 -4
  87. package/dist/postgres/pgQuerierPool.js +2 -6
  88. package/dist/postgres/postgresDialect.d.ts +5 -5
  89. package/dist/postgres/postgresDialect.js +5 -5
  90. package/dist/postgres/postgresWireDriverCapabilities.d.ts +2 -2
  91. package/dist/postgres/postgresWireDriverCapabilities.js +2 -2
  92. package/dist/querier/abstractPoolQuerier.d.ts +1 -1
  93. package/dist/querier/abstractPoolQuerier.js +1 -1
  94. package/dist/querier/abstractQuerier.d.ts +17 -22
  95. package/dist/querier/abstractQuerier.js +80 -57
  96. package/dist/querier/abstractSqlQuerier.d.ts +20 -13
  97. package/dist/querier/abstractSqlQuerier.js +96 -100
  98. package/dist/schema/schemaASTBuilder.js +7 -7
  99. package/dist/sqlite/hranaQuerier.d.ts +6 -8
  100. package/dist/sqlite/hranaQuerier.js +13 -30
  101. package/dist/sqlite/hranaQuerierPool.d.ts +3 -4
  102. package/dist/sqlite/hranaQuerierPool.js +2 -1
  103. package/dist/sqlite/localSqliteQuerierPool.d.ts +3 -3
  104. package/dist/sqlite/localSqliteQuerierPool.js +4 -2
  105. package/dist/sqlite/nodeSqliteAdapter.d.ts +0 -1
  106. package/dist/sqlite/nodeSqliteQuerierPool.js +0 -2
  107. package/dist/sqlite/sqlitePragmas.d.ts +12 -0
  108. package/dist/sqlite/sqlitePragmas.js +15 -0
  109. package/dist/sqlite/sqliteQuerierPool.d.ts +0 -5
  110. package/dist/sqlite/sqliteQuerierPool.js +2 -13
  111. package/dist/turso/index.d.ts +0 -1
  112. package/dist/turso/index.js +0 -1
  113. package/dist/turso/tursoLocalQuerier.d.ts +0 -1
  114. package/dist/turso/tursoLocalQuerierPool.js +2 -2
  115. package/dist/turso/tursoQuerierPool.d.ts +1 -3
  116. package/dist/turso/tursoQuerierPool.js +0 -4
  117. package/dist/type/dialect.d.ts +1 -1
  118. package/dist/type/entity.d.ts +6 -11
  119. package/dist/type/migration.d.ts +0 -3
  120. package/dist/type/query.d.ts +12 -12
  121. package/dist/type/query.js +0 -6
  122. package/dist/type/universalQuerier.d.ts +3 -3
  123. package/dist/util/dialect.util.d.ts +4 -3
  124. package/dist/util/dialect.util.js +2 -1
  125. package/dist/util/field.util.d.ts +4 -16
  126. package/dist/util/field.util.js +6 -19
  127. package/dist/util/fieldOption.util.d.ts +1 -4
  128. package/dist/util/fieldOption.util.js +0 -2
  129. package/dist/util/logger.d.ts +10 -11
  130. package/dist/util/logger.js +21 -11
  131. package/dist/util/raw.d.ts +3 -10
  132. package/dist/util/raw.js +3 -3
  133. package/dist/util/sql.util.js +2 -2
  134. package/dist/util/sqlLiteral.js +3 -8
  135. package/dist/util/string.util.js +2 -6
  136. package/dist/util/wideNumber.d.ts +14 -0
  137. package/dist/util/wideNumber.js +24 -0
  138. package/package.json +1 -1
  139. package/dist/cockroachdb/crdbQuerier.d.ts +0 -8
  140. package/dist/cockroachdb/crdbQuerier.js +0 -6
  141. package/dist/libsql/libsqlQuerier.d.ts +0 -10
  142. package/dist/libsql/libsqlQuerier.js +0 -10
  143. package/dist/mongo/mongodbNativeDialect.d.ts +0 -9
  144. package/dist/mongo/mongodbNativeDialect.js +0 -9
  145. package/dist/mysql/mysql2Dialect.d.ts +0 -9
  146. package/dist/mysql/mysql2Dialect.js +0 -9
  147. package/dist/neon/neonDialect.d.ts +0 -10
  148. package/dist/neon/neonDialect.js +0 -10
  149. package/dist/neon/neonQuerier.d.ts +0 -5
  150. package/dist/neon/neonQuerier.js +0 -3
  151. package/dist/pglite/pgliteDialect.d.ts +0 -14
  152. package/dist/pglite/pgliteDialect.js +0 -14
  153. package/dist/postgres/abstractPgQuerier.d.ts +0 -24
  154. package/dist/postgres/abstractPgQuerier.js +0 -32
  155. package/dist/postgres/pgDialect.d.ts +0 -10
  156. package/dist/postgres/pgDialect.js +0 -10
  157. package/dist/turso/tursoQuerier.d.ts +0 -10
  158. package/dist/turso/tursoQuerier.js +0 -10
@@ -1,6 +1,6 @@
1
1
  import { COUNT_ALIAS, TOTAL_ALIAS } from '../dialect/aliases.js';
2
2
  import { decodeColumn } from '../dialect/hydrateColumn.js';
3
- import { getMeta, idOf, soleIdOf } from '../entity/index.js';
3
+ import { getMeta, idOf, namesKey } from '../entity/index.js';
4
4
  import { buildUpdateResult, cascadesOnDelete, clone, getInsertFieldKeys, insertShapeOf, getRelationRequestSummary, hasKeys, idOnlyQuery, isAutoIncrement, isPagedQuery, obtainAttrsPaths, throwNoPendingTransaction, throwPendingTransaction, unflatObject, unflatObjects, whereIds, withoutSoftDeleteFilter, } from '../util/index.js';
5
5
  import { AbstractQuerier } from './abstractQuerier.js';
6
6
  import { enrichError } from './queryError.js';
@@ -22,50 +22,36 @@ function partitionBySuppliedId(payload, idKey) {
22
22
  return supplied.length && generated.length ? [supplied, generated] : [supplied.length ? supplied : generated];
23
23
  }
24
24
  /**
25
- * Rows grouped by the columns they carry, payload order kept within each group - or `undefined` when
26
- * they all carry the same ones, which is the batch as it stands and needs no grouping at all.
25
+ * Row indexes grouped by the columns their rows carry, payload order kept within each group.
27
26
  *
28
27
  * Deliberately finer than {@link partitionBySuppliedId}: an upsert's `DO UPDATE SET` is one
29
28
  * assignment list for the whole statement, so rows of different shapes cannot share one at all.
30
29
  */
31
30
  function groupByInsertShape(meta, payload) {
32
- const first = insertShapeOf(meta, payload[0]);
33
- let index = 1;
34
- while (index < payload.length && insertShapeOf(meta, payload[index]) === first) {
35
- index++;
36
- }
37
- if (index === payload.length) {
38
- return undefined;
39
- }
40
- const groups = new Map([[first, payload.slice(0, index)]]);
41
- for (; index < payload.length; index++) {
42
- const row = payload[index];
43
- const shape = insertShapeOf(meta, row);
31
+ const groups = new Map();
32
+ for (let index = 0; index < payload.length; index++) {
33
+ const shape = insertShapeOf(meta, payload[index]);
44
34
  const group = groups.get(shape);
45
35
  if (group) {
46
- group.push(row);
36
+ group.push(index);
47
37
  }
48
38
  else {
49
- groups.set(shape, [row]);
39
+ groups.set(shape, [index]);
50
40
  }
51
41
  }
52
42
  return [...groups.values()];
53
43
  }
54
44
  /**
55
- * How many rows one statement can carry within the dialect's bind budget. `DEFAULT` cells bind no
56
- * parameter, so fields-per-record is a safe upper bound. Every multi-row write splits on this: D1
57
- * allows 100 binds, which a couple of dozen rows reach.
45
+ * A group's row indexes split into statements within the dialect's bind budget, payload order kept.
46
+ * `DEFAULT` cells bind no parameter, so fields-per-record is a safe upper bound. Every multi-row
47
+ * write splits on this: D1 allows 100 binds, which a couple of dozen rows reach.
58
48
  */
59
- function bindBudgetChunkSize(meta, rows, maxBindValues) {
60
- const fieldsPerRecord = getInsertFieldKeys(meta, rows).length || 1;
61
- return Math.max(1, Math.floor(maxBindValues / fieldsPerRecord));
62
- }
63
- /** `rows` split into statement-sized slices, payload order kept. */
64
- function chunkByBindBudget(meta, rows, maxBindValues) {
65
- const size = bindBudgetChunkSize(meta, rows, maxBindValues);
49
+ function chunkByBindBudget(meta, payload, group, maxBindValues) {
50
+ const fieldsPerRecord = getInsertFieldKeys(meta, group.map((index) => payload[index])).length;
51
+ const size = Math.max(1, Math.floor(maxBindValues / (fieldsPerRecord || 1)));
66
52
  const chunks = [];
67
- for (let start = 0; start < rows.length; start += size) {
68
- chunks.push(rows.slice(start, start + size));
53
+ for (let start = 0; start < group.length; start += size) {
54
+ chunks.push(group.slice(start, start + size));
69
55
  }
70
56
  return chunks;
71
57
  }
@@ -186,17 +172,6 @@ export class AbstractSqlQuerier extends AbstractQuerier {
186
172
  this.dialect.findPerParent(ctx, entity, q, partition);
187
173
  return this.hydrateRows(entity, q, await this.all(ctx.sql, ctx.values));
188
174
  }
189
- /**
190
- * One statement for both: the page carries its own unpaged total in an extra column. An empty page
191
- * has no row to carry it, which is the one case still needing a count of its own - a `$skip` past
192
- * the end, or a filter nothing matched.
193
- *
194
- * A `$required` relation needs no special case: the window counts what the INNER JOIN left, which
195
- * is exactly the total a caller of a filtered read is asking for. A `$lock` is the one clause an
196
- * engine may refuse to have in the same statement, which {@link AbstractSqlDialect.supportsWindowWithRowLock}
197
- * answers; where it does, the total comes from a count of its own. A `$distinct` read needs one
198
- * too, and a deduplicating one: see {@link AbstractSqlDialect.countDistinct}.
199
- */
200
175
  /**
201
176
  * How to count when the total cannot ride along in the read's own `COUNT(*) OVER ()` column, or
202
177
  * `undefined` when it can. Two clauses rule the window out: `$distinct`, because a window counts
@@ -212,6 +187,17 @@ export class AbstractSqlQuerier extends AbstractQuerier {
212
187
  }
213
188
  return undefined;
214
189
  }
190
+ /**
191
+ * One statement for both: the page carries its own unpaged total in an extra column. An empty page
192
+ * has no row to carry it, which is the one case still needing a count of its own - a `$skip` past
193
+ * the end, or a filter nothing matched.
194
+ *
195
+ * A `$required` relation needs no special case: the window counts what the INNER JOIN left, which
196
+ * is exactly the total a caller of a filtered read is asking for. A `$lock` is the one clause an
197
+ * engine may refuse to have in the same statement, which {@link AbstractSqlDialect.supportsWindowWithRowLock}
198
+ * answers; where it does, the total comes from a count of its own. A `$distinct` read needs one
199
+ * too, and a deduplicating one: see {@link AbstractSqlDialect.countDistinct}.
200
+ */
215
201
  async internalFindManyAndCount(entity, q, opts) {
216
202
  const separately = this.countedSeparately(entity, q, opts);
217
203
  if (separately) {
@@ -255,18 +241,19 @@ export class AbstractSqlQuerier extends AbstractQuerier {
255
241
  // The one path that does not go through `all`/`run`, so it connects on its own: streaming first on
256
242
  // a freshly acquired querier used to reach `getConn()` with nothing acquired.
257
243
  await this.lazyConnect();
244
+ // No `normalizeValues` here, unlike `all`/`run`: those also take raw SQL, while every value a
245
+ // context holds was normalized as it was bound.
258
246
  const ctx = this.dialect.createContext();
259
247
  this.dialect.find(ctx, entity, q, opts);
260
- const normalizedParams = this.dialect.normalizeValues(ctx.values);
261
248
  let attrsPaths;
262
249
  try {
263
- for await (const row of this.internalStream(ctx.sql, normalizedParams)) {
250
+ for await (const row of this.internalStream(ctx.sql, ctx.values)) {
264
251
  attrsPaths ??= obtainAttrsPaths(row);
265
252
  yield this.hydrateFields(entity, unflatObject(row, attrsPaths));
266
253
  }
267
254
  }
268
255
  catch (err) {
269
- throw enrichError(err, this.logger, ctx.sql, normalizedParams);
256
+ throw enrichError(err, this.logger, ctx.sql, ctx.values);
270
257
  }
271
258
  }
272
259
  /**
@@ -275,8 +262,7 @@ export class AbstractSqlQuerier extends AbstractQuerier {
275
262
  * Drivers with native cursor/streaming APIs (SQLite, Pg) should override this.
276
263
  */
277
264
  async *internalStream(query, values) {
278
- const rows = await this.internalAll(query, this.dialect.normalizeValues(values));
279
- yield* rows;
265
+ yield* await this.internalAll(query, values);
280
266
  }
281
267
  /**
282
268
  * Turn what a driver returned back into the types the entity declares, for the row and everything
@@ -359,24 +345,15 @@ export class AbstractSqlQuerier extends AbstractQuerier {
359
345
  }
360
346
  return res;
361
347
  }
362
- async internalInsertMany(entity, payload) {
363
- if (!payload?.length) {
364
- return [];
365
- }
366
- payload = clone(payload);
348
+ async internalInsertMany(entity, rows) {
367
349
  const meta = getMeta(entity);
368
- // What comes back is one column's value, so a composite reports nothing here and `insertMany`
369
- // names those rows from the payload instead. `sole` is what keeps every id path off a key that
370
- // is several columns.
350
+ // What comes back is one column's value, so nothing is read back for a composite: its rows
351
+ // already carry every column of it. `sole` is what keeps every id path off such a key.
371
352
  const [idKey] = meta.ids;
372
353
  const sole = meta.ids.length === 1;
373
354
  const idField = sole ? meta.fields[idKey] : undefined;
374
355
  const generatedKey = !!idField && isAutoIncrement(idField, true);
375
- const payloadIds = new Array(payload.length);
376
- for (const group of partitionBySuppliedId(payload, idKey)) {
377
- // Per group, not per batch: the two carry different columns - one names the key, one does not -
378
- // so a budget taken over their union would under-fill the statement that is missing one.
379
- const chunkSize = bindBudgetChunkSize(meta, group.map((index) => payload[index]), this.dialect.maxBindValues);
356
+ for (const group of partitionBySuppliedId(rows, idKey)) {
380
357
  // RETURNING-based ids are exact per row. Header-derived ones (LAST_INSERT_ID / lastInsertRowid
381
358
  // arithmetic) are only sound when the key is database-generated and every row *in this
382
359
  // statement* left it to the database. That is a property of the statement, not of the batch:
@@ -385,28 +362,26 @@ export class AbstractSqlQuerier extends AbstractQuerier {
385
362
  // most one extra statement and keeps each of them inferable.
386
363
  const idsReliable = sole &&
387
364
  (this.dialect.insertIdSource === 'returning' ||
388
- (generatedKey && group.every((index) => payload[index][idKey] === undefined)));
365
+ (generatedKey && group.every((index) => rows[index][idKey] === undefined)));
389
366
  // Inferring multiple ids from the single header id (MySQL) assumes a known stride; a clustered
390
367
  // server may set `auto_increment_increment` > 1, so probe it (once, cached) before inferring.
391
368
  if (idsReliable && group.length > 1 && this.dialect.insertIdSource === 'firstId') {
392
369
  this.#insertIdIncrement ??= await this.loadInsertIdIncrement();
393
370
  }
394
- for (let start = 0; start < group.length; start += chunkSize) {
395
- const indexes = group.slice(start, start + chunkSize);
371
+ // Per group, not per batch: the two carry different columns - one names the key, one does not -
372
+ // so a budget taken over their union would under-fill the statement that is missing one.
373
+ for (const indexes of chunkByBindBudget(meta, rows, group, this.dialect.maxBindValues)) {
396
374
  const ctx = this.dialect.createContext();
397
- this.dialect.insert(ctx, entity, indexes.map((index) => payload[index]));
375
+ this.dialect.insert(ctx, entity, indexes.map((index) => rows[index]));
398
376
  const { ids = [] } = await this.run(ctx.sql, ctx.values);
399
- indexes.forEach((index, position) => {
400
- const it = payload[index];
401
- if (idsReliable) {
402
- it[idKey] ??= ids[position];
377
+ if (idsReliable) {
378
+ for (let position = 0; position < indexes.length; position++) {
379
+ rows[indexes[position]][idKey] ??= ids[position];
403
380
  }
404
- payloadIds[index] = sole ? it[idKey] : undefined;
405
- });
381
+ }
406
382
  }
407
383
  }
408
- await this.insertRelations(entity, payload);
409
- return payloadIds;
384
+ await this.insertRelations(entity, rows);
410
385
  }
411
386
  async internalUpdateMany(entity, q, payload, opts) {
412
387
  payload = clone(payload);
@@ -446,8 +421,7 @@ export class AbstractSqlQuerier extends AbstractQuerier {
446
421
  // One statement per shape, each split again to stay inside the bind budget. Grouping first is
447
422
  // what makes the budget arithmetic right: every row of a group carries the same columns, so the
448
423
  // `DO UPDATE SET` resolves to non-binding `EXCLUDED` references rather than inlined values.
449
- const groups = groupByInsertShape(meta, payload);
450
- const statements = (groups ?? [payload]).flatMap((group) => chunkByBindBudget(meta, group, this.dialect.maxBindValues));
424
+ const statements = groupByInsertShape(meta, payload).flatMap((group) => chunkByBindBudget(meta, payload, group, this.dialect.maxBindValues));
451
425
  if (statements.length === 1) {
452
426
  return this.runUpsert(entity, conflictPaths, payload);
453
427
  }
@@ -459,31 +433,41 @@ export class AbstractSqlQuerier extends AbstractQuerier {
459
433
  // than one statement; `transaction` is re-entrant, so this is free inside a caller's own.
460
434
  return this.transaction(async () => {
461
435
  let changes = 0;
462
- const ids = [];
463
- for (const statement of statements) {
464
- const result = await this.runUpsert(entity, conflictPaths, statement);
465
- changes += result.changes ?? 0;
466
- if (result.ids) {
467
- ids.push(...result.ids);
436
+ // Placed by index, since grouping by shape reorders the rows. A statement reporting fewer ids
437
+ // than it wrote places none.
438
+ const ids = new Array(payload.length);
439
+ for (const indexes of statements) {
440
+ const { changes: written = 0, ids: reported } = await this.runUpsert(entity, conflictPaths, indexes.map((index) => payload[index]));
441
+ changes += written;
442
+ if (reported?.length === indexes.length) {
443
+ for (let position = 0; position < indexes.length; position++) {
444
+ ids[indexes[position]] = reported[position];
445
+ }
468
446
  }
469
447
  }
470
- // No `created`/`firstId`: both speak for a single statement, and there were several.
471
- return ids.length ? { changes, ids } : { changes };
448
+ // No `created`: it speaks for a single statement, and there were several.
449
+ return { changes, ids };
472
450
  });
473
451
  }
474
452
  async runUpsert(entity, conflictPaths, payload) {
453
+ const meta = getMeta(entity);
454
+ // Asked first: the statement fills an `onInsert` key into these rows whether it inserts them or not.
455
+ const unnamed = meta.ids.length === 1 && payload.some((row) => !namesKey(meta, row));
475
456
  const ctx = this.dialect.createContext();
476
457
  this.dialect.upsert(ctx, entity, conflictPaths, payload);
477
458
  const result = await this.run(ctx.sql, ctx.values);
478
- // On a `firstId` dialect (MySQL: no `RETURNING`), a multi-row upsert's `affectedRows` is a
479
- // per-row weighted sum (1=insert, 2=update, 0=no-op), not a row count, so `buildUpdateResult`'s
480
- // header-derived `ids`/`firstId`/`created` can't be trusted the moment more than one row is
481
- // involved (verified: a 1-insert-1-update batch reports `changes: 3`, which would otherwise
482
- // infer 3 sequential ids for only 2 real rows). A single-row batch (`upsertOne`) is unambiguous.
483
- if (this.dialect.insertIdSource !== 'returning' && payload.length > 1) {
484
- return { changes: result.changes };
485
- }
486
- return result;
459
+ const ordered = payload.length === 1 || (this.dialect.insertIdSource === 'returning' && this.dialect.upsertReturningOrdered);
460
+ if (ordered && result.ids?.length === payload.length) {
461
+ return result;
462
+ }
463
+ // The statement's ids name its rows only in order and for every one. A MySQL batch reports a
464
+ // weighted count (1=insert, 2=update) instead, CockroachDB and SQL Server answer out of order, and
465
+ // `DO NOTHING` skips rows, so there the ids are read back by the conflict columns.
466
+ const { changes } = result;
467
+ const created = payload.length === 1 ? result.created : undefined;
468
+ return unnamed
469
+ ? { changes, created, ids: await this.idsByConflict(entity, conflictPaths, payload) }
470
+ : { changes, created };
487
471
  }
488
472
  async internalDeleteMany(entity, q, opts) {
489
473
  const meta = getMeta(entity);
@@ -520,35 +504,47 @@ export class AbstractSqlQuerier extends AbstractQuerier {
520
504
  throwPendingTransaction();
521
505
  }
522
506
  await this.lazyConnect();
523
- for (const sql of this.dialect.getBeginTransactionStatements(opts?.isolationLevel)) {
524
- await this.runTransactionCommand(sql);
525
- }
507
+ await this.internalBegin(opts);
526
508
  this.hasPendingTransaction = true;
527
509
  });
528
510
  }
511
+ /**
512
+ * Only an end that succeeded ends the transaction. A `COMMIT` that fails can leave it open (SQLite
513
+ * answers `SQLITE_BUSY` and keeps it), so the flag has to stay set for the `catch` in
514
+ * {@link AbstractQuerier.transaction} or {@link AbstractQuerier.release} to roll it back.
515
+ */
529
516
  async commitTransaction() {
530
517
  return this.serialize(async () => {
531
518
  if (!this.hasPendingTransaction) {
532
519
  throwNoPendingTransaction();
533
520
  }
534
- await this.endTransactionWith(this.dialect.commitTransactionCommand);
521
+ await this.internalCommit();
522
+ this.hasPendingTransaction = false;
535
523
  });
536
524
  }
537
525
  async rollbackTransaction() {
538
526
  return this.serialize(async () => {
539
527
  if (this.hasPendingTransaction) {
540
- await this.endTransactionWith(this.dialect.rollbackTransactionCommand);
528
+ await this.internalRollback();
529
+ this.hasPendingTransaction = false;
541
530
  }
542
531
  });
543
532
  }
544
533
  /**
545
- * Only a statement that succeeded ends the transaction. A `COMMIT` that fails can leave it open
546
- * (SQLite answers `SQLITE_BUSY` and keeps it), so the flag has to stay set for the `catch` in
547
- * {@link AbstractQuerier.transaction} or {@link AbstractQuerier.release} to roll it back.
534
+ * How this driver opens, commits and rolls back: the dialect's statements, unless its transactions
535
+ * are objects rather than statements - Hrana's session handle, `mssql`'s `Transaction` - in which
536
+ * case it overrides all three. The bookkeeping around them stays above, written once.
548
537
  */
549
- async endTransactionWith(command) {
550
- await this.runTransactionCommand(command);
551
- this.hasPendingTransaction = false;
538
+ async internalBegin(opts) {
539
+ for (const sql of this.dialect.getBeginTransactionStatements(opts?.isolationLevel)) {
540
+ await this.runTransactionCommand(sql);
541
+ }
542
+ }
543
+ internalCommit() {
544
+ return this.runTransactionCommand(this.dialect.commitTransactionCommand);
545
+ }
546
+ internalRollback() {
547
+ return this.runTransactionCommand(this.dialect.rollbackTransactionCommand);
552
548
  }
553
549
  /** Transaction statements skip `timed()`, so they attach their own query context to a failure. */
554
550
  async runTransactionCommand(sql) {
@@ -7,7 +7,7 @@
7
7
  */
8
8
  import { getMeta, soleIdOf } from '../entity/metadata/definition.js';
9
9
  import { ddlText } from '../util/ddlExpression.util.js';
10
- import { computedExpression, isInlinedExpression } from '../util/field.util.js';
10
+ import { isInlinedExpression } from '../util/field.util.js';
11
11
  import { isSoleIdField } from '../util/field.util.js';
12
12
  import { isAutoIncrement } from '../util/field.util.js';
13
13
  import { derivedForeignKeyName, derivedIndexName, qualifyName } from '../util/sql.util.js';
@@ -101,7 +101,7 @@ function addTableFromEntity(ctx, meta) {
101
101
  isPrimaryKey,
102
102
  isAutoIncrement: isAutoIncrement(field, isSoleKey),
103
103
  isUnique: field.unique ?? false,
104
- generatedAs: ddlText(computedExpression(field), `the computed column '${columnName}'`),
104
+ generatedAs: ddlText(field.computed, `the computed column '${columnName}'`),
105
105
  comment: field.comment,
106
106
  enum: field.enum,
107
107
  table,
@@ -202,16 +202,16 @@ function addIndexesFromEntity(ctx, meta) {
202
202
  addCompositeIndex(ctx, table, meta, idxMeta);
203
203
  }
204
204
  }
205
- /**
206
- * One `@Index([...])`. Its entries keep the authored form (expression, prefix length, order) with
207
- * names resolved, so the generator renders exactly what was declared; `columns` is the resolvable
208
- * subset, which is what diffing and introspection compare.
209
- */
210
205
  /** An `include` column is named like any other, so a naming strategy has to reach it too. */
211
206
  function resolveIncludeColumn(ctx, meta, column) {
212
207
  const field = meta.fields[column];
213
208
  return field ? ctx.resolveColumnName(column, field) : column;
214
209
  }
210
+ /**
211
+ * One `@Index([...])`. Its entries keep the authored form (expression, prefix length, order) with
212
+ * names resolved, so the generator renders exactly what was declared; `columns` is the resolvable
213
+ * subset, which is what diffing and introspection compare.
214
+ */
215
215
  function addCompositeIndex(ctx, table, meta, idxMeta) {
216
216
  // An entry survives if it is an expression (nothing to resolve) or names a column that exists;
217
217
  // an index left with none is dropped, the same as one naming only unknown columns always was.
@@ -1,4 +1,4 @@
1
- import type { ExtraOptions, TransactionOptions } from '../type/index.js';
1
+ import type { ExtraOptions } from '../type/index.js';
2
2
  import { AbstractSqliteQuerier, type SqliteBindValue } from './abstractSqliteQuerier.js';
3
3
  import type { SqliteDialect } from './sqliteDialect.js';
4
4
  /**
@@ -46,14 +46,12 @@ export declare class HranaQuerier extends AbstractSqliteQuerier {
46
46
  constructor(client: HranaClient, dialect: SqliteDialect, extra?: ExtraOptions | undefined, connection?: HranaQuerierConnectionOptions);
47
47
  internalAll<T>(query: string, values?: unknown[]): Promise<T[]>;
48
48
  internalRun(query: string, values?: unknown[]): Promise<import("../type/query.js").QueryUpdateResult>;
49
- get hasOpenTransaction(): boolean;
50
- beginTransaction(_opts?: TransactionOptions): Promise<void>;
49
+ protected internalBegin(): Promise<void>;
51
50
  /**
52
- * Both drop the handle before the call, not after: one that outlived a failed commit or rollback left
53
- * the querier unreleasable. The optional call in the rollback is also what makes it a no-op when
54
- * there is nothing open.
51
+ * Both drop the handle before the call, not after: one that outlived a failed commit would carry
52
+ * every later statement into a transaction the server may already have ended.
55
53
  */
56
- commitTransaction(): Promise<void>;
57
- rollbackTransaction(): Promise<void>;
54
+ protected internalCommit(): Promise<void>;
55
+ protected internalRollback(): Promise<void>;
58
56
  internalRelease(): Promise<void>;
59
57
  }
@@ -1,4 +1,3 @@
1
- import { throwNoPendingTransaction, throwPendingTransaction } from '../util/index.js';
2
1
  import { AbstractSqliteQuerier } from './abstractSqliteQuerier.js';
3
2
  /**
4
3
  * Querier for SQLite databases reached through a Hrana client.
@@ -30,38 +29,22 @@ export class HranaQuerier extends AbstractSqliteQuerier {
30
29
  // the actual row count when rows were returned.
31
30
  return this.buildUpdateResult({ rows, changes: rows.length || res.rowsAffected, id: res.lastInsertRowid });
32
31
  }
33
- get hasOpenTransaction() {
34
- return !!this.tx;
35
- }
36
- async beginTransaction(_opts) {
37
- return this.serialize(async () => {
38
- if (this.tx) {
39
- throwPendingTransaction();
40
- }
41
- this.tx = await this.client.transaction('write');
42
- });
32
+ async internalBegin() {
33
+ this.tx = await this.client.transaction('write');
43
34
  }
44
35
  /**
45
- * Both drop the handle before the call, not after: one that outlived a failed commit or rollback left
46
- * the querier unreleasable. The optional call in the rollback is also what makes it a no-op when
47
- * there is nothing open.
36
+ * Both drop the handle before the call, not after: one that outlived a failed commit would carry
37
+ * every later statement into a transaction the server may already have ended.
48
38
  */
49
- async commitTransaction() {
50
- return this.serialize(async () => {
51
- const tx = this.tx;
52
- if (!tx) {
53
- throwNoPendingTransaction();
54
- }
55
- this.tx = undefined;
56
- await tx.commit();
57
- });
58
- }
59
- async rollbackTransaction() {
60
- return this.serialize(async () => {
61
- const tx = this.tx;
62
- this.tx = undefined;
63
- await tx?.rollback();
64
- });
39
+ async internalCommit() {
40
+ const tx = this.tx;
41
+ this.tx = undefined;
42
+ await tx?.commit();
43
+ }
44
+ async internalRollback() {
45
+ const tx = this.tx;
46
+ this.tx = undefined;
47
+ await tx?.rollback();
65
48
  }
66
49
  async internalRelease() {
67
50
  await super.internalRelease();
@@ -1,5 +1,5 @@
1
1
  import { AbstractSqlQuerierPool } from '../querier/index.js';
2
- import type { HranaClient, HranaQuerier, HranaQuerierConnectionOptions } from './hranaQuerier.js';
2
+ import { type HranaClient, HranaQuerier } from './hranaQuerier.js';
3
3
  import type { SqliteDialect } from './sqliteDialect.js';
4
4
  /**
5
5
  * Pool for SQLite databases reached over the Hrana wire protocol (`@libsql/client`,
@@ -10,12 +10,11 @@ import type { SqliteDialect } from './sqliteDialect.js';
10
10
  * constructor, so building a pool never throws when the optional driver peer is absent, which is what
11
11
  * lets a Workers bundle construct one at module scope.
12
12
  */
13
- export declare abstract class AbstractHranaQuerierPool<Q extends HranaQuerier, D extends SqliteDialect> extends AbstractSqlQuerierPool<Q, D> {
13
+ export declare abstract class AbstractHranaQuerierPool<D extends SqliteDialect> extends AbstractSqlQuerierPool<HranaQuerier, D> {
14
14
  private client?;
15
15
  /** False when the caller injected their own client, in which case they own its lifecycle. */
16
16
  protected readonly ownsClient: boolean;
17
17
  protected abstract openClient(): Promise<HranaClient>;
18
- protected abstract buildQuerier(client: HranaClient, connection?: HranaQuerierConnectionOptions): Q;
19
- getQuerier(): Promise<Q>;
18
+ getQuerier(): Promise<HranaQuerier>;
20
19
  end(): Promise<void>;
21
20
  }
@@ -1,4 +1,5 @@
1
1
  import { AbstractSqlQuerierPool } from '../querier/index.js';
2
+ import { HranaQuerier } from './hranaQuerier.js';
2
3
  /**
3
4
  * Pool for SQLite databases reached over the Hrana wire protocol (`@libsql/client`,
4
5
  * `@tursodatabase/serverless/compat`).
@@ -14,7 +15,7 @@ export class AbstractHranaQuerierPool extends AbstractSqlQuerierPool {
14
15
  ownsClient = true;
15
16
  async getQuerier() {
16
17
  this.client ??= await this.openClient();
17
- return this.buildQuerier(this.client);
18
+ return new HranaQuerier(this.client, this.dialect, this.extra);
18
19
  }
19
20
  async end() {
20
21
  if (this.ownsClient) {
@@ -15,13 +15,13 @@ export type LocalSqlitePoolOptions = {
15
15
  * Pool for a SQLite database opened in this process, whichever driver provides it. SQLite gives one
16
16
  * connection per file, so the shared-handle lifecycle is {@link AbstractSharedHandleQuerierPool}'s.
17
17
  *
18
- * Subclasses supply only {@link createDb}: loading the extensions on the way up is the same for
19
- * `better-sqlite3`, `bun:sqlite` and `node:sqlite`, and was written out once per pool before.
18
+ * Subclasses supply only {@link createDb}: configuring the connection on the way up - the pragmas,
19
+ * then the extensions - is the same for `better-sqlite3`, `bun:sqlite` and `node:sqlite`.
20
20
  */
21
21
  export declare abstract class AbstractLocalSqliteQuerierPool<O extends LocalSqlitePoolOptions> extends AbstractSharedHandleQuerierPool<SqliteDatabase, SqliteQuerier, SqliteDialect> {
22
22
  readonly opts?: O | undefined;
23
23
  constructor(opts?: O | undefined, extra?: ExtraOptions);
24
- /** Opens the driver's database. Extensions are loaded by the caller, not here. */
24
+ /** Opens the driver's database, and nothing more: the caller configures it. */
25
25
  protected abstract createDb(): Promise<SqliteDatabase>;
26
26
  protected openDb(): Promise<SqliteDatabase>;
27
27
  protected buildQuerier(db: SqliteDatabase): SqliteQuerier;
@@ -1,13 +1,14 @@
1
1
  import { dialectOptionsFrom } from '../dialect/abstractDialect.js';
2
2
  import { AbstractSharedHandleQuerierPool } from '../querier/abstractSharedHandleQuerierPool.js';
3
3
  import { SqliteDialect } from './sqliteDialect.js';
4
+ import { applySqlitePragmas } from './sqlitePragmas.js';
4
5
  import { SqliteQuerier } from './sqliteQuerier.js';
5
6
  /**
6
7
  * Pool for a SQLite database opened in this process, whichever driver provides it. SQLite gives one
7
8
  * connection per file, so the shared-handle lifecycle is {@link AbstractSharedHandleQuerierPool}'s.
8
9
  *
9
- * Subclasses supply only {@link createDb}: loading the extensions on the way up is the same for
10
- * `better-sqlite3`, `bun:sqlite` and `node:sqlite`, and was written out once per pool before.
10
+ * Subclasses supply only {@link createDb}: configuring the connection on the way up - the pragmas,
11
+ * then the extensions - is the same for `better-sqlite3`, `bun:sqlite` and `node:sqlite`.
11
12
  */
12
13
  export class AbstractLocalSqliteQuerierPool extends AbstractSharedHandleQuerierPool {
13
14
  opts;
@@ -17,6 +18,7 @@ export class AbstractLocalSqliteQuerierPool extends AbstractSharedHandleQuerierP
17
18
  }
18
19
  async openDb() {
19
20
  const db = await this.createDb();
21
+ await applySqlitePragmas(db);
20
22
  for (const extension of this.opts?.extensions ?? []) {
21
23
  db.loadExtension(extension);
22
24
  }
@@ -19,7 +19,6 @@ type NodeSqliteStatement = {
19
19
  */
20
20
  export type NodeSqliteDatabase = {
21
21
  prepare(sql: string): NodeSqliteStatement;
22
- exec(sql: string): void;
23
22
  loadExtension(path: string): void;
24
23
  close(): void;
25
24
  };
@@ -23,8 +23,6 @@ export class NodeSqliteQuerierPool extends AbstractLocalSqliteQuerierPool {
23
23
  // `node:sqlite` refuses `loadExtension` unless the database was opened with this on.
24
24
  ...(extensions?.length ? { allowExtension: true } : undefined),
25
25
  });
26
- nodeDb.exec('PRAGMA journal_mode = WAL');
27
- nodeDb.exec('PRAGMA foreign_keys = ON');
28
26
  return adaptNodeSqlite(nodeDb);
29
27
  }
30
28
  }
@@ -0,0 +1,12 @@
1
+ import type { SqlitePreparedStatement } from './abstractSqliteQuerier.js';
2
+ /**
3
+ * How every local SQLite connection opens. WAL, so a reader does not wait on the writer, and
4
+ * `foreign_keys`, which SQLite ships off per connection for backward compatibility: without it the
5
+ * constraints uql's own DDL declares are decorative - a declared `onDelete: 'CASCADE'` silently does
6
+ * nothing and a dangling reference is accepted.
7
+ */
8
+ export declare const SQLITE_PRAGMAS: readonly ['journal_mode = WAL', 'foreign_keys = ON'];
9
+ /** Runs {@link SQLITE_PRAGMAS} through a driver's own statements, whether it answers now or later. */
10
+ export declare function applySqlitePragmas(db: {
11
+ prepare(sql: string): SqlitePreparedStatement | Promise<SqlitePreparedStatement>;
12
+ }): Promise<void>;
@@ -0,0 +1,15 @@
1
+ /**
2
+ * How every local SQLite connection opens. WAL, so a reader does not wait on the writer, and
3
+ * `foreign_keys`, which SQLite ships off per connection for backward compatibility: without it the
4
+ * constraints uql's own DDL declares are decorative - a declared `onDelete: 'CASCADE'` silently does
5
+ * nothing and a dangling reference is accepted.
6
+ */
7
+ export const SQLITE_PRAGMAS = ['journal_mode = WAL', 'foreign_keys = ON'];
8
+ /** Runs {@link SQLITE_PRAGMAS} through a driver's own statements, whether it answers now or later. */
9
+ export async function applySqlitePragmas(db) {
10
+ for (const pragma of SQLITE_PRAGMAS) {
11
+ const stmt = await db.prepare(`PRAGMA ${pragma}`);
12
+ // `journal_mode` answers with a row and `foreign_keys` with none; `reader` picks the call for each.
13
+ await (stmt.reader ? stmt.all() : stmt.run());
14
+ }
15
+ }
@@ -11,10 +11,5 @@ export type Sqlite3PoolOptions = Options & LocalSqlitePoolOptions;
11
11
  export declare class Sqlite3QuerierPool extends AbstractLocalSqliteQuerierPool<Sqlite3PoolOptions> {
12
12
  readonly filename: string | Buffer;
13
13
  constructor(filename?: string | Buffer, opts?: Sqlite3PoolOptions, extra?: ExtraOptions);
14
- /**
15
- * SQLite ships with foreign keys unenforced, per connection, for backward compatibility. UQL emits the
16
- * constraints in its DDL, so leaving them off means a declared `onDelete: 'CASCADE'` silently does
17
- * nothing and a dangling reference is accepted. Enabled here on every driver, as TypeORM also does.
18
- */
19
14
  protected createDb(): Promise<SqliteDatabase>;
20
15
  }