velocious 1.0.613 → 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 (32) hide show
  1. package/README.md +1 -1
  2. package/build/database/drivers/base.js +19 -0
  3. package/build/database/drivers/mysql/index.js +12 -0
  4. package/build/database/drivers/mysql/sql/alter-table.js +14 -2
  5. package/build/database/drivers/pgsql/index.js +13 -0
  6. package/build/database/migration/change-table.js +324 -0
  7. package/build/database/migration/index.js +197 -13
  8. package/build/src/database/drivers/base.d.ts +13 -0
  9. package/build/src/database/drivers/base.d.ts.map +1 -1
  10. package/build/src/database/drivers/base.js +18 -1
  11. package/build/src/database/drivers/mysql/index.d.ts +10 -0
  12. package/build/src/database/drivers/mysql/index.d.ts.map +1 -1
  13. package/build/src/database/drivers/mysql/index.js +11 -1
  14. package/build/src/database/drivers/mysql/sql/alter-table.d.ts.map +1 -1
  15. package/build/src/database/drivers/mysql/sql/alter-table.js +14 -3
  16. package/build/src/database/drivers/pgsql/index.d.ts +11 -0
  17. package/build/src/database/drivers/pgsql/index.d.ts.map +1 -1
  18. package/build/src/database/drivers/pgsql/index.js +12 -1
  19. package/build/src/database/migration/change-table.d.ts +403 -0
  20. package/build/src/database/migration/change-table.d.ts.map +1 -0
  21. package/build/src/database/migration/change-table.js +286 -0
  22. package/build/src/database/migration/index.d.ts +46 -38
  23. package/build/src/database/migration/index.d.ts.map +1 -1
  24. package/build/src/database/migration/index.js +179 -14
  25. package/build/tsconfig.tsbuildinfo +1 -1
  26. package/package.json +1 -1
  27. package/src/database/drivers/base.js +19 -0
  28. package/src/database/drivers/mysql/index.js +12 -0
  29. package/src/database/drivers/mysql/sql/alter-table.js +14 -2
  30. package/src/database/drivers/pgsql/index.js +13 -0
  31. package/src/database/migration/change-table.js +324 -0
  32. 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))
@@ -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
+ }
@@ -1,17 +1,5 @@
1
1
  // @ts-check
2
2
 
3
- /**
4
- * AddColumnArgsType type.
5
- * @typedef {object} AddColumnArgsType
6
- * @property {ReturnType<typeof JSON.parse>} [default] - Default value for the column.
7
- * @property {object} [foreignKey] - Foreign key definition for the column.
8
- * @property {boolean | {unique: boolean}} [index] - Whether to add an index (optionally unique).
9
- * @property {number} [limit] - Alias for maxLength (varchar length limit) on string-like columns.
10
- * @property {number} [maxLength] - Maximum length for string-like columns (e.g. varchar length).
11
- * @property {boolean} [null] - Whether the column allows null values.
12
- * @property {boolean} [primaryKey] - Whether the column is a primary key.
13
- * @property {boolean} [unique] - Whether the column enforces uniqueness.
14
- */
15
3
  /**
16
4
  * CreateTableIdArgsType type.
17
5
  * @typedef {object} CreateTableIdArgsType
@@ -39,8 +27,11 @@
39
27
  import { convertLegacyDateValueToUtcStorage } from "../datetime-storage.js"
40
28
  import * as inflection from "inflection"
41
29
  import restArgsError from "../../utils/rest-args-error.js"
30
+ import ChangeTable from "./change-table.js"
42
31
  import CreateIndexBase from "../query/create-index-base.js"
32
+ import TableColumn from "../table-data/table-column.js"
43
33
  import TableData from "../table-data/index.js"
34
+ import TableIndex from "../table-data/table-index.js"
44
35
  class NotImplementedError extends Error {}
45
36
 
46
37
  export {NotImplementedError}
@@ -118,7 +109,7 @@ export default class VelociousDatabaseMigration {
118
109
  * @param {string} tableName - Table name.
119
110
  * @param {string} columnName - Column name.
120
111
  * @param {string} columnType - Column type.
121
- * @param {AddColumnArgsType} [args] - Options object.
112
+ * @param {import("../table-data/table-column.js").TableColumnArgsType} [args] - Options object.
122
113
  * @returns {Promise<void>} - Resolves when complete.
123
114
  */
124
115
  async addColumn(tableName, columnName, columnType, args) {
@@ -687,6 +678,199 @@ export default class VelociousDatabaseMigration {
687
678
  }
688
679
  }
689
680
 
