velocious 1.0.551 → 1.0.554

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 (43) hide show
  1. package/build/background-jobs/json-socket.js +25 -0
  2. package/build/background-jobs/socket-request.js +48 -6
  3. package/build/background-jobs/status-reporter.js +29 -4
  4. package/build/background-jobs/worker.js +5 -1
  5. package/build/configuration-types.js +1 -0
  6. package/build/database/drivers/base.js +12 -0
  7. package/build/database/drivers/mssql/index.js +52 -3
  8. package/build/database/drivers/mysql/index.js +34 -0
  9. package/build/database/structure-sql-loader.js +12 -6
  10. package/build/src/background-jobs/json-socket.d.ts +20 -0
  11. package/build/src/background-jobs/json-socket.d.ts.map +1 -1
  12. package/build/src/background-jobs/json-socket.js +25 -1
  13. package/build/src/background-jobs/socket-request.d.ts +13 -1
  14. package/build/src/background-jobs/socket-request.d.ts.map +1 -1
  15. package/build/src/background-jobs/socket-request.js +44 -7
  16. package/build/src/background-jobs/status-reporter.d.ts +15 -2
  17. package/build/src/background-jobs/status-reporter.d.ts.map +1 -1
  18. package/build/src/background-jobs/status-reporter.js +29 -5
  19. package/build/src/background-jobs/worker.d.ts.map +1 -1
  20. package/build/src/background-jobs/worker.js +6 -2
  21. package/build/src/configuration-types.d.ts +5 -0
  22. package/build/src/configuration-types.d.ts.map +1 -1
  23. package/build/src/configuration-types.js +2 -1
  24. package/build/src/database/drivers/base.d.ts +9 -0
  25. package/build/src/database/drivers/base.d.ts.map +1 -1
  26. package/build/src/database/drivers/base.js +12 -1
  27. package/build/src/database/drivers/mssql/index.d.ts +9 -0
  28. package/build/src/database/drivers/mssql/index.d.ts.map +1 -1
  29. package/build/src/database/drivers/mssql/index.js +45 -2
  30. package/build/src/database/drivers/mysql/index.d.ts.map +1 -1
  31. package/build/src/database/drivers/mysql/index.js +35 -1
  32. package/build/src/database/structure-sql-loader.d.ts.map +1 -1
  33. package/build/src/database/structure-sql-loader.js +14 -8
  34. package/package.json +1 -1
  35. package/src/background-jobs/json-socket.js +25 -0
  36. package/src/background-jobs/socket-request.js +48 -6
  37. package/src/background-jobs/status-reporter.js +29 -4
  38. package/src/background-jobs/worker.js +5 -1
  39. package/src/configuration-types.js +1 -0
  40. package/src/database/drivers/base.js +12 -0
  41. package/src/database/drivers/mssql/index.js +52 -3
  42. package/src/database/drivers/mysql/index.js +34 -0
  43. package/src/database/structure-sql-loader.js +12 -6
@@ -23,6 +23,17 @@ import Upsert from "./sql/upsert.js"
23
23
  import Update from "./sql/update.js"
24
24
  import UUID from "pure-uuid"
25
25
 
