velocious 1.0.591 → 1.0.593

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 (118) hide show
  1. package/README.md +9 -1
  2. package/build/configuration-types.js +1 -1
  3. package/build/database/drivers/base.js +53 -34
  4. package/build/database/drivers/mssql/index.js +32 -0
  5. package/build/database/drivers/mysql/index.js +17 -0
  6. package/build/database/drivers/pgsql/index.js +11 -0
  7. package/build/database/drivers/sqlite/base.js +12 -0
  8. package/build/database/drivers/sqlite/connection-sql-js.js +3 -2
  9. package/build/database/drivers/sqlite/index.js +7 -1
  10. package/build/database/drivers/sqlite/index.native.js +9 -3
  11. package/build/database/drivers/sqlite/index.web.js +6 -3
  12. package/build/environment-handlers/base.js +18 -0
  13. package/build/environment-handlers/node.js +32 -0
  14. package/build/src/configuration-types.d.ts +2 -2
  15. package/build/src/configuration-types.js +2 -2
  16. package/build/src/database/drivers/base.d.ts +11 -0
  17. package/build/src/database/drivers/base.d.ts.map +1 -1
  18. package/build/src/database/drivers/base.js +54 -37
  19. package/build/src/database/drivers/mssql/index.d.ts +7 -0
  20. package/build/src/database/drivers/mssql/index.d.ts.map +1 -1
  21. package/build/src/database/drivers/mssql/index.js +15 -1
  22. package/build/src/database/drivers/mysql/index.d.ts +7 -0
  23. package/build/src/database/drivers/mysql/index.d.ts.map +1 -1
  24. package/build/src/database/drivers/mysql/index.js +15 -1
  25. package/build/src/database/drivers/pgsql/index.d.ts +6 -0
  26. package/build/src/database/drivers/pgsql/index.d.ts.map +1 -1
  27. package/build/src/database/drivers/pgsql/index.js +10 -1
  28. package/build/src/database/drivers/sqlite/base.d.ts +7 -0
  29. package/build/src/database/drivers/sqlite/base.d.ts.map +1 -1
  30. package/build/src/database/drivers/sqlite/base.js +11 -1
  31. package/build/src/database/drivers/sqlite/connection-sql-js.d.ts +4 -1
  32. package/build/src/database/drivers/sqlite/connection-sql-js.d.ts.map +1 -1
  33. package/build/src/database/drivers/sqlite/connection-sql-js.js +4 -3
  34. package/build/src/database/drivers/sqlite/index.d.ts +2 -1
  35. package/build/src/database/drivers/sqlite/index.d.ts.map +1 -1
  36. package/build/src/database/drivers/sqlite/index.js +7 -2
  37. package/build/src/database/drivers/sqlite/index.native.d.ts +2 -1
  38. package/build/src/database/drivers/sqlite/index.native.d.ts.map +1 -1
  39. package/build/src/database/drivers/sqlite/index.native.js +9 -4
  40. package/build/src/database/drivers/sqlite/index.web.d.ts +2 -1
  41. package/build/src/database/drivers/sqlite/index.web.d.ts.map +1 -1
  42. package/build/src/database/drivers/sqlite/index.web.js +7 -4
  43. package/build/src/environment-handlers/base.d.ts +16 -0
  44. package/build/src/environment-handlers/base.d.ts.map +1 -1
  45. package/build/src/environment-handlers/base.js +17 -1
  46. package/build/src/environment-handlers/node.d.ts +20 -0
  47. package/build/src/environment-handlers/node.d.ts.map +1 -1
  48. package/build/src/environment-handlers/node.js +29 -1
  49. package/build/src/sync/conflict-strategy.d.ts.map +1 -1
  50. package/build/src/sync/conflict-strategy.js +7 -1
  51. package/build/src/sync/local-mutation-log.d.ts +10 -0
  52. package/build/src/sync/local-mutation-log.d.ts.map +1 -1
  53. package/build/src/sync/local-mutation-log.js +21 -1
  54. package/build/src/sync/sync-api-client-types.d.ts +15 -0
  55. package/build/src/sync/sync-api-client-types.d.ts.map +1 -1
  56. package/build/src/sync/sync-api-client-types.js +4 -1
  57. package/build/src/sync/sync-api-client.d.ts +91 -0
  58. package/build/src/sync/sync-api-client.d.ts.map +1 -1
  59. package/build/src/sync/sync-api-client.js +184 -1
  60. package/build/src/sync/sync-client-types.d.ts +57 -0
  61. package/build/src/sync/sync-client-types.d.ts.map +1 -1
  62. package/build/src/sync/sync-client-types.js +16 -1
  63. package/build/src/sync/sync-client.d.ts +56 -7
  64. package/build/src/sync/sync-client.d.ts.map +1 -1
  65. package/build/src/sync/sync-client.js +180 -15
  66. package/build/src/sync/sync-envelope-replay-service.d.ts +31 -2
  67. package/build/src/sync/sync-envelope-replay-service.d.ts.map +1 -1
  68. package/build/src/sync/sync-envelope-replay-service.js +98 -16
  69. package/build/src/sync/sync-model-change-feed-service.d.ts.map +1 -1
  70. package/build/src/sync/sync-model-change-feed-service.js +3 -4
  71. package/build/src/sync/sync-replay-persisted-data.d.ts +26 -0
  72. package/build/src/sync/sync-replay-persisted-data.d.ts.map +1 -0
  73. package/build/src/sync/sync-replay-persisted-data.js +43 -0
  74. package/build/src/testing/shared-transaction-broker.d.ts +14 -0
  75. package/build/src/testing/shared-transaction-broker.d.ts.map +1 -1
  76. package/build/src/testing/shared-transaction-broker.js +44 -7
  77. package/build/src/testing/shared-transaction-connection-coordinator.d.ts +5 -4
  78. package/build/src/testing/shared-transaction-connection-coordinator.d.ts.map +1 -1
  79. package/build/src/testing/shared-transaction-connection-coordinator.js +21 -10
  80. package/build/sync/conflict-strategy.js +3 -0
  81. package/build/sync/local-mutation-log.js +24 -0
  82. package/build/sync/sync-api-client-types.js +3 -0
  83. package/build/sync/sync-api-client.js +203 -0
  84. package/build/sync/sync-client-types.js +16 -0
  85. package/build/sync/sync-client.js +198 -14
  86. package/build/sync/sync-envelope-replay-service.js +109 -12
  87. package/build/sync/sync-model-change-feed-service.js +2 -4
  88. package/build/sync/sync-replay-persisted-data.js +50 -0
  89. package/build/testing/shared-transaction-broker.js +48 -7
  90. package/build/testing/shared-transaction-connection-coordinator.js +23 -9
  91. package/build/tsconfig.tsbuildinfo +1 -1
  92. package/package.json +1 -1
  93. package/scripts/browser-test-session.js +489 -0
  94. package/scripts/prewarm-chromedriver.js +3 -22
  95. package/scripts/test-browser.js +47 -15
  96. package/src/configuration-types.js +1 -1
  97. package/src/database/drivers/base.js +53 -34
  98. package/src/database/drivers/mssql/index.js +32 -0
  99. package/src/database/drivers/mysql/index.js +17 -0
  100. package/src/database/drivers/pgsql/index.js +11 -0
  101. package/src/database/drivers/sqlite/base.js +12 -0
  102. package/src/database/drivers/sqlite/connection-sql-js.js +3 -2
  103. package/src/database/drivers/sqlite/index.js +7 -1
  104. package/src/database/drivers/sqlite/index.native.js +9 -3
  105. package/src/database/drivers/sqlite/index.web.js +6 -3
  106. package/src/environment-handlers/base.js +18 -0
  107. package/src/environment-handlers/node.js +32 -0
  108. package/src/sync/conflict-strategy.js +3 -0
  109. package/src/sync/local-mutation-log.js +24 -0
  110. package/src/sync/sync-api-client-types.js +3 -0
  111. package/src/sync/sync-api-client.js +203 -0
  112. package/src/sync/sync-client-types.js +16 -0
  113. package/src/sync/sync-client.js +198 -14
  114. package/src/sync/sync-envelope-replay-service.js +109 -12
  115. package/src/sync/sync-model-change-feed-service.js +2 -4
  116. package/src/sync/sync-replay-persisted-data.js +50 -0
  117. package/src/testing/shared-transaction-broker.js +48 -7
  118. package/src/testing/shared-transaction-connection-coordinator.js +23 -9
