velocious 1.0.568 → 1.0.569

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 (152) hide show
  1. package/README.md +20 -0
  2. package/build/configuration.js +36 -0
  3. package/build/database/drivers/base.js +167 -54
  4. package/build/database/drivers/mssql/index.js +10 -7
  5. package/build/database/drivers/mysql/index.js +10 -5
  6. package/build/database/drivers/pgsql/index.js +4 -4
  7. package/build/database/drivers/sqlite/base.js +2 -2
  8. package/build/database/drivers/sqlite/connection-sql-js.js +62 -2
  9. package/build/database/drivers/sqlite/index.web.js +74 -0
  10. package/build/database/operation-connection.js +110 -0
  11. package/build/database/operation-lease.js +47 -0
  12. package/build/database/operation.js +140 -0
  13. package/build/database/pool/base.js +26 -1
  14. package/build/database/pool/single-multi-use.js +49 -0
  15. package/build/database/query/model-class-query.js +38 -7
  16. package/build/database/query/preloader/belongs-to.js +3 -2
  17. package/build/database/query/preloader/has-many.js +4 -3
  18. package/build/database/query/preloader/has-one.js +2 -1
  19. package/build/database/query/preloader/query-for-model.js +17 -0
  20. package/build/database/query/query-data.js +12 -3
  21. package/build/database/query/with-count.js +5 -3
  22. package/build/database/record/acts-as-list.js +11 -8
  23. package/build/database/record/attachments/store.js +15 -11
  24. package/build/database/record/auditing.js +12 -9
  25. package/build/database/record/counter-cache-magnitude.js +5 -3
  26. package/build/database/record/index.js +95 -14
  27. package/build/database/record/instance-relationships/belongs-to.js +4 -2
  28. package/build/database/record/instance-relationships/has-many.js +7 -5
  29. package/build/database/record/instance-relationships/has-one.js +4 -2
  30. package/build/database/record/validators/uniqueness.js +3 -2
  31. package/build/frontend-models/websocket-publishers.js +2 -2
  32. package/build/src/configuration.d.ts +12 -0
  33. package/build/src/configuration.d.ts.map +1 -1
  34. package/build/src/configuration.js +35 -1
  35. package/build/src/database/drivers/base.d.ts +66 -20
  36. package/build/src/database/drivers/base.d.ts.map +1 -1
  37. package/build/src/database/drivers/base.js +158 -54
  38. package/build/src/database/drivers/mssql/index.d.ts +4 -1
  39. package/build/src/database/drivers/mssql/index.d.ts.map +1 -1
  40. package/build/src/database/drivers/mssql/index.js +11 -8
  41. package/build/src/database/drivers/mysql/index.d.ts +2 -1
  42. package/build/src/database/drivers/mysql/index.d.ts.map +1 -1
  43. package/build/src/database/drivers/mysql/index.js +10 -6
  44. package/build/src/database/drivers/pgsql/index.d.ts +2 -1
  45. package/build/src/database/drivers/pgsql/index.d.ts.map +1 -1
  46. package/build/src/database/drivers/pgsql/index.js +5 -5
  47. package/build/src/database/drivers/sqlite/base.d.ts +1 -1
  48. package/build/src/database/drivers/sqlite/base.d.ts.map +1 -1
  49. package/build/src/database/drivers/sqlite/base.js +3 -3
  50. package/build/src/database/drivers/sqlite/connection-sql-js.d.ts +20 -0
  51. package/build/src/database/drivers/sqlite/connection-sql-js.d.ts.map +1 -1
  52. package/build/src/database/drivers/sqlite/connection-sql-js.js +56 -3
  53. package/build/src/database/drivers/sqlite/index.web.d.ts.map +1 -1
  54. package/build/src/database/drivers/sqlite/index.web.js +64 -1
  55. package/build/src/database/operation-connection.d.ts +61 -0
  56. package/build/src/database/operation-connection.d.ts.map +1 -0
  57. package/build/src/database/operation-connection.js +93 -0
  58. package/build/src/database/operation-lease.d.ts +27 -0
  59. package/build/src/database/operation-lease.d.ts.map +1 -0
  60. package/build/src/database/operation-lease.js +44 -0
  61. package/build/src/database/operation.d.ts +77 -0
  62. package/build/src/database/operation.d.ts.map +1 -0
  63. package/build/src/database/operation.js +123 -0
  64. package/build/src/database/pool/base.d.ts +14 -0
  65. package/build/src/database/pool/base.d.ts.map +1 -1
  66. package/build/src/database/pool/base.js +24 -2
  67. package/build/src/database/pool/single-multi-use.d.ts +1 -0
  68. package/build/src/database/pool/single-multi-use.d.ts.map +1 -1
  69. package/build/src/database/pool/single-multi-use.js +49 -1
  70. package/build/src/database/query/model-class-query.d.ts +14 -0
  71. package/build/src/database/query/model-class-query.d.ts.map +1 -1
  72. package/build/src/database/query/model-class-query.js +32 -8
  73. package/build/src/database/query/preloader/belongs-to.d.ts.map +1 -1
  74. package/build/src/database/query/preloader/belongs-to.js +4 -3
  75. package/build/src/database/query/preloader/has-many.d.ts.map +1 -1
  76. package/build/src/database/query/preloader/has-many.js +5 -4
  77. package/build/src/database/query/preloader/has-one.d.ts.map +1 -1
  78. package/build/src/database/query/preloader/has-one.js +3 -2
  79. package/build/src/database/query/preloader/query-for-model.d.ts +10 -0
  80. package/build/src/database/query/preloader/query-for-model.d.ts.map +1 -0
  81. package/build/src/database/query/preloader/query-for-model.js +16 -0
  82. package/build/src/database/query/query-data.d.ts.map +1 -1
  83. package/build/src/database/query/query-data.js +13 -4
  84. package/build/src/database/query/with-count.d.ts.map +1 -1
  85. package/build/src/database/query/with-count.js +6 -4
  86. package/build/src/database/record/acts-as-list.js +12 -9
  87. package/build/src/database/record/attachments/attachment-record.d.ts +9 -0
  88. package/build/src/database/record/attachments/attachment-record.d.ts.map +1 -1
  89. package/build/src/database/record/attachments/store.d.ts +4 -2
  90. package/build/src/database/record/attachments/store.d.ts.map +1 -1
  91. package/build/src/database/record/attachments/store.js +16 -12
  92. package/build/src/database/record/auditing.d.ts.map +1 -1
  93. package/build/src/database/record/auditing.js +13 -10
  94. package/build/src/database/record/counter-cache-magnitude.js +6 -4
  95. package/build/src/database/record/index.d.ts +42 -2
  96. package/build/src/database/record/index.d.ts.map +1 -1
  97. package/build/src/database/record/index.js +87 -15
  98. package/build/src/database/record/instance-relationships/belongs-to.d.ts.map +1 -1
  99. package/build/src/database/record/instance-relationships/belongs-to.js +4 -3
  100. package/build/src/database/record/instance-relationships/has-many.d.ts.map +1 -1
  101. package/build/src/database/record/instance-relationships/has-many.js +7 -5
  102. package/build/src/database/record/instance-relationships/has-one.d.ts.map +1 -1
  103. package/build/src/database/record/instance-relationships/has-one.js +4 -3
  104. package/build/src/database/record/validators/uniqueness.d.ts.map +1 -1
  105. package/build/src/database/record/validators/uniqueness.js +4 -3
  106. package/build/src/frontend-models/websocket-publishers.js +3 -3
  107. package/build/src/sync/server-sequence-allocator.d.ts +16 -4
  108. package/build/src/sync/server-sequence-allocator.d.ts.map +1 -1
  109. package/build/src/sync/server-sequence-allocator.js +37 -11
  110. package/build/src/sync/sync-client.d.ts.map +1 -1
  111. package/build/src/sync/sync-client.js +7 -3
  112. package/build/src/sync/sync-publisher.d.ts +2 -1
  113. package/build/src/sync/sync-publisher.d.ts.map +1 -1
  114. package/build/src/sync/sync-publisher.js +11 -6
  115. package/build/sync/server-sequence-allocator.js +43 -10
  116. package/build/sync/sync-client.js +6 -2
  117. package/build/sync/sync-publisher.js +10 -5
  118. package/build/tsconfig.tsbuildinfo +1 -1
  119. package/package.json +1 -1
  120. package/src/configuration.js +36 -0
  121. package/src/database/drivers/base.js +167 -54
  122. package/src/database/drivers/mssql/index.js +10 -7
  123. package/src/database/drivers/mysql/index.js +10 -5
  124. package/src/database/drivers/pgsql/index.js +4 -4
  125. package/src/database/drivers/sqlite/base.js +2 -2
  126. package/src/database/drivers/sqlite/connection-sql-js.js +62 -2
  127. package/src/database/drivers/sqlite/index.web.js +74 -0
  128. package/src/database/operation-connection.js +110 -0
  129. package/src/database/operation-lease.js +47 -0
  130. package/src/database/operation.js +140 -0
  131. package/src/database/pool/base.js +26 -1
  132. package/src/database/pool/single-multi-use.js +49 -0
  133. package/src/database/query/model-class-query.js +38 -7
  134. package/src/database/query/preloader/belongs-to.js +3 -2
  135. package/src/database/query/preloader/has-many.js +4 -3
  136. package/src/database/query/preloader/has-one.js +2 -1
  137. package/src/database/query/preloader/query-for-model.js +17 -0
  138. package/src/database/query/query-data.js +12 -3
  139. package/src/database/query/with-count.js +5 -3
  140. package/src/database/record/acts-as-list.js +11 -8
  141. package/src/database/record/attachments/store.js +15 -11
  142. package/src/database/record/auditing.js +12 -9
  143. package/src/database/record/counter-cache-magnitude.js +5 -3
  144. package/src/database/record/index.js +95 -14
  145. package/src/database/record/instance-relationships/belongs-to.js +4 -2
  146. package/src/database/record/instance-relationships/has-many.js +7 -5
  147. package/src/database/record/instance-relationships/has-one.js +4 -2
  148. package/src/database/record/validators/uniqueness.js +3 -2
  149. package/src/frontend-models/websocket-publishers.js +2 -2
  150. package/src/sync/server-sequence-allocator.js +43 -10
  151. package/src/sync/sync-client.js +6 -2
  152. package/src/sync/sync-publisher.js +10 -5