681
+ /**
682
+ * ChangeTableArgsType type.
683
+ * @typedef {object} ChangeTableArgsType
684
+ * @property {boolean} [bulk] - Combine compatible contiguous DDL into single
685
+ * ALTER TABLE statements on drivers that support bulk alters (MySQL/MariaDB
686
+ * and PostgreSQL). `bulk` controls DDL grouping only, not transactional
687
+ * atomicity; unchanged drivers execute the recorded commands sequentially.
688
+ */
689
+
690
+ /**
691
+ * ChangeTableCallbackType type.
692
+ * @typedef {(table: import("./change-table.js").default) => void | Promise<void>} ChangeTableCallbackType
693
+ */
694
+
695
+ /**
696
+ * Changes a table using a Rails-style table-scoped recorder.
697
+ * @overload
698
+ * @param {string} tableName - Table name.
699
+ * @param {ChangeTableCallbackType} callback - Callback function.
700
+ * @returns {Promise<void>} - Resolves when complete.
701
+ */
702
+ /**
703
+ * Changes a table with explicit options.
704
+ * @overload
705
+ * @param {string} tableName - Table name.
706
+ * @param {ChangeTableArgsType} args - Options object.
707
+ * @param {ChangeTableCallbackType} callback - Callback function.
708
+ * @returns {Promise<void>} - Resolves when complete.
709
+ */
710
+ /**
711
+ * Runs change table.
712
+ * @param {string} tableName - Table name.
713
+ * @param {ChangeTableArgsType | ChangeTableCallbackType} arg1 - Arg1.
714
+ * @param {ChangeTableCallbackType | undefined} [arg2] - Arg2.
715
+ * @returns {Promise<void>} - Resolves when complete.
716
+ */
717
+ async changeTable(tableName, arg1, arg2) {
718
+ let args = /** @type {ChangeTableArgsType} */ ({})
719
+ let callback
720
+
721
+ if (typeof arg1 == "function") {
722
+ callback = arg1
723
+ } else {
724
+ args = arg1 || {}
725
+ callback = arg2
726
+ }
727
+
728
+ if (typeof callback != "function") throw new Error("No callback given")
729
+
730
+ const {bulk = false, ...restArgs} = args
731
+
732
+ restArgsError(restArgs)
733
+
734
+ const table = new ChangeTable({tableName})
735
+
736
+ await callback(table)
737
+
738
+ await this._executeChangeTableOperations(tableName, table.getOperations(), {bulk})
739
+ }
740
+
741
+ /**
742
+ * Executes recorded changeTable operations. With `bulk` enabled on a
743
+ * supporting driver, compatible contiguous column operations accumulate into
744
+ * a single TableData flushed through `alterTableSQLs`; incompatible commands
745
+ * flush the batch first and run through the existing migration helpers.
746
+ * @param {string} tableName - Table name.
747
+ * @param {import("./change-table.js").ChangeTableOperationType[]} operations - Recorded operations.
748
+ * @param {object} args - Options object.
749
+ * @param {boolean} args.bulk - Whether to enable bulk command grouping.
750
+ * @returns {Promise<void>} - Resolves when complete.
751
+ */
752
+ async _executeChangeTableOperations(tableName, operations, {bulk}) {
753
+ const driver = this.getDriver()
754
+ const bulkSupported = bulk && driver.supportsBulkAlter()
755
+
756
+ if (!bulkSupported) {
757
+ for (const operation of operations) {
758
+ await this._executeChangeTableOperation(tableName, operation)
759
+ }
760
+
761
+ return
762
+ }
763
+
764
+ let batch = new TableData(tableName)
765
+
766
+ const flushBatch = async () => {
767
+ if (batch.getColumns().length == 0 && batch.getIndexes().length == 0) return
768
+
769
+ const sqls = await driver.alterTableSQLs(batch)
770
+
771
+ for (const sql of sqls) {
772
+ await driver.query(sql)
773
+ }
774
+
775
+ batch = new TableData(tableName)
776
+ }
777
+
778
+ for (const operation of operations) {
779
+ switch (operation.type) {
780
+ case "addColumn": {
781
+ if (!operation.columnType) throw new Error("No column type given")
782
+
783
+ // Flush an already-recorded index batch first so the emitted SQL keeps
784
+ // the recorded declaration order (index before column).
785
+ if (batch.getIndexes().length > 0) await flushBatch()
786
+
787
+ batch.addColumn(new TableColumn(operation.columnName, Object.assign({isNewColumn: true, type: operation.columnType}, operation.args)))
788
+ break
789
+ }
790
+ case "removeColumn":
791
+ // Flush an already-recorded index batch first so the emitted SQL keeps
792
+ // the recorded declaration order (index before column).
793
+ if (batch.getIndexes().length > 0) await flushBatch()
794
+
795
+ batch.addColumn(new TableColumn(operation.columnName, {dropColumn: true}))
796
+ break
797
+ case "addIndex":
798
+ // Drivers without `supportsBulkAlterIndexes` (PostgreSQL) keep indexes
799
+ // standalone because their ALTER TABLE does not carry CREATE INDEX
800
+ // clauses. An ifNotExists index is never combined because the combined
801
+ // bulk form cannot express that guard.
802
+ if (!driver.supportsBulkAlterIndexes() || operation.args?.ifNotExists) {
803
+ await flushBatch()
804
+ await this._executeChangeTableOperation(tableName, operation)
805
+ } else {
806
+ batch.addIndex(this._changeTableTableIndex(tableName, operation))
807
+ }
808
+ break
809
+ default:
810
+ await flushBatch()
811
+ await this._executeChangeTableOperation(tableName, operation)
812
+ }
813
+ }
814
+
815
+ await flushBatch()
816
+ }
817
+
818
+ /**
819
+ * Builds a TableIndex for a batch from a recorded addIndex operation,
820
+ * resolving the default addIndex name eagerly so a combined MySQL ALTER
821
+ * never silently names the index differently.
822
+ * @param {string} tableName - Table name.
823
+ * @param {import("./change-table.js").ChangeTableAddIndexOperationType} operation - Recorded operation.
824
+ * @returns {TableIndex} - The table index.
825
+ */
826
+ _changeTableTableIndex(tableName, operation) {
827
+ const {args, columns} = operation
828
+ // An ifNotExists index never reaches batching (it is flushed standalone),
829
+ // so the combined ALTER cannot carry that guard.
830
+ const {name, ...restIndexArgs} = args || {}
831
+ const indexName = name || new CreateIndexBase({columns, driver: this.getDriver(), tableName}).generateIndexName()
832
+
833
+ return new TableIndex(columns, Object.assign({}, restIndexArgs, {name: indexName}))
834
+ }
835
+
836
+ /**
837
+ * Executes a single recorded changeTable operation through the existing
838
+ * migration helper with the same semantics as a direct helper call.
839
+ * @param {string} tableName - Table name.
840
+ * @param {import("./change-table.js").ChangeTableOperationType} operation - Recorded operation.
841
+ * @returns {Promise<void>} - Resolves when complete.
842
+ */
843
+ async _executeChangeTableOperation(tableName, operation) {
844
+ switch (operation.type) {
845
+ case "addColumn":
846
+ await this.addColumn(tableName, operation.columnName, operation.columnType, operation.args)
847
+ break
848
+ case "removeColumn":
849
+ await this.removeColumn(tableName, operation.columnName)
850
+ break
851
+ case "addIndex":
852
+ await this.addIndex(tableName, operation.columns, operation.args)
853
+ break
854
+ case "removeIndex":
855
+ await this.removeIndex(tableName, operation.nameOrColumns, operation.args)
856
+ break
857
+ case "addReference":
858
+ await this.addReference(tableName, operation.referenceName, operation.args || {})
859
+ break
860
+ case "removeReference":
861
+ await this.removeReference(tableName, operation.referenceName, operation.args)
862
+ break
863
+ case "renameColumn":
864
+ await this.renameColumn(tableName, operation.oldColumnName, operation.newColumnName)
865
+ break
866
+ case "changeColumnNull":
867
+ await this.changeColumnNull(tableName, operation.columnName, operation.nullable)
868
+ break
869
+ default:
870
+ throw new Error("Unknown change table operation")
871
+ }
872
+ }
873
+
690
874
  /**
691
875
  * Runs drop table.
692
876
  * @param {string} tableName - Table name.
@@ -717,6 +717,19 @@ export default class VelociousDatabaseDriversBase {
717
717
  * @returns {string} - The type.
718
718
  */
719
719
  getType(): string;
720
+ /**
721
+ * Whether this driver can combine unrelated alter-table operations into a
722
+ * single `ALTER TABLE` statement (Rails' `supports_bulk_alter`).
723
+ * @returns {boolean} - Whether bulk alter is supported.
724
+ */
725
+ supportsBulkAlter(): boolean;
726
+ /**
727
+ * Whether a bulk `ALTER TABLE` statement can also carry `ADD INDEX` clauses.
728
+ * Only drivers that support this keep index adds inside the combined batch;
729
+ * the rest execute each index as its own statement.
730
+ * @returns {boolean} - Whether indexes can be added inside a bulk alter.
731
+ */
732
+ supportsBulkAlterIndexes(): boolean;
720
733
  /**
721
734
  * Runs insert.
722
735
  * @param {InsertSqlArgsType} args - Options object.