@remix-run/data-table-mysql 0.1.0 → 0.2.0

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.
@@ -1,51 +1,33 @@
1
1
  import type {
2
2
  AdapterCapabilityOverrides,
3
- AdapterExecuteRequest,
4
- AdapterResult,
3
+ DataManipulationRequest,
4
+ DataMigrationRequest,
5
+ DataMigrationResult,
6
+ DataMigrationOperation,
7
+ DataManipulationResult,
8
+ DataManipulationOperation,
5
9
  DatabaseAdapter,
10
+ ColumnDefinition,
11
+ SqlStatement,
12
+ TableRef,
6
13
  TransactionOptions,
7
14
  TransactionToken,
8
15
  } from '@remix-run/data-table'
9
16
  import { getTablePrimaryKey } from '@remix-run/data-table'
17
+ import {
18
+ isDataManipulationOperation as isDataManipulationOperationHelper,
19
+ quoteLiteral as quoteLiteralHelper,
20
+ quoteTableRef as quoteTableRefHelper,
21
+ } from '@remix-run/data-table/sql-helpers'
22
+ import type {
23
+ Connection as MysqlConnection,
24
+ Pool as MysqlPool,
25
+ PoolConnection as MysqlPoolConnection,
26
+ ResultSetHeader,
27
+ RowDataPacket,
28
+ } from 'mysql2/promise'
10
29
 
11
- import { compileMysqlStatement } from './sql-compiler.ts'
12
-
13
- /**
14
- * Row-array response shape for mysql query calls.
15
- */
16
- export type MysqlQueryRows = Record<string, unknown>[]
17
-
18
- /**
19
- * Metadata shape for mysql write results.
20
- */
21
- export type MysqlQueryResultHeader = {
22
- affectedRows: number
23
- insertId: unknown
24
- }
25
-
26
- /**
27
- * Supported mysql `query()` response tuple.
28
- */
29
- export type MysqlQueryResponse = [result: unknown, fields?: unknown]
30
-
31
- /**
32
- * Single mysql connection contract used by this adapter.
33
- */
34
- export type MysqlDatabaseConnection = {
35
- query(text: string, values?: unknown[]): Promise<MysqlQueryResponse>
36
- beginTransaction(): Promise<void>
37
- commit(): Promise<void>
38
- rollback(): Promise<void>
39
- release?: () => void
40
- }
41
-
42
- /**
43
- * Mysql pool contract used by this adapter.
44
- */
45
- export type MysqlDatabasePool = {
46
- query(text: string, values?: unknown[]): Promise<MysqlQueryResponse>
47
- getConnection(): Promise<MysqlDatabaseConnection>
48
- }
30
+ import { compileMysqlOperation } from './sql-compiler.ts'
49
31
 
50
32
  /**
51
33
  * Mysql adapter configuration.
@@ -55,17 +37,30 @@ export type MysqlDatabaseAdapterOptions = {
55
37
  }
56
38
 
57
39
  type TransactionState = {
58
- connection: MysqlDatabaseConnection
40
+ connection: MysqlTransactionConnection
59
41
  releaseOnClose: boolean
60
42
  }
61
43
 
62
- type MysqlQueryable = MysqlDatabasePool | MysqlDatabaseConnection
44
+ type MysqlQueryRows = RowDataPacket[]
45
+ type MysqlQueryResultHeader = {
46
+ affectedRows: number
47
+ insertId: unknown
48
+ }
49
+ type MysqlTransactionConnection = MysqlConnection | MysqlPoolConnection
50
+ type MysqlQueryable = MysqlPool | MysqlTransactionConnection
63
51
 
64
52
  /**
65
53
  * `DatabaseAdapter` implementation for mysql-compatible clients.
66
54
  */
67
55
  export class MysqlDatabaseAdapter implements DatabaseAdapter {
56
+ /**
57
+ * The SQL dialect identifier reported by this adapter.
58
+ */
68
59
  dialect = 'mysql'
60
+
61
+ /**
62
+ * Feature flags describing the mysql behaviors supported by this adapter.
63
+ */
69
64
  capabilities
70
65
 
71
66
  #client: MysqlQueryable
@@ -78,26 +73,48 @@ export class MysqlDatabaseAdapter implements DatabaseAdapter {
78
73
  returning: options?.capabilities?.returning ?? false,
79
74
  savepoints: options?.capabilities?.savepoints ?? true,
80
75
  upsert: options?.capabilities?.upsert ?? true,
76
+ transactionalDdl: options?.capabilities?.transactionalDdl ?? false,
77
+ migrationLock: options?.capabilities?.migrationLock ?? true,
81
78
  }
82
79
  }
