velocious 1.0.612 → 1.0.614

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 (40) hide show
  1. package/README.md +2 -2
  2. package/build/background-jobs/pooled-runner-child.js +1 -0
  3. package/build/background-jobs/worker.js +65 -27
  4. package/build/database/drivers/base.js +19 -0
  5. package/build/database/drivers/mysql/index.js +12 -0
  6. package/build/database/drivers/mysql/sql/alter-table.js +14 -2
  7. package/build/database/drivers/pgsql/index.js +13 -0
  8. package/build/database/migration/change-table.js +324 -0
  9. package/build/database/migration/index.js +197 -13
  10. package/build/src/background-jobs/pooled-runner-child.js +3 -1
  11. package/build/src/background-jobs/worker.d.ts +23 -10
  12. package/build/src/background-jobs/worker.d.ts.map +1 -1
  13. package/build/src/background-jobs/worker.js +67 -29
  14. package/build/src/database/drivers/base.d.ts +13 -0
  15. package/build/src/database/drivers/base.d.ts.map +1 -1
  16. package/build/src/database/drivers/base.js +18 -1
  17. package/build/src/database/drivers/mysql/index.d.ts +10 -0
  18. package/build/src/database/drivers/mysql/index.d.ts.map +1 -1
  19. package/build/src/database/drivers/mysql/index.js +11 -1
  20. package/build/src/database/drivers/mysql/sql/alter-table.d.ts.map +1 -1
  21. package/build/src/database/drivers/mysql/sql/alter-table.js +14 -3
  22. package/build/src/database/drivers/pgsql/index.d.ts +11 -0
  23. package/build/src/database/drivers/pgsql/index.d.ts.map +1 -1
  24. package/build/src/database/drivers/pgsql/index.js +12 -1
  25. package/build/src/database/migration/change-table.d.ts +403 -0
  26. package/build/src/database/migration/change-table.d.ts.map +1 -0
  27. package/build/src/database/migration/change-table.js +286 -0
  28. package/build/src/database/migration/index.d.ts +46 -38
  29. package/build/src/database/migration/index.d.ts.map +1 -1
  30. package/build/src/database/migration/index.js +179 -14
  31. package/build/tsconfig.tsbuildinfo +1 -1
  32. package/package.json +1 -1
  33. package/src/background-jobs/pooled-runner-child.js +1 -0
  34. package/src/background-jobs/worker.js +65 -27
  35. package/src/database/drivers/base.js +19 -0
  36. package/src/database/drivers/mysql/index.js +12 -0
  37. package/src/database/drivers/mysql/sql/alter-table.js +14 -2
  38. package/src/database/drivers/pgsql/index.js +13 -0
  39. package/src/database/migration/change-table.js +324 -0
  40. package/src/database/migration/index.js +197 -13
package/README.md CHANGED
@@ -7,7 +7,7 @@
7
7
  * Connection-scoped advisory locks with automatic cleanup before pooled connections are reused or closed (see [docs/advisory-locks.md](docs/advisory-locks.md))
8
8
  * Built-in record auditing for model lifecycle changes (see [docs/auditing.md](docs/auditing.md))
9
9
  * Declarative state machines for models, with typed event methods generated into the base model (see [docs/state-machine.md](docs/state-machine.md))
10
- * Migrations for schema changes and UTC datetime storage (see [docs/database-migrations.md](docs/database-migrations.md))
10
+ * Migrations for schema changes and UTC datetime storage, including recorded `changeTable` batches that combine operations into one `ALTER` on bulk-capable drivers (see [docs/database-migrations.md](docs/database-migrations.md) and [docs/change-table.md](docs/change-table.md))
11
11
  * Tenant-selected base-model and structure generation with one immutable, fail-closed physical database context; tenant-only model metadata initializes only after that context is active (see [docs/tenant-selected-database-generation.md](docs/tenant-selected-database-generation.md))
12
12
  * Read-only tenant migration deploy preflight with stable JSON output and fail-closed ledger reads (see [docs/tenant-migration-deploy-preflight.md](docs/tenant-migration-deploy-preflight.md))
13
13
  * External packages (engines) that contribute data models, frontend-model resources and migrations to a consuming app (see [docs/packages.md](docs/packages.md))
@@ -2396,7 +2396,7 @@ VELOCIOUS_BACKGROUND_JOBS_JOB_TIMEOUT_MS=5400000
2396
2396
 
2397
2397
  `maxConcurrentInlineJobs` (default: `4`) caps how many `executionMode: "inline"` jobs a single `background-jobs-worker` process runs in parallel. Concurrency is at the JS event-loop level: every job in flight shares the worker's process and DB connection pool, so the cap should fit the pool, not the CPU count. Forking remains the right tool when you need memory isolation across long-running jobs or want to use more cores; select it with `executionMode: "forked"`.