package/README.md CHANGED
@@ -14,7 +14,7 @@
14
14
  * Controllers and views for HTTP endpoints
15
15
  * Frontend-model transport for creating, updating, querying, and subscribing to query-filtered lifecycle events over HTTP/WebSocket, with structured per-attribute validation error responses and one-budget WebSocket startup controls (see [docs/frontend-models.md](docs/frontend-models.md) and [docs/websocket-channels.md](docs/websocket-channels.md))
16
16
  * Client-side offline sync mutation logs and frontend-model optimistic queueing primitives (see the [shared-resource sync developer guide](docs/shared-resource-sync-guide.md) and [offline sync architecture](docs/offline-sync.md))
17
- * Declarative client sync scopes with per-scope cursors, automatic mutation tracking, realtime delivery whose per-recipient authorization hook receives the complete persisted sync envelope, and `sync`/`pull` progress reporting for "X of Y" import screens (see [docs/sync-client.md](docs/sync-client.md) and [docs/offline-sync.md](docs/offline-sync.md))
17
+ * Declarative client sync scopes with per-scope cursors, automatic mutation tracking, opt-in durable base-version conflict replay, realtime delivery whose per-recipient authorization hook receives the complete persisted sync envelope, and `sync`/`pull` progress reporting for "X of Y" import screens (see [docs/sync-client.md](docs/sync-client.md) and [docs/offline-sync.md](docs/offline-sync.md))
18
18
  * Reactive `useLiveQuery(Model.where(...))` queries that stay current from committed local model changes across local writes, pulls, and realtime (see [docs/live-queries.md](docs/live-queries.md))
