velocious 1.0.592 → 1.0.594

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 (197) hide show
  1. package/README.md +11 -2
  2. package/build/configuration-types.js +1 -1
  3. package/build/configuration.js +68 -13
  4. package/build/database/drivers/base.js +83 -34
  5. package/build/database/drivers/mssql/index.js +32 -0
  6. package/build/database/drivers/mysql/index.js +17 -0
  7. package/build/database/drivers/pgsql/index.js +11 -0
  8. package/build/database/drivers/sqlite/base.js +12 -0
  9. package/build/database/drivers/sqlite/connection-sql-js.js +3 -2
  10. package/build/database/drivers/sqlite/index.js +7 -1
  11. package/build/database/drivers/sqlite/index.native.js +9 -3
  12. package/build/database/drivers/sqlite/index.web.js +6 -3
  13. package/build/database/migrator.js +90 -24
  14. package/build/database/operation.js +55 -4
  15. package/build/database/query/join-object.js +7 -4
  16. package/build/database/query/model-class-query.js +38 -7
  17. package/build/database/query/preloader/belongs-to.js +9 -6
  18. package/build/database/query/preloader/has-many.js +25 -14
  19. package/build/database/query/preloader/has-one.js +7 -4
  20. package/build/database/query/preloader/query-for-model.js +16 -1
  21. package/build/database/query/preloader.js +6 -4
  22. package/build/database/query/query-data.js +3 -3
  23. package/build/database/query/where-model-class-hash.js +8 -4
  24. package/build/database/query/with-count.js +4 -3
  25. package/build/database/record/index.js +148 -5
  26. package/build/database/record/instance-relationships/base.js +13 -0
  27. package/build/database/record/instance-relationships/belongs-to.js +1 -1
  28. package/build/database/record/instance-relationships/has-many.js +5 -2
  29. package/build/database/record/instance-relationships/has-one.js +1 -1
  30. package/build/database/record/relationships/base.js +17 -0
  31. package/build/database/use-database.js +81 -28
  32. package/build/environment-handlers/base.js +18 -0
  33. package/build/environment-handlers/node.js +32 -0
  34. package/build/src/configuration-types.d.ts +2 -2
  35. package/build/src/configuration-types.js +2 -2
  36. package/build/src/configuration.d.ts +13 -4
  37. package/build/src/configuration.d.ts.map +1 -1
  38. package/build/src/configuration.js +56 -16
  39. package/build/src/database/drivers/base.d.ts +22 -0
  40. package/build/src/database/drivers/base.d.ts.map +1 -1
  41. package/build/src/database/drivers/base.js +81 -37
  42. package/build/src/database/drivers/mssql/index.d.ts +7 -0
  43. package/build/src/database/drivers/mssql/index.d.ts.map +1 -1
  44. package/build/src/database/drivers/mssql/index.js +15 -1
  45. package/build/src/database/drivers/mysql/index.d.ts +7 -0
  46. package/build/src/database/drivers/mysql/index.d.ts.map +1 -1
  47. package/build/src/database/drivers/mysql/index.js +15 -1
  48. package/build/src/database/drivers/pgsql/index.d.ts +6 -0
  49. package/build/src/database/drivers/pgsql/index.d.ts.map +1 -1
  50. package/build/src/database/drivers/pgsql/index.js +10 -1
  51. package/build/src/database/drivers/sqlite/base.d.ts +7 -0
  52. package/build/src/database/drivers/sqlite/base.d.ts.map +1 -1
  53. package/build/src/database/drivers/sqlite/base.js +11 -1
  54. package/build/src/database/drivers/sqlite/connection-sql-js.d.ts +4 -1
  55. package/build/src/database/drivers/sqlite/connection-sql-js.d.ts.map +1 -1
  56. package/build/src/database/drivers/sqlite/connection-sql-js.js +4 -3
  57. package/build/src/database/drivers/sqlite/index.d.ts +2 -1
  58. package/build/src/database/drivers/sqlite/index.d.ts.map +1 -1
  59. package/build/src/database/drivers/sqlite/index.js +7 -2
  60. package/build/src/database/drivers/sqlite/index.native.d.ts +2 -1
  61. package/build/src/database/drivers/sqlite/index.native.d.ts.map +1 -1
  62. package/build/src/database/drivers/sqlite/index.native.js +9 -4
  63. package/build/src/database/drivers/sqlite/index.web.d.ts +2 -1
  64. package/build/src/database/drivers/sqlite/index.web.d.ts.map +1 -1
  65. package/build/src/database/drivers/sqlite/index.web.js +7 -4
  66. package/build/src/database/migrator.d.ts +34 -0
  67. package/build/src/database/migrator.d.ts.map +1 -1
  68. package/build/src/database/migrator.js +83 -23
  69. package/build/src/database/operation.d.ts +15 -1
  70. package/build/src/database/operation.d.ts.map +1 -1
  71. package/build/src/database/operation.js +52 -5
  72. package/build/src/database/query/join-object.d.ts.map +1 -1
  73. package/build/src/database/query/join-object.js +7 -5
  74. package/build/src/database/query/model-class-query.d.ts +6 -0
  75. package/build/src/database/query/model-class-query.d.ts.map +1 -1
  76. package/build/src/database/query/model-class-query.js +33 -8
  77. package/build/src/database/query/preloader/belongs-to.d.ts.map +1 -1
  78. package/build/src/database/query/preloader/belongs-to.js +9 -7
  79. package/build/src/database/query/preloader/has-many.d.ts.map +1 -1
  80. package/build/src/database/query/preloader/has-many.js +23 -16
  81. package/build/src/database/query/preloader/has-one.d.ts.map +1 -1
  82. package/build/src/database/query/preloader/has-one.js +7 -5
  83. package/build/src/database/query/preloader/query-for-model.d.ts +8 -0
  84. package/build/src/database/query/preloader/query-for-model.d.ts.map +1 -1
  85. package/build/src/database/query/preloader/query-for-model.js +15 -2
  86. package/build/src/database/query/preloader.d.ts.map +1 -1
  87. package/build/src/database/query/preloader.js +6 -5
  88. package/build/src/database/query/query-data.js +4 -4
  89. package/build/src/database/query/where-model-class-hash.d.ts.map +1 -1
  90. package/build/src/database/query/where-model-class-hash.js +7 -5
  91. package/build/src/database/query/with-count.js +5 -4
  92. package/build/src/database/record/index.d.ts +95 -0
  93. package/build/src/database/record/index.d.ts.map +1 -1
  94. package/build/src/database/record/index.js +126 -6
  95. package/build/src/database/record/instance-relationships/base.d.ts +6 -0
  96. package/build/src/database/record/instance-relationships/base.d.ts.map +1 -1
  97. package/build/src/database/record/instance-relationships/base.js +12 -1
  98. package/build/src/database/record/instance-relationships/belongs-to.js +2 -2
  99. package/build/src/database/record/instance-relationships/has-many.d.ts.map +1 -1
  100. package/build/src/database/record/instance-relationships/has-many.js +6 -3
  101. package/build/src/database/record/instance-relationships/has-one.js +2 -2
  102. package/build/src/database/record/relationships/base.d.ts +11 -0
  103. package/build/src/database/record/relationships/base.d.ts.map +1 -1
  104. package/build/src/database/record/relationships/base.js +15 -1
  105. package/build/src/database/use-database.d.ts +33 -5
  106. package/build/src/database/use-database.d.ts.map +1 -1
  107. package/build/src/database/use-database.js +81 -30
  108. package/build/src/environment-handlers/base.d.ts +16 -0
  109. package/build/src/environment-handlers/base.d.ts.map +1 -1
  110. package/build/src/environment-handlers/base.js +17 -1
  111. package/build/src/environment-handlers/node.d.ts +20 -0
  112. package/build/src/environment-handlers/node.d.ts.map +1 -1
  113. package/build/src/environment-handlers/node.js +29 -1
  114. package/build/src/sync/server-change-feed.d.ts +14 -2
  115. package/build/src/sync/server-change-feed.d.ts.map +1 -1
  116. package/build/src/sync/server-change-feed.js +50 -10
  117. package/build/src/sync/sync-scope-store.d.ts +13 -2
  118. package/build/src/sync/sync-scope-store.d.ts.map +1 -1
  119. package/build/src/sync/sync-scope-store.js +48 -7
  120. package/build/src/tenants/frontend-tenant-sqlite-lifecycle.d.ts +43 -1
  121. package/build/src/tenants/frontend-tenant-sqlite-lifecycle.d.ts.map +1 -1
  122. package/build/src/tenants/frontend-tenant-sqlite-lifecycle.js +141 -17
  123. package/build/src/tenants/tenant-handle.d.ts +36 -0
  124. package/build/src/tenants/tenant-handle.d.ts.map +1 -1
  125. package/build/src/tenants/tenant-handle.js +73 -3
  126. package/build/src/testing/browser-test-app.js +3 -1
  127. package/build/src/testing/browser-use-database-hook-scenarios.d.ts +16 -0
  128. package/build/src/testing/browser-use-database-hook-scenarios.d.ts.map +1 -0
  129. package/build/src/testing/browser-use-database-hook-scenarios.js +182 -0
  130. package/build/src/testing/shared-transaction-broker.d.ts +14 -0
  131. package/build/src/testing/shared-transaction-broker.d.ts.map +1 -1
  132. package/build/src/testing/shared-transaction-broker.js +44 -7
  133. package/build/src/testing/shared-transaction-connection-coordinator.d.ts +5 -4
  134. package/build/src/testing/shared-transaction-connection-coordinator.d.ts.map +1 -1
  135. package/build/src/testing/shared-transaction-connection-coordinator.js +21 -10
  136. package/build/src/utils/model-scope.d.ts +2 -2
  137. package/build/src/utils/model-scope.d.ts.map +1 -1
  138. package/build/src/utils/model-scope.js +5 -3
  139. package/build/src/utils/ransack.d.ts.map +1 -1
  140. package/build/src/utils/ransack.js +5 -3
  141. package/build/sync/server-change-feed.js +58 -8
  142. package/build/sync/sync-scope-store.js +55 -5
  143. package/build/tenants/frontend-tenant-sqlite-lifecycle.js +150 -15
  144. package/build/tenants/tenant-handle.js +90 -6
  145. package/build/testing/browser-test-app.js +2 -0
  146. package/build/testing/browser-use-database-hook-scenarios.js +204 -0
  147. package/build/testing/shared-transaction-broker.js +48 -7
  148. package/build/testing/shared-transaction-connection-coordinator.js +23 -9
  149. package/build/tsconfig.tsbuildinfo +1 -1
  150. package/build/utils/model-scope.js +5 -2
  151. package/build/utils/ransack.js +5 -2
  152. package/package.json +1 -1
  153. package/scripts/browser-test-session.js +489 -0
  154. package/scripts/prewarm-chromedriver.js +3 -22
  155. package/scripts/test-browser.js +52 -16
  156. package/src/configuration-types.js +1 -1
  157. package/src/configuration.js +68 -13
  158. package/src/database/drivers/base.js +83 -34
  159. package/src/database/drivers/mssql/index.js +32 -0
  160. package/src/database/drivers/mysql/index.js +17 -0
  161. package/src/database/drivers/pgsql/index.js +11 -0
  162. package/src/database/drivers/sqlite/base.js +12 -0
  163. package/src/database/drivers/sqlite/connection-sql-js.js +3 -2
  164. package/src/database/drivers/sqlite/index.js +7 -1
  165. package/src/database/drivers/sqlite/index.native.js +9 -3
  166. package/src/database/drivers/sqlite/index.web.js +6 -3
  167. package/src/database/migrator.js +90 -24
  168. package/src/database/operation.js +55 -4
  169. package/src/database/query/join-object.js +7 -4
  170. package/src/database/query/model-class-query.js +38 -7
  171. package/src/database/query/preloader/belongs-to.js +9 -6
  172. package/src/database/query/preloader/has-many.js +25 -14
  173. package/src/database/query/preloader/has-one.js +7 -4
  174. package/src/database/query/preloader/query-for-model.js +16 -1
  175. package/src/database/query/preloader.js +6 -4
  176. package/src/database/query/query-data.js +3 -3
  177. package/src/database/query/where-model-class-hash.js +8 -4
  178. package/src/database/query/with-count.js +4 -3
  179. package/src/database/record/index.js +148 -5
  180. package/src/database/record/instance-relationships/base.js +13 -0
  181. package/src/database/record/instance-relationships/belongs-to.js +1 -1
  182. package/src/database/record/instance-relationships/has-many.js +5 -2
  183. package/src/database/record/instance-relationships/has-one.js +1 -1
  184. package/src/database/record/relationships/base.js +17 -0
  185. package/src/database/use-database.js +81 -28
  186. package/src/environment-handlers/base.js +18 -0
  187. package/src/environment-handlers/node.js +32 -0
  188. package/src/sync/server-change-feed.js +58 -8
  189. package/src/sync/sync-scope-store.js +55 -5
  190. package/src/tenants/frontend-tenant-sqlite-lifecycle.js +150 -15
  191. package/src/tenants/tenant-handle.js +90 -6
  192. package/src/testing/browser-test-app.js +2 -0
  193. package/src/testing/browser-use-database-hook-scenarios.js +204 -0
  194. package/src/testing/shared-transaction-broker.js +48 -7
  195. package/src/testing/shared-transaction-connection-coordinator.js +23 -9
  196. package/src/utils/model-scope.js +5 -2
  197. package/src/utils/ransack.js +5 -2