2398
2398
 
2399
- New jobs default to `executionMode: "pooled"`: a worker runs them in warm, reusable Node child runners. `pooledRunnerCount` (default: `4`) bounds this independent per-worker pool, and `pooledRunnerConcurrency` (default: `1`) sets how many jobs each child runs at once on its own event loop, so total pooled capacity is `pooledRunnerCount × pooledRunnerConcurrency` — raise concurrency for I/O-bound jobs to get high throughput from a bounded, isolated set of processes. Workers advertise that exact free-slot count and the main consumes one slot per durable handoff, allowing one readiness notification to fill the pool. `pooledRunnerCount`, `pooledRunnerConcurrency`, and `pooledRunnerMaxJobs` must be finite positive integers; the RSS and lifetime limits must be finite positive numbers. A child is recycled after an acknowledged job when it reaches `pooledRunnerMaxJobs` (default: `100`), `pooledRunnerMaxRssBytes` (default: `536870912`, or 512 MiB), or `pooledRunnerMaxLifetimeMs` (default: `3600000`, or one hour). `execution_mode` is the single source of truth for a job's runtime — pooled rows persist as `execution_mode = "pooled"` directly. See [execution modes and pooled runners](docs/background-jobs.md#execution-modes-and-pooled-runners).
2399
+ New jobs default to `executionMode: "pooled"`: a worker runs them in warm, reusable Node child runners. `pooledRunnerCount` (default: `4`) bounds this independent per-worker pool, and `pooledRunnerConcurrency` (default: `1`) sets how many jobs each child runs at once on its own event loop, so total pooled capacity is `pooledRunnerCount × pooledRunnerConcurrency` — raise concurrency for I/O-bound jobs to get high throughput from a bounded, isolated set of processes. Workers advertise that exact free-slot count and the main consumes one slot per durable handoff, allowing one readiness notification to fill the pool. If an initialized child exits unexpectedly, the worker immediately advertises the freed capacity while failure reports retry; the replacement is spawned lazily by the next dispatch, and a pre-startup crash does not trigger a respawn loop. `pooledRunnerCount`, `pooledRunnerConcurrency`, and `pooledRunnerMaxJobs` must be finite positive integers; the RSS and lifetime limits must be finite positive numbers. A child is recycled after an acknowledged job when it reaches `pooledRunnerMaxJobs` (default: `100`), `pooledRunnerMaxRssBytes` (default: `536870912`, or 512 MiB), or `pooledRunnerMaxLifetimeMs` (default: `3600000`, or one hour). `execution_mode` is the single source of truth for a job's runtime — pooled rows persist as `execution_mode = "pooled"` directly. See [execution modes and pooled runners](docs/background-jobs.md#execution-modes-and-pooled-runners).
2400
2400
 
2401
2401
  Cold pooled jobs share one atomic model-bootstrap phase. If that phase fails, all
2402
2402
  waiting jobs receive the failure; a later job in the surviving child cannot run