package/README.md CHANGED
@@ -34,6 +34,7 @@
34
34
  * In-process driver schema metadata caching (see [docs/schema-metadata-cache.md](docs/schema-metadata-cache.md))
35
35
  * Planned local-first shared-resource sync architecture (see [docs/offline-sync.md](docs/offline-sync.md))
36
36
  * Selective named database connection checkouts, bounded pool waits, and debugging held connections (see [docs/database-connections.md](docs/database-connections.md))
37
+ * Explicit singular-database operation transactions whose model scopes preserve ownership through records, relationships, lifecycle work, nested savepoints, and commit callbacks (see [docs/operation-scoped-transactions.md](docs/operation-scoped-transactions.md))
37
38
  * AbortSignal-driven MySQL/MariaDB query cancellation for raw, model, and cross-tenant aggregate queries (see [docs/database-query-cancellation.md](docs/database-query-cancellation.md))
38
39
  * Optional built-in debug endpoint for inspecting server and database connection state (see [docs/debug-endpoint.md](docs/debug-endpoint.md))
39
40
  * Optional built-in API manifest endpoint describing every registered frontend-model resource as human- and machine-readable JSON (see [docs/api-manifest-endpoint.md](docs/api-manifest-endpoint.md))
@@ -51,6 +52,25 @@ npx velocious init
51
52
 