package/README.md CHANGED
@@ -20,7 +20,7 @@
20
20
  * Self-sustaining sync feeds: upstream imports triggered by the changes pull itself, with framework-owned coalescing and throttling (see [docs/sync-upstream-imports.md](docs/sync-upstream-imports.md))
21
21
  * AwesomeTasks-shaped offline sync proof using routed resources, domain commands, signed offline grants, and peer-forwarded mutations (see the [developer guide](docs/shared-resource-sync-guide.md) and [proof](docs/awesome-tasks-offline-sync-proof.md))
22
22
  * SQLite web persistence that automatically prefers OPFS, then IndexedDB, and migrates legacy persisted bytes when possible (see [docs/sqlite-web-persistence.md](docs/sqlite-web-persistence.md))
23
- * Bounded frontend tenant SQLite handles with durable flush/close, backend-complete deletion, clean-only LRU eviction, and scoped pins (see [docs/frontend-tenant-sqlite-lifecycle.md](docs/frontend-tenant-sqlite-lifecycle.md))
23
+ * Bounded frontend tenant SQLite handles with independently deduplicated per-database migrations/model readiness, React lifecycle integration, durable flush/close, backend-complete deletion, clean-only LRU eviction, and scoped pins (see [docs/frontend-tenant-sqlite-lifecycle.md](docs/frontend-tenant-sqlite-lifecycle.md))
24
24
  * Expo / Metro compatibility guidance and a real Expo export check (see [docs/expo-metro-compatibility.md](docs/expo-metro-compatibility.md))