26
+ /**
27
+ * SQL Server error number raised by `sp_releaseapplock` when the current
28
+ * session does not hold the requested application lock. Releasing a lock the
29
+ * session no longer holds is a normal race (a shared connection's final
30
+ * check-in may already have auto-released it), which the cross-driver
31
+ * `releaseAdvisoryLock` contract models by resolving to `false`. We translate
32
+ * this specific error into that result rather than letting it escape.
33
+ * @type {number}
34
+ */
35
+ const APPLOCK_NOT_HELD_ERROR_NUMBER = 1223
36
+
26
37
  export default class VelociousDatabaseDriversMssql extends Base{
27
38
  async connect() {
28
39
  const args = this.getArgs()
@@ -640,18 +651,56 @@ export default class VelociousDatabaseDriversMssql extends Base{
640
651
 
641
652
  /**
642
653
  * Runs release advisory lock.
654
+ *
655
+ * `sp_releaseapplock` returns 0 when the lock was released, but SQL Server
656
+ * raises error {@link APPLOCK_NOT_HELD_ERROR_NUMBER} instead of returning a
657
+ * failure code when the session does not currently hold the lock. That
658
+ * error aborts the batch before the trailing `SELECT` can run, so we catch
659
+ * it and resolve to `false` to honor the cross-driver contract for an
660
+ * already-unheld lock.
643
661
  * @param {string} name - Lock name.
644
662
  * @returns {Promise<boolean>} - True if the lock was held by this session and has now been released.
645
663
  */
646
664
  async _releaseAdvisoryLock(name) {
647
- const rows = await this.query(
648
- `DECLARE @velocious_advisory_lock_result INT; EXEC @velocious_advisory_lock_result = sp_releaseapplock @Resource = ${this.quote(name)}, @LockOwner = 'Session'; SELECT @velocious_advisory_lock_result AS velocious_advisory_lock_result`
649
- )
665
+ let rows
666
+
667
+ try {
668
+ rows = await this.query(
669
+ `DECLARE @velocious_advisory_lock_result INT; EXEC @velocious_advisory_lock_result = sp_releaseapplock @Resource = ${this.quote(name)}, @LockOwner = 'Session'; SELECT @velocious_advisory_lock_result AS velocious_advisory_lock_result`
670
+ )
671
+ } catch (error) {
672
+ if (this._isApplockNotHeldError(error)) return false
673
+
674
+ throw error
675
+ }
676
+
650
677
  const result = Number(rows?.[0]?.velocious_advisory_lock_result)
651
678
 
652
679
  return result === 0
653
680
  }
654
681
 
682
+ /**
683
+ * Detects the SQL Server "application lock is not currently held" error
684
+ * raised by `sp_releaseapplock`. It walks the wrapped-error cause chain
685
+ * because `query` re-wraps the driver's `RequestError` in a plain `Error`,
686
+ * and matches on the stable numeric error number rather than the message.
687
+ * @param {unknown} error - Error thrown while releasing the lock.
688
+ * @returns {boolean} - True if the error means the lock was not held by this session.
689
+ */
690
+ _isApplockNotHeldError(error) {
691
+ let current = error
692
+
693
+ while (current instanceof Error) {
694
+ const errorNumber = /** @type {{number?: unknown}} */ (current).number
695
+
696
+ if (typeof errorNumber === "number" && errorNumber === APPLOCK_NOT_HELD_ERROR_NUMBER) return true
697
+
698
+ current = current.cause
699
+ }
700
+
701
+ return false
702
+ }
703
+
655
704
  /**
656
705
  * Returns true if any session currently holds the application lock.
657
706
  *
@@ -190,6 +190,10 @@ export default class VelociousDatabaseDriversMysql extends Base{
190
190
 
191
191
  if ("username" in args) connectArgs["user"] = args["username"]
192
192
  if ("charset" in args) connectArgs["charset"] = args["charset"]
193
+ // Opt-in only. Lets a whole structure SQL dump run in one round-trip via
194
+ // {@link execStructureScript}; off by default so ordinary queries keep rejecting
195
+ // stacked statements.
196
+ if ("multipleStatements" in args) connectArgs["multipleStatements"] = Boolean(digg(args, "multipleStatements"))
193
197
 
194
198
  return connectArgs
195
199
  }
@@ -417,6 +421,36 @@ export default class VelociousDatabaseDriversMysql extends Base{
417
421
  })
418
422
  }
419
423
 
424
+ /**
425
+ * Executes a full multi-statement structure SQL script in one round-trip when the
426
+ * connection was configured with `multipleStatements: true`. Runs on the pooled
427
+ * connection so the caller's `SET FOREIGN_KEY_CHECKS = 0` applies. Returns false so
428
+ * the caller runs statements individually when multi-statement queries are off.
429
+ * @param {string} structureSql - Full multi-statement structure SQL.
430
+ * @returns {Promise<boolean>} - Whether the script was executed as one batch.
431
+ */
432
+ async execStructureScript(structureSql) {
433
+ if (!this.getArgs().multipleStatements) return false
434
+
435
+ // The batched pool call below bypasses Base#query, so re-run the same read-only
436
+ // write guard the per-statement path applies before executing the dump.
437
+ this._assertWritableQuery(structureSql)
438
+
439
+ if (!this.pool) await this.connect()
440
+ if (!this.pool) throw new Error("MySQL pool failed to initialize")
441
+
442
+ const pool = this.pool
443
+
444
+ await new Promise((resolve, reject) => {
445
+ pool.query(structureSql, (error) => {
446
+ if (error) reject(error)
447
+ else resolve(undefined)
448
+ })
449
+ })
450
+
451
+ return true
452
+ }
453
+
420
454
  /**
421
455
  * Runs query to sql.
422
456
  * @param {import("../../query/index.js").default} query - Query instance.
@@ -31,17 +31,23 @@ export default class StructureSqlLoader {
31
31
  await db.disableForeignKeys()
32
32
 
33
33
  try {
34
- if (executableConnection) {
35
- await executableConnection.exec(structureSql)
36
- } else {
37
- for (const statement of statements) {
38
- await db.query(statement)
34
+ // Prefer a single round-trip for the whole dump: a driver-native multi-statement
35
+ // batch (e.g. MySQL with `multipleStatements`) first, then a native `exec`
36
+ // connection (e.g. SQLite), and finally per-statement execution. Running the
37
+ // whole dump at once is far faster than issuing every CREATE separately.
38
+ if (!await db.execStructureScript(structureSql)) {
39
+ if (executableConnection) {
40
+ await executableConnection.exec(structureSql)
41
+ } else {
42
+ for (const statement of statements) {
43
+ await db.query(statement)
44
+ }
39
45
  }
40
46
  }
41
47
  } finally {
42
48
  await db.enableForeignKeys()
43
49
 
44
- // The native `exec` path mutates the schema outside `Base#query`, so the
50
+ // The batch / native `exec` paths mutate the schema outside `Base#query`, so the
45
51
  // usual post-DDL schema-cache invalidation never runs. Clear it here so a
46
52
  // caller that read schema metadata before provisioning (e.g. an empty table
47
53
  // list) does not keep seeing the pre-load schema afterwards. Harmless for the