52
53
  By default, Velocious looks for your configuration in `src/config/configuration.js`. If you keep the configuration elsewhere, make sure your app imports it early and calls `configuration.setCurrent()`.
53
54
 
55
+ # Operation-scoped transactions
56
+
57
+ Use `configuration.withTransaction` for an atomic unit of model work on one database:
58
+
59
+ ```js
60
+ await configuration.withTransaction({databaseIdentifier: "default", name: "accept ticket"}, async (operation) => {
61
+ const ticket = await operation.forModel(Ticket).find(ticketId)
62
+
63
+ ticket.setAccepted(true)
64
+ await ticket.save()
65
+
66
+ await operation.afterCommit(async () => {
67
+ await publishAcceptedTicket(ticket.id())
68
+ })
69
+ })
70
+ ```
71
+
72
+ Use operation-bound model scopes and their loaded records throughout the callback. `operation.transaction` adds a nested savepoint, and `operation.connection()` is the deliberate escape hatch for owned raw SQL. Cross-database models, same-identifier tenant switches to another physical database, and operation handles used after the callback are rejected. On shared SQLite/SQL.js pools, unrelated work waits for the operation lease, while admission during an already-open ordinary transaction is rejected. See [operation-scoped transactions](docs/operation-scoped-transactions.md) for pool behavior, after-commit failure semantics, and migration guidance.
73
+
54
74
  # Development
55
75
 
56
76
  When working on Velocious itself, npm scripts are cross-platform (Windows `cmd`/PowerShell and POSIX shells):
@@ -16,6 +16,7 @@ import {digg} from "diggerize"
16
16
  import gettextConfig from "gettext-universal/build/src/config.js"
17
17
  import translate from "gettext-universal/build/src/translate.js"
18
18
  import Ability from "./authorization/ability.js"
19
+ import DatabaseOperation from "./database/operation.js"
19
20
  import {initializeAuditedModelRelationships} from "./database/record/auditing.js"
20
21
  import EventEmitter from "./utils/event-emitter.js"
21
22
  import VelociousWebsocketChannelSubscribers from "./http-server/websocket-channel-subscribers.js"
@@ -2896,6 +2897,41 @@ export default class VelociousConfiguration {
2896
2897
  })
2897
2898
  }
2898
2899
 
2900
+ /**
2901
+ * Runs explicit model work in a transaction pinned to one database connection.
2902
+ * @template T
2903
+ * @param {{databaseIdentifier: string, name?: string}} options - Operation options.
2904
+ * @param {(operation: DatabaseOperation) => Promise<T>} callback - Operation callback.
2905
+ * @returns {Promise<T>} - Resolves with the callback result.
2906
+ */
2907
+ async withTransaction({databaseIdentifier, name = "Configuration.withTransaction", ...restArgs}, callback) {
2908
+ restArgsError(restArgs)
2909
+
2910
+ if (!databaseIdentifier) throw new Error("Configuration.withTransaction requires a databaseIdentifier")
2911
+ if (typeof callback != "function") throw new Error("Configuration.withTransaction requires a callback")
2912
+ if (!this.getDatabaseIdentifiers().includes(databaseIdentifier)) {
2913
+ throw new Error(`Unknown or inactive database identifier: ${databaseIdentifier}`)
2914
+ }
2915
+
2916
+ const pool = this.getDatabasePool(databaseIdentifier)
2917
+
2918
+ return await pool.withOperationConnection({name}, async (connection, owner) => {
2919
+ const operation = new DatabaseOperation({
2920
+ configuration: this,
2921
+ configurationReuseKey: pool.getConnectionConfigurationReuseKey(connection),
2922
+ connection,
2923
+ databaseIdentifier,
2924
+ owner
2925
+ })
2926
+
2927
+ try {
2928
+ return await operation.transaction(async () => await callback(operation))
2929
+ } finally {
2930
+ operation.complete()
2931
+ }
2932
+ })
2933
+ }
2934
+
2899
2935
  /**
2900
2936
  * Runs callback with database connections for the requested identifiers.
2901
2937
  * @template T
@@ -61,6 +61,7 @@
61
61
  * @property {boolean} [sessionTimeZone] - Whether to ensure the configured database session time zone before the query.
62
62
  * @property {AbortSignal} [signal] - Aborts the in-flight query (destroying its connection) when it fires.
63
63
  * @property {string} [sourceStack] - Stack captured at the caller boundary.
64
+ * @property {symbol} [operationOwner] - Opaque owner for an operation-leased connection.
64
65
  */
65
66
 