@@ -141,3 +141,4 @@ function handleMessage(message) {
141
141
  process.on("message", (message) => handleMessage(message))
142
142
  process.once("disconnect", () => void shutdownRunner(0))
143
143
  for (const signal of ["SIGTERM", "SIGINT"]) process.once(signal, () => void shutdownRunner(1))
144
+ if (process.send) process.send({type: "ready"})
@@ -194,8 +194,10 @@ export default class BackgroundJobsWorker {
194
194
  this.inflightPooledJobs = new Set()
195
195
  /** @type {Set<import("node:child_process").ChildProcess>} */
196
196
  this.pooledChildren = new Set()
197
- /** @type {Map<import("node:child_process").ChildProcess, {createdAtMs: number, jobsRun: number, inflight: Map<string, {payload: import("./types.js").BackgroundJobPayload & {id: string}, resolve?: (value: void) => void, timeoutTimer?: ReturnType<typeof setTimeout> | null}>, lastDispatchSeq: number, retiring: boolean, settling?: boolean, timeoutSigkillTimer?: ReturnType<typeof setTimeout> | null}>} */
197
+ /** @type {Map<import("node:child_process").ChildProcess, {createdAtMs: number, jobsRun: number, inflight: Map<string, {payload: import("./types.js").BackgroundJobPayload & {id: string}, resolve?: (value: void) => void, pooledJob?: Promise<void>, timeoutTimer?: ReturnType<typeof setTimeout> | null}>, lastDispatchSeq: number, retiring: boolean, started?: boolean, settling?: boolean, timeoutSigkillTimer?: ReturnType<typeof setTimeout> | null}>} */
198
198
  this.pooledChildStates = new Map()
199
+ /** @type {WeakSet<Promise<void>>} */
200
+ this._pooledStartupFailureJobs = new WeakSet()
199
201
  // Monotonic dispatch counter for round-robin child selection: each dispatch stamps
200
202
  // the chosen child, and selection prefers the child dispatched least recently.
201
203
  this._pooledDispatchSeq = 0
@@ -600,16 +602,16 @@ export default class BackgroundJobsWorker {
600
602
  }
601
603
 
602
604
  /**
603
- * Tells main we're ready for the next job — but only if we haven't been
604
- * asked to drain. Once we've sent `draining` we don't want to take more
605
- * work.
605
+ * Advertises current worker capacity unless the worker is draining.
606
+ * @param {object} [options] - Advertisement options.
607
+ * @param {boolean} [options.revokePooledAdmission] - Revoke pooled credits while preserving other execution modes.
606
608
  * @returns {void}
607
609
  */
608
- _sendReadyIfRunning() {
610
+ _sendReadyIfRunning({revokePooledAdmission = false} = {}) {
609
611
  if (this.shouldStop) return
610
612
  if (!this.jsonSocket) return
611
613
 
612
- const readyMessage = this._readyMessage()
614
+ const readyMessage = this._readyMessage({revokePooledAdmission})
613
615
 
614
616
  if (!readyMessage) return
615
617
  this.jsonSocket.send(readyMessage)
@@ -617,21 +619,24 @@ export default class BackgroundJobsWorker {
617
619
 
618
620
  /**
619
621
  * Runs ready message.
622
+ * @param {object} [options] - Advertisement options.
623
+ * @param {boolean} [options.revokePooledAdmission] - Revoke pooled credits while preserving other execution modes.
620
624
  * @returns {import("./types.js").BackgroundJobSocketMessage | null} - Ready message or null when the worker has no capacity.
621
625
  */
622
- _readyMessage() {
626
+ _readyMessage({revokePooledAdmission = false} = {}) {
623
627
  const acceptsProcessJob = this.inflightProcessJobs.size < this.maxConcurrentForkedJobs
624
628
  const acceptsInline = this.inflightInlineJobs.size < this.maxConcurrentInlineJobs
625
- const acceptsPooled = this._availablePooledSlots() > 0
629
+ const availablePooledSlots = revokePooledAdmission ? 0 : this._availablePooledSlots()
630
+ const acceptsPooled = availablePooledSlots > 0
626
631
 
627
- if (!acceptsProcessJob && !acceptsInline && !acceptsPooled) return null
632
+ if (!revokePooledAdmission && !acceptsProcessJob && !acceptsInline && !acceptsPooled) return null
628
633
 
629
634
  return {
630
635
  type: "ready",
631
636
  acceptsForked: acceptsProcessJob,
632
637
  acceptsInline,
633
638
  acceptsPooled,
634
- availablePooledSlots: this._availablePooledSlots(),
639
+ availablePooledSlots,
635
640
  acceptsSpawned: acceptsProcessJob
636
641
  }
637
642
  }
@@ -646,7 +651,7 @@ export default class BackgroundJobsWorker {
646
651
  let inflight
647
652
  inflight = pooledJob.finally(() => {
648
653
  this.inflightPooledJobs.delete(inflight)
649
- if (!this.shouldStop) this._sendReadyIfRunning()
654
+ if (!this.shouldStop && !this._pooledStartupFailureJobs.has(pooledJob)) this._sendReadyIfRunning()
650
655
  })
651
656
  this.inflightPooledJobs.add(inflight)
652
657
  }
@@ -689,16 +694,22 @@ export default class BackgroundJobsWorker {
689
694
  // Stamp the round-robin cursor so the next dispatch prefers a different child.
690
695
  state.lastDispatchSeq = ++this._pooledDispatchSeq
691
696
 
692
- return new Promise((resolve) => {
693
- const timeoutTimer = this._armPooledJobTimeout({child, payload})
697
+ /**
698
+ * Resolves the pooled job promise.
699
+ * @type {(value: void) => void}
700
+ */
701
+ let resolvePooledJob = () => {}
702
+ const pooledJob = new Promise((resolve) => { resolvePooledJob = resolve })
703
+ const timeoutTimer = this._armPooledJobTimeout({child, payload})
694
704
 
695
- state.inflight.set(payload.id, {payload, resolve, timeoutTimer})
696
- try {
697
- child.send({type: "job", payload, sharedTransactionBroker: this._pooledJobSharedTransactionBrokerConfig()})
698
- } catch (error) {
699
- void this._handlePooledChildFailure({child, error})
700
- }
701
- })
705
+ state.inflight.set(payload.id, {payload, resolve: resolvePooledJob, pooledJob, timeoutTimer})
706
+ try {
707
+ child.send({type: "job", payload, sharedTransactionBroker: this._pooledJobSharedTransactionBrokerConfig()})
708
+ } catch (error) {
709
+ void this._handlePooledChildFailure({child, error})
710
+ }
711
+
712
+ return pooledJob
702
713
  }
703
714
 
704
715
  /**
@@ -769,7 +780,8 @@ export default class BackgroundJobsWorker {
769
780
  * (SIGTERM, then SIGKILL after the grace) — a hung JS job cannot be cancelled
770
781
  * any other way. The non-clean exit flows through `_handlePooledChildFailure`,
771
782
  * which reports every in-flight job on the child failed (so they requeue) and
772
- * drops it from tracking; capacity is refilled on the next dispatch.
783
+ * drops it from tracking; the failure path immediately re-advertises the
784
+ * resulting capacity once the runner has completed startup.
773
785
  * @param {object} args - Options.
774
786
  * @param {import("node:child_process").ChildProcess} args.child - Pooled child.
775
787
  * @param {string} args.jobId - Job id that overran.
@@ -810,7 +822,7 @@ export default class BackgroundJobsWorker {
810
822
  })
811
823
  this.pooledChildren.add(child)
812
824
  this.inflightProcessChildren.add(child)
813
- this.pooledChildStates.set(child, {createdAtMs: Date.now(), jobsRun: 0, inflight: new Map(), lastDispatchSeq: 0, retiring: false})
825
+ this.pooledChildStates.set(child, {createdAtMs: Date.now(), jobsRun: 0, inflight: new Map(), lastDispatchSeq: 0, retiring: false, started: false})
814
826
  child.on("message", (message) => this._handlePooledChildMessage({child, message}))
815
827
  child.once("exit", (code, signal) => this._handlePooledChildFailure({child, error: new Error(`Pooled background job runner exited: code=${code} signal=${signal || "none"}`)}))
816
828
  child.once("error", (error) => this._handlePooledChildFailure({child, error}))
@@ -829,7 +841,12 @@ export default class BackgroundJobsWorker {
829
841
  if (!message || typeof message !== "object") return
830
842
  const record = /** @type {{type?: ReturnType<typeof JSON.parse>, jobId?: ReturnType<typeof JSON.parse>, acknowledged?: ReturnType<typeof JSON.parse>, rssBytes?: ReturnType<typeof JSON.parse>, error?: ReturnType<typeof JSON.parse>}} */ (message)
831
843
  const state = this.pooledChildStates.get(child)
844
+ if (record.type === "ready") {
845
+ if (state) state.started = true
846
+ return
847
+ }
832
848
  if (record.type !== "job-outcome" || !state || state.settling || typeof record.jobId !== "string") return
849
+ state.started = true
833
850
  const entry = state.inflight.get(record.jobId)
834
851
  if (!entry) return
835
852
 
@@ -907,9 +924,11 @@ export default class BackgroundJobsWorker {
907
924
  /**
908
925
  * Removes an exited/unhealthy pooled child and reports every job that was
909
926
  * in-flight on it as failed — a process-level crash's blast radius is the
910
- * child's whole in-flight set. Capacity is refilled lazily on the next
911
- * dispatch (a spawnable slot is still advertised), avoiding a tight respawn
912
- * loop when a child crashes on startup.
927
+ * child's whole in-flight set. Once the child has completed startup, its
928
+ * freed capacity is advertised immediately; the replacement itself is still
929
+ * spawned lazily by the next dispatch. A child that exits before its startup
930
+ * handshake does not re-announce, avoiding a tight respawn loop on startup
931
+ * failure.
913
932
  * @param {object} args - Failure details.
914
933
  * @param {import("node:child_process").ChildProcess} args.child - Pooled child.
915
934
  * @param {ReturnType<typeof JSON.parse>} args.error - Failure.
@@ -934,7 +953,7 @@ export default class BackgroundJobsWorker {
934
953
  if (state) state.inflight.clear()
935
954
  this.pooledChildStates.delete(child)
936
955
 
937
- await Promise.allSettled(entries.map(async (entry) => {
956
+ const failureReports = entries.map(async (entry) => {
938
957
  await this._reportJobResult({
939
958
  jobId: entry.payload.id,
940
959
  status: "failed",
@@ -944,7 +963,26 @@ export default class BackgroundJobsWorker {
944
963
  workerId: entry.payload.workerId || this.workerId
945
964
  })
946
965
  if (entry.resolve) entry.resolve(undefined)
947
- }))
966
+ })
967
+
968
+ // Start every fallback report before announcing capacity so the main cannot
969
+ // observe a replacement slot before the failed jobs' reports are in flight.
970
+ // The report promises remain tracked below; a slow retry must not hold the
971
+ // newly freed runner capacity hostage.
972
+ if (state && state.started !== false) {
973
+ this._sendReadyIfRunning()
974
+ } else if (state) {
975
+ for (const entry of entries) {
976
+ if (entry.pooledJob) this._pooledStartupFailureJobs.add(entry.pooledJob)
977
+ }
978
+ // A previous ready message may still have unconsumed pooled credits at the
979
+ // main. Revoke them authoritatively without suppressing valid inline or
980
+ // process-runner readiness; otherwise queued jobs can trigger a startup
981
+ // crash loop using the stale credits.
982
+ this._sendReadyIfRunning({revokePooledAdmission: true})
983
+ }
984
+
985
+ await Promise.allSettled(failureReports)
948
986
  }
949
987
 
950
988
  /**
@@ -908,6 +908,25 @@ export default class VelociousDatabaseDriversBase {
908
908
  throw new Error("'type' not implemented")
909
909
  }
910
910
 
911
+ /**
912
+ * Whether this driver can combine unrelated alter-table operations into a
913
+ * single `ALTER TABLE` statement (Rails' `supports_bulk_alter`).
914
+ * @returns {boolean} - Whether bulk alter is supported.
915
+ */
916
+ supportsBulkAlter() {
917
+ return false
918
+ }
919
+
920
+ /**
921
+ * Whether a bulk `ALTER TABLE` statement can also carry `ADD INDEX` clauses.
922
+ * Only drivers that support this keep index adds inside the combined batch;
923
+ * the rest execute each index as its own statement.
924
+ * @returns {boolean} - Whether indexes can be added inside a bulk alter.
925
+ */
926
+ supportsBulkAlterIndexes() {
927
+ return false
928
+ }
929
+
911
930
  /**
912
931
  * Runs insert.
913
932
  * @param {InsertSqlArgsType} args - Options object.
@@ -345,6 +345,18 @@ export default class VelociousDatabaseDriversMysql extends Base{
345
345
  */
346
346
  getType() { return "mysql" }
347
347
 
348
+ /**
349
+ * Whether this driver supports combining operations into one bulk `ALTER`.
350
+ * @returns {boolean} - Whether bulk alter is supported.
351
+ */
352
+ supportsBulkAlter() { return true }
353
+
354
+ /**
355
+ * Whether the bulk `ALTER` can also carry `ADD INDEX` clauses.
356
+ * @returns {boolean} - Whether indexes can be added inside a bulk alter.
357
+ */
358
+ supportsBulkAlterIndexes() { return true }
359
+
348
360
  /**
349
361
  * Runs retryable database error.
350
362
  * @param {Error} error - Error instance.
@@ -26,13 +26,25 @@ export default class VelociousDatabaseConnectionDriversMysqlSqlAlterTable extend
26
26
  const indexes = this.tableData.getIndexes()
27
27
 
28
28
  if (indexes.length === 0) return sqls
29
- if (sqls.length !== 1) throw new Error("Expected one MySQL ALTER TABLE statement when adding indexes")
29
+ if (sqls.length > 1) throw new Error("Expected one MySQL ALTER TABLE statement when adding indexes")
30
30
 
31
31
  const options = this.getOptions()
32
32
  let sql = sqls[0]
33
+ let needsIndexSeparator = true
34
+
35
+ if (sql === undefined) {
36
+ sql = `ALTER TABLE ${options.quoteTableName(this.tableData.getName())} `
37
+ needsIndexSeparator = false
38
+ }
33
39
 
34
40
  for (const index of indexes) {
35
- sql += ", ADD"
41
+ if (needsIndexSeparator) {
42
+ sql += ", "
43
+ } else {
44
+ needsIndexSeparator = true
45
+ }
46
+
47
+ sql += "ADD"
36
48
 
37
49
  if (index.getUnique()) sql += " UNIQUE"
38
50
 
@@ -203,6 +203,19 @@ export default class VelociousDatabaseDriversPgsql extends Base{
203
203
 
204
204
  getType() { return "pgsql" }
205
205
 
206
+ /**
207
+ * Whether this driver supports combining operations into one bulk `ALTER`.
208
+ * @returns {boolean} - Whether bulk alter is supported.
209
+ */
210
+ supportsBulkAlter() { return true }
211
+
212
+ /**
213
+ * Whether the bulk `ALTER` can also carry `ADD INDEX` clauses. PostgreSQL's
214
+ * `ALTER TABLE` cannot express index creation, so indexes stay standalone.
215
+ * @returns {boolean} - Whether indexes can be added inside a bulk alter.
216
+ */
217
+ supportsBulkAlterIndexes() { return false }
218
+
206
219
  /**
207
220
  * Runs query actual.
208
221
  * @param {string} sql - SQL string.
@@ -0,0 +1,324 @@
1
+ // @ts-check
2
+
3
+ /**
4
+ * ChangeTableAddIndexArgsType type.
5
+ * @typedef {object} ChangeTableAddIndexArgsType
6
+ * @property {boolean} [ifNotExists] - Skip creation if the index already exists.
7
+ * @property {string} [name] - Explicit index name to use.
8
+ * @property {boolean} [unique] - Whether the index should be unique.
9
+ */
10
+
11
+ /**
12
+ * ChangeTableRemoveIndexArgsType type.
13
+ * @typedef {object} ChangeTableRemoveIndexArgsType
14
+ * @property {string} [name] - Explicit index name to remove.
15
+ */
16
+
17
+ /**
18
+ * ChangeTableRemoveReferenceArgsType type.
19
+ * @typedef {object} ChangeTableRemoveReferenceArgsType
20
+ * @property {string} [columnName] - Override the derived reference column name.
21
+ * @property {string} [indexName] - Explicit generated index name to remove.
22
+ */
23
+
24
+ /**
25
+ * ChangeTableAddColumnOperationType type.
26
+ * @typedef {object} ChangeTableAddColumnOperationType
27
+ * @property {"addColumn"} type - Operation type.
28
+ * @property {string} columnName - Column name.
29
+ * @property {string} columnType - Column type.
30
+ * @property {import("../table-data/table-column.js").TableColumnArgsType | undefined} args - Column args.
31
+ */
32
+
33
+ /**
34
+ * ChangeTableRemoveColumnOperationType type.
35
+ * @typedef {object} ChangeTableRemoveColumnOperationType
36
+ * @property {"removeColumn"} type - Operation type.
37
+ * @property {string} columnName - Column name.
38
+ */
39
+
40
+ /**
41
+ * ChangeTableAddIndexOperationType type.
42
+ * @typedef {object} ChangeTableAddIndexOperationType
43
+ * @property {"addIndex"} type - Operation type.
44
+ * @property {Array<string | import("../table-data/table-column.js").default>} columns - Columns to index.
45
+ * @property {ChangeTableAddIndexArgsType | undefined} args - Index args.
46
+ */
47
+
48
+ /**
49
+ * ChangeTableRemoveIndexOperationType type.
50
+ * @typedef {object} ChangeTableRemoveIndexOperationType
51
+ * @property {"removeIndex"} type - Operation type.
52
+ * @property {string | Array<string | import("../table-data/table-column.js").default>} nameOrColumns - Index name or columns.
53
+ * @property {ChangeTableRemoveIndexArgsType | undefined} args - Index args.
54
+ */
55
+
56
+ /**
57
+ * ChangeTableAddReferenceOperationType type.
58
+ * @typedef {object} ChangeTableAddReferenceOperationType
59
+ * @property {"addReference"} type - Operation type.
60
+ * @property {string} referenceName - Reference name.
61
+ * @property {object | undefined} args - Reference args.
62
+ */
63
+
64
+ /**
65
+ * ChangeTableRemoveReferenceOperationType type.
66
+ * @typedef {object} ChangeTableRemoveReferenceOperationType
67
+ * @property {"removeReference"} type - Operation type.
68
+ * @property {string} referenceName - Reference name.
69
+ * @property {ChangeTableRemoveReferenceArgsType | undefined} args - Reference args.
70
+ */
71
+
72
+ /**
73
+ * ChangeTableRenameColumnOperationType type.
74
+ * @typedef {object} ChangeTableRenameColumnOperationType
75
+ * @property {"renameColumn"} type - Operation type.
76
+ * @property {string} oldColumnName - Previous column name.
77
+ * @property {string} newColumnName - New column name.
78
+ */
79
+
80
+ /**
81
+ * ChangeTableChangeColumnNullOperationType type.
82
+ * @typedef {object} ChangeTableChangeColumnNullOperationType
83
+ * @property {"changeColumnNull"} type - Operation type.
84
+ * @property {string} columnName - Column name.
85
+ * @property {boolean} nullable - Whether the column becomes nullable.
86
+ */
87
+
88
+ /**
89
+ * ChangeTableOperationType type.
90
+ * @typedef {ChangeTableAddColumnOperationType | ChangeTableRemoveColumnOperationType | ChangeTableAddIndexOperationType | ChangeTableRemoveIndexOperationType | ChangeTableAddReferenceOperationType | ChangeTableRemoveReferenceOperationType | ChangeTableRenameColumnOperationType | ChangeTableChangeColumnNullOperationType} ChangeTableOperationType
91
+ */
92
+
93
+ /**
94
+ * Table-scoped recorder used by `migration.changeTable`. Each call records a
95
+ * single DDL operation synchronously; `changeTable` replays them after the
96
+ * callback completes so a failed callback executes zero recorded DDL.
97
+ */
98
+ export default class VelociousDatabaseMigrationChangeTable {
99
+ /**
100
+ * Operations.
101
+ * @type {ChangeTableOperationType[]} */
102
+ _operations = []
103
+
104
+ /**
105
+ * Runs constructor.
106
+ * @param {object} args - Options object.
107
+ * @param {string} args.tableName - Table name.
108
+ */
109
+ constructor({tableName}) {
110
+ if (!tableName) throw new Error(`Invalid table name: ${tableName}`)
111
+
112
+ this._tableName = tableName
113
+ }
114
+
115
+ /**
116
+ * Runs get table name.
117
+ * @returns {string} - The table name.
118
+ */
119
+ getTableName() { return this._tableName }
120
+
121
+ /**
122
+ * Runs get operations.
123
+ * @returns {ChangeTableOperationType[]} - The recorded operations.
124
+ */
125
+ getOperations() { return this._operations }
126
+
127
+ /**
128
+ * Records a new column.
129
+ * @param {string} name - Column name.
130
+ * @param {string} type - Column type.
131
+ * @param {import("../table-data/table-column.js").TableColumnArgsType} [args] - Options object.
132
+ * @returns {void} - No return value.
133
+ */
134
+ column(name, type, args) {
135
+ this._operations.push({type: "addColumn", columnName: name, columnType: type, args})
136
+ }
137
+
138
+ /**
139
+ * Records a bigint column.
140
+ * @param {string} name - Column name.
141
+ * @param {import("../table-data/table-column.js").TableColumnArgsType} [args] - Options object.
142
+ * @returns {void} - No return value.
143
+ */
144
+ bigint(name, args) { this.column(name, "bigint", args) }
145
+
146
+ /**
147
+ * Records a blob column.
148
+ * @param {string} name - Column name.
149
+ * @param {import("../table-data/table-column.js").TableColumnArgsType} [args] - Options object.
150
+ * @returns {void} - No return value.
151
+ */
152
+ blob(name, args) { this.column(name, "blob", args) }
153
+
154
+ /**
155
+ * Records a boolean column.
156
+ * @param {string} name - Column name.
157
+ * @param {import("../table-data/table-column.js").TableColumnArgsType} [args] - Options object.
158
+ * @returns {void} - No return value.
159
+ */
160
+ boolean(name, args) { this.column(name, "boolean", args) }
161
+
162
+ /**
163
+ * Records a datetime column.
164
+ * @param {string} name - Column name.
165
+ * @param {import("../table-data/table-column.js").TableColumnArgsType} [args] - Options object.
166
+ * @returns {void} - No return value.
167
+ */
168
+ datetime(name, args) { this.column(name, "datetime", args) }
169
+
170
+ /**
171
+ * Records a decimal column.
172
+ * @param {string} name - Column name.
173
+ * @param {import("../table-data/table-column.js").TableColumnArgsType} [args] - Options object.
174
+ * @returns {void} - No return value.
175
+ */
176
+ decimal(name, args) { this.column(name, "decimal", args) }
177
+
178
+ /**
179
+ * Records an integer column.
180
+ * @param {string} name - Column name.
181
+ * @param {import("../table-data/table-column.js").TableColumnArgsType} [args] - Options object.
182
+ * @returns {void} - No return value.
183
+ */
184
+ integer(name, args) { this.column(name, "integer", args) }
185
+
186
+ /**
187
+ * Records a json column.
188
+ * @param {string} name - Column name.
189
+ * @param {import("../table-data/table-column.js").TableColumnArgsType} [args] - Options object.
190
+ * @returns {void} - No return value.
191
+ */
192
+ json(name, args) { this.column(name, "json", args) }
193
+
194
+ /**
195
+ * Records a string column.
196
+ * @param {string} name - Column name.
197
+ * @param {import("../table-data/table-column.js").TableColumnArgsType} [args] - Options object.
198
+ * @returns {void} - No return value.
199
+ */
200
+ string(name, args) { this.column(name, "string", args) }
201
+
202
+ /**
203
+ * Records a text column.
204
+ * @param {string} name - Column name.
205
+ * @param {import("../table-data/table-column.js").TableColumnArgsType} [args] - Options object.
206
+ * @returns {void} - No return value.
207
+ */
208
+ text(name, args) { this.column(name, "text", args) }
209
+
210
+ /**
211
+ * Records a tinyint column.
212
+ * @param {string} name - Column name.
213
+ * @param {import("../table-data/table-column.js").TableColumnArgsType} [args] - Options object.
214
+ * @returns {void} - No return value.
215
+ */
216
+ tinyint(name, args) { this.column(name, "tinyint", args) }
217
+
218
+ /**
219
+ * Records a uuid column.
220
+ * @param {string} name - Column name.
221
+ * @param {import("../table-data/table-column.js").TableColumnArgsType} [args] - Options object.
222
+ * @returns {void} - No return value.
223
+ */
224
+ uuid(name, args) { this.column(name, "uuid", args) }
225
+
226
+ /**
227
+ * Records created_at and updated_at datetime columns.
228
+ * @param {import("../table-data/table-column.js").TableColumnArgsType} [args] - Options object.
229
+ * @returns {void} - No return value.
230
+ */
231
+ timestamps(args) {
232
+ this.datetime("created_at", args)
233
+ this.datetime("updated_at", args)
234
+ }
235
+
236
+ /**
237
+ * Records a new index.
238
+ * @param {string | Array<string | import("../table-data/table-column.js").default>} columns - Column name or array of column names.
239
+ * @param {ChangeTableAddIndexArgsType} [args] - Options object.
240
+ * @returns {void} - No return value.
241
+ */
242
+ index(columns, args) {
243
+ const normalizedColumns = typeof columns == "string" ? [columns] : columns
244
+
245
+ this._operations.push({type: "addIndex", columns: normalizedColumns, args})
246
+ }
247
+
248
+ /**
249
+ * Records a reference column, index, and optional foreign key.
250
+ * @param {string} name - Reference name.
251
+ * @param {object} [args] - Options object.
252
+ * @returns {void} - No return value.
253
+ */
254
+ references(name, args) {
255
+ this._operations.push({type: "addReference", referenceName: name, args})
256
+ }
257
+
258
+ /**
259
+ * Alias for {@link references}.
260
+ * @param {string} name - Reference name.
261
+ * @param {object} [args] - Options object.
262
+ * @returns {void} - No return value.
263
+ */
264
+ belongsTo(name, args) { this.references(name, args) }
265
+
266
+ /**
267
+ * Records removal of one or more columns.
268
+ * @param {string[]} columnNames - Column names to remove.
269
+ * @returns {void} - No return value.
270
+ */
271
+ remove(...columnNames) {
272
+ for (const columnName of columnNames) {
273
+ this._operations.push({type: "removeColumn", columnName})
274
+ }
275
+ }
276
+
277
+ /**
278
+ * Records removal of an index.
279
+ * @param {string | Array<string | import("../table-data/table-column.js").default>} nameOrColumns - Index name or columns.
280
+ * @param {ChangeTableRemoveIndexArgsType} [args] - Options object.
281
+ * @returns {void} - No return value.
282
+ */
283
+ removeIndex(nameOrColumns, args) {
284
+ this._operations.push({type: "removeIndex", nameOrColumns, args})
285
+ }
286
+
287
+ /**
288
+ * Records removal of a reference column and its generated index and foreign keys.
289
+ * @param {string} name - Reference name.
290
+ * @param {ChangeTableRemoveReferenceArgsType} [args] - Options object.
291
+ * @returns {void} - No return value.
292
+ */
293
+ removeReferences(name, args) {
294
+ this._operations.push({type: "removeReference", referenceName: name, args})
295
+ }
296
+
297
+ /**
298
+ * Records removal of the created_at and updated_at columns.
299
+ * @returns {void} - No return value.
300
+ */
301
+ removeTimestamps() {
302
+ this.remove("created_at", "updated_at")
303
+ }
304
+
305
+ /**
306
+ * Records a column rename.
307
+ * @param {string} oldColumnName - Previous column name.
308
+ * @param {string} newColumnName - New column name.
309
+ * @returns {void} - No return value.
310
+ */
311
+ rename(oldColumnName, newColumnName) {
312
+ this._operations.push({type: "renameColumn", oldColumnName, newColumnName})
313
+ }
314
+
315
+ /**
316
+ * Records a change to a column's nullability.
317
+ * @param {string} columnName - Column name.
318
+ * @param {boolean} nullable - Whether the column becomes nullable.
319
+ * @returns {void} - No return value.
320
+ */
321
+ changeNull(columnName, nullable) {
322
+ this._operations.push({type: "changeColumnNull", columnName, nullable})
323
+ }
324
+ }