25
25
  * Gap-less positional lists with automatic reordering via `actsAsList`, including models with numeric, string, or UUID primary keys (see [docs/acts-as-list.md](docs/acts-as-list.md))
26
26
  * Rails-style nested-attribute writes on frontend-model `save()` (see [docs/nested-attributes.md](docs/nested-attributes.md))
@@ -328,6 +328,7 @@ npm run test:browser
328
328
  ```
329
329
 
330
330
  Browser system tests must be named `*.browser-test.js` or `*.browser-spec.js` (override with `VELOCIOUS_BROWSER_TEST_PATTERN`).
331
+ The runner validates and persists the exact Chrome/ChromeDriver pair selected by `scripts/prewarm-chromedriver.js`, then owns ChromeDriver and Chrome as a managed process group. Startup failures report the runtime paths and versions, service URL, retained logs under `tmp/browser-test-chrome/`, and Chrome process state before and after cleanup.
331
332
 
332
333
  Use beforeAll/afterAll for suite-level setup/teardown.
333
334
 
@@ -954,7 +955,8 @@ Translated models also get a `currentTranslation` `hasOne` relationship scoped t
954
955
  Async class APIs initialize record metadata on first use when a model has not
955
956
  already been initialized eagerly. See [docs/model-initialization.md](docs/model-initialization.md)
956
957
  for the eager and lazy initialization behavior, including atomic shared bootstrap
957
- and complete recovery after an eager initialization failure.
958
+ and complete recovery after an eager initialization failure or database-connection
959
+ closure without overlapping stale and current bootstrap side effects.
958
960
 
959
961
  ## Lifecycle callbacks
960
962
 
@@ -2070,6 +2072,13 @@ If you are developing on Velocious, you can run the tests with:
2070
2072
 
2071
2073
  Tests default to a 60-second timeout. Override per test with `{timeoutSeconds: 5}` or set a suite-wide default via `configureTests({defaultTimeoutSeconds: 30})`.
2072
2074
 
2075
+ Truncation-based test cleanup batches eligible tables into one request on PostgreSQL,
2076
+ SQL Server, and SQLite while preserving each driver's existing identity behavior,
2077
+ foreign-key restoration, stale-schema retry, and SQL.js persistence guarantees.
2078
+ MySQL/MariaDB batching requires the database's existing `multipleStatements: true`
2079
+ option; the default configuration keeps sequential `TRUNCATE TABLE` requests. See
2080
+ [database cleanup guidance](docs/testing-guidelines.md#truncation-cleanup).
2081
+
2073
2082
  Request tests share transaction-active, non-tenant database connections with their in-process HTTP handlers. Eligibility is evaluated when each request is dispatched, so a hook can start a transaction and issue a request in the same callback. This makes uncommitted setup visible to handlers while preserving rollback isolation. Without an active transaction, handlers use independent pooled connections, so concurrency and locking tests can opt out of transaction cleanup and exercise production-style connections. Shared connection state is scoped to the test lifecycle and cleared around each test. See [docs/testing-guidelines.md](docs/testing-guidelines.md#request-test-database-connections).
2074
2083
 
2075
2084
  Transactional tests also share active non-tenant connections with real forked, reusable pooled, and spawned background-job child runners through a per-attempt test-only loopback broker. Parent setup and child writes therefore occupy the same physical transaction and roll back together, including background-job persistence. Multiple configured databases route by identifier; tenant-only databases remain excluded. Tests using `{transaction: false, truncate: true}` retain ordinary independent physical connections for concurrency and locking coverage. See [docs/testing-guidelines.md](docs/testing-guidelines.md#request-test-database-connections).
@@ -72,7 +72,7 @@
72
72
  * @property {() => ReturnType<typeof JSON.parse>} [getConnection] - Custom connection factory override.
73
73
  * @property {string} [host] - Database host.
74
74
  * @property {boolean} [migrations] - Whether migrations are enabled for this database.
75
- * @property {boolean} [multipleStatements] - (MySQL) Opt in to multi-statement queries so a whole structure SQL dump loads in one round-trip via `StructureSqlLoader`. Off by default; ordinary queries otherwise reject stacked statements.
75
+ * @property {boolean} [multipleStatements] - (MySQL) Opt in to multi-statement queries so structure SQL loads and all-table cleanup can batch into one round-trip. Off by default; ordinary queries otherwise reject stacked statements.
76
76
  * @property {number} [maxRowsPerInsert] - Maximum rows per `INSERT ... VALUES (...), (...), ...` statement generated by `Record.insertMultiple`. Defaults to 500.
77
77
  * @property {number} [maxInsertSqlBytes] - Maximum serialized SQL size, in bytes, for a single `INSERT ... VALUES (...), (...), ...` statement. Defaults to 1 MiB (1048576).
78
78
  * @property {number} [maxInClauseValues] - Maximum values in a single `IN (...)` cohort used by preloads, association counts, and queryData aggregates. Defaults to 999.
@@ -302,8 +302,9 @@ export default class VelociousConfiguration {
302
302
  */
303
303
  this._initializeModelsPromise = undefined
304
304
  /**
305
- * In-progress `initialize()` promise, memoized so concurrent callers await
306
- * the same bootstrap. Reset to undefined if initialization fails.
305
+ * Current `initialize()` promise, memoized so concurrent callers await the
306
+ * same bootstrap. Retained across a connection close until stale bootstrap
307
+ * work settles, then cleared by identity before the new generation retries.
307
308
  * @type {Promise<void> | undefined}
308
309
  */
309
310
  this._initializePromise = undefined
@@ -1102,6 +1103,18 @@ export default class VelociousConfiguration {
1102
1103
  }
1103
1104
  }
1104
1105
 
1106
+ /**
1107
+ * Invalidates record metadata owned by one closed/deleted physical tenant
1108
+ * database while preserving every other tenant generation.
1109
+ * @param {string} databaseIdentity - Logical identifier plus pool reuse key.
1110
+ * @returns {void}
1111
+ */
1112
+ clearRecordMetadataForDatabaseIdentity(databaseIdentity) {
1113
+ for (const modelClass of Object.values(this.modelClasses)) {
1114
+ modelClass.clearRecordMetadataValuesForDatabaseIdentity(databaseIdentity)
1115
+ }
1116
+ }
1117
+
1105
1118
  /**
1106
1119
  * Runs get database pool type.
1107
1120
  * @param {string} identifier - Identifier.
@@ -2054,10 +2067,25 @@ export default class VelociousConfiguration {
2054
2067
  * @returns {Promise<void>} - Resolves when complete.
2055
2068
  */
2056
2069
  async initializeModels(args = {type: "server"}) {
2070
+ const modelInitializationGeneration = this._modelInitializationGeneration
2071
+
2057
2072
  if (this._modelsInitialized) return
2058
- if (this._initializeModelsPromise) return await this._initializeModelsPromise
2073
+ if (this._initializeModelsPromise) {
2074
+ const initializeModelsPromise = this._initializeModelsPromise
2075
+
2076
+ await initializeModelsPromise
2077
+
2078
+ if (this._modelInitializationGeneration === modelInitializationGeneration && !this._modelsInitialized) {
2079
+ if (this._initializeModelsPromise === initializeModelsPromise) {
2080
+ this._initializeModelsPromise = undefined
2081
+ }
2082
+
2083
+ return await this.initializeModels(args)
2084
+ }
2085
+
2086
+ return
2087
+ }
2059
2088
 
2060
- const modelInitializationGeneration = this._modelInitializationGeneration
2061
2089
  const initializeModelsPromise = (async () => {
2062
2090
  const shouldSkipDummyModelInitialization = process.env.VELOCIOUS_SKIP_DUMMY_MODEL_INITIALIZATION === "1"
2063
2091
  && process.env.VELOCIOUS_BROWSER_TESTS === "true"
@@ -2110,6 +2138,12 @@ export default class VelociousConfiguration {
2110
2138
  * @returns {Promise<void>} - Resolves when complete.
2111
2139
  */
2112
2140
  async initialize({type} = {type: "undefined"}) {
2141
+ if (this._closeDatabaseConnectionsPromise) {
2142
+ await this._closeDatabaseConnectionsPromise
2143
+ }
2144
+
2145
+ const initializationGeneration = this._modelInitializationGeneration
2146
+
2113
2147
  if (this._isInitialized) return
2114
2148
  // Memoize the in-progress initialization so concurrent callers await the same
2115
2149
  // bootstrap instead of racing. `_isInitialized` was previously set to `true`
@@ -2117,15 +2151,29 @@ export default class VelociousConfiguration {
2117
2151
  // `pooledRunnerConcurrency > 1` starting several jobs on a cold child) could
2118
2152
  // skip initialization and load models / perform a job while the first call
2119
2153
  // was still awaiting model discovery and initializers. Mirrors connectBeacon.
2120
- if (this._initializePromise) return await this._initializePromise
2154
+ if (this._initializePromise) {
2155
+ const initializePromise = this._initializePromise
2121
2156
 
2122
- this._initializePromise = (async () => {
2157
+ await initializePromise
2158
+
2159
+ if (this._modelInitializationGeneration === initializationGeneration && !this._isInitialized) {
2160
+ if (this._initializePromise === initializePromise) {
2161
+ this._initializePromise = undefined
2162
+ }
2163
+
2164
+ return await this.initialize({type})
2165
+ }
2166
+
2167
+ return
2168
+ }
2169
+
2170
+ const initializePromise = (async () => {
2123
2171
  await this.initializeModels({type})
2124
2172
 
2125
2173
  // Model initialization can be invalidated by a concurrent connection close.
2126
2174
  // If models are not ready, stop without marking the configuration initialized
2127
2175
  // so the next caller retries a full bootstrap.
2128
- if (!this._modelsInitialized) return
2176
+ if (this._modelInitializationGeneration !== initializationGeneration || !this._modelsInitialized) return
2129
2177
 
2130
2178
  await this.getEnvironmentHandler().autoDiscoverResources(this)
2131
2179
  this._mergeDiscoveredAbilityResources()
@@ -2147,22 +2195,28 @@ export default class VelociousConfiguration {
2147
2195
  }
2148
2196
  }
2149
2197
 
2150
- this._isInitialized = true
2198
+ if (this._modelInitializationGeneration === initializationGeneration) {
2199
+ this._isInitialized = true
2200
+ }
2151
2201
  })()
2152
2202
 
2203
+ this._initializePromise = initializePromise
2204
+
2153
2205
  try {
2154
- await this._initializePromise
2206
+ await initializePromise
2155
2207
  } catch (error) {
2156
2208
  // Let a later call retry a failed initialization instead of every future
2157
2209
  // caller awaiting the same cached rejection.
2158
- this._initializePromise = undefined
2210
+ if (this._initializePromise === initializePromise) {
2211
+ this._initializePromise = undefined
2212
+ }
2159
2213
  throw error
2160
2214
  }
2161
2215
 
2162
2216
  // If the inner IIFE returned without marking the configuration initialized
2163
2217
  // (e.g. because models were invalidated mid-bootstrap), clear the promise so
2164
2218
  // a later call retries a full bootstrap.
2165
- if (!this._isInitialized) {
2219
+ if (!this._isInitialized && this._initializePromise === initializePromise) {
2166
2220
  this._initializePromise = undefined
2167
2221
  }
2168
2222
  }
@@ -3072,11 +3126,11 @@ export default class VelociousConfiguration {
3072
3126
  * database configuration. No ambient tenant value is read during checkout or
3073
3127
  * execution.
3074
3128
  * @template T
3075
- * @param {{databaseConfiguration: import("./configuration-types.js").DatabaseConfigurationType, databaseIdentifier: string, name?: string, tenant?: object}} options - Captured operation options.
3129
+ * @param {{databaseConfiguration: import("./configuration-types.js").DatabaseConfigurationType, databaseIdentifier: string, name?: string, schemaGeneration?: string, tenant?: object}} options - Captured operation options.
3076
3130
  * @param {(operation: DatabaseOperation) => Promise<T>} callback - Operation callback.
3077
3131
  * @returns {Promise<T>} - Callback result.
3078
3132
  */
3079
- async withDatabaseOperation({databaseConfiguration, databaseIdentifier, name = "Configuration.withDatabaseOperation", tenant, ...restArgs}, callback) {
3133
+ async withDatabaseOperation({databaseConfiguration, databaseIdentifier, name = "Configuration.withDatabaseOperation", schemaGeneration, tenant, ...restArgs}, callback) {
3080
3134
  restArgsError(restArgs)
3081
3135
 
3082
3136
  if (!databaseIdentifier) throw new Error("Configuration.withDatabaseOperation requires a databaseIdentifier")
@@ -3095,6 +3149,7 @@ export default class VelociousConfiguration {
3095
3149
  databaseIdentifier,
3096
3150
  enforceCurrentTenantReuseKey: false,
3097
3151
  owner,
3152
+ schemaGeneration,
3098
3153
  tenant
3099
3154
  })
3100
3155
 
@@ -65,6 +65,7 @@
65
65
  * @property {boolean} [processListComment] - Whether to add process-list comments to the query.
66
66
  * @property {boolean} [retry] - Whether retryable errors may retry the query; defaults to true.
67
67
  * @property {boolean} [sessionTimeZone] - Whether to ensure the configured database session time zone before the query.
68
+ * @property {boolean} [sqliteScript] - Internal SQLite flag selecting native multi-statement script execution.
68
69
  * @property {AbortSignal} [signal] - Aborts the in-flight query (destroying its connection) when it fires.
69
70
  * @property {string} [sourceStack] - Stack captured at the caller boundary.
70
71
  * @property {symbol} [operationOwner] - Opaque owner for an operation-leased connection.
@@ -141,7 +142,8 @@ import TableData from "../table-data/index.js"
141
142
  import TableColumn from "../table-data/table-column.js"
142
143
  import TableForeignKey from "../table-data/table-foreign-key.js"
143
144
  import wait from "awaitery/build/wait.js"
144
- import {optionalPositiveInteger} from "typanic"
145
+ import { optionalPositiveInteger } from "typanic"
146
+ import { coordinateSharedTransactionConnection } from "../../testing/shared-transaction-connection-coordinator.js"
145
147
 
146
148
  /** Maximum characters inspected when building the debug SQL preview. */
147
149
  const SQL_PREVIEW_SCAN_LIMIT = 4096
@@ -193,6 +195,10 @@ export default class VelociousDatabaseDriversBase {
193
195
  * Narrows the runtime value to the documented type.
194
196
  * @type {TransactionCallbackFrame[]} */
195
197
  _transactionCallbackFrames
198
+ /** @type {Promise<void>} */
199
+ _transactionCompletionPromise
200
+ /** @type {(() => void) | undefined} */
201
+ _resolveTransactionCompletion
196
202
  /**
197
203
  * Narrows the runtime value to the documented type.
198
204
  * @type {Map<string, Promise<ReturnType<typeof JSON.parse>>>} */
@@ -229,6 +235,8 @@ export default class VelociousDatabaseDriversBase {
229
235
  this.logger = new Logger(this)
230
236
  this._transactionCallbackFrames = []
231
237
  this._transactionsCount = 0
238
+ this._transactionCompletionPromise = Promise.resolve()
239
+ this._resolveTransactionCompletion = undefined
232
240
  this._transactionsActionsMutex = new Mutex()
233
241
  this._schemaCache = new Map()
234
242
  }
@@ -1424,6 +1432,12 @@ export default class VelociousDatabaseDriversBase {
1424
1432
  */
1425
1433
  insideTransaction() { return this._transactionsCount > 0 }
1426
1434
 
1435
+ /**
1436
+ * Returns the completion promise identifying the current outer transaction.
1437
+ * @returns {Promise<void>} Resolves after that transaction commits or rolls back.
1438
+ */
1439
+ transactionCompletion() { return this._transactionCompletionPromise }
1440
+
1427
1441
  /**
1428
1442
  * Runs start transaction.
1429
1443
  * @param {Pick<QueryOptions, "operationOwner">} [options] - Transaction ownership.
@@ -1444,6 +1458,12 @@ export default class VelociousDatabaseDriversBase {
1444
1458
 
1445
1459
  await this._startTransactionAction(options)
1446
1460
  this._transactionsCount++
1461
+
1462
+ if (this._transactionsCount === 1) {
1463
+ this._transactionCompletionPromise = new Promise((resolve) => {
1464
+ this._resolveTransactionCompletion = resolve
1465
+ })
1466
+ }
1447
1467
  })
1448
1468
 
1449
1469
  if (!blockingOperationLease) return
@@ -1470,9 +1490,20 @@ export default class VelociousDatabaseDriversBase {
1470
1490
  await this._transactionsActionsMutex.sync(async () => {
1471
1491
  await this._commitTransactionAction(options)
1472
1492
  this._transactionsCount--
1493
+ this._resolveCompletedTransaction()
1473
1494
  })
1474
1495
  }
1475
1496
 
1497
+ /** Resolves the current outer transaction completion when it has finished. */
1498
+ _resolveCompletedTransaction() {
1499
+ if (this._transactionsCount !== 0) return
1500
+
1501
+ const resolve = this._resolveTransactionCompletion
1502
+
1503
+ this._resolveTransactionCompletion = undefined
1504
+ if (resolve) resolve()
1505
+ }
1506
+
1476
1507
  /**
1477
1508
  * Runs commit transaction action.
1478
1509
  * @param {Pick<QueryOptions, "operationOwner">} [options] - Transaction ownership.
@@ -1594,13 +1625,16 @@ export default class VelociousDatabaseDriversBase {
1594
1625
  async affectedRows(sql, options = {}) {
1595
1626
  await this._waitForOperationLease(options.operationOwner)
1596
1627
  this._assertWritableQuery(sql)
1597
- await this.beforeQuery(sql, options)
1598
1628
 
1599
- try {
1600
- return await this._affectedRowsActual(sql)
1601
- } finally {
1602
- await this.afterQuery(sql, options)
1603
- }
1629
+ return await coordinateSharedTransactionConnection(this, async () => {
1630
+ await this.beforeQuery(sql, options)
1631
+
1632
+ try {
1633
+ return await this._affectedRowsActual(sql)
1634
+ } finally {
1635
+ await this.afterQuery(sql, options)
1636
+ }
1637
+ }, options.operationOwner)
1604
1638
  }
1605
1639
 
1606
1640
  /**
@@ -1663,13 +1697,15 @@ export default class VelociousDatabaseDriversBase {
1663
1697
  * @returns {Promise<QueryResultType>} - Resolves with the query.
1664
1698
  */
1665
1699
  async _queryActualWithHooks(sql, options) {
1666
- await this.beforeQuery(sql, options)
1700
+ return await coordinateSharedTransactionConnection(this, async () => {
1701
+ await this.beforeQuery(sql, options)
1667
1702
 
1668
- try {
1669
- return await this._queryActual(sql, options)
1670
- } finally {
1671
- await this.afterQuery(sql, options)
1672
- }
1703
+ try {
1704
+ return await this._queryActual(sql, options)
1705
+ } finally {
1706
+ await this.afterQuery(sql, options)
1707
+ }
1708
+ }, options.operationOwner)
1673
1709
  }
1674
1710
 
1675
1711
  /**
@@ -2084,6 +2120,7 @@ export default class VelociousDatabaseDriversBase {
2084
2120
  await this._rollbackTransactionAction(options)
2085
2121
  } finally {
2086
2122
  this._transactionsCount--
2123
+ this._resolveCompletedTransaction()
2087
2124
 
2088
2125
  // A rolled-back transaction may have reverted DDL (e.g. a CREATE TABLE
2089
2126
  // run lazily inside the transaction), so any cached schema metadata is
@@ -2214,42 +2251,54 @@ export default class VelociousDatabaseDriversBase {
2214
2251
  await this.query(`ROLLBACK TO SAVEPOINT ${savePointName}`, options)
2215
2252
  }
2216
2253
 
2254
+ /**
2255
+ * Truncates the given table snapshot. Drivers can override this to issue one batch.
2256
+ * @protected
2257
+ * @param {Array<import("./base-table.js").default>} tables - Eligible tables for this cleanup attempt.
2258
+ * @returns {Promise<void>} - Resolves when every table has been cleaned.
2259
+ */
2260
+ async truncateTables(tables) {
2261
+ const truncateErrors = []
2262
+
2263
+ for (const table of tables) {
2264
+ try {
2265
+ await table.truncate({cascade: true})
2266
+ } catch (error) {
2267
+ truncateErrors.push(error)
2268
+ }
2269
+ }
2270
+
2271
+ if (truncateErrors.length > 0) throw truncateErrors[0]
2272
+ }
2273
+
2217
2274
  /**
2218
2275
  * Runs truncate all tables.
2219
2276
  * @returns {Promise<void>} - Resolves when complete.
2220
2277
  */
2221
2278
  async truncateAllTables() {
2222
2279
  this._assertNotReadOnly()
2223
- await this.withDisabledForeignKeys(async () => {
2224
- let tries = 0
2280
+ let tables = (await this.getTables()).filter((table) => table.getName() != "schema_migrations")
2225
2281
 
2226
- while(tries <= 5) {
2227
- tries++
2282
+ if (tables.length == 0) return
2228
2283
 
2229
- const tables = await this.getTables()
2230
- const truncateErrors = []
2284
+ await this.withDisabledForeignKeys(async () => {
2285
+ for (let tries = 1; tries <= 6; tries++) {
2286
+ try {
2287
+ await this.truncateTables(tables)
2288
+ return
2289
+ } catch (error) {
2290
+ console.error(error)
2231
2291
 
2232
- for (const table of tables) {
2233
- if (table.getName() != "schema_migrations") {
2234
- try {
2235
- await table.truncate({cascade: true})
2236
- } catch (error) {
2237
- console.error(error)
2238
- truncateErrors.push(error)
2239
- }
2240
- }
2241
- }
2292
+ if (tries == 6) throw error
2242
2293
 
2243
- if (truncateErrors.length == 0) {
2244
- break
2245
- } else if (tries <= 5) {
2246
2294
  // A truncate failed — the schema cache may still list a table that was
2247
2295
  // dropped out from under us (e.g. a db:rollback test that left the
2248
2296
  // shared DB rolled back). Clear it so the next pass re-reads the live
2249
2297
  // table list and no longer tries to truncate a table that is gone.
2250
2298
  this.clearSchemaCache()
2251
- } else {
2252
- throw truncateErrors[0]
2299
+ tables = (await this.getTables()).filter((table) => table.getName() != "schema_migrations")
2300
+
2301
+ if (tables.length == 0) return
2253
2302
  }
2254
2303
  }
2255
2304
  })
@@ -506,6 +506,38 @@ export default class VelociousDatabaseDriversMssql extends Base{
506
506
  })
507
507
  }
508
508
 
509
+ /**
510
+ * Truncates all eligible tables in one SQL Server request, retaining the
511
+ * recognized foreign-key fallback used by the per-table implementation.
512
+ * @param {Array<import("../base-table.js").default>} tables - Eligible tables.
513
+ * @returns {Promise<void>} - Resolves when the batch completes.
514
+ */
515
+ async truncateTables(tables) {
516
+ const statements = []
517
+
518
+ for (const table of tables) {
519
+ const quotedTable = this.quoteTable(table.getName())
520
+
521
+ statements.push(
522
+ "BEGIN TRY",
523
+ ` TRUNCATE TABLE ${quotedTable};`,
524
+ "END TRY",
525
+ "BEGIN CATCH",
526
+ " IF ERROR_NUMBER() = 4712",
527
+ " BEGIN",
528
+ ` DELETE FROM ${quotedTable};`,
529
+ " END",
530
+ " ELSE",
531
+ " BEGIN",
532
+ " THROW;",
533
+ " END",
534
+ "END CATCH;"
535
+ )
536
+ }
537
+
538
+ await this.query(statements.join("\n"))
539
+ }
540
+
509
541
  async lastInsertID(options = {}) {
510
542
  const result = await this.query("SELECT SCOPE_IDENTITY() AS last_insert_id", options)
511
543
  const lastInsertID = digg(result, 0, "last_insert_id")
@@ -486,6 +486,23 @@ export default class VelociousDatabaseDriversMysql extends Base{
486
486
  return true
487
487
  }
488
488
 
489
+ /**
490
+ * Uses one multi-statement request only when the existing connection option
491
+ * explicitly allows it; otherwise retains the base sequential behavior.
492
+ * @param {Array<import("../base-table.js").default>} tables - Eligible tables.
493
+ * @returns {Promise<void>} - Resolves when every table has been truncated.
494
+ */
495
+ async truncateTables(tables) {
496
+ if (!this.getArgs().multipleStatements) {
497
+ await super.truncateTables(tables)
498
+ return
499
+ }
500
+
501
+ const statements = tables.map((table) => `TRUNCATE TABLE ${this.quoteTable(table.getName())}`)
502
+
503
+ await this.query(statements.join(";\n"))
504
+ }
505
+
489
506
  /**
490
507
  * Runs query to sql.
491
508
  * @param {import("../../query/index.js").default} query - Query instance.
@@ -326,6 +326,17 @@ export default class VelociousDatabaseDriversPgsql extends Base{
326
326
  })
327
327
  }
328
328
 
329
+ /**
330
+ * Truncates all eligible tables in one PostgreSQL request.
331
+ * @param {Array<import("../base-table.js").default>} tables - Eligible tables.
332
+ * @returns {Promise<void>} - Resolves when the batch completes.
333
+ */
334
+ async truncateTables(tables) {
335
+ const quotedTables = tables.map((table) => this.quoteTable(table.getName()))
336
+
337
+ await this.query(`TRUNCATE TABLE ${quotedTables.join(", ")} CASCADE`)
338
+ }
339
+
329
340
  async lastInsertID(options = {}) {
330
341
  const result = await this.query("SELECT LASTVAL() AS last_insert_id", options)
331
342
 
@@ -161,6 +161,18 @@ export default class VelociousDatabaseDriversSqliteBase extends Base {
161
161
  })
162
162
  }
163
163
 
164
+ /**
165
+ * Deletes every eligible table through the platform driver's native SQLite
166
+ * script path so the whole cleanup is submitted as one request.
167
+ * @param {Array<import("../base-table.js").default>} tables - Eligible tables.
168
+ * @returns {Promise<void>} - Resolves when the script completes.
169
+ */
170
+ async truncateTables(tables) {
171
+ const statements = tables.map((table) => `DELETE FROM ${this.quoteTable(table.getName())}`)
172
+
173
+ await this.query(statements.join(";\n"), {sqliteScript: true})
174
+ }
175
+
164
176
  /**
165
177
  * Runs insert multiple.
166
178
  * @param {string} tableName - Table name.
@@ -89,14 +89,15 @@ export default class VelociousDatabaseDriversSqliteConnectionSqlJs {
89
89
  /**
90
90
  * Runs query.
91
91
  * @param {string} sql - SQL string.
92
+ * @param {{mutation?: boolean}} [options] - Internal query classification options.
92
93
  * @returns {Promise<Record<string, ReturnType<typeof JSON.parse>>[]>} - Resolves with the query.
93
94
  */
94
- async query(sql) {
95
+ async query(sql, {mutation = false} = {}) {
95
96
  const result = await queryWeb(this.connection, sql)
96
97
  const downcasedSQL = sql.toLowerCase().trim()
97
98
 
98
99
  // Auto-save database in local storage in case we can find manipulating instructions in the SQL
99
- if (downcasedSQL.startsWith("delete ") || downcasedSQL.startsWith("insert into ") || downcasedSQL.startsWith("update ")) {
100
+ if (mutation || downcasedSQL.startsWith("delete ") || downcasedSQL.startsWith("insert into ") || downcasedSQL.startsWith("update ")) {
100
101
  this.saveDatabaseDebounce()
101
102
  }
102
103
 
@@ -84,11 +84,17 @@ export default class VelociousDatabaseDriversSqliteNode extends Base {
84
84
  /**
85
85
  * Runs query actual.
86
86
  * @param {string} sql - SQL string.
87
+ * @param {import("../base.js").QueryOptions} [options] - Query options.
87
88
  * @returns {Promise<Record<string, ReturnType<typeof JSON.parse>>[]>} - Resolves with the query actual.
88
89
  */
89
- async _queryActual(sql) {
90
+ async _queryActual(sql, options = {}) {
90
91
  if (!this.connection) throw new Error("No connection")
91
92
 
93
+ if (options.sqliteScript) {
94
+ await this.connection.exec(sql)
95
+ return []
96
+ }
97
+
92
98
  return await query(this.connection, sql)
93
99
  }
94
100
 
@@ -82,13 +82,19 @@ export default class VelociousDatabaseDriversSqliteNative extends Base {
82
82
  /**
83
83
  * Runs query actual.
84
84
  * @param {string} sql - SQL string.
85
+ * @param {import("../base.js").QueryOptions} [options] - Query options.
85
86
  * @returns {Promise<Record<string, ReturnType<typeof JSON.parse>>[]>} - Query result rows.
86
87
  */
87
- async _queryActual(sql) {
88
- return await this._queryMutex.sync(() => {
88
+ async _queryActual(sql, options = {}) {
89
+ return await this._queryMutex.sync(async () => {
89
90
  if (!this.connection) throw new Error("Not connected yet")
90
91
 
91
- return query(this.connection, sql)
92
+ if (options.sqliteScript) {
93
+ await this.connection.execAsync(sql)
94
+ return []
95
+ }
96
+
97
+ return await query(this.connection, sql)
92
98
  })
93
99
  }
94
100
 
@@ -178,13 +178,16 @@ export default class VelociousDatabaseDriversSqliteWeb extends Base {
178
178
  /**
179
179
  * Runs query actual.
180
180
  * @param {string} sql - SQL string.
181
+ * @param {import("../base.js").QueryOptions} [options] - Query options.
181
182
  * @returns {Promise<Record<string, ReturnType<typeof JSON.parse>>[]>} - Resolves with the query actual.
182
183
  */
183
- async _queryActual(sql) {
184
- const result = await this.getConnection().query(sql)
184
+ async _queryActual(sql, options = {}) {
185
+ const connection = this.getConnection()
186
+ const result = connection instanceof ConnectionSqlJs
187
+ ? await connection.query(sql, {mutation: options.sqliteScript === true})
188
+ : await connection.query(sql)
185
189
 
186
190
  if (!Array.isArray(result)) {
187
- const connection = this.getConnection()
188
191
  const connectionName = connection?.constructor?.name || "UnknownConnection"
189
192
 
190
193
  throw new Error(`Sqlite web connection ${connectionName} returned a non-array result: ${typeof result}`)