66
67
  /**
@@ -127,6 +128,21 @@ import TableForeignKey from "../table-data/table-foreign-key.js"
127
128
  import wait from "awaitery/build/wait.js"
128
129
  import {optionalPositiveInteger} from "typanic"
129
130
 
131
+ /**
132
+ * Marks a callback failure that happened after the owning transaction was durably committed.
133
+ * The public transaction boundary unwraps it before deadlock classification.
134
+ */
135
+ class VelociousDatabaseAfterCommitCallbackError extends Error {
136
+ /**
137
+ * Runs constructor.
138
+ * @param {?} callbackError - Original callback failure.
139
+ */
140
+ constructor(callbackError) {
141
+ super("Database afterCommit callback failed")
142
+ this.callbackError = callbackError
143
+ }
144
+ }
145
+
130
146
  /**
131
147
  * Runs now ms.
132
148
  * @returns {number} - Current high-resolution-ish timestamp in milliseconds.
@@ -175,6 +191,11 @@ export default class VelociousDatabaseDriversBase {
175
191
  _activeQuery = null
176
192
  /** @type {Map<string, number>} */
177
193
  _heldAdvisoryLocks = new Map()
194
+ /**
195
+ * Exclusive operation lease installed by a single-multi-use pool.
196
+ * @type {import("../operation-lease.js").default | undefined}
197
+ */
198
+ _operationLease = undefined
178
199
 
179
200
  /**
180
201
  * Runs constructor.
@@ -488,6 +509,46 @@ export default class VelociousDatabaseDriversBase {
488
509
  return this.configuration
489
510
  }
490
511
 
512
+ /**
513
+ * Installs an operation lease atomically with ordinary transaction admission.
514
+ * @param {import("../operation-lease.js").default} operationLease - Active lease.
515
+ * @returns {Promise<void>} - Resolves once the lease owns transaction admission.
516
+ */
517
+ async setOperationLease(operationLease) {
518
+ await this._transactionsActionsMutex.sync(async () => {
519
+ if (this._operationLease) throw new Error("A database operation lease is already active")
520
+ if (this._transactionsCount > 0) {
521
+ throw new Error("Cannot start a database operation while an unrelated ordinary transaction is already active")
522
+ }
523
+
524
+ this._operationLease = operationLease
525
+ })
526
+ }
527
+
528
+ /**
529
+ * Clears the matching operation lease.
530
+ * @param {import("../operation-lease.js").default} operationLease - Lease to clear.
531
+ * @returns {void}
532
+ */
533
+ clearOperationLease(operationLease) {
534
+ if (this._operationLease !== operationLease) {
535
+ throw new Error("Cannot clear a database operation lease owned by another operation")
536
+ }
537
+
538
+ this._operationLease = undefined
539
+ }
540
+
541
+ /**
542
+ * Waits for an unrelated operation lease to release.
543
+ * @param {symbol | undefined} operationOwner - Candidate operation owner.
544
+ * @returns {Promise<void>}
545
+ */
546
+ async _waitForOperationLease(operationOwner) {
547
+ const operationLease = this._operationLease
548
+
549
+ if (operationLease) await operationLease.wait(operationOwner)
550
+ }
551
+
491
552
  /**
492
553
  * Runs get id seq.
493
554
  * @returns {number | undefined} - The id seq.
@@ -739,9 +800,10 @@ export default class VelociousDatabaseDriversBase {
739
800
  /**
740
801
  * Runs last insert id.
741
802
  * @abstract
803
+ * @param {QueryOptions} [_options] - Query ownership options.
742
804
  * @returns {Promise<number>} - Resolves with the last insert id.
743
805
  */