83
80
 
84
- async execute(request: AdapterExecuteRequest): Promise<AdapterResult> {
85
- if (request.statement.kind === 'insertMany' && request.statement.values.length === 0) {
81
+ /**
82
+ * Compiles a data or migration operation to mysql SQL statements.
83
+ * @param operation Operation to compile.
84
+ * @returns Compiled SQL statements.
85
+ */
86
+ compileSql(operation: DataManipulationOperation | DataMigrationOperation): SqlStatement[] {
87
+ if (isDataManipulationOperation(operation)) {
88
+ let compiled = compileMysqlOperation(operation)
89
+ return [{ text: compiled.text, values: compiled.values }]
90
+ }
91
+
92
+ return compileMysqlMigrationOperations(operation)
93
+ }
94
+
95
+ /**
96
+ * Executes a mysql data-manipulation request.
97
+ * @param request Request to execute.
98
+ * @returns Execution result.
99
+ */
100
+ async execute(request: DataManipulationRequest): Promise<DataManipulationResult> {
101
+ if (request.operation.kind === 'insertMany' && request.operation.values.length === 0) {
86
102
  return {
87
103
  affectedRows: 0,
88
104
  insertId: undefined,
89
- rows: request.statement.returning ? [] : undefined,
105
+ rows: request.operation.returning ? [] : undefined,
90
106
  }
91
107
  }
92
108
 
93
- let statement = compileMysqlStatement(request.statement)
109
+ let statements = this.compileSql(request.operation)
110
+ let statement = statements[0]
94
111
  let client = this.#resolveClient(request.transaction)
95
112
  let [result] = await client.query(statement.text, statement.values)
96
113
 
97
114
  if (isRowsResult(result)) {
98
115
  let rows = normalizeRows(result)
99
116
 
100
- if (request.statement.kind === 'count' || request.statement.kind === 'exists') {
117
+ if (request.operation.kind === 'count' || request.operation.kind === 'exists') {
101
118
  rows = normalizeCountRows(rows)
102
119
  }
103
120
 
@@ -108,13 +125,85 @@ export class MysqlDatabaseAdapter implements DatabaseAdapter {
108
125
 
109
126
  return {
110
127
  affectedRows: header.affectedRows,
111
- insertId: normalizeInsertId(request.statement.kind, request.statement, header),
128
+ insertId: normalizeInsertId(request.operation.kind, request.operation, header),
129
+ }
130
+ }
131
+
132
+ /**
133
+ * Executes mysql migration operations.
134
+ * @param request Migration request to execute.
135
+ * @returns Migration result.
136
+ */
137
+ async migrate(request: DataMigrationRequest): Promise<DataMigrationResult> {
138
+ let statements = this.compileSql(request.operation)
139
+ let client = this.#resolveClient(request.transaction)
140
+
141
+ for (let statement of statements) {
142
+ await client.query(statement.text, statement.values)
143
+ }
144
+
145
+ return {
146
+ affectedOperations: statements.length,
112
147
  }
113
148
  }
114
149
 
150
+ /**
151
+ * Checks whether a table exists in mysql.
152
+ * @param table Table reference to inspect.
153
+ * @param transaction Optional transaction token.
154
+ * @returns `true` when the table exists.
155
+ */
156
+ async hasTable(table: TableRef, transaction?: TransactionToken): Promise<boolean> {
157
+ let schema = table.schema
158
+ let sql = schema
159
+ ? 'select exists(select 1 from information_schema.tables where table_schema = ? and table_name = ?) as `exists`'
160
+ : 'select exists(select 1 from information_schema.tables where table_schema = database() and table_name = ?) as `exists`'
161
+ let values = schema ? [schema, table.name] : [table.name]
162
+ let client = this.#resolveClient(transaction)
163
+ let [result] = await client.query(sql, values)
164
+
165
+ if (!isRowsResult(result)) {
166
+ return false
167
+ }
168
+
169
+ return toBooleanExists(result[0]?.exists)
170
+ }
171
+
172
+ /**
173
+ * Checks whether a column exists in mysql.
174
+ * @param table Table reference to inspect.
175
+ * @param column Column name to look up.
176
+ * @param transaction Optional transaction token.
177
+ * @returns `true` when the column exists.
178
+ */
179
+ async hasColumn(
180
+ table: TableRef,
181
+ column: string,
182
+ transaction?: TransactionToken,
183
+ ): Promise<boolean> {
184
+ let schema = table.schema
185
+ let sql = schema
186
+ ? 'select exists(select 1 from information_schema.columns where table_schema = ? and table_name = ? and column_name = ?) as `exists`'
187
+ : 'select exists(select 1 from information_schema.columns where table_schema = database() and table_name = ? and column_name = ?) as `exists`'
188
+ let values = schema ? [schema, table.name, column] : [table.name, column]
189
+ let client = this.#resolveClient(transaction)
190
+ let [result] = await client.query(sql, values)
191
+
192
+ if (!isRowsResult(result)) {
193
+ return false
194
+ }
195
+
196
+ return toBooleanExists(result[0]?.exists)
197
+ }
198
+
199
+ /**
200
+ * Starts a mysql transaction.
201
+ * @param options Transaction options.
202
+ * @returns Transaction token.
203
+ */
115
204
  async beginTransaction(options?: TransactionOptions): Promise<TransactionToken> {
116
205
  let releaseOnClose = false
117
- let connection: MysqlDatabaseConnection
206
+ let connection: MysqlTransactionConnection
118
207
 
119
208
  if (isMysqlPool(this.#client)) {
120
209
  connection = await this.#client.getConnection()
@@ -146,6 +235,11 @@ export class MysqlDatabaseAdapter implements DatabaseAdapter {
146
235
  return token
147
236
  }
148
237
 
238
+ /**
239
+ * Commits an open mysql transaction.
240
+ * @param token Transaction token to commit.
241
+ * @returns A promise that resolves when the transaction is committed.
242
+ */
149
243
  async commitTransaction(token: TransactionToken): Promise<void> {
150
244
  let transaction = this.#transactions.get(token.id)
151
245
 
@@ -158,12 +252,17 @@ export class MysqlDatabaseAdapter implements DatabaseAdapter {
158
252
  } finally {
159
253
  this.#transactions.delete(token.id)
160
254
 
161
- if (transaction.releaseOnClose) {
162
- transaction.connection.release?.()
255
+ if (transaction.releaseOnClose && isMysqlPoolConnection(transaction.connection)) {
256
+ transaction.connection.release()
163
257
  }
164
258
  }
165
259
  }
166
260
 
261
+ /**
262
+ * Rolls back an open mysql transaction.
263
+ * @param token Transaction token to roll back.
264
+ * @returns A promise that resolves when the transaction is rolled back.
265
+ */
167
266
  async rollbackTransaction(token: TransactionToken): Promise<void> {
168
267
  let transaction = this.#transactions.get(token.id)
169
268
 
@@ -176,28 +275,62 @@ export class MysqlDatabaseAdapter implements DatabaseAdapter {
176
275
  } finally {
177
276
  this.#transactions.delete(token.id)
178
277
 
179
- if (transaction.releaseOnClose) {
180
- transaction.connection.release?.()
278
+ if (transaction.releaseOnClose && isMysqlPoolConnection(transaction.connection)) {
279
+ transaction.connection.release()
181
280
  }
182
281
  }
183
282
  }
184
283
 
284
+ /**
285
+ * Creates a savepoint in an open mysql transaction.
286
+ * @param token Transaction token to use.
287
+ * @param name Savepoint name.
288
+ * @returns A promise that resolves when the savepoint is created.
289
+ */
185
290
  async createSavepoint(token: TransactionToken, name: string): Promise<void> {
186
291
  let connection = this.#transactionConnection(token)
187
292
  await connection.query('savepoint ' + quoteIdentifier(name))
188
293
  }
189
294
 
295
+ /**
296
+ * Rolls back to a savepoint in an open mysql transaction.
297
+ * @param token Transaction token to use.
298
+ * @param name Savepoint name.
299
+ * @returns A promise that resolves when the rollback completes.
300
+ */
190
301
  async rollbackToSavepoint(token: TransactionToken, name: string): Promise<void> {
191
302
  let connection = this.#transactionConnection(token)
192
303
  await connection.query('rollback to savepoint ' + quoteIdentifier(name))
193
304
  }
194
305
 
306
+ /**
307
+ * Releases a savepoint in an open mysql transaction.
308
+ * @param token Transaction token to use.
309
+ * @param name Savepoint name.
310
+ * @returns A promise that resolves when the savepoint is released.
311
+ */
195
312
  async releaseSavepoint(token: TransactionToken, name: string): Promise<void> {
196
313
  let connection = this.#transactionConnection(token)
197
314
  await connection.query('release savepoint ' + quoteIdentifier(name))
198
315
  }
199
316
 
200
- #resolveClient(token: TransactionToken | undefined): MysqlDatabaseConnection | MysqlDatabasePool {
317
+ /**
318
+ * Acquires the mysql migration lock.
319
+ * @returns A promise that resolves when the lock is acquired.
320
+ */
321
+ async acquireMigrationLock(): Promise<void> {
322
+ await this.#client.query('select get_lock(?, 60)', ['data_table_migrations'])
323
+ }
324
+
325
+ /**
326
+ * Releases the mysql migration lock.
327
+ * @returns A promise that resolves when the lock is released.
328
+ */
329
+ async releaseMigrationLock(): Promise<void> {
330
+ await this.#client.query('select release_lock(?)', ['data_table_migrations'])
331
+ }
332
+
333
+ #resolveClient(token: TransactionToken | undefined): MysqlQueryable {
201
334
  if (!token) {
202
335
  return this.#client
203
336
  }
@@ -205,7 +338,7 @@ export class MysqlDatabaseAdapter implements DatabaseAdapter {
205
338
  return this.#transactionConnection(token)
206
339
  }
207
340
 
208
- #transactionConnection(token: TransactionToken): MysqlDatabaseConnection {
341
+ #transactionConnection(token: TransactionToken): MysqlTransactionConnection {
209
342
  let transaction = this.#transactions.get(token.id)
210
343
 
211
344
  if (!transaction) {
@@ -221,6 +354,16 @@ export class MysqlDatabaseAdapter implements DatabaseAdapter {
221
354
  * @param client Mysql pool or connection.
222
355
  * @param options Optional adapter capability overrides.
223
356
  * @returns A configured mysql adapter.
357
+ * @example
358
+ * ```ts
359
+ * import { createPool } from 'mysql2/promise'
360
+ * import { createDatabase } from 'remix/data-table'
361
+ * import { createMysqlDatabaseAdapter } from 'remix/data-table-mysql'
362
+ *
363
+ * let pool = createPool({ uri: process.env.DATABASE_URL })
364
+ * let adapter = createMysqlDatabaseAdapter(pool)
365
+ * let db = createDatabase(adapter)
366
+ * ```
224
367
  */
225
368
  export function createMysqlDatabaseAdapter(
226
369
  client: MysqlQueryable,
@@ -229,21 +372,47 @@ export function createMysqlDatabaseAdapter(
229
372
  return new MysqlDatabaseAdapter(client, options)
230
373
  }
231
374
 
232
- function isMysqlPool(client: MysqlQueryable): client is MysqlDatabasePool {
233
- return typeof (client as MysqlDatabasePool).getConnection === 'function'
375
+ function isMysqlPool(client: MysqlQueryable): client is MysqlPool {
376
+ return 'getConnection' in client && typeof client.getConnection === 'function'
377
+ }
378
+
379
+ function isMysqlPoolConnection(
380
+ connection: MysqlTransactionConnection,
381
+ ): connection is MysqlPoolConnection {
382
+ return 'release' in connection && typeof connection.release === 'function'
234
383
  }
235
384
 
236
385
  function isRowsResult(result: unknown): result is MysqlQueryRows {
237
386
  return Array.isArray(result) && (result.length === 0 || !Array.isArray(result[0]))
238
387
  }
239
388
 
389
+ function toBooleanExists(value: unknown): boolean {
390
+ if (typeof value === 'boolean') {
391
+ return value
392
+ }
393
+
394
+ if (typeof value === 'number') {
395
+ return value > 0
396
+ }
397
+
398
+ if (typeof value === 'bigint') {
399
+ return value > 0n
400
+ }
401
+
402
+ if (typeof value === 'string') {
403
+ return value === '1' || value.toLowerCase() === 'true'
404
+ }
405
+
406
+ return false
407
+ }
408
+
240
409
  function normalizeRows(rows: MysqlQueryRows): Record<string, unknown>[] {
241
410
  return rows.map((row) => ({ ...row }))
242
411
  }
243
412
 
244
413
  function normalizeHeader(result: unknown): MysqlQueryResultHeader {
245
414
  if (typeof result === 'object' && result !== null) {
246
- let header = result as { affectedRows?: unknown; insertId?: unknown }
415
+ let header = result as Partial<ResultSetHeader>
247
416
 
248
417
  return {
249
418
  affectedRows: typeof header.affectedRows === 'number' ? header.affectedRows : 0,
@@ -284,15 +453,15 @@ function normalizeCountRows(rows: Record<string, unknown>[]): Record<string, unk
284
453
  }
285
454
 
286
455
  function normalizeInsertId(
287
- kind: AdapterExecuteRequest['statement']['kind'],
288
- statement: AdapterExecuteRequest['statement'],
456
+ kind: DataManipulationRequest['operation']['kind'],
457
+ operation: DataManipulationRequest['operation'],
289
458
  header: MysqlQueryResultHeader,
290
459
  ): unknown {
291
- if (!isInsertStatementKind(kind) || !isInsertStatement(statement)) {
460
+ if (!isInsertOperationKind(kind) || !isInsertOperation(operation)) {
292
461
  return undefined
293
462
  }
294
463
 
295
- if (getTablePrimaryKey(statement.table).length !== 1) {
464
+ if (getTablePrimaryKey(operation.table).length !== 1) {
296
465
  return undefined
297
466
  }
298
467
 
@@ -303,17 +472,460 @@ function quoteIdentifier(value: string): string {
303
472
  return '`' + value.replace(/`/g, '``') + '`'
304
473
  }
305
474
 
306
- function isInsertStatementKind(kind: AdapterExecuteRequest['statement']['kind']): boolean {
475
+ function quoteTableRef(table: TableRef): string {
476
+ return quoteTableRefHelper(table, quoteIdentifier)
477
+ }
478
+
479
+ function quoteLiteral(value: unknown): string {
480
+ return quoteLiteralHelper(value)
481
+ }
482
+
483
+ function isInsertOperationKind(kind: DataManipulationRequest['operation']['kind']): boolean {
307
484
  return kind === 'insert' || kind === 'insertMany' || kind === 'upsert'
308
485
  }
309
486
 
310
- function isInsertStatement(
311
- statement: AdapterExecuteRequest['statement'],
312
- ): statement is Extract<
313
- AdapterExecuteRequest['statement'],
487
+ function isInsertOperation(
488
+ operation: DataManipulationRequest['operation'],
489
+ ): operation is Extract<
490
+ DataManipulationRequest['operation'],
314
491
  { kind: 'insert' | 'insertMany' | 'upsert' }
315
492
  > {
316
493
  return (
317
- statement.kind === 'insert' || statement.kind === 'insertMany' || statement.kind === 'upsert'
494
+ operation.kind === 'insert' || operation.kind === 'insertMany' || operation.kind === 'upsert'
318
495
  )
319
496
  }
497
+
498
+ function isDataManipulationOperation(
499
+ operation: DataManipulationOperation | DataMigrationOperation,
500
+ ): operation is DataManipulationOperation {
501
+ return isDataManipulationOperationHelper(operation)
502
+ }
503
+
504
+ function compileMysqlMigrationOperations(operation: DataMigrationOperation): SqlStatement[] {
505
+ if (operation.kind === 'raw') {
506
+ return [{ text: operation.sql.text, values: [...operation.sql.values] }]
507
+ }
508
+
509
+ if (operation.kind === 'createTable') {
510
+ let columns = Object.keys(operation.columns).map(
511
+ (columnName) =>
512
+ quoteIdentifier(columnName) + ' ' + compileMysqlColumn(operation.columns[columnName]),
513
+ )
514
+ let constraints: string[] = []
515
+
516
+ if (operation.primaryKey) {
517
+ constraints.push(
518
+ 'constraint ' +
519
+ quoteIdentifier(operation.primaryKey.name) +
520
+ ' primary key (' +
521
+ operation.primaryKey.columns.map((column) => quoteIdentifier(column)).join(', ') +
522
+ ')',
523
+ )
524
+ }
525
+
526
+ for (let unique of operation.uniques ?? []) {
527
+ constraints.push(
528
+ 'constraint ' +
529
+ quoteIdentifier(unique.name) +
530
+ ' ' +
531
+ 'unique (' +
532
+ unique.columns.map((column) => quoteIdentifier(column)).join(', ') +
533
+ ')',
534
+ )
535
+ }
536
+
537
+ for (let check of operation.checks ?? []) {
538
+ constraints.push(
539
+ 'constraint ' + quoteIdentifier(check.name) + ' ' + 'check (' + check.expression + ')',
540
+ )
541
+ }
542
+
543
+ for (let foreignKey of operation.foreignKeys ?? []) {
544
+ let clause =
545
+ 'constraint ' +
546
+ quoteIdentifier(foreignKey.name) +
547
+ ' ' +
548
+ 'foreign key (' +
549
+ foreignKey.columns.map((column) => quoteIdentifier(column)).join(', ') +
550
+ ') references ' +
551
+ quoteTableRef(foreignKey.references.table) +
552
+ ' (' +
553
+ foreignKey.references.columns.map((column) => quoteIdentifier(column)).join(', ') +
554
+ ')'
555
+
556
+ if (foreignKey.onDelete) {
557
+ clause += ' on delete ' + foreignKey.onDelete
558
+ }
559
+
560
+ if (foreignKey.onUpdate) {
561
+ clause += ' on update ' + foreignKey.onUpdate
562
+ }
563
+
564
+ constraints.push(clause)
565
+ }
566
+
567
+ let sql =
568
+ 'create table ' +
569
+ (operation.ifNotExists ? 'if not exists ' : '') +
570
+ quoteTableRef(operation.table) +
571
+ ' (' +
572
+ [...columns, ...constraints].join(', ') +
573
+ ')'
574
+
575
+ let statements: SqlStatement[] = [{ text: sql, values: [] }]
576
+
577
+ if (operation.comment) {
578
+ statements.push({
579
+ text:
580
+ 'alter table ' +
581
+ quoteTableRef(operation.table) +
582
+ ' comment = ' +
583
+ quoteLiteral(operation.comment),
584
+ values: [],
585
+ })
586
+ }
587
+
588
+ return statements
589
+ }
590
+
591
+ if (operation.kind === 'alterTable') {
592
+ let statements: SqlStatement[] = []
593
+
594
+ for (let change of operation.changes) {
595
+ let sql = 'alter table ' + quoteTableRef(operation.table) + ' '
596
+
597
+ if (change.kind === 'addColumn') {
598
+ sql +=
599
+ 'add column ' +
600
+ quoteIdentifier(change.column) +
601
+ ' ' +
602
+ compileMysqlColumn(change.definition)
603
+ } else if (change.kind === 'changeColumn') {
604
+ sql +=
605
+ 'modify column ' +
606
+ quoteIdentifier(change.column) +
607
+ ' ' +
608
+ compileMysqlColumn(change.definition)
609
+ } else if (change.kind === 'renameColumn') {
610
+ sql += 'rename column ' + quoteIdentifier(change.from) + ' to ' + quoteIdentifier(change.to)
611
+ } else if (change.kind === 'dropColumn') {
612
+ sql += 'drop column ' + quoteIdentifier(change.column)
613
+ } else if (change.kind === 'addPrimaryKey') {
614
+ sql +=
615
+ 'add primary key (' +
616
+ change.constraint.columns.map((column) => quoteIdentifier(column)).join(', ') +
617
+ ')'
618
+ } else if (change.kind === 'dropPrimaryKey') {
619
+ sql += 'drop primary key'
620
+ } else if (change.kind === 'addUnique') {
621
+ sql +=
622
+ 'add ' +
623
+ 'constraint ' +
624
+ quoteIdentifier(change.constraint.name) +
625
+ ' ' +
626
+ 'unique (' +
627
+ change.constraint.columns.map((column) => quoteIdentifier(column)).join(', ') +
628
+ ')'
629
+ } else if (change.kind === 'dropUnique') {
630
+ sql += 'drop index ' + quoteIdentifier(change.name)
631
+ } else if (change.kind === 'addForeignKey') {
632
+ sql +=
633
+ 'add ' +
634
+ 'constraint ' +
635
+ quoteIdentifier(change.constraint.name) +
636
+ ' ' +
637
+ 'foreign key (' +
638
+ change.constraint.columns.map((column) => quoteIdentifier(column)).join(', ') +
639
+ ') references ' +
640
+ quoteTableRef(change.constraint.references.table) +
641
+ ' (' +
642
+ change.constraint.references.columns.map((column) => quoteIdentifier(column)).join(', ') +
643
+ ')'
644
+ } else if (change.kind === 'dropForeignKey') {
645
+ sql += 'drop foreign key ' + quoteIdentifier(change.name)
646
+ } else if (change.kind === 'addCheck') {
647
+ sql +=
648
+ 'add ' +
649
+ 'constraint ' +
650
+ quoteIdentifier(change.constraint.name) +
651
+ ' ' +
652
+ 'check (' +
653
+ change.constraint.expression +
654
+ ')'
655
+ } else if (change.kind === 'dropCheck') {
656
+ sql += 'drop check ' + quoteIdentifier(change.name)
657
+ } else if (change.kind === 'setTableComment') {
658
+ sql += 'comment = ' + quoteLiteral(change.comment)
659
+ } else {
660
+ continue
661
+ }
662
+
663
+ statements.push({ text: sql, values: [] })
664
+ }
665
+
666
+ return statements
667
+ }
668
+
669
+ if (operation.kind === 'renameTable') {
670
+ return [
671
+ {
672
+ text:
673
+ 'rename table ' + quoteTableRef(operation.from) + ' to ' + quoteTableRef(operation.to),
674
+ values: [],
675
+ },
676
+ ]
677
+ }
678
+
679
+ if (operation.kind === 'dropTable') {
680
+ return [
681
+ {
682
+ text:
683
+ 'drop table ' + (operation.ifExists ? 'if exists ' : '') + quoteTableRef(operation.table),
684
+ values: [],
685
+ },
686
+ ]
687
+ }
688
+
689
+ if (operation.kind === 'createIndex') {
690
+ return [
691
+ {
692
+ text:
693
+ 'create ' +
694
+ (operation.index.unique ? 'unique ' : '') +
695
+ 'index ' +
696
+ quoteIdentifier(operation.index.name) +
697
+ ' on ' +
698
+ quoteTableRef(operation.index.table) +
699
+ (operation.index.using ? ' using ' + operation.index.using : '') +
700
+ ' (' +
701
+ operation.index.columns.map((column) => quoteIdentifier(column)).join(', ') +
702
+ ')' +
703
+ (operation.index.where ? ' where ' + operation.index.where : ''),
704
+ values: [],
705
+ },
706
+ ]
707
+ }
708
+
709
+ if (operation.kind === 'dropIndex') {
710
+ return [
711
+ {
712
+ text:
713
+ 'drop index ' + quoteIdentifier(operation.name) + ' on ' + quoteTableRef(operation.table),
714
+ values: [],
715
+ },
716
+ ]
717
+ }
718
+
719
+ if (operation.kind === 'renameIndex') {
720
+ return [
721
+ {
722
+ text:
723
+ 'alter table ' +
724
+ quoteTableRef(operation.table) +
725
+ ' rename index ' +
726
+ quoteIdentifier(operation.from) +
727
+ ' to ' +
728
+ quoteIdentifier(operation.to),
729
+ values: [],
730
+ },
731
+ ]
732
+ }
733
+
734
+ if (operation.kind === 'addForeignKey') {
735
+ return [
736
+ {
737
+ text:
738
+ 'alter table ' +
739
+ quoteTableRef(operation.table) +
740
+ ' add ' +
741
+ 'constraint ' +
742
+ quoteIdentifier(operation.constraint.name) +
743
+ ' ' +
744
+ 'foreign key (' +
745
+ operation.constraint.columns.map((column) => quoteIdentifier(column)).join(', ') +
746
+ ') references ' +
747
+ quoteTableRef(operation.constraint.references.table) +
748
+ ' (' +
749
+ operation.constraint.references.columns
750
+ .map((column) => quoteIdentifier(column))
751
+ .join(', ') +
752
+ ')' +
753
+ (operation.constraint.onDelete ? ' on delete ' + operation.constraint.onDelete : '') +
754
+ (operation.constraint.onUpdate ? ' on update ' + operation.constraint.onUpdate : ''),
755
+ values: [],
756
+ },
757
+ ]
758
+ }
759
+
760
+ if (operation.kind === 'dropForeignKey') {
761
+ return [
762
+ {
763
+ text:
764
+ 'alter table ' +
765
+ quoteTableRef(operation.table) +
766
+ ' drop foreign key ' +
767
+ quoteIdentifier(operation.name),
768
+ values: [],
769
+ },
770
+ ]
771
+ }
772
+
773
+ if (operation.kind === 'addCheck') {
774
+ return [
775
+ {
776
+ text:
777
+ 'alter table ' +
778
+ quoteTableRef(operation.table) +
779
+ ' add ' +
780
+ 'constraint ' +
781
+ quoteIdentifier(operation.constraint.name) +
782
+ ' ' +
783
+ 'check (' +
784
+ operation.constraint.expression +
785
+ ')',
786
+ values: [],
787
+ },
788
+ ]
789
+ }
790
+
791
+ if (operation.kind === 'dropCheck') {
792
+ return [
793
+ {
794
+ text:
795
+ 'alter table ' +
796
+ quoteTableRef(operation.table) +
797
+ ' drop check ' +
798
+ quoteIdentifier(operation.name),
799
+ values: [],
800
+ },
801
+ ]
802
+ }
803
+
804
+ throw new Error('Unsupported data migration operation kind')
805
+ }
806
+
807
+ function compileMysqlColumn(definition: ColumnDefinition): string {
808
+ let parts = [compileMysqlColumnType(definition)]
809
+
810
+ if (definition.nullable === false) {
811
+ parts.push('not null')
812
+ }
813
+
814
+ if (definition.default) {
815
+ if (definition.default.kind === 'now') {
816
+ parts.push('default current_timestamp')
817
+ } else if (definition.default.kind === 'sql') {
818
+ parts.push('default ' + definition.default.expression)
819
+ } else {
820
+ parts.push('default ' + quoteLiteral(definition.default.value))
821
+ }
822
+ }
823
+
824
+ if (definition.autoIncrement) {
825
+ parts.push('auto_increment')
826
+ }
827
+
828
+ if (definition.primaryKey) {
829
+ parts.push('primary key')
830
+ }
831
+
832
+ if (definition.unique) {
833
+ parts.push('unique')
834
+ }
835
+
836
+ if (definition.computed) {
837
+ parts.push('generated always as (' + definition.computed.expression + ')')
838
+ parts.push(definition.computed.stored ? 'stored' : 'virtual')
839
+ }
840
+
841
+ if (definition.references) {
842
+ let clause =
843
+ 'references ' +
844
+ quoteTableRef(definition.references.table) +
845
+ ' (' +
846
+ definition.references.columns.map((column) => quoteIdentifier(column)).join(', ') +
847
+ ')'
848
+
849
+ if (definition.references.onDelete) {
850
+ clause += ' on delete ' + definition.references.onDelete
851
+ }
852
+
853
+ if (definition.references.onUpdate) {
854
+ clause += ' on update ' + definition.references.onUpdate
855
+ }
856
+
857
+ parts.push(clause)
858
+ }
859
+
860
+ if (definition.checks && definition.checks.length > 0) {
861
+ for (let check of definition.checks) {
862
+ parts.push('check (' + check.expression + ')')
863
+ }
864
+ }
865
+
866
+ return parts.join(' ')
867
+ }
868
+
869
+ function compileMysqlColumnType(definition: ColumnDefinition): string {
870
+ if (definition.type === 'varchar') {
871
+ return 'varchar(' + String(definition.length ?? 255) + ')'
872
+ }
873
+
874
+ if (definition.type === 'text') {
875
+ return 'text'
876
+ }
877
+
878
+ if (definition.type === 'integer') {
879
+ return definition.unsigned ? 'int unsigned' : 'int'
880
+ }
881
+
882
+ if (definition.type === 'bigint') {
883
+ return definition.unsigned ? 'bigint unsigned' : 'bigint'
884
+ }
885
+
886
+ if (definition.type === 'decimal') {
887
+ if (definition.precision !== undefined && definition.scale !== undefined) {
888
+ return 'decimal(' + String(definition.precision) + ', ' + String(definition.scale) + ')'
889
+ }
890
+
891
+ return 'decimal'
892
+ }
893
+
894
+ if (definition.type === 'boolean') {
895
+ return 'boolean'
896
+ }
897
+
898
+ if (definition.type === 'uuid') {
899
+ return 'char(36)'
900
+ }
901
+
902
+ if (definition.type === 'date') {
903
+ return 'date'
904
+ }
905
+
906
+ if (definition.type === 'time') {
907
+ return 'time'
908
+ }
909
+
910
+ if (definition.type === 'timestamp') {
911
+ return 'timestamp'
912
+ }
913
+
914
+ if (definition.type === 'json') {
915
+ return 'json'
916
+ }
917
+
918
+ if (definition.type === 'binary') {
919
+ return 'blob'
920
+ }
921
+
922
+ if (definition.type === 'enum') {
923
+ if (definition.enumValues && definition.enumValues.length > 0) {
924
+ return 'enum(' + definition.enumValues.map((value) => quoteLiteral(value)).join(', ') + ')'
925
+ }
926
+
927
+ return 'text'
928
+ }
929
+
930
+ return 'text'
931
+ }