19
19
  * Server-side sync envelope replay orchestration for app-owned sync receivers, including allowlisted authoritative values for conflict resolution (see [docs/sync-envelope-replay-service.md](docs/sync-envelope-replay-service.md))
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))
@@ -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
 
@@ -2070,6 +2071,13 @@ If you are developing on Velocious, you can run the tests with:
2070
2071
 
2071
2072
  Tests default to a 60-second timeout. Override per test with `{timeoutSeconds: 5}` or set a suite-wide default via `configureTests({defaultTimeoutSeconds: 30})`.
2072
2073
 
2074
+ Truncation-based test cleanup batches eligible tables into one request on PostgreSQL,
2075
+ SQL Server, and SQLite while preserving each driver's existing identity behavior,
2076
+ foreign-key restoration, stale-schema retry, and SQL.js persistence guarantees.
2077
+ MySQL/MariaDB batching requires the database's existing `multipleStatements: true`
2078
+ option; the default configuration keeps sequential `TRUNCATE TABLE` requests. See
2079
+ [database cleanup guidance](docs/testing-guidelines.md#truncation-cleanup).
2080
+
2073
2081
  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
2082
 
2075
2083
  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.
@@ -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
@@ -1594,13 +1596,16 @@ export default class VelociousDatabaseDriversBase {
1594
1596
  async affectedRows(sql, options = {}) {
1595
1597
  await this._waitForOperationLease(options.operationOwner)
1596
1598
  this._assertWritableQuery(sql)
1597
- await this.beforeQuery(sql, options)
1598
1599
 
1599
- try {
1600
- return await this._affectedRowsActual(sql)
1601
- } finally {
1602
- await this.afterQuery(sql, options)
1603
- }
1600
+ return await coordinateSharedTransactionConnection(this, async () => {
1601
+ await this.beforeQuery(sql, options)
1602
+
1603
+ try {
1604
+ return await this._affectedRowsActual(sql)
1605
+ } finally {
1606
+ await this.afterQuery(sql, options)
1607
+ }
1608
+ }, options.operationOwner)
1604
1609
  }
1605
1610
 
1606
1611
  /**
@@ -1663,13 +1668,15 @@ export default class VelociousDatabaseDriversBase {
1663
1668
  * @returns {Promise<QueryResultType>} - Resolves with the query.
1664
1669
  */
1665
1670
  async _queryActualWithHooks(sql, options) {
1666
- await this.beforeQuery(sql, options)
1671
+ return await coordinateSharedTransactionConnection(this, async () => {
1672
+ await this.beforeQuery(sql, options)
1667
1673
 
1668
- try {
1669
- return await this._queryActual(sql, options)
1670
- } finally {
1671
- await this.afterQuery(sql, options)
1672
- }
1674
+ try {
1675
+ return await this._queryActual(sql, options)
1676
+ } finally {
1677
+ await this.afterQuery(sql, options)
1678
+ }
1679
+ }, options.operationOwner)
1673
1680
  }
1674
1681
 
1675
1682
  /**
@@ -2214,42 +2221,54 @@ export default class VelociousDatabaseDriversBase {
2214
2221
  await this.query(`ROLLBACK TO SAVEPOINT ${savePointName}`, options)
2215
2222
  }
2216
2223
 
2224
+ /**
2225
+ * Truncates the given table snapshot. Drivers can override this to issue one batch.
2226
+ * @protected
2227
+ * @param {Array<import("./base-table.js").default>} tables - Eligible tables for this cleanup attempt.
2228
+ * @returns {Promise<void>} - Resolves when every table has been cleaned.
2229
+ */
2230
+ async truncateTables(tables) {
2231
+ const truncateErrors = []
2232
+
2233
+ for (const table of tables) {
2234
+ try {
2235
+ await table.truncate({cascade: true})
2236
+ } catch (error) {
2237
+ truncateErrors.push(error)
2238
+ }
2239
+ }
2240
+
2241
+ if (truncateErrors.length > 0) throw truncateErrors[0]
2242
+ }
2243
+
2217
2244
  /**
2218
2245
  * Runs truncate all tables.
2219
2246
  * @returns {Promise<void>} - Resolves when complete.
2220
2247
  */
2221
2248
  async truncateAllTables() {
2222
2249
  this._assertNotReadOnly()
2223
- await this.withDisabledForeignKeys(async () => {
2224
- let tries = 0
2250
+ let tables = (await this.getTables()).filter((table) => table.getName() != "schema_migrations")
2225
2251
 
2226
- while(tries <= 5) {
2227
- tries++
2252
+ if (tables.length == 0) return
2228
2253
 
2229
- const tables = await this.getTables()
2230
- const truncateErrors = []
2254
+ await this.withDisabledForeignKeys(async () => {
2255
+ for (let tries = 1; tries <= 6; tries++) {
2256
+ try {
2257
+ await this.truncateTables(tables)
2258
+ return
2259
+ } catch (error) {
2260
+ console.error(error)
2231
2261
 
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
- }
2262
+ if (tries == 6) throw error
2242
2263
 
2243
- if (truncateErrors.length == 0) {
2244
- break
2245
- } else if (tries <= 5) {
2246
2264
  // A truncate failed — the schema cache may still list a table that was
2247
2265
  // dropped out from under us (e.g. a db:rollback test that left the
2248
2266
  // shared DB rolled back). Clear it so the next pass re-reads the live
2249
2267
  // table list and no longer tries to truncate a table that is gone.
2250
2268
  this.clearSchemaCache()
2251
- } else {
2252
- throw truncateErrors[0]
2269
+ tables = (await this.getTables()).filter((table) => table.getName() != "schema_migrations")
2270
+
2271
+ if (tables.length == 0) return
2253
2272
  }
2254
2273
  }
2255
2274
  })
@@ -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}`)
@@ -27,6 +27,24 @@ export default class VelociousEnvironmentHandlerBase {
27
27
  */
28
28
  async createTestSharedTransactionConnection(_args) { return undefined }
29
29
 
30
+ /**
31
+ * Gets the active shared-transaction coordinator owner for a connection.
32
+ * Browser runtimes never install the Node test broker.
33
+ * @param {object} _connection - Parent physical connection.
34
+ * @returns {symbol | undefined} - Active coordinator owner.
35
+ */
36
+ getSharedTransactionCoordinatorOwner(_connection) { return undefined }
37
+
38
+ /**
39
+ * Runs work as the current shared-transaction coordinator owner.
40
+ * @template T
41
+ * @param {object} _connection - Parent physical connection.
42
+ * @param {symbol} _owner - Coordinator owner.
43
+ * @param {() => T} callback - Owned work.
44
+ * @returns {T} - Callback result.
45
+ */
46
+ runWithSharedTransactionCoordinatorOwner(_connection, _owner, callback) { return callback() }
47
+
30
48
  /**
31
49
  * Mutable ambient tenant used by runtimes without async-context storage.
32
50
  * @type {ReturnType<typeof JSON.parse> | undefined}
@@ -80,6 +80,38 @@ export default class VelociousEnvironmentHandlerNode extends Base{
80
80
  * @type {import("node:async_hooks").AsyncLocalStorage<TimezoneStore> | undefined} */
81
81
  _timezoneAsyncLocalStorage = NodeAsyncLocalStorage ? new NodeAsyncLocalStorage() : undefined
82
82
 
83
+ /**
84
+ * Shared-transaction coordinator ownership by physical connection.
85
+ * @type {import("node:async_hooks").AsyncLocalStorage<Map<object, symbol>> | undefined}
86
+ */
87
+ _sharedTransactionCoordinatorAsyncLocalStorage = NodeAsyncLocalStorage ? new NodeAsyncLocalStorage() : undefined
88
+
89
+ /**
90
+ * Gets the active shared-transaction coordinator owner for a connection.
91
+ * @param {object} connection - Parent physical connection.
92
+ * @returns {symbol | undefined} - Active coordinator owner.
93
+ */
94
+ getSharedTransactionCoordinatorOwner(connection) {
95
+ return this._sharedTransactionCoordinatorAsyncLocalStorage?.getStore()?.get(connection)
96
+ }
97
+
98
+ /**
99
+ * Runs work as the current shared-transaction coordinator owner.
100
+ * @template T
101
+ * @param {object} connection - Parent physical connection.
102
+ * @param {symbol} owner - Coordinator owner.
103
+ * @param {() => T} callback - Owned work.
104
+ * @returns {T} - Callback result.
105
+ */
106
+ runWithSharedTransactionCoordinatorOwner(connection, owner, callback) {
107
+ if (!this._sharedTransactionCoordinatorAsyncLocalStorage) return callback()
108
+
109
+ const owners = new Map(this._sharedTransactionCoordinatorAsyncLocalStorage.getStore())
110
+
111
+ owners.set(connection, owner)
112
+ return this._sharedTransactionCoordinatorAsyncLocalStorage.run(owners, callback)
113
+ }
114
+
83
115
  /**
84
116
  * Find commands result.
85
117
  * @type {import("./base.js").CommandFileObjectType[] | undefined} */
@@ -154,7 +154,7 @@ export type DatabaseConfigurationType = {
154
154
  */
155
155
  migrations?: boolean;
156
156
  /**
157
- * - (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.
157
+ * - (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.
158
158
  */
159
159
  multipleStatements?: boolean;
160
160
  /**
@@ -1660,7 +1660,7 @@ export type ConfigurationArgsType = {
1660
1660
  * @property {() => ReturnType<typeof JSON.parse>} [getConnection] - Custom connection factory override.
1661
1661
  * @property {string} [host] - Database host.
1662
1662
  * @property {boolean} [migrations] - Whether migrations are enabled for this database.
1663
- * @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.
1663
+ * @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.
1664
1664
  * @property {number} [maxRowsPerInsert] - Maximum rows per `INSERT ... VALUES (...), (...), ...` statement generated by `Record.insertMultiple`. Defaults to 500.
1665
1665
  * @property {number} [maxInsertSqlBytes] - Maximum serialized SQL size, in bytes, for a single `INSERT ... VALUES (...), (...), ...` statement. Defaults to 1 MiB (1048576).
1666
1666
  * @property {number} [maxInClauseValues] - Maximum values in a single `IN (...)` cohort used by preloads, association counts, and queryData aggregates. Defaults to 999.