744
- lastInsertID() {
806
+ lastInsertID(_options = {}) {
745
807
  throw new Error(`${this.constructor.name}#lastInsertID not implemented`)
746
808
  }
747
809
 
@@ -932,12 +994,16 @@ export default class VelociousDatabaseDriversBase {
932
994
  * The outermost transaction retries the whole callback on a deadlock / lock-wait-timeout,
933
995
  * because such errors roll the entire transaction back and the standard recovery is to
934
996
  * restart it. Nested savepoints let the deadlock bubble up to this outer retry.
935
- * @param {() => Promise<void>} callback - Callback function.
936
- * @returns {Promise<?>} - Resolves with the transaction result.
997
+ * @template T
998
+ * @param {() => Promise<T>} callback - Callback function.
999
+ * @param {Pick<QueryOptions, "operationOwner">} [options] - Transaction ownership.
1000
+ * @returns {Promise<T>} - Resolves with the transaction result.
937
1001
  */
938
- async transaction(callback) {
1002
+ async transaction(callback, options = {}) {
1003
+ await this._waitForOperationLease(options.operationOwner)
1004
+
939
1005
  if (this._transactionsCount > 0) {
940
- return await this._runTransactionAttempt(callback)
1006
+ return await this._runTransactionAttempt(callback, options)
941
1007
  }
942
1008
 
943
1009
  const args = this.getArgs()
@@ -950,8 +1016,10 @@ export default class VelociousDatabaseDriversBase {
950
1016
  attempt++
951
1017
 
952
1018
  try {
953
- return await this._runTransactionAttempt(callback)
1019
+ return await this._runTransactionAttempt(callback, options)
954
1020
  } catch (error) {
1021
+ if (error instanceof VelociousDatabaseAfterCommitCallbackError) throw error.callbackError
1022
+
955
1023
  const retryInfo = error instanceof Error ? this.retryableDatabaseError(error) : {retry: false, reconnect: false}
956
1024
 
957
1025
  if (retryInfo.deadlock && attempt < maxAttempts && this._transactionsCount == 0) {
@@ -993,10 +1061,12 @@ export default class VelociousDatabaseDriversBase {
993
1061
  * Runs a single transaction attempt: starts a transaction (or a savepoint when nested), runs
994
1062
  * `callback`, and commits — rolling back on error. {@link transaction} wraps this with deadlock
995
1063
  * retry at the outermost level.
996
- * @param {() => Promise<void>} callback - Callback function.
1064
+ * @template T
1065
+ * @param {() => Promise<T>} callback - Callback function.
1066
+ * @param {Pick<QueryOptions, "operationOwner">} options - Transaction ownership.
997
1067
  * @returns {Promise<?>} - Resolves with the transaction result.
998
1068
  */
999
- async _runTransactionAttempt(callback) {
1069
+ async _runTransactionAttempt(callback, options) {
1000
1070
  const savePointName = this.generateSavePointName()
1001
1071
  /**
1002
1072
  * Callback frame.
@@ -1007,14 +1077,19 @@ export default class VelociousDatabaseDriversBase {
1007
1077
 
1008
1078
  this._afterCommitCallbackFrames.push(callbackFrame)
1009
1079
 
1010
- if (this._transactionsCount == 0) {
1011
- this.logger.debug("Start transaction")
1012
- await this.startTransaction()
1013
- transactionStarted = true
1014
- } else {
1015
- this.logger.debug("Start savepoint", savePointName)
1016
- await this.startSavePoint(savePointName)
1017
- savePointStarted = true
1080
+ try {
1081
+ if (this._transactionsCount == 0) {
1082
+ this.logger.debug("Start transaction")
1083
+ await this.startTransaction(options)
1084
+ transactionStarted = true
1085
+ } else {
1086
+ this.logger.debug("Start savepoint", savePointName)
1087
+ await this.startSavePoint(savePointName, options)
1088
+ savePointStarted = true
1089
+ }
1090
+ } catch (error) {
1091
+ this._afterCommitCallbackFrames.pop()
1092
+ throw error
1018
1093
  }
1019
1094
 
1020
1095
  let result
@@ -1024,15 +1099,13 @@ export default class VelociousDatabaseDriversBase {
1024
1099
 
1025
1100
  if (savePointStarted) {
1026
1101
  this.logger.debug("Release savepoint", savePointName)
1027
- await this.releaseSavePoint(savePointName)
1102
+ await this.releaseSavePoint(savePointName, options)
1028
1103
  }
1029
1104
 
1030
1105
  if (transactionStarted) {
1031
1106
  this.logger.debug("Commit transaction")
1032
- await this.commitTransaction()
1107
+ await this.commitTransaction(options)
1033
1108
  }
1034
-
1035
- await this._commitAfterCommitCallbackFrame()
1036
1109
  } catch (error) {
1037
1110
  if (error instanceof Error) {
1038
1111
  this.logger.debug("Transaction error", error.message)
@@ -1045,14 +1118,14 @@ export default class VelociousDatabaseDriversBase {
1045
1118
  if (savePointStarted) {
1046
1119
  this.logger.debug("Rollback savepoint", savePointName)
1047
1120
  try {
1048
- await this.rollbackSavePoint(savePointName)
1121
+ await this.rollbackSavePoint(savePointName, options)
1049
1122
  } catch (savePointError) {
1050
1123
  const message = savePointError instanceof Error ? savePointError.message : `${savePointError}`
1051
1124
 
1052
1125
  // MySQL sometimes drops savepoints unexpectedly; fall back to rolling back the full transaction
1053
1126
  if (message.includes("SAVEPOINT") || message.includes("ER_SP_DOES_NOT_EXIST")) {
1054
1127
  this.logger.debug("Savepoint rollback failed; rolling back entire transaction instead")
1055
- await this.rollbackTransaction()
1128
+ await this.rollbackTransaction(options)
1056
1129
  transactionRolledBack = true
1057
1130
  } else {
1058
1131
  throw savePointError
@@ -1066,7 +1139,7 @@ export default class VelociousDatabaseDriversBase {
1066
1139
  // `_transactionsCount` below zero, which would then defeat the outermost deadlock-retry guard.
1067
1140
  if (transactionStarted && !transactionRolledBack && this._transactionsCount > 0) {
1068
1141
  this.logger.debug("Rollback transaction")
1069
- await this.rollbackTransaction()
1142
+ await this.rollbackTransaction(options)
1070
1143
  }
1071
1144
 
1072
1145
  this._afterCommitCallbackFrames.pop()
@@ -1074,6 +1147,12 @@ export default class VelociousDatabaseDriversBase {
1074
1147
  throw error
1075
1148
  }
1076
1149
 
1150
+ try {
1151
+ await this._commitAfterCommitCallbackFrame()
1152
+ } catch (error) {
1153
+ throw new VelociousDatabaseAfterCommitCallbackError(error)
1154
+ }
1155
+
1077
1156
  return result
1078
1157
  }
1079
1158
 
@@ -1081,9 +1160,12 @@ export default class VelociousDatabaseDriversBase {
1081
1160
  * Runs a callback after the surrounding transaction commits.
1082
1161
  * If no transaction is active, the callback runs immediately.
1083
1162
  * @param {() => void | Promise<void>} callback - Callback.
1163
+ * @param {Pick<QueryOptions, "operationOwner">} [options] - Callback ownership.
1084
1164
  * @returns {Promise<void>} - Resolves when the callback has been registered or run.
1085
1165
  */
1086
- async afterCommit(callback) {
1166
+ async afterCommit(callback, options = {}) {
1167
+ await this._waitForOperationLease(options.operationOwner)
1168
+
1087
1169
  const currentFrame = this._afterCommitCallbackFrames[this._afterCommitCallbackFrames.length - 1]
1088
1170
 
1089
1171
  if (!currentFrame) {
@@ -1102,40 +1184,60 @@ export default class VelociousDatabaseDriversBase {
1102
1184
 
1103
1185
  /**
1104
1186
  * Runs start transaction.
1187
+ * @param {Pick<QueryOptions, "operationOwner">} [options] - Transaction ownership.
1105
1188
  * @returns {Promise<void>} - Resolves when complete.
1106
1189
  */
1107
- async startTransaction() {
1108
- await this._transactionsActionsMutex.sync(async () => {
1109
- await this._startTransactionAction()
1110
- this._transactionsCount++
1111
- })
1190
+ async startTransaction(options = {}) {
1191
+ while (true) {
1192
+ /** @type {import("../operation-lease.js").default | undefined} */
1193
+ let blockingOperationLease
1194
+
1195
+ await this._transactionsActionsMutex.sync(async () => {
1196
+ const operationLease = this._operationLease
1197
+
1198
+ if (operationLease && options.operationOwner !== operationLease.owner) {
1199
+ blockingOperationLease = operationLease
1200
+ return
1201
+ }
1202
+
1203
+ await this._startTransactionAction(options)
1204
+ this._transactionsCount++
1205
+ })
1206
+
1207
+ if (!blockingOperationLease) return
1208
+
1209
+ await blockingOperationLease.wait(options.operationOwner)
1210
+ }
1112
1211
  }
1113
1212
 
1114
1213
  /**
1115
1214
  * Runs start transaction action.
1215
+ * @param {Pick<QueryOptions, "operationOwner">} [options] - Transaction ownership.
1116
1216
  * @returns {Promise<void>} - Resolves when complete.
1117
1217
  */
1118
- async _startTransactionAction() {
1119
- await this.query("BEGIN TRANSACTION")
1218
+ async _startTransactionAction(options = {}) {
1219
+ await this.query("BEGIN TRANSACTION", options)
1120
1220
  }
1121
1221
 
1122
1222
  /**
1123
1223
  * Runs commit transaction.
1224
+ * @param {Pick<QueryOptions, "operationOwner">} [options] - Transaction ownership.
1124
1225
  * @returns {Promise<void>} - Resolves when complete.
1125
1226
  */
1126
- async commitTransaction() {
1227
+ async commitTransaction(options = {}) {
1127
1228
  await this._transactionsActionsMutex.sync(async () => {
1128
- await this._commitTransactionAction()
1229
+ await this._commitTransactionAction(options)
1129
1230
  this._transactionsCount--
1130
1231
  })
1131
1232
  }
1132
1233
 
1133
1234
  /**
1134
1235
  * Runs commit transaction action.
1236
+ * @param {Pick<QueryOptions, "operationOwner">} [options] - Transaction ownership.
1135
1237
  * @returns {Promise<void>} - Resolves when complete.
1136
1238
  */
1137
- async _commitTransactionAction() {
1138
- await this.query("COMMIT")
1239
+ async _commitTransactionAction(options = {}) {
1240
+ await this.query("COMMIT", options)
1139
1241
  }
1140
1242
 
1141
1243
  /**
@@ -1183,6 +1285,7 @@ export default class VelociousDatabaseDriversBase {
1183
1285
  * @returns {Promise<QueryResultType>} - Resolves with the query.
1184
1286
  */
1185
1287
  async query(sql, options = {}) {
1288
+ await this._waitForOperationLease(options.operationOwner)
1186
1289
  this._assertWritableQuery(sql)
1187
1290
 
1188
1291
  let tries = 0
@@ -1232,16 +1335,18 @@ export default class VelociousDatabaseDriversBase {
1232
1335
  /**
1233
1336
  * Executes a mutation and returns the number of rows changed by that statement.
1234
1337
  * @param {string} sql - Mutation SQL string.
1338
+ * @param {QueryOptions} [options] - Query ownership options.
1235
1339
  * @returns {Promise<number>} - Affected row count.
1236
1340
  */
1237
- async affectedRows(sql) {
1341
+ async affectedRows(sql, options = {}) {
1342
+ await this._waitForOperationLease(options.operationOwner)
1238
1343
  this._assertWritableQuery(sql)
1239
- await this.beforeQuery(sql, {})
1344
+ await this.beforeQuery(sql, options)
1240
1345
 
1241
1346
  try {
1242
1347
  return await this._affectedRowsActual(sql)
1243
1348
  } finally {
1244
- await this.afterQuery(sql, {})
1349
+ await this.afterQuery(sql, options)
1245
1350
  }
1246
1351
  }
1247
1352
 
@@ -1609,12 +1714,13 @@ export default class VelociousDatabaseDriversBase {
1609
1714
 
1610
1715
  /**
1611
1716
  * Runs rollback transaction.
1717
+ * @param {Pick<QueryOptions, "operationOwner">} [options] - Transaction ownership.
1612
1718
  * @returns {Promise<void>} - Resolves when complete.
1613
1719
  */
1614
- async rollbackTransaction() {
1720
+ async rollbackTransaction(options = {}) {
1615
1721
  await this._transactionsActionsMutex.sync(async () => {
1616
1722
  try {
1617
- await this._rollbackTransactionAction()
1723
+ await this._rollbackTransactionAction(options)
1618
1724
  } finally {
1619
1725
  this._transactionsCount--
1620
1726
 
@@ -1630,10 +1736,11 @@ export default class VelociousDatabaseDriversBase {
1630
1736
 
1631
1737
  /**
1632
1738
  * Runs rollback transaction action.
1739
+ * @param {Pick<QueryOptions, "operationOwner">} [options] - Transaction ownership.
1633
1740
  * @returns {Promise<void>} - Resolves when complete.
1634
1741
  */
1635
- async _rollbackTransactionAction() {
1636
- await this.query("ROLLBACK")
1742
+ async _rollbackTransactionAction(options = {}) {
1743
+ await this.query("ROLLBACK", options)
1637
1744
  }
1638
1745
 
1639
1746
  /**
@@ -1647,21 +1754,23 @@ export default class VelociousDatabaseDriversBase {
1647
1754
  /**
1648
1755
  * Runs start save point.
1649
1756
  * @param {string} savePointName - Save point name.
1757
+ * @param {Pick<QueryOptions, "operationOwner">} [options] - Transaction ownership.
1650
1758
  * @returns {Promise<void>} - Resolves when complete.
1651
1759
  */
1652
- async startSavePoint(savePointName) {
1760
+ async startSavePoint(savePointName, options = {}) {
1653
1761
  await this._transactionsActionsMutex.sync(async () => {
1654
- await this._startSavePointAction(savePointName)
1762
+ await this._startSavePointAction(savePointName, options)
1655
1763
  })
1656
1764
  }
1657
1765
 
1658
1766
  /**
1659
1767
  * Runs start save point action.
1660
1768
  * @param {string} savePointName - Save point name.
1769
+ * @param {Pick<QueryOptions, "operationOwner">} [options] - Transaction ownership.
1661
1770
  * @returns {Promise<void>} - Resolves when complete.
1662
1771
  */
1663
- async _startSavePointAction(savePointName) {
1664
- await this.query(`SAVEPOINT ${savePointName}`)
1772
+ async _startSavePointAction(savePointName, options = {}) {
1773
+ await this.query(`SAVEPOINT ${savePointName}`, options)
1665
1774
  }
1666
1775
 
1667
1776
  /**
@@ -1691,22 +1800,24 @@ export default class VelociousDatabaseDriversBase {
1691
1800
  /**
1692
1801
  * Runs release save point.
1693
1802
  * @param {string} savePointName - Save point name.
1803
+ * @param {Pick<QueryOptions, "operationOwner">} [options] - Transaction ownership.
1694
1804
  * @returns {Promise<void>} - Resolves when complete.
1695
1805
  */
1696
- async releaseSavePoint(savePointName) {
1806
+ async releaseSavePoint(savePointName, options = {}) {
1697
1807
  await this._transactionsActionsMutex.sync(async () => {
1698
- await this._releaseSavePointAction(savePointName)
1808
+ await this._releaseSavePointAction(savePointName, options)
1699
1809
  })
1700
1810
  }
1701
1811
 
1702
1812
  /**
1703
1813
  * Runs release save point action.
1704
1814
  * @param {string} savePointName - Save point name.
1815
+ * @param {Pick<QueryOptions, "operationOwner">} [options] - Transaction ownership.
1705
1816
  * @returns {Promise<void>} - Resolves when complete.
1706
1817
  */
1707
- async _releaseSavePointAction(savePointName) {
1818
+ async _releaseSavePointAction(savePointName, options = {}) {
1708
1819
  try {
1709
- await this.query(`RELEASE SAVEPOINT ${savePointName}`)
1820
+ await this.query(`RELEASE SAVEPOINT ${savePointName}`, options)
1710
1821
  } catch (error) {
1711
1822
  const message = error instanceof Error ? error.message : `${error}`
1712
1823
 
@@ -1723,21 +1834,23 @@ export default class VelociousDatabaseDriversBase {
1723
1834
  /**
1724
1835
  * Runs rollback save point.
1725
1836
  * @param {string} savePointName - Save point name.
1837
+ * @param {Pick<QueryOptions, "operationOwner">} [options] - Transaction ownership.
1726
1838
  * @returns {Promise<void>} - Resolves when complete.
1727
1839
  */
1728
- async rollbackSavePoint(savePointName) {
1840
+ async rollbackSavePoint(savePointName, options = {}) {
1729
1841
  await this._transactionsActionsMutex.sync(async () => {
1730
- await this._rollbackSavePointAction(savePointName)
1842
+ await this._rollbackSavePointAction(savePointName, options)
1731
1843
  })
1732
1844
  }
1733
1845
 
1734
1846
  /**
1735
1847
  * Runs rollback save point action.
1736
1848
  * @param {string} savePointName - Save point name.
1849
+ * @param {Pick<QueryOptions, "operationOwner">} [options] - Transaction ownership.
1737
1850
  * @returns {Promise<void>} - Resolves when complete.
1738
1851
  */
1739
- async _rollbackSavePointAction(savePointName) {
1740
- await this.query(`ROLLBACK TO SAVEPOINT ${savePointName}`)
1852
+ async _rollbackSavePointAction(savePointName, options = {}) {
1853
+ await this.query(`ROLLBACK TO SAVEPOINT ${savePointName}`, options)
1741
1854
  }
1742
1855
 
1743
1856
  /**
@@ -463,8 +463,8 @@ export default class VelociousDatabaseDriversMssql extends Base{
463
463
  })
464
464
  }
465
465
 
466
- async lastInsertID() {
467
- const result = await this.query("SELECT SCOPE_IDENTITY() AS last_insert_id")
466
+ async lastInsertID(options = {}) {
467
+ const result = await this.query("SELECT SCOPE_IDENTITY() AS last_insert_id", options)
468
468
  const lastInsertID = digg(result, 0, "last_insert_id")
469
469
 
470
470
  if (lastInsertID === null) throw new Error("Couldn't get the last inserted ID")
@@ -534,29 +534,32 @@ export default class VelociousDatabaseDriversMssql extends Base{
534
534
  /**
535
535
  * Runs start save point action.
536
536
  * @param {string} savePointName - Save point name.
537
+ * @param {Pick<import("../base.js").QueryOptions, "operationOwner">} [options] - Transaction ownership.
537
538
  * @returns {Promise<void>} - Resolves when complete.
538
539
  */
539
- async _startSavePointAction(savePointName) {
540
- await this.query(`SAVE TRANSACTION [${savePointName}]`)
540
+ async _startSavePointAction(savePointName, options = {}) {
541
+ await this.query(`SAVE TRANSACTION [${savePointName}]`, options)
541
542
  }
542
543
 
543
544
  /**
544
545
  * Runs release save point action.
545
546
  * @param {string} savePointName - Save point name.
547
+ * @param {Pick<import("../base.js").QueryOptions, "operationOwner">} [_options] - Transaction ownership.
546
548
  * @returns {Promise<void>} - Resolves when complete.
547
549
  */
548
- async _releaseSavePointAction(savePointName) { // eslint-disable-line no-unused-vars
550
+ async _releaseSavePointAction(savePointName, _options = {}) {
549
551
  // Do nothing in MS-SQL.
550
552
  }
551
553
 
552
554
  /**
553
555
  * Runs rollback save point action.
554
556
  * @param {string} savePointName - Save point name.
557
+ * @param {Pick<import("../base.js").QueryOptions, "operationOwner">} [options] - Transaction ownership.
555
558
  * @returns {Promise<void>} - Resolves when complete.
556
559
  */
557
- async _rollbackSavePointAction(savePointName) {
560
+ async _rollbackSavePointAction(savePointName, options = {}) {
558
561
  try {
559
- await this.query(`ROLLBACK TRANSACTION [${savePointName}]`)
562
+ await this.query(`ROLLBACK TRANSACTION [${savePointName}]`, options)
560
563
  } catch (error) {
561
564
  const message = error instanceof Error ? error.message : `${error}`
562
565
 
@@ -403,9 +403,12 @@ export default class VelociousDatabaseDriversMysql extends Base{
403
403
  * large result set is read incrementally instead of being buffered. Overrides the base
404
404
  * buffered fallback with true server-side streaming.
405
405
  * @param {string} sql - SQL string to stream.
406
+ * @param {import("../base.js").QueryOptions} [options] - Query ownership options.
406
407
  * @yields {Record<string, unknown>} - The result rows, one at a time.
407
408
  */
408
- async *queryStream(sql) {
409
+ async *queryStream(sql, options = {}) {
410
+ await this._waitForOperationLease(options.operationOwner)
411
+
409
412
  if (!this.pool) await this.connect()
410
413
  if (!this.pool) throw new Error("MySQL pool failed to initialize")
411
414
 
@@ -554,10 +557,11 @@ export default class VelociousDatabaseDriversMysql extends Base{
554
557
 
555
558
  /**
556
559
  * Runs last insert id.
560
+ * @param {import("../base.js").QueryOptions} [options] - Query ownership options.
557
561
  * @returns {Promise<number>} - Resolves with the last insert id.
558
562
  */
559
- async lastInsertID() {
560
- const result = await this.query("SELECT LAST_INSERT_ID() AS last_insert_id")
563
+ async lastInsertID(options = {}) {
564
+ const result = await this.query("SELECT LAST_INSERT_ID() AS last_insert_id", options)
561
565
 
562
566
  return digg(result, 0, "last_insert_id")
563
567
  }
@@ -574,10 +578,11 @@ export default class VelociousDatabaseDriversMysql extends Base{
574
578
 
575
579
  /**
576
580
  * Runs start transaction action.
581
+ * @param {Pick<import("../base.js").QueryOptions, "operationOwner">} [options] - Transaction ownership.
577
582
  * @returns {Promise<void>} - Resolves when complete.
578
583
  */
579
- async _startTransactionAction() {
580
- await this.query("START TRANSACTION")
584
+ async _startTransactionAction(options = {}) {
585
+ await this.query("START TRANSACTION", options)
581
586
  }
582
587
 
583
588
  /**
@@ -326,8 +326,8 @@ export default class VelociousDatabaseDriversPgsql extends Base{
326
326
  })
327
327
  }
328
328
 
329
- async lastInsertID() {
330
- const result = await this.query("SELECT LASTVAL() AS last_insert_id")
329
+ async lastInsertID(options = {}) {
330
+ const result = await this.query("SELECT LASTVAL() AS last_insert_id", options)
331
331
 
332
332
  return digg(result, 0, "last_insert_id")
333
333
  }
@@ -338,8 +338,8 @@ export default class VelociousDatabaseDriversPgsql extends Base{
338
338
  return this._options
339
339
  }
340
340
 
341
- async _startTransactionAction() {
342
- await this.query("START TRANSACTION")
341
+ async _startTransactionAction(options = {}) {
342
+ await this.query("START TRANSACTION", options)
343
343
  }
344
344
 
345
345
  /**
@@ -262,8 +262,8 @@ export default class VelociousDatabaseDriversSqliteBase extends Base {
262
262
  })
263
263
  }
264
264
 
265
- async lastInsertID() {
266
- const result = await this.query("SELECT LAST_INSERT_ROWID() AS last_insert_id")
265
+ async lastInsertID(options = {}) {
266
+ const result = await this.query("SELECT LAST_INSERT_ROWID() AS last_insert_id", options)
267
267
 
268
268
  return digg(result, 0, "last_insert_id")
269
269
  }