velocious 1.0.480 → 1.0.482

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.
@@ -0,0 +1,147 @@
1
+ // @ts-check
2
+
3
+ import {digg} from "diggerize"
4
+ import TableData from "./table-data/index.js"
5
+
6
+ const TABLE_NAME = "schema_migrations"
7
+
8
+ /**
9
+ * Single owner of the `schema_migrations` ledger shape and the only place that reads
10
+ * or writes applied migration versions for a database connection. The migrator uses
11
+ * it to record versions as it runs migrations; provisioning / schema-clone paths use
12
+ * `markApplied` / `baselineFromDatabase` to record versions as applied WITHOUT
13
+ * re-running them (the Rails `schema:load` / Flyway `baseline` idea). That keeps the
14
+ * ledger honest when a database's schema was advanced out of band — e.g. by cloning
15
+ * table structure between databases — so the migrator does not later re-run a
16
+ * migration whose schema object already exists.
17
+ */
18
+ export default class MigrationsLedger {
19
+ /**
20
+ * The ledger table name.
21
+ * @returns {string}
22
+ */
23
+ static tableName() {
24
+ return TABLE_NAME
25
+ }
26
+
27
+ /**
28
+ * Whether the ledger table exists on the given database.
29
+ * @param {import("./drivers/base.js").default} db
30
+ * @returns {Promise<boolean>}
31
+ */
32
+ static async tableExists(db) {
33
+ const table = await db.getTableByName(TABLE_NAME, {throwError: false})
34
+
35
+ return Boolean(table)
36
+ }
37
+
38
+ /**
39
+ * Creates the ledger table if it does not exist. This is the single definition of
40
+ * the `schema_migrations` table shape.
41
+ * @param {import("./drivers/base.js").default} db
42
+ * @returns {Promise<void>}
43
+ */
44
+ static async ensureTable(db) {
45
+ if (await MigrationsLedger.tableExists(db)) return
46
+
47
+ const tableData = new TableData(TABLE_NAME, {ifNotExists: true})
48
+
49
+ tableData.string("version", {null: false, primaryKey: true})
50
+
51
+ for (const sql of await db.createTableSql(tableData)) {
52
+ await db.query(sql)
53
+ }
54
+
55
+ db.clearSchemaCache()
56
+ }
57
+
58
+ /**
59
+ * Every applied migration version recorded in the ledger.
60
+ * @param {import("./drivers/base.js").default} db
61
+ * @returns {Promise<string[]>}
62
+ */
63
+ static async appliedVersions(db) {
64
+ const rows = await db.select(TABLE_NAME)
65
+
66
+ return rows.map((row) => `${digg(row, "version")}`)
67
+ }
68
+
69
+ /**
70
+ * Whether the given version is recorded as applied.
71
+ * @param {import("./drivers/base.js").default} db
72
+ * @param {string} version
73
+ * @returns {Promise<boolean>}
74
+ */
75
+ static async hasVersion(db, version) {
76
+ const rows = await db.newQuery()
77
+ .from(TABLE_NAME)
78
+ .where({version})
79
+ .results()
80
+
81
+ return rows.length > 0
82
+ }
83
+
84
+ /**
85
+ * Records a single version as applied. The targeted existence check keeps the
86
+ * migrator's per-migration hot path cheap (no full-table load per migration).
87
+ * @param {import("./drivers/base.js").default} db
88
+ * @param {string} version
89
+ * @returns {Promise<void>}
90
+ */
91
+ static async recordVersion(db, version) {
92
+ if (await MigrationsLedger.hasVersion(db, version)) return
93
+
94
+ await db.insert({tableName: TABLE_NAME, data: {version}})
95
+ }
96
+
97
+ /**
98
+ * Removes a version from the ledger (used when migrating down).
99
+ * @param {import("./drivers/base.js").default} db
100
+ * @param {string} version
101
+ * @returns {Promise<void>}
102
+ */
103
+ static async removeVersion(db, version) {
104
+ await db.delete({tableName: TABLE_NAME, conditions: {version}})
105
+ }
106
+
107
+ /**
108
+ * Baselines a database's ledger: records each version as applied without running
109
+ * its migration. Idempotent — already-recorded versions are skipped. Ensures the
110
+ * ledger table exists first, then loads the existing set once for the whole batch.
111
+ * @param {import("./drivers/base.js").default} db
112
+ * @param {string[]} versions
113
+ * @returns {Promise<string[]>} The versions that were newly recorded.
114
+ */
115
+ static async markApplied(db, versions) {
116
+ await MigrationsLedger.ensureTable(db)
117
+
118
+ const existing = new Set(await MigrationsLedger.appliedVersions(db))
119
+ const recorded = []
120
+
121
+ for (const version of versions) {
122
+ const normalizedVersion = `${version}`
123
+
124
+ if (existing.has(normalizedVersion)) continue
125
+
126
+ await db.insert({tableName: TABLE_NAME, data: {version: normalizedVersion}})
127
+ existing.add(normalizedVersion)
128
+ recorded.push(normalizedVersion)
129
+ }
130
+
131
+ return recorded
132
+ }
133
+
134
+ /**
135
+ * Baselines `targetDb` to match the applied versions of `sourceDb`. Use when a
136
+ * provisioning path advanced `targetDb`'s schema to match `sourceDb` out of band
137
+ * (e.g. cloning table structure between databases): the migrations are, by
138
+ * construction, already applied on the target, so record them without re-running.
139
+ * @param {{sourceDb: import("./drivers/base.js").default, targetDb: import("./drivers/base.js").default}} args
140
+ * @returns {Promise<string[]>} The versions that were newly recorded on the target.
141
+ */
142
+ static async baselineFromDatabase({sourceDb, targetDb}) {
143
+ const sourceVersions = await MigrationsLedger.appliedVersions(sourceDb)
144
+
145
+ return await MigrationsLedger.markApplied(targetDb, sourceVersions)
146
+ }
147
+ }
@@ -3,9 +3,9 @@
3
3
  import {digg} from "diggerize"
4
4
  import * as inflection from "inflection"
5
5
  import Logger from "../logger.js"
6
+ import MigrationsLedger from "./migrations-ledger.js"
6
7
  import {NotImplementedError} from "./migration/index.js"
7
8
  import restArgsError from "../utils/rest-args-error.js"
8
- import TableData from "./table-data/index.js"
9
9
 
10
10
  export default class VelociousDatabaseMigrator {
11
11
  /**
@@ -79,22 +79,11 @@ export default class VelociousDatabaseMigrator {
79
79
  return
80
80
  }
81
81
 
82
- const exists = await this.migrationsTableExist(db)
83
-
84
- if (exists) {
82
+ if (await this.migrationsTableExist(db)) {
85
83
  this.logger.debug(`${dbIdentifier} migrations table already exists - skipping`)
86
84
  } else {
87
- this.logger.debug("New TableData for schema_migrations")
88
- const schemaMigrationsTable = new TableData("schema_migrations", {ifNotExists: true})
89
-
90
- schemaMigrationsTable.string("version", {null: false, primaryKey: true})
91
-
92
- const createSchemaMigrationsTableSqls = await db.createTableSql(schemaMigrationsTable)
93
-
94
- for (const createSchemaMigrationsTableSql of createSchemaMigrationsTableSqls) {
95
- this.logger.debug(`Creating migrations table with SQL`, createSchemaMigrationsTableSql)
96
- await db.query(createSchemaMigrationsTableSql)
97
- }
85
+ this.logger.debug("Creating schema_migrations table via MigrationsLedger")
86
+ await MigrationsLedger.ensureTable(db)
98
87
  }
99
88
  }
100
89
 
@@ -255,13 +244,11 @@ export default class VelociousDatabaseMigrator {
255
244
  return
256
245
  }
257
246
 
258
- const rows = await db.select("schema_migrations")
247
+ const versions = await MigrationsLedger.appliedVersions(db)
259
248
 
260
249
  this.migrationsVersions[dbIdentifier] = {}
261
250
 
262
- for (const row of rows) {
263
- const version = digg(row, "version")
264
-
251
+ for (const version of versions) {
265
252
  this.migrationsVersions[dbIdentifier][version] = true
266
253
  }
267
254
  }
@@ -272,11 +259,7 @@ export default class VelociousDatabaseMigrator {
272
259
  * @returns {Promise<boolean>} - Resolves with Whether migrations table exist.
273
260
  */
274
261
  async migrationsTableExist(db) {
275
- const schemaTable = await db.getTableByName("schema_migrations", {throwError: false})
276
-
277
- if (!schemaTable) return false
278
-
279
- return true
262
+ return await MigrationsLedger.tableExists(db)
280
263
  }
281
264
 
282
265
  /**
@@ -525,14 +508,7 @@ export default class VelociousDatabaseMigrator {
525
508
  }
526
509
  }
527
510
 
528
- const existingSchemaMigrations = await db.newQuery()
529
- .from("schema_migrations")
530
- .where({version: dateString})
531
- .results()
532
-
533
- if (existingSchemaMigrations.length == 0) {
534
- await db.insert({tableName: "schema_migrations", data: {version: dateString}})
535
- }
511
+ await MigrationsLedger.recordVersion(db, dateString)
536
512
  } else if (direction == "down") {
537
513
  try {
538
514
  await migrationInstance.down()
@@ -544,7 +520,7 @@ export default class VelociousDatabaseMigrator {
544
520
  }
545
521
  }
546
522
 
547
- await db.delete({tableName: "schema_migrations", conditions: {version: dateString}})
523
+ await MigrationsLedger.removeVersion(db, dateString)
548
524
  } else {
549
525
  throw new Error(`Unknown direction: ${direction}`)
550
526
  }
@@ -0,0 +1,390 @@
1
+ // @ts-check
2
+
3
+ import MigrationsLedger from "../migrations-ledger.js"
4
+ import TableData from "../table-data/index.js"
5
+ import TableIndex from "../table-data/table-index.js"
6
+
7
+ const SIMPLE_DEFAULT_PATTERN = /^(?:-?\d+(?:\.\d+)?|[A-Za-z0-9 _.,:/@+-]*)$/
8
+
9
+ /** @type {Record<string, number>} */
10
+ const TEXT_TYPE_RANKS = {
11
+ tinytext: 1,
12
+ text: 2,
13
+ mediumtext: 3,
14
+ longtext: 4
15
+ }
16
+
17
+ /**
18
+ * Clones table structure (columns, text-type widening, indexes) from a source database
19
+ * into a target database for a given set of tables, then baselines the target's
20
+ * `schema_migrations` ledger to the source via {@link MigrationsLedger}. This is the
21
+ * mechanism multi-tenant apps use to provision a tenant database from a template/global
22
+ * database without re-running migrations: the structure is copied and the ledger is
23
+ * recorded as already-applied, so a later `db:tenants:migrate` does not re-run an
24
+ * `addColumn` whose column already exists.
25
+ *
26
+ * The cloner is intentionally policy-free — the caller decides which tables to sync and
27
+ * which databases are source/target. It is idempotent: missing tables are created,
28
+ * missing columns added, too-narrow text columns widened, and missing indexes created;
29
+ * an index whose definition diverges from the source is treated as drift and throws.
30
+ */
31
+ export default class SchemaCloner {
32
+ /**
33
+ * Creates a cloner that copies table structure from `sourceDb` into `targetDb`.
34
+ * @param {{sourceDb: import("../drivers/base.js").default, targetDb: import("../drivers/base.js").default}} args
35
+ */
36
+ constructor({sourceDb, targetDb}) {
37
+ this.sourceDb = sourceDb
38
+ this.targetDb = targetDb
39
+ }
40
+
41
+ /**
42
+ * Clones every given table from the source into the target, then baselines the
43
+ * target's ledger so the cloned schema is recorded as already-migrated.
44
+ * @param {string[]} tableNames
45
+ * @returns {Promise<void>}
46
+ */
47
+ async syncTables(tableNames) {
48
+ for (const tableName of tableNames) {
49
+ await this.syncTable(tableName)
50
+ }
51
+
52
+ await this.reconcileLedger()
53
+ }
54
+
55
+ /**
56
+ * Clones a single table from the source into the target, creating it or adding and
57
+ * widening columns and indexes as needed.
58
+ * @param {string} tableName
59
+ * @returns {Promise<void>}
60
+ */
61
+ async syncTable(tableName) {
62
+ const sourceTable = await this.sourceDb.getTableByName(tableName)
63
+
64
+ if (!sourceTable) {
65
+ throw new Error(`Expected source table to exist: ${tableName}`)
66
+ }
67
+
68
+ if (!await this.targetDb.tableExists(tableName)) {
69
+ await this.createTargetTable({sourceTable, tableName})
70
+ return
71
+ }
72
+
73
+ const changedColumns = await this.ensureTargetColumns({sourceTable, tableName})
74
+
75
+ if (changedColumns) {
76
+ this.targetDb.clearSchemaCache()
77
+ }
78
+
79
+ await this.ensureTargetIndexes({sourceTable, tableName})
80
+ }
81
+
82
+ /**
83
+ * Creates the table in the target from the source table's columns and its
84
+ * non-primary-key indexes.
85
+ * @param {{sourceTable: import("../drivers/base-table.js").default, tableName: string}} args
86
+ * @returns {Promise<void>}
87
+ */
88
+ async createTargetTable({sourceTable, tableName}) {
89
+ const tableData = new TableData(tableName)
90
+
91
+ for (const sourceColumn of await sourceTable.getColumns()) {
92
+ tableData.addColumn(sourceColumn.getName(), this.columnArgsFromSourceColumn(sourceColumn, {isNewColumn: false}))
93
+ }
94
+
95
+ for (const sourceIndex of await sourceTable.getIndexes()) {
96
+ if (!sourceIndex.isPrimaryKey()) {
97
+ tableData.addIndex(this.tableDataIndexFromSourceIndex(sourceIndex))
98
+ }
99
+ }
100
+
101
+ await this.targetDb.createTable(tableData)
102
+ this.targetDb.clearSchemaCache()
103
+ }
104
+
105
+ /**
106
+ * Adds columns present on the source but missing from the target, and widens
107
+ * too-narrow target text columns.
108
+ * @param {{sourceTable: import("../drivers/base-table.js").default, tableName: string}} args
109
+ * @returns {Promise<boolean>} Whether any column was added or widened.
110
+ */
111
+ async ensureTargetColumns({sourceTable, tableName}) {
112
+ const sourceColumns = await sourceTable.getColumns()
113
+ const targetTable = await this.targetDb.getTableByNameOrFail(tableName)
114
+ const targetColumnsByName = new Map()
115
+ const missingColumns = []
116
+ const columnsNeedingWidening = []
117
+
118
+ for (const targetColumn of await targetTable.getColumns()) {
119
+ targetColumnsByName.set(targetColumn.getName(), targetColumn)
120
+ }
121
+
122
+ for (const sourceColumn of sourceColumns) {
123
+ const targetColumn = targetColumnsByName.get(sourceColumn.getName())
124
+
125
+ if (!targetColumn) {
126
+ missingColumns.push(sourceColumn)
127
+ } else if (this.columnNeedsWidening(sourceColumn, targetColumn)) {
128
+ columnsNeedingWidening.push(sourceColumn)
129
+ }
130
+ }
131
+
132
+ if (missingColumns.length <= 0 && columnsNeedingWidening.length <= 0) {
133
+ return false
134
+ }
135
+
136
+ const tableData = new TableData(tableName)
137
+
138
+ for (const sourceColumn of missingColumns) {
139
+ tableData.addColumn(sourceColumn.getName(), this.columnArgsFromSourceColumn(sourceColumn, {isNewColumn: true}))
140
+ }
141
+
142
+ for (const sourceColumn of columnsNeedingWidening) {
143
+ tableData.addColumn(sourceColumn.getName(), this.columnArgsFromSourceColumn(sourceColumn, {isNewColumn: false}))
144
+ }
145
+
146
+ for (const alterSql of await this.targetDb.alterTableSQLs(tableData)) {
147
+ await this.targetDb.query(alterSql)
148
+ }
149
+
150
+ return true
151
+ }
152
+
153
+ /**
154
+ * Creates non-primary-key indexes present on the source but missing from the target,
155
+ * and throws if an existing target index diverges from the source.
156
+ * @param {{sourceTable: import("../drivers/base-table.js").default, tableName: string}} args
157
+ * @returns {Promise<void>}
158
+ */
159
+ async ensureTargetIndexes({sourceTable, tableName}) {
160
+ const targetTable = await this.targetDb.getTableByNameOrFail(tableName)
161
+ /** @type {Map<string, import("../drivers/base-columns-index.js").default>} */
162
+ const targetIndexesByName = new Map()
163
+ const targetIndexSignatures = new Set()
164
+ let createdIndex = false
165
+
166
+ for (const targetIndex of await targetTable.getIndexes()) {
167
+ targetIndexesByName.set(targetIndex.getName(), targetIndex)
168
+ targetIndexSignatures.add(this.indexSignature(targetIndex))
169
+ }
170
+
171
+ for (const sourceIndex of await sourceTable.getIndexes()) {
172
+ if (sourceIndex.isPrimaryKey()) {
173
+ continue
174
+ }
175
+
176
+ const sourceIndexSignature = this.indexSignature(sourceIndex)
177
+
178
+ // SQLite index names are unique per-database, not per-table, so match cloned
179
+ // indexes by their column/uniqueness signature rather than their name.
180
+ if (this.targetDb.getType() === "sqlite" && targetIndexSignatures.has(sourceIndexSignature)) {
181
+ continue
182
+ }
183
+
184
+ const targetIndex = this.targetDb.getType() === "sqlite" ? undefined : targetIndexesByName.get(sourceIndex.getName())
185
+
186
+ if (!targetIndex) {
187
+ const createIndexSqls = await this.targetDb.createIndexSQLs(this.createIndexArgsFromSourceIndex({sourceIndex, tableName}))
188
+
189
+ for (const createIndexSql of createIndexSqls) {
190
+ await this.targetDb.query(createIndexSql)
191
+ }
192
+
193
+ createdIndex = true
194
+ targetIndexSignatures.add(sourceIndexSignature)
195
+ continue
196
+ }
197
+
198
+ if (!this.indexesMatch(sourceIndex, targetIndex)) {
199
+ throw new Error(`Schema clone index drift for ${tableName}.${sourceIndex.getName()}`)
200
+ }
201
+ }
202
+
203
+ if (createdIndex) {
204
+ this.targetDb.clearSchemaCache()
205
+ }
206
+ }
207
+
208
+ /**
209
+ * Baselines the target ledger so the cloned schema is recorded as already-applied.
210
+ * @returns {Promise<string[]>} The versions newly recorded on the target.
211
+ */
212
+ async reconcileLedger() {
213
+ return await MigrationsLedger.baselineFromDatabase({sourceDb: this.sourceDb, targetDb: this.targetDb})
214
+ }
215
+
216
+ /**
217
+ * Whether the target ledger is missing any version applied on the source — i.e. the
218
+ * target schema may have been advanced out of band without recording it.
219
+ * @returns {Promise<boolean>}
220
+ */
221
+ async ledgerDriftsFromSource() {
222
+ if (!await MigrationsLedger.tableExists(this.targetDb)) {
223
+ return true
224
+ }
225
+
226
+ const sourceVersions = await MigrationsLedger.appliedVersions(this.sourceDb)
227
+ const targetVersionSet = new Set(await MigrationsLedger.appliedVersions(this.targetDb))
228
+
229
+ return sourceVersions.some((version) => !targetVersionSet.has(version))
230
+ }
231
+
232
+ /**
233
+ * Maps a source index into a TableData index for table creation (SQLite omits the
234
+ * index name so the driver can generate a unique one).
235
+ * @param {import("../drivers/base-columns-index.js").default} sourceIndex
236
+ * @returns {TableIndex}
237
+ */
238
+ tableDataIndexFromSourceIndex(sourceIndex) {
239
+ /** @type {{name?: string, unique: boolean}} */
240
+ const args = {unique: sourceIndex.isUnique()}
241
+
242
+ // SQLite index names are unique per-database, not per-table, so let the driver
243
+ // generate one; other drivers preserve the source index name. Build the TableIndex
244
+ // directly (rather than via the driver's getTableDataIndex, which only MySQL and
245
+ // SQLite implement) so cloning a PostgreSQL or MS-SQL source table works too.
246
+ if (this.targetDb.getType() !== "sqlite") {
247
+ args.name = sourceIndex.getName()
248
+ }
249
+
250
+ return new TableIndex(sourceIndex.getColumnNames(), args)
251
+ }
252
+
253
+ /**
254
+ * Builds driver create-index args from a source index (the index name is omitted on
255
+ * SQLite, where index names are unique per-database rather than per-table).
256
+ * @param {{sourceIndex: import("../drivers/base-columns-index.js").default, tableName: string}} args
257
+ * @returns {{columns: string[], name?: string, tableName: string, unique: boolean}}
258
+ */
259
+ createIndexArgsFromSourceIndex({sourceIndex, tableName}) {
260
+ /** @type {{columns: string[], name?: string, tableName: string, unique: boolean}} */
261
+ const createIndexArgs = {
262
+ columns: sourceIndex.getColumnNames(),
263
+ tableName,
264
+ unique: sourceIndex.isUnique()
265
+ }
266
+
267
+ if (this.targetDb.getType() !== "sqlite") {
268
+ createIndexArgs.name = sourceIndex.getName()
269
+ }
270
+
271
+ return createIndexArgs
272
+ }
273
+
274
+ /**
275
+ * Whether two indexes have the same uniqueness and ordered column list.
276
+ * @param {import("../drivers/base-columns-index.js").default} sourceIndex
277
+ * @param {import("../drivers/base-columns-index.js").default} targetIndex
278
+ * @returns {boolean}
279
+ */
280
+ indexesMatch(sourceIndex, targetIndex) {
281
+ const sourceColumnNames = sourceIndex.getColumnNames()
282
+ const targetColumnNames = targetIndex.getColumnNames()
283
+
284
+ if (sourceIndex.isUnique() !== targetIndex.isUnique()) {
285
+ return false
286
+ }
287
+
288
+ if (sourceColumnNames.length !== targetColumnNames.length) {
289
+ return false
290
+ }
291
+
292
+ for (let columnIndex = 0; columnIndex < sourceColumnNames.length; columnIndex++) {
293
+ if (sourceColumnNames[columnIndex] !== targetColumnNames[columnIndex]) {
294
+ return false
295
+ }
296
+ }
297
+
298
+ return true
299
+ }
300
+
301
+ /**
302
+ * A stable signature for an index, used to match cloned indexes by shape.
303
+ * @param {import("../drivers/base-columns-index.js").default} index
304
+ * @returns {string}
305
+ */
306
+ indexSignature(index) {
307
+ return `${index.isUnique() ? "unique" : "index"}:${index.getColumnNames().join(",")}`
308
+ }
309
+
310
+ /**
311
+ * Normalizes a column type to its canonical lowercase form (`int` becomes `integer`).
312
+ * @param {string} columnType
313
+ * @returns {string}
314
+ */
315
+ normalizedColumnType(columnType) {
316
+ const normalizedType = columnType.toLowerCase()
317
+
318
+ if (normalizedType === "int") {
319
+ return "integer"
320
+ }
321
+
322
+ return normalizedType
323
+ }
324
+
325
+ /**
326
+ * The widening rank of a text column type (0 when not a text type).
327
+ * @param {string} columnType
328
+ * @returns {number}
329
+ */
330
+ textTypeRank(columnType) {
331
+ return TEXT_TYPE_RANKS[this.normalizedColumnType(columnType)] || 0
332
+ }
333
+
334
+ /**
335
+ * Whether the target's text column is narrower than the source's and must be widened.
336
+ * @param {import("../drivers/base-column.js").default} sourceColumn
337
+ * @param {import("../drivers/base-column.js").default} targetColumn
338
+ * @returns {boolean}
339
+ */
340
+ columnNeedsWidening(sourceColumn, targetColumn) {
341
+ const sourceRank = this.textTypeRank(sourceColumn.getType())
342
+ const targetRank = this.textTypeRank(targetColumn.getType())
343
+
344
+ return sourceRank > 0 && targetRank > 0 && sourceRank > targetRank
345
+ }
346
+
347
+ /**
348
+ * Builds TableData column args from a source column, copying type, nullability,
349
+ * length, notes, simple defaults and (for full clones) primary-key flag.
350
+ * @param {import("../drivers/base-column.js").default} sourceColumn
351
+ * @param {{isNewColumn: boolean}} args
352
+ * @returns {Record<string, unknown>}
353
+ */
354
+ columnArgsFromSourceColumn(sourceColumn, {isNewColumn}) {
355
+ /** @type {{autoIncrement?: boolean, default?: unknown, isNewColumn: boolean, maxLength?: number, notes?: string, null: boolean, primaryKey?: boolean, type: string}} */
356
+ const columnArgs = {
357
+ isNewColumn,
358
+ null: sourceColumn.getNull(),
359
+ type: this.normalizedColumnType(sourceColumn.getType())
360
+ }
361
+ const defaultValue = sourceColumn.getDefault()
362
+ const maxLength = sourceColumn.getMaxLength()
363
+ const notes = sourceColumn.getNotes()
364
+
365
+ if (!isNewColumn && sourceColumn.getPrimaryKey()) {
366
+ columnArgs.primaryKey = true
367
+ }
368
+
369
+ if (sourceColumn.getAutoIncrement()) {
370
+ columnArgs.autoIncrement = true
371
+ }
372
+
373
+ // A maxLength of -1 is the MS-SQL "max" sentinel (NVARCHAR(MAX) / VARBINARY(MAX),
374
+ // backing Velocious text/json/blob columns); the column type drives the unbounded
375
+ // SQL, so don't forward -1 as an explicit length (it would emit NVARCHAR(-1)).
376
+ if (maxLength !== undefined && maxLength >= 0) {
377
+ columnArgs.maxLength = maxLength
378
+ }
379
+
380
+ if (notes) {
381
+ columnArgs.notes = notes
382
+ }
383
+
384
+ if (defaultValue !== null && defaultValue !== undefined && SIMPLE_DEFAULT_PATTERN.test(String(defaultValue))) {
385
+ columnArgs.default = defaultValue
386
+ }
387
+
388
+ return columnArgs
389
+ }
